const {
  clamp,
  displayCompactDate,
  escapeHtml,
  formatNumber,
  formatTimestamp,
  localDateString,
  number,
  parseLocalDate,
  todayString,
} = window.XiaobaiUI;

const {
  emptyStateHtml,
  renderEmptyState,
} = window.XiaobaiComponents;

const HEART_BREATH_INHALE_MS = 3_000;
const HEART_BREATH_HOLD_MS = 2_000;
const HEART_BREATH_EXHALE_MS = 4_000;
const HEART_BREATH_PREPARE_MS = 1_000;
const HEART_BREATH_CYCLE_MS = HEART_BREATH_INHALE_MS + HEART_BREATH_HOLD_MS + HEART_BREATH_EXHALE_MS;
const HEART_BREATH_ACTIVE_MS = HEART_BREATH_CYCLE_MS * 5;
const HEART_BREATH_TOTAL_MS = HEART_BREATH_PREPARE_MS + HEART_BREATH_ACTIVE_MS;
const THEME_STORAGE_KEY = "xiaobaiTheme";
let activeThemeTransition = null;
let themeSwitchSequence = 0;

const state = window.XiaobaiState.create({
  session: {
    user: null,
    csrfToken: "",
    authMode: "login",
    started: false,
    activeView: "sentimentCycleView",
    dashboardLoading: false,
    dashboardRequestSequence: 0,
    dashboardRequestDate: "",
    adminModels: [],
    globalSearchResults: [],
    globalSearchActiveIndex: -1,
    globalSearchRequestSequence: 0,
  },
  market: {
    dashboard: null,
    filter: "all",
    query: "",
    sortKey: "streak",
    sortDirection: "desc",
    brokenQuery: "",
    brokenSortKey: "",
    brokenSortDirection: "desc",
    downQuery: "",
    downSortKey: "",
    downSortDirection: "asc",
    yesterdayFilter: "all",
    yesterdayQuery: "",
    yesterdaySortKey: "",
    yesterdaySortDirection: "desc",
    dragonTiger: null,
    dragonViewMode: "daily",
    dragonFilter: "all",
    dragonQuery: "",
    selectedDragonTraderId: "",
    hotMoneyProfiles: null,
    hotMoneyProfileQuery: "",
    selectedHotMoneyProfileId: "",
    rotationHistory: null,
    rotationHistoryKey: "",
    rotationSelectedSector: "",
    rotationSelectedDate: "",
    rotationMembers: null,
    rotationMembersKey: "",
    rotationMembersLoading: false,
    rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
    rotationLoading: false,
    auctionData: null,
    auctionDataset: "focus",
    auctionFilter: "all",
    auctionQuery: "",
    auctionSortKey: "attention_score",
    auctionSortDirection: "desc",
    auctionLoading: false,
    auctionTimer: null,
    themeLibrary: null,
    themeQuery: "",
    selectedThemeCode: "",
    themeDetail: null,
    themeLoading: false,
    popularityData: null,
    popularitySource: "combined",
    popularityQuery: "",
    popularityLoading: false,
    expandedLadderLevels: new Set(),
    ladderSortMode: "time",
    sentimentHistory: null,
    sentimentRange: 20,
    sentimentHistoryKey: "",
    sentimentLoading: false,
  },
  details: {
    stockDetail: null,
    activeStock: null,
    stockDetailChartMode: "daily",
    stockDetailIntraday: null,
    stockDetailRequestSequence: 0,
    entityDetailItem: null,
    entityDetailPayload: null,
    entityDetailChartMode: "daily",
    entityDetailIntraday: null,
    entityDetailRequestSequence: 0,
    stockPreviewCode: "",
    stockPreviewType: "stock",
    stockPreviewItem: null,
    stockPreviewPayload: null,
    stockPreviewChart: "daily",
    stockPreviewFallback: null,
    initialStockOpened: false,
  },
  review: {
    watchlist: [],
    watchlistSelection: null,
    watchlistSearchResults: [],
    watchlistSearchRequestSequence: 0,
    editingDailyNoteId: 0,
    notes: [],
    tradeEntries: [],
    tradeSummary: {},
    editingTradeId: 0,
    alerts: [],
    alertFilter: "all",
    alertUnreadCount: 0,
    assistantMessages: [],
    assistantLoading: false,
    assistantController: null,
  },
  screener: {
    screenerSetup: null,
    screenerSetupKey: "",
    screenerSetupRequestKey: "",
    screenerSetupPromise: null,
    selectedRegime: "",
    selectedStrategy: null,
    customStrategyDraft: null,
    screenerRunning: false,
    screenerRunningMode: "",
    screenerResults: { smart: null, curated: null, quant: null },
    screenerResultContexts: { smart: null, curated: null, quant: null },
    screenerResultStore: {},
    screenerTracking: null,
    screenerMode: ["smart", "curated", "quant"].includes(localStorage.getItem("xiaobaiScreenerMode"))
      ? localStorage.getItem("xiaobaiScreenerMode")
      : "smart",
    curatedCategory: "全部",
    curatedSchool: "全部",
    curatedQuery: "",
    curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
    selectedCuratedStrategyId: 0,
    quantFilters: [],
    quantScores: [],
    screenerMobileView: "strategy",
  },
  mentor: {
    mentorSetup: null,
    selectedMentorId: "",
    mentorMessages: [],
    mentorLoading: false,
    mentorQuery: "",
    mentorGrade: "all",
    mentorDirectoryOpen: false,
    mentorSortMode: false,
    mentorSavingPreferences: false,
    mentorController: null,
  },
  heaven: {
    heavenSetup: null,
    heavenManualData: null,
    personalField: null,
    heavenPanel: "trend",
    heavenInterpretations: { trend: "", fortune: "", heart: "" },
    heavenReadingMode: "trend",
    heavenReadingTab: "current",
    heavenReadingHistory: { trend: [], fortune: [], heart: [] },
    heavenReadingSelectedId: 0,
    heavenReadingLoading: false,
    heavenReadingError: "",
    heartStage: "intro",
    heartTimer: null,
    heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
    heartBreathingEndsAt: 0,
    heartLines: [],
    heartThrows: [],
    heartHexagram: null,
    heartCurtainTimer: null,
    heartStageToken: 0,
    heartRevealToken: 0,
    heavenPerformanceKey: "",
    heavenPerformancePanels: new Set(),
    heavenPerformanceActive: "",
    heavenRequestSequence: 0,
  },
});

window.XiaobaiAPI.configure({
  csrfToken: () => state.csrfToken,
  onUnauthorized: () => showAuthGate("登录状态已失效，请重新登录。"),
});

const applicationShell = window.XiaobaiShell.create({
  state,
  pages: window.XiaobaiPages,
  motionEnabled,
  animateRows,
  refreshIcons,
  tradeDate: () => displayCompactDate(
    state.dashboard?.meta?.trade_date || document.querySelector("#tradeDate")?.value || "",
  ),
  onNavigate: (viewId) => openView(viewId),
  onNavigationSync: () => toggleAccountDropdown(false),
});

const pageModules = window.XiaobaiPageModules.create({
  pages: window.XiaobaiPages,
  actions: {
    closeTransientUi: () => closeStockPreview(),
    applyAccess: () => applyMembershipAccess(),
    clearAuction: () => clearAuctionTimer(),
    stopHeaven: () => {
      stopQiFieldCanvas();
      stopHeartDust();
      cancelHeavenPerformance();
    },
    loadSentiment: () => loadSentimentHistory(),
    loadRotation: () => loadRotationHistory(),
    loadAuction: () => loadAuctionCenter(),
    loadThemes: () => loadThemeLibrary(),
    loadPopularity: () => loadPopularity(),
    loadDragonTiger: () => loadDragonTiger(),
    loadReview: () => loadReviewWorkspace(),
    loadScreener: () => {
      if (hasMemberAccess() && state.dashboard) loadScreenerSetup();
    },
    loadMentor: () => {
      if (hasMemberAccess()) loadMentorSetup();
    },
    loadHeaven: () => {
      if (!hasMemberAccess()) return;
      loadHeavenSetup(false, "", document.querySelector("#heavenStockInput").value.trim());
    },
  },
});

const elements = {
  tradeDate: document.querySelector("#tradeDate"),
  loading: document.querySelector("#loadingOverlay"),
  toast: document.querySelector("#toast"),
  stockDialog: document.querySelector("#stockDialog"),
  tradeLogDialog: document.querySelector("#tradeLogDialog"),
  watchlistDialog: document.querySelector("#watchlistDialog"),
  alertsDialog: document.querySelector("#alertsDialog"),
  assistantDialog: document.querySelector("#assistantDialog"),
  heavenReadingDialog: document.querySelector("#heavenReadingDialog"),
  globalSearchDialog: document.querySelector("#globalSearchDialog"),
  globalSearchInput: document.querySelector("#globalSearchInput"),
  globalSearchResults: document.querySelector("#globalSearchResults"),
  entityDetailDialog: document.querySelector("#entityDetailDialog"),
  entityDetailChart: document.querySelector("#entityDetailChart"),
  settingsDialog: document.querySelector("#settingsDialog"),
  adminDialog: document.querySelector("#adminDialog"),
  priceChart: document.querySelector("#priceChart"),
  stockPreview: document.querySelector("#stockPreview"),
  stockPreviewBackdrop: document.querySelector("#stockPreviewBackdrop"),
  stockPreviewChart: document.querySelector("#stockPreviewChart"),
};

function openModalDialog(dialog) {
  applicationShell.openModalDialog(dialog);
}

const metricAnimationFrames = new WeakMap();
const stockPreviewCache = new Map();
const STOCK_PREVIEW_DELAY = 380;
const STOCK_PREVIEW_CACHE_MS = 5 * 60 * 1000;
const LIVE_REFRESH_DEFAULT_MS = 10 * 1000;
let qiFieldAnimationFrame = 0;
let qiFieldSoloElement = "";
let heavenPerformanceToken = 0;
let heavenReadingAnimation = null;
let heartHoldTimer = null;
let heartHoldTriggered = false;
let heartHoldStartedAt = 0;
let heartHoldAnimationFrame = 0;
let heartCastingBusy = false;
let heartDustAnimationFrame = 0;
let heartDustParticles = [];
let heartIncenseAnimation = null;
const heartCoinRotations = [0, 0, 0];
let rowAnimationObserver = null;
let stockPreviewOpenTimer = null;
let stockPreviewCloseTimer = null;
let stockPreviewAbortController = null;
let stockPreviewAnchor = null;
let sentimentChartAnimationFrame = null;
let heavenResizeTimer = null;
let globalSearchTimer = null;
let watchlistSearchTimer = null;
let assistantRenderFrame = 0;

const heartSound = {
  enabled: false,
  context: null,
  ensure() {
    if (!this.context) {
      const AudioContextClass = window.AudioContext || window.webkitAudioContext;
      if (!AudioContextClass) return null;
      this.context = new AudioContextClass();
    }
    if (this.context.state === "suspended") this.context.resume();
    return this.context;
  },
  tone(frequency, duration, gain, type = "sine", delay = 0) {
    if (!this.enabled) return;
    const context = this.ensure();
    if (!context) return;
    const start = context.currentTime + delay;
    const oscillator = context.createOscillator();
    const volume = context.createGain();
    oscillator.type = type;
    oscillator.frequency.value = frequency;
    volume.gain.setValueAtTime(0.0001, start);
    volume.gain.linearRampToValueAtTime(gain, start + 0.015);
    volume.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    oscillator.connect(volume).connect(context.destination);
    oscillator.start(start);
    oscillator.stop(start + duration + 0.05);
  },
  chime(frequency = 640) {
    this.tone(frequency, 4.8, 0.12);
    this.tone(frequency * 2.02, 3.6, 0.045);
    this.tone(frequency * 3.96, 2.2, 0.018);
  },
  coin(delay = 0) {
    this.tone(2350 + Math.random() * 260, 0.28, 0.055, "triangle", delay);
    this.tone(3250 + Math.random() * 260, 0.18, 0.025, "triangle", delay + 0.01);
  },
};

const HEART_WHISPERS = [
  ["应无所住，而生其心", 10, 12, 0],
  ["不是风动，不是幡动，仁者心动", 89, 8, 1],
  ["菩提本无树，明镜亦非台", 16, 52, 2],
  ["本来无一物，何处惹尘埃", 84, 54, 3],
  ["心外无物，心外无理", 22, 18, 4],
  ["知行合一", 78, 30, 5],
  ["此心光明，亦复何言", 90, 60, 6],
];

window.addEventListener("resize", () => {
  clearTimeout(heavenResizeTimer);
  heavenResizeTimer = setTimeout(() => {
    if (state.activeView !== "heavenView") return;
    if (state.heavenPanel === "fortune" && state.heavenSetup?.field) {
      renderQiFieldCanvas(state.heavenSetup.field.balance || [], { intro: false });
      drawQiUseConnections(false);
    }
    if (state.heavenPanel === "heart") startHeartDust();
  }, 120);
});

document.addEventListener("DOMContentLoaded", initialize);

function syncThemeControl() {
  const theme = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
  const button = document.querySelector("#themeToggle");
  if (!button) return;
  const dark = theme === "dark";
  const label = dark ? "切换到日间模式" : "切换到夜间模式";
  button.title = label;
  button.setAttribute("aria-label", label);
  button.setAttribute("aria-pressed", String(dark));
  button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon");
}

function clearThemeTransitionEffects() {
  document.querySelectorAll(".row-enter, .row-pending, .view-entering").forEach((element) => {
    element.classList.remove("row-enter", "row-pending", "view-entering");
    element.style.removeProperty("--row-delay");
  });
}

function redrawThemeSensitiveVisuals() {
  if (!elements.stockPreview.hidden && state.stockPreviewPayload) {
    selectStockPreviewChart(state.stockPreviewChart);
  }
  if (elements.stockDialog.open) {
    if (state.stockDetailChartMode === "intraday" && state.stockDetailIntraday?.points?.length) {
      drawIntradayCanvas(
        elements.priceChart,
        state.stockDetailIntraday.points,
        [],
        state.stockDetailIntraday.meta?.previous_close,
      );
    } else if (state.stockDetail?.prices) drawPriceChart(state.stockDetail.prices);
  }
  if (elements.entityDetailDialog.open) {
    if (state.entityDetailChartMode === "intraday" && state.entityDetailIntraday?.points?.length) {
      drawIntradayCanvas(
        elements.entityDetailChart,
        state.entityDetailIntraday.points,
        [],
        state.entityDetailIntraday.meta?.previous_close,
      );
    } else if (state.entityDetailPayload?.series) {
      drawEntityDetailChart(state.entityDetailPayload.series);
    }
  }
  if (state.activeView === "sentimentCycleView" && state.sentimentHistory) {
    drawSentimentTrendChart(state.sentimentHistory.rows || []);
  }
  if (state.activeView === "heavenView") {
    if (state.heavenPanel === "fortune" && state.heavenSetup?.field) {
      renderQiFieldCanvas(state.heavenSetup.field.balance || [], { intro: false });
      drawQiUseConnections(false);
    }
    if (state.heavenPanel === "heart") startHeartDust();
  }
}

function commitTheme(normalized, persist) {
  document.documentElement.dataset.theme = normalized;
  document.documentElement.style.colorScheme = normalized;
  if (persist) {
    try {
      localStorage.setItem(THEME_STORAGE_KEY, normalized);
    } catch (_error) {
      // The selected theme still applies for the current page when storage is unavailable.
    }
  }
  syncThemeControl();
  refreshIcons();
  redrawThemeSensitiveVisuals();
}

function applyTheme(theme, persist = true) {
  const normalized = theme === "dark" ? "dark" : "light";
  const root = document.documentElement;
  if (root.dataset.theme === normalized) {
    commitTheme(normalized, persist);
    return;
  }
  const sequence = ++themeSwitchSequence;
  activeThemeTransition?.skipTransition?.();
  clearThemeTransitionEffects();
  root.classList.add("theme-switching");

  const update = () => commitTheme(normalized, persist);
  const finish = () => {
    if (sequence !== themeSwitchSequence) return;
    clearThemeTransitionEffects();
    root.classList.remove("theme-switching");
    activeThemeTransition = null;
  };
  const reducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
  if (!reducedMotion && typeof document.startViewTransition === "function") {
    activeThemeTransition = document.startViewTransition(update);
    activeThemeTransition.finished.then(finish, finish);
    return;
  }
  update();
  requestAnimationFrame(() => requestAnimationFrame(finish));
}

function toggleTheme() {
  applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
}

async function initialize() {
  syncThemeControl();
  refreshIcons();
  applicationShell.initialize();
  elements.tradeDate.value = todayString();
  const initialUrl = new URL(window.location.href);
  if (initialUrl.searchParams.has("date")) {
    initialUrl.searchParams.delete("date");
    history.replaceState(null, "", initialUrl);
  }
  elements.tradeDate.max = todayString();
  document.querySelector("#journalDate").value = elements.tradeDate.value;
  document.querySelector("#journalDate").max = todayString();
  document.querySelector("#tradeLogDate").value = elements.tradeDate.value;
  document.querySelector("#tradeLogDate").max = todayString();
  document.querySelector("#backfillStart").value = todayString();
  document.querySelector("#backfillEnd").value = todayString();
  document.querySelector("#backfillStart").max = todayString();
  document.querySelector("#backfillEnd").max = todayString();
  document.querySelector("#qiObservationDate").value = elements.tradeDate.value;
  document.querySelector("#qiObservationDate").max = todayString();
  document.querySelector("#accountBirthDate").max = todayString();
  document.querySelector("#alertDate").value = todayString();
  bindEvents();
  try {
    const session = await apiRequest("/api/auth/me");
    if (!session.authenticated) {
      if (session.registration_required) selectAuthMode("register");
      showAuthGate();
      return;
    }
    await applyAuthenticatedSession(session);
  } catch (error) {
    showAuthGate(error.message || "无法连接本地服务");
  }
}

async function startAuthenticatedApp() {
  if (state.started) return;
  state.started = true;
  const searchParams = new URLSearchParams(window.location.search);
  const requestedHeavenPanel = searchParams.get("heaven");
  if (["trend", "fortune", "heart"].includes(requestedHeavenPanel)) {
    state.heavenPanel = requestedHeavenPanel;
  }
  const requestedView = window.XiaobaiPages.resolve(searchParams.get("view"));
  if (
    requestedView
    && window.XiaobaiPages.has(requestedView)
    && document.getElementById(requestedView)?.classList.contains("workspace-view")
  ) {
    openView(requestedView, false);
    if (requestedView !== searchParams.get("view")) {
      const url = new URL(window.location.href);
      url.searchParams.set("view", requestedView);
      history.replaceState(null, "", url);
    }
  }
  loadDashboard();
  loadAlerts();
  if (new URLSearchParams(window.location.search).get("settings") === "1") {
    setTimeout(openSettings, 0);
  }
}

function selectAuthMode(mode) {
  state.authMode = mode === "register" ? "register" : "login";
  document.querySelectorAll("[data-auth-mode]").forEach((button) => {
    button.classList.toggle("active", button.dataset.authMode === state.authMode);
  });
  const registering = state.authMode === "register";
  document.querySelector("#authConfirmField").hidden = !registering;
  document.querySelector("#authPasswordConfirm").required = registering;
  document.querySelector("#authPassword").autocomplete = registering ? "new-password" : "current-password";
  document.querySelector("#authSubmitButton").textContent = registering ? "注册并进入" : "登录";
  document.querySelector("#authError").hidden = true;
}

async function submitAuthForm(event) {
  event.preventDefault();
  const username = document.querySelector("#authUsername").value.trim();
  const password = document.querySelector("#authPassword").value;
  const errorElement = document.querySelector("#authError");
  if (state.authMode === "register" && password !== document.querySelector("#authPasswordConfirm").value) {
    errorElement.textContent = "两次输入的密码不一致。";
    errorElement.hidden = false;
    return;
  }
  const button = document.querySelector("#authSubmitButton");
  button.disabled = true;
  try {
    const session = await apiRequest(`/api/auth/${state.authMode}`, "POST", { username, password });
    document.querySelector("#authForm").reset();
    await applyAuthenticatedSession(session);
  } catch (error) {
    errorElement.textContent = error.message || "账号操作失败";
    errorElement.hidden = false;
  } finally {
    button.disabled = false;
  }
}

async function applyAuthenticatedSession(session) {
  state.user = session.user;
  state.csrfToken = session.csrf_token || "";
  setText("accountName", session.user?.username || "账号");
  const isAdmin = session.user?.role === "admin";
  updateAccountIdentityBadges(session.user?.membership || {});
  document.querySelector("#settingsButton").hidden = !isAdmin;
  document.querySelector("#syncButton").hidden = !isAdmin;
  document.querySelector("#reasonForm").hidden = !isAdmin;
  document.querySelector("#sectorPhaseManager").hidden = !isAdmin;
  document.querySelector("#authGate").hidden = true;
  applyMembershipAccess();
  await startAuthenticatedApp();
}

function showAuthGate(message = "") {
  state.user = null;
  state.csrfToken = "";
  const gate = document.querySelector("#authGate");
  gate.hidden = false;
  const errorElement = document.querySelector("#authError");
  errorElement.textContent = message;
  errorElement.hidden = !message;
  document.querySelector("#authUsername").focus();
}

async function logoutAccount() {
  toggleAccountDropdown(false);
  try {
    await apiRequest("/api/auth/logout", "POST", {});
  } catch (error) {
    showToast(error.message || "退出失败");
    return;
  }
  window.location.reload();
}

function bindEvents() {
  document.querySelectorAll("[data-auth-mode]").forEach((button) => {
    button.addEventListener("click", () => selectAuthMode(button.dataset.authMode));
  });
  document.querySelector("#authForm").addEventListener("submit", submitAuthForm);
  document.querySelector("#refreshButton").addEventListener("click", async (event) => {
    const button = event.currentTarget;
    button.disabled = true;
    try {
      await loadDashboard(false, false, false);
    } finally {
      button.disabled = false;
    }
  });
  document.querySelector("#syncButton").addEventListener("click", startAdminRefresh);
  elements.tradeDate.addEventListener("change", () => {
    state.dashboardRequestSequence += 1;
    state.heavenRequestSequence += 1;
    state.heavenManualData = null;
    document.querySelector("#qiObservationDate").value = elements.tradeDate.value;
    loadDashboard();
  });
  document.querySelector("#prevDate").addEventListener("click", () => shiftDate(-1));
  document.querySelector("#nextDate").addEventListener("click", () => shiftDate(1));
  document.querySelector("#stockSearch").addEventListener("input", (event) => {
    state.query = event.target.value.trim().toLowerCase();
    renderLimitTable();
  });
  document.querySelectorAll("[data-table-search]").forEach((input) => {
    input.addEventListener("input", () => {
      const query = input.value.trim().toLowerCase();
      const body = document.querySelector(`#${CSS.escape(input.dataset.tableSearch)}`);
      body?.querySelectorAll("tr").forEach((row) => {
        row.hidden = Boolean(query) && !row.textContent.toLowerCase().includes(query);
      });
    });
  });

  document.querySelectorAll("[data-filter]").forEach((button) => {
    button.addEventListener("click", () => {
      document.querySelectorAll("[data-filter]").forEach((item) => item.classList.remove("active"));
      button.classList.add("active");
      state.filter = button.dataset.filter;
      renderLimitTable();
    });
  });

  document.querySelector("#globalSearchButton").addEventListener("click", openGlobalSearch);
  document.querySelector("#themeToggle").addEventListener("click", toggleTheme);
  document.querySelector("#alertButton").addEventListener("click", openAlerts);
  document.querySelector("#assistantButton").addEventListener("click", openReviewAssistant);
  document.querySelector("#closeAssistantDialog").addEventListener("click", () => elements.assistantDialog.close());
  document.querySelector("#assistantForm").addEventListener("submit", sendAssistantQuestion);
  document.querySelector("#stopAssistant").addEventListener("click", stopAssistantResponse);
  document.querySelector("#clearAssistantMessages").addEventListener("click", clearAssistantConversation);
  document.querySelectorAll("[data-assistant-prompt]").forEach((button) => {
    button.addEventListener("click", () => useAssistantPrompt(button.dataset.assistantPrompt));
  });
  document.querySelector("#closeAlertsDialog").addEventListener("click", () => elements.alertsDialog.close());
  document.querySelector("#alertForm").addEventListener("submit", saveAlert);
  document.querySelector("#markAllAlertsRead").addEventListener("click", markAllAlertsRead);
  document.querySelector("#alertList").addEventListener("click", handleAlertAction);
  document.querySelectorAll("[data-alert-filter]").forEach((button) => {
    button.addEventListener("click", () => selectAlertFilter(button.dataset.alertFilter));
  });
  document.querySelector("#closeGlobalSearch").addEventListener("click", closeGlobalSearch);
  document.querySelector("#closeEntityDetail").addEventListener("click", () => elements.entityDetailDialog.close());
  document.querySelectorAll("[data-entity-detail-chart]").forEach((button) => {
    button.addEventListener("click", () => selectEntityDetailChart(button.dataset.entityDetailChart));
  });
  elements.globalSearchDialog.addEventListener("click", (event) => {
    if (event.target === elements.globalSearchDialog) closeGlobalSearch();
  });
  elements.globalSearchInput.addEventListener("input", scheduleGlobalSearch);
  elements.globalSearchInput.addEventListener("keydown", handleGlobalSearchInputKeydown);
  elements.globalSearchResults.addEventListener("click", (event) => {
    const result = event.target.closest("[data-search-result-index]");
    if (result) openGlobalSearchResult(number(result.dataset.searchResultIndex));
  });
  document.addEventListener("click", (event) => {
    if (!event.target.closest(".account-menu-shell")) toggleAccountDropdown(false);
  });
  window.addEventListener("keydown", handleGlobalSearchShortcut);
  document.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      toggleAccountDropdown(false, true);
      toggleMentorDirectory(false);
    }
    handleAccountMenuKeydown(event);
  });
  window.addEventListener("resize", () => {
    if (window.innerWidth > 720) toggleMentorDirectory(false);
    if (!elements.stockPreview.hidden) closeStockPreview();
    if (state.activeView === "dragonView") layoutDragonCards();
  });
  document.querySelectorAll("[data-open-account]").forEach((button) => {
    button.addEventListener("click", () => openSettings("membership"));
  });
  document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
    header.addEventListener("click", () => changeSort(header.dataset.sort));
  });
  document.querySelector("#brokenSearch").addEventListener("input", (event) => {
    state.brokenQuery = event.target.value.trim().toLowerCase();
    renderBrokenTable(state.dashboard?.broken || []);
  });
  document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
    header.addEventListener("click", () => changeBrokenSort(header.dataset.brokenSort));
  });
  document.querySelector("#downSearch").addEventListener("input", (event) => {
    state.downQuery = event.target.value.trim().toLowerCase();
    renderDownTable(state.dashboard?.down_limits || []);
  });
  document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
    header.addEventListener("click", () => changeDownSort(header.dataset.downSort));
  });
  document.querySelector("#yesterdaySearch").addEventListener("input", (event) => {
    state.yesterdayQuery = event.target.value.trim().toLowerCase();
    renderYesterdayTable(state.dashboard?.yesterday_limits || []);
  });
  document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
    button.addEventListener("click", () => {
      state.yesterdayFilter = button.dataset.yesterdayFilter;
      renderYesterdayTable(state.dashboard?.yesterday_limits || []);
    });
  });
  document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
    header.addEventListener("click", () => changeYesterdaySort(header.dataset.yesterdaySort));
  });
  document.querySelectorAll("[data-ladder-sort]").forEach((button) => {
    button.addEventListener("click", () => {
      state.ladderSortMode = button.dataset.ladderSort === "open" ? "open" : "time";
      document.querySelectorAll("[data-ladder-sort]").forEach((item) => {
        const active = item === button;
        item.classList.toggle("active", active);
        item.setAttribute("aria-pressed", String(active));
      });
      renderLadderBoard(state.dashboard?.ladders || []);
    });
  });

  document.querySelector("#exportButton").addEventListener("click", exportStocks);
  document.querySelector("#brokenExportButton").addEventListener("click", exportBroken);
  document.querySelector("#downExportButton").addEventListener("click", exportDown);
  document.querySelector("#yesterdayExportButton").addEventListener("click", exportYesterday);
  document.querySelector("#ladderExportButton").addEventListener("click", exportLadder);
  document.querySelector("#rotationExportButton").addEventListener("click", exportRotation);
  document.querySelectorAll("[data-rotation-order]").forEach((button) => {
    button.addEventListener("click", () => {
      state.rotationOrder = button.dataset.rotationOrder === "latest" ? "latest" : "oldest";
      localStorage.setItem("xiaobaiRotationOrder", state.rotationOrder);
      renderRotationHistory();
    });
  });
  document.querySelector("#sentimentExportButton").addEventListener("click", exportSentimentHistory);
  document.querySelectorAll("[data-sentiment-range]").forEach((button) => {
    button.addEventListener("click", () => {
      state.sentimentRange = number(button.dataset.sentimentRange) || 20;
      document.querySelectorAll("[data-sentiment-range]").forEach((item) => {
        item.classList.toggle("active", item === button);
      });
      loadSentimentHistory(true);
    });
  });
  document.querySelector("#settingsButton").addEventListener("click", () => openAdminSettings());
  document.querySelector("#accountButton").addEventListener("click", (event) => {
    event.stopPropagation();
    toggleAccountDropdown();
  });
  document.querySelector("#accountVipBadge").addEventListener("click", () => openSettings("membership"));
  document.querySelectorAll("[data-account-panel]").forEach((button) => {
    button.addEventListener("click", () => openSettings(button.dataset.accountPanel));
  });
  document.querySelector("#switchAccountMenuButton").addEventListener("click", switchAccount);
  document.querySelector("#logoutMenuButton").addEventListener("click", logoutAccount);
  document.querySelector("#closeSettingsDialog").addEventListener("click", () => elements.settingsDialog.close());
  document.querySelector("#closeAdminDialog").addEventListener("click", () => elements.adminDialog.close());
  document.querySelector("#closeStockDialog").addEventListener("click", () => elements.stockDialog.close());
  document.querySelectorAll("[data-stock-detail-chart]").forEach((button) => {
    button.addEventListener("click", () => selectStockDetailChart(button.dataset.stockDetailChart));
  });
  document.querySelector("#closeStockPreview").addEventListener("click", closeStockPreview);
  elements.stockPreviewBackdrop.addEventListener("click", closeStockPreview);
  document.querySelector("#openStockDetailFromPreview").addEventListener("click", openStockDetailFromPreview);
  document.querySelectorAll("[data-preview-chart]").forEach((button) => {
    button.addEventListener("click", () => selectStockPreviewChart(button.dataset.previewChart));
  });
  elements.stockPreview.addEventListener("pointerenter", cancelStockPreviewClose);
  elements.stockPreview.addEventListener("pointerleave", scheduleStockPreviewClose);
  document.addEventListener("pointerover", handleStockPreviewPointerOver);
  document.addEventListener("pointerout", handleStockPreviewPointerOut);
  document.addEventListener("focusin", handleStockPreviewFocus);
  document.addEventListener("focusout", handleStockPreviewFocusOut);
  document.addEventListener("click", handleMobileStockPreviewClick, true);
  document.addEventListener("keydown", handleStockPreviewKeydown);
  document.addEventListener("scroll", repositionStockPreview, true);
  document.querySelector("#auctionRefreshButton").addEventListener("click", () => loadAuctionCenter(true));
  document.querySelector("#auctionExportButton").addEventListener("click", exportAuctionRows);
  document.querySelector("#auctionSearch").addEventListener("input", (event) => {
    state.auctionQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderAuctionTable();
  });
  document.querySelectorAll("[data-auction-dataset]").forEach((button) => {
    button.addEventListener("click", () => {
      state.auctionDataset = button.dataset.auctionDataset || "focus";
      state.auctionFilter = "all";
      state.auctionSortKey = state.auctionDataset === "onePrice" ? "amount_million" : "attention_score";
      state.auctionSortDirection = "desc";
      document.querySelectorAll("[data-auction-dataset]").forEach((item) => {
        const active = item === button;
        item.classList.toggle("active", active);
        item.setAttribute("aria-selected", String(active));
      });
      document.querySelectorAll("[data-auction-filter]").forEach((item) => item.classList.toggle("active", item.dataset.auctionFilter === "all"));
      renderAuctionTable();
    });
  });
  document.querySelectorAll("[data-auction-filter]").forEach((button) => {
    button.addEventListener("click", () => {
      state.auctionFilter = button.dataset.auctionFilter || "all";
      document.querySelectorAll("[data-auction-filter]").forEach((item) => item.classList.toggle("active", item === button));
      renderAuctionTable();
    });
  });
  document.querySelector("#auctionTable").addEventListener("click", (event) => {
    const header = event.target.closest("th[data-auction-sort]");
    if (!header) return;
    const key = header.dataset.auctionSort;
    if (state.auctionSortKey === key) state.auctionSortDirection = state.auctionSortDirection === "asc" ? "desc" : "asc";
    else {
      state.auctionSortKey = key;
      state.auctionSortDirection = "desc";
    }
    renderAuctionTable();
  });
  document.querySelector("#openStrategyDrawerButton").addEventListener("click", openCustomStrategyDrawer);
  document.querySelector("#closeStrategyDrawerButton").addEventListener("click", () => document.querySelector("#strategyDrawer").close());
  document.querySelector("#strategyDrawer").addEventListener("click", (event) => {
    if (event.target === event.currentTarget) event.currentTarget.close();
  });
  document.querySelector("#themeRefreshButton").addEventListener("click", () => loadThemeLibrary(true));
  document.querySelector("#themeSearch").addEventListener("input", (event) => {
    state.themeQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderThemeDirectory();
  });
  document.querySelector("#themeDirectory").addEventListener("click", (event) => {
    const button = event.target.closest("[data-theme-code]");
    if (button) selectTheme(button.dataset.themeCode);
  });
  document.querySelector("#popularityRefreshButton").addEventListener("click", () => loadPopularity(true));
  document.querySelector("#popularitySearch").addEventListener("input", (event) => {
    state.popularityQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderPopularityTable();
  });
  document.querySelectorAll("[data-popularity-source]").forEach((button) => {
    button.addEventListener("click", () => {
      state.popularitySource = button.dataset.popularitySource || "combined";
      document.querySelectorAll("[data-popularity-source]").forEach((item) => {
        const active = item === button;
        item.classList.toggle("active", active);
        item.setAttribute("aria-selected", String(active));
      });
      renderPopularityTable();
    });
  });
  document.querySelector("#dragonRefreshButton").addEventListener("click", () => {
    if (state.dragonViewMode === "profiles") loadHotMoneyProfiles(true);
    else loadDragonTiger(true);
  });
  document.querySelector("#dragonEmptyRefreshButton").addEventListener("click", () => loadDragonTiger(true));
  document.querySelector("#dragonPreviousButton").addEventListener("click", () => shiftDate(-1));
  document.querySelector("#dragonExportButton").addEventListener("click", () => {
    if (state.dragonViewMode === "profiles") exportHotMoneyProfiles();
    else exportDragonTiger();
  });
  document.querySelectorAll("[data-dragon-view-mode]").forEach((button) => {
    button.addEventListener("click", () => selectDragonViewMode(button.dataset.dragonViewMode));
  });
  document.querySelector("#dragonSearch").addEventListener("input", (event) => {
    state.dragonQuery = event.target.value.trim().toLowerCase();
    renderDragonTraderList();
  });
  document.querySelectorAll("[data-dragon-filter]").forEach((button) => {
    button.addEventListener("click", () => {
      state.dragonFilter = button.dataset.dragonFilter;
      document.querySelectorAll("[data-dragon-filter]").forEach((item) => {
        item.classList.toggle("active", item === button);
      });
      renderDragonTraderList();
    });
  });
  document.querySelector("#hotMoneyProfileSearch").addEventListener("input", (event) => {
    state.hotMoneyProfileQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderHotMoneyProfiles();
  });
  document.querySelector("#hotMoneyProfileList").addEventListener("click", (event) => {
    const button = event.target.closest("[data-hot-money-profile]");
    if (!button) return;
    state.selectedHotMoneyProfileId = button.dataset.hotMoneyProfile;
    renderHotMoneyProfiles();
  });
  document.querySelector("#journalForm").addEventListener("submit", saveJournal);
  document.querySelector("#journalDate").addEventListener("change", populateJournalForm);
  document.querySelector("#openWatchlistDialog").addEventListener("click", () => openWatchlistDialog());
  document.querySelector("#closeWatchlistDialog").addEventListener("click", closeWatchlistDialog);
  document.querySelector("#cancelWatchlistEdit").addEventListener("click", closeWatchlistDialog);
  document.querySelector("#changeWatchlistSelection").addEventListener("click", clearWatchlistSelection);
  document.querySelector("#watchlistSearchInput").addEventListener("input", scheduleWatchlistSearch);
  document.querySelector("#watchlistForm").addEventListener("submit", saveWatchlistFromDialog);
  document.querySelector("#watchlistSearchResults").addEventListener("click", handleWatchlistSearchResult);
  document.querySelector("#reviewHistoryToggle").addEventListener("click", (event) => {
    const panel = document.querySelector("#reviewHistoryPanel");
    const expanded = event.currentTarget.getAttribute("aria-expanded") === "true";
    event.currentTarget.setAttribute("aria-expanded", String(!expanded));
    event.currentTarget.querySelector("span").textContent = expanded ? "历史复盘" : "收起历史";
    panel.hidden = expanded;
    if (!expanded) panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
  });
  document.querySelector("#openTradeLogDialog").addEventListener("click", openTradeLogDialog);
  document.querySelector("#closeTradeLogDialog").addEventListener("click", closeTradeLogDialog);
  document.querySelector("#tradeLogForm").addEventListener("submit", saveTradeLog);
  document.querySelector("#cancelTradeEdit").addEventListener("click", closeTradeLogDialog);
  elements.tradeLogDialog.addEventListener("close", resetTradeLogForm);
  document.querySelector("#tradeLogTableBody").addEventListener("click", handleTradeLogAction);
  document.querySelector("#stockNoteForm").addEventListener("submit", saveStockNote);
  document.querySelector("#watchStockButton").addEventListener("click", toggleActiveWatchlist);
  document.querySelector("#stockHeavenButton").addEventListener("click", openActiveStockInHeaven);
  document.querySelector("#stockReminderButton").addEventListener("click", openStockReminder);
  document.querySelector("#reasonForm").addEventListener("submit", saveReasonOverride);
  document.querySelector("#backfillButton").addEventListener("click", backfillData);
  document.querySelector("#openScreenerTrackingButton").addEventListener("click", async () => {
    await loadScreenerTracking(true);
    openView("screenerTrackingView");
  });
  document.querySelector("#closeScreenerTrackingButton").addEventListener("click", () => openView("screenerView"));
  document.querySelector("#refreshTrackingButton").addEventListener("click", refreshScreenerTracking);
  document.querySelector("#trackingTableBody").addEventListener("click", handleTrackingTableAction);
  document.querySelectorAll("[data-screener-mobile-view]").forEach((button) => {
    button.addEventListener("click", () => selectScreenerMobileView(button.dataset.screenerMobileView));
  });
  document.querySelector("#compileStrategyButton").addEventListener("click", compileStrategy);
  document.querySelector("#saveStrategyButton").addEventListener("click", saveCurrentStrategy);
  document.querySelector("#deleteStrategyButton").addEventListener("click", deleteCurrentStrategy);
  document.querySelector("#screenerExportButton").addEventListener("click", exportScreenerResults);
  document.querySelector("#runBacktestToggle").addEventListener("change", updateBacktestTaskStatus);
  document.querySelectorAll("[data-screener-mode]").forEach((button) => {
    button.addEventListener("click", () => selectScreenerMode(button.dataset.screenerMode));
  });
  document.querySelector("#curatedStrategyList").addEventListener("click", (event) => {
    if (event.target.closest("button")) return;
    const card = event.target.closest("[data-curated-strategy]");
    if (!card) return;
    state.selectedCuratedStrategyId = number(card.dataset.curatedStrategy);
    renderCuratedStrategyLibrary();
    renderScreenerResult();
  });
  document.querySelector("#curatedStrategySearch").addEventListener("input", (event) => {
    state.curatedQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderCuratedStrategyLibrary();
  });
  document.querySelector("#curatedCategoryFilter").addEventListener("change", (event) => {
    state.curatedCategory = event.target.value;
    renderCuratedStrategyLibrary();
  });
  document.querySelector("#curatedSchoolFilters").addEventListener("click", (event) => {
    const button = event.target.closest("[data-curated-school]");
    if (!button) return;
    state.curatedSchool = button.dataset.curatedSchool;
    renderCuratedStrategyLibrary();
  });
  document.querySelectorAll("[data-curated-view]").forEach((button) => {
    button.addEventListener("click", () => {
      state.curatedViewMode = button.dataset.curatedView === "grid" ? "grid" : "list";
      localStorage.setItem("xiaobaiCuratedViewMode", state.curatedViewMode);
      renderCuratedStrategyLibrary();
    });
  });
  document.querySelector("#quantResetButton").addEventListener("click", resetQuantBuilder);
  document.querySelector("#addQuantFilterButton").addEventListener("click", () => addQuantFilter());
  document.querySelector("#addQuantScoreButton").addEventListener("click", () => addQuantScore());
  document.querySelector("#quantFilterRows").addEventListener("input", handleQuantBuilderInput);
  document.querySelector("#quantFilterRows").addEventListener("change", handleQuantBuilderInput);
  document.querySelector("#quantFilterRows").addEventListener("click", handleQuantBuilderClick);
  document.querySelector("#quantScoreRows").addEventListener("input", handleQuantBuilderInput);
  document.querySelector("#quantScoreRows").addEventListener("change", handleQuantBuilderInput);
  document.querySelector("#quantScoreRows").addEventListener("click", handleQuantBuilderClick);
  ["quantListedDays", "quantLimit", "quantMinScore", "quantExcludeSt"].forEach((id) => {
    document.querySelector(`#${id}`).addEventListener("input", renderQuantSummary);
    document.querySelector(`#${id}`).addEventListener("change", renderQuantSummary);
  });
  document.querySelector("#quantRunButton").addEventListener("click", runQuantStrategy);
  document.querySelector("#quantSaveButton").addEventListener("click", saveQuantAsStrategy);
  document.querySelector("#quantBacktestToggle").addEventListener("change", updateBacktestTaskStatus);
  document.querySelector("#mentorChatForm").addEventListener("submit", sendMentorQuestion);
  document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
  document.querySelector("#mentorDirectoryToggle").addEventListener("click", () => {
    toggleMentorDirectory(!state.mentorDirectoryOpen);
  });
  document.querySelector("#closeMentorDirectory").addEventListener("click", () => toggleMentorDirectory(false));
  document.querySelector("#mentorDirectoryBackdrop").addEventListener("click", () => toggleMentorDirectory(false));
  document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
  document.querySelector("#mentorSearchInput").addEventListener("input", (event) => {
    state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
    renderMentorDirectory();
  });
  document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
    button.addEventListener("click", () => {
      state.mentorGrade = button.dataset.mentorGrade || "all";
      document.querySelectorAll("[data-mentor-grade]").forEach((item) => {
        item.classList.toggle("active", item === button);
      });
      renderMentorDirectory();
    });
  });
  document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
    button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
  });
  document.querySelectorAll("[data-heaven-panel]").forEach((button) => {
    button.addEventListener("click", () => selectHeavenPanel(button.dataset.heavenPanel, true));
  });
  document.querySelector("#loadHeavenSelectionButton").addEventListener("click", loadHeavenSelection);
  document.querySelector("#heavenCalibrationForm").addEventListener("submit", applyHeavenCalibration);
  document.querySelector("#resetHeavenCalibrationButton").addEventListener("click", resetHeavenCalibration);
  document.querySelector("#heavenStockInput").addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      event.preventDefault();
      loadHeavenSelection();
    }
  });
  document.querySelector("#interpretTrendButton").addEventListener("click", () => interpretHeaven("trend"));
  document.querySelector("#interpretFortuneButton").addEventListener("click", () => interpretHeaven("fortune"));
  document.querySelector("#historyTrendButton").addEventListener("click", () => openHeavenHistory("trend"));
  document.querySelector("#historyFortuneButton").addEventListener("click", () => openHeavenHistory("fortune"));
  document.querySelector("#qiObservationDate").addEventListener("change", () => {
    state.personalField = null;
    state.heavenManualData = null;
    state.heavenInterpretations.fortune = "";
    loadHeavenSetup(
      true,
      "",
      document.querySelector("#heavenStockInput").value.trim(),
    );
  });
  document.querySelector("#openPersonalSettingsButton").addEventListener("click", () => openSettings("profile"));
  document.querySelector("#accountBirthForm").addEventListener("submit", saveAccountBirthProfile);
  document.querySelector("#deleteBirthProfileButton").addEventListener("click", deleteAccountBirthProfile);
  document.querySelector("#passwordForm").addEventListener("submit", changeAccountPassword);
  document.querySelector("#sectorPhaseForm").addEventListener("submit", saveSectorPhaseOverride);
  document.querySelector("#startBreathingButton").addEventListener("click", startHeartBreathing);
  document.querySelector("#beginCastingButton").addEventListener("click", beginHeartCasting);
  document.querySelector("#heartSoundToggle").addEventListener("click", toggleHeartSound);
  document.querySelector("#historyHeartButton").addEventListener("click", () => openHeavenHistory("heart"));
  initializeHeartCoinHold();
  initializeHeartLineInspection();
  document.querySelector("#interpretHeartButton").addEventListener("click", () => interpretHeaven("heart"));
  document.querySelector("#viewHeartReadingButton").addEventListener("click", () => openHeavenReading("heart"));
  document.querySelector("#restartHeartButton").addEventListener("click", resetHeartRitual);
  document.querySelector("#closeHeavenReadingDialog").addEventListener("click", () => elements.heavenReadingDialog.close());
  elements.heavenReadingDialog.addEventListener("close", stopHeavenReadingAnimation);
  document.querySelectorAll("[data-heaven-reading-tab]").forEach((button) => {
    button.addEventListener("click", () => selectHeavenReadingTab(button.dataset.heavenReadingTab));
  });
  document.querySelector("#heavenReadingHistoryList").addEventListener("click", handleHeavenHistorySelection);
  document.querySelector("#heavenReadingHistoryDetail").addEventListener("click", handleHeavenHistoryAction);
  document.querySelectorAll("[data-heart-return]").forEach((button) => {
    button.addEventListener("click", resetHeartRitual);
  });
  document.querySelector("#adminSectionSelect").addEventListener("change", (event) => selectAdminPanel(event.target.value));
  document.querySelector("#systemMarketForm").addEventListener("submit", saveMarketSettings);
  document.querySelector("#systemModelsForm").addEventListener("submit", saveModelPool);
  document.querySelector("#membershipSettingsForm").addEventListener("submit", saveMembershipSettings);
  document.querySelector("#addPlatformModel").addEventListener("click", addPlatformModel);
  document.querySelector("#adminRefreshButton").addEventListener("click", startAdminRefresh);
  window.addEventListener("resize", redrawThemeSensitiveVisuals);
  initializeAutoTableSorting();
}

async function loadDashboard(force = false, background = false, showOverlay = true) {
  const requestedDate = elements.tradeDate.value;
  if (state.dashboardLoading && state.dashboardRequestDate === requestedDate) return;
  state.dashboardLoading = true;
  state.dashboardRequestDate = requestedDate;
  const requestSequence = ++state.dashboardRequestSequence;
  if (force) stockPreviewCache.clear();
  if (!background && showOverlay) {
    setLoading(true, "正在加载市场数据");
    setStatus("正在加载市场数据");
  } else if (!background) {
    setStatus("正在刷新行情");
  }
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    if (force) query.set("force", "1");
    const payload = await apiRequest(`/api/dashboard?${query}`);
    if (
      requestSequence !== state.dashboardRequestSequence
      || requestedDate !== elements.tradeDate.value
    ) return;
    applyDashboard(payload, background);
  } catch (error) {
    if (background) {
      setStatus("实时刷新暂时中断，正在等待重试");
    } else {
      showToast(error.message || "无法连接本地服务");
      setStatus("加载失败");
    }
  } finally {
    if (requestSequence === state.dashboardRequestSequence) {
      state.dashboardLoading = false;
      state.dashboardRequestDate = "";
      if (!background && showOverlay) setLoading(false);
      updateDateButtons();
    }
  }
}

async function startAdminRefresh() {
  const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean);
  buttons.forEach((button) => { button.disabled = true; });
  try {
    const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: elements.tradeDate.value });
    showToast(payload.message || "后台刷新已提交");
    setStatus("后台刷新运行中，当前页面保持不变");
  } catch (error) {
    showToast(error.message || "后台刷新启动失败");
  } finally {
    buttons.forEach((button) => { button.disabled = false; });
  }
}

function applyDashboard(payload, background = false) {
  state.dashboard = payload;
  const selectedDate = payload.meta.requested_date || payload.meta.trade_date;
  elements.tradeDate.value = selectedDate;
  document.querySelector("#qiObservationDate").value = selectedDate;
  document.querySelector("#journalDate").value = selectedDate;
  renderDashboard();
  setStatus(`${dashboardSourceLabel(payload.meta)} · 数据已更新`);
  if (!background) {
    if (state.activeView === "dragonView") loadDragonTiger();
    if (state.activeView === "screenerView") loadScreenerSetup();
    if (state.activeView === "screenerTrackingView") loadScreenerTracking(true);
    if (state.activeView === "mentorView") loadMentorSetup(true);
    if (state.activeView === "heavenView") loadHeavenSetup(true);
    if (state.activeView === "sentimentCycleView") loadSentimentHistory(true);
    if (state.activeView === "rotationView") loadRotationHistory(true);
    if (state.activeView === "auctionView") loadAuctionCenter(true);
    if (state.activeView === "themeLibraryView") loadThemeLibrary(true);
    if (state.activeView === "popularityView") loadPopularity(true);
  }
  const requestedStock = new URLSearchParams(window.location.search).get("stock");
  if (!state.initialStockOpened && /^\d{6}$/.test(requestedStock || "")) {
    state.initialStockOpened = true;
    openStock(requestedStock);
  }
}

function dashboardSourceLabel(meta = {}) {
  if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "盘中行情";
  if (meta.carried_forward) return "最近收盘行情";
  if (meta.market_status === "historical") return "历史行情";
  return "收盘行情";
}

function renderDashboard() {
  const { meta, overview, ladders, sectors } = state.dashboard;
  animateMetric("tapeUp", overview.up_count, (value) => Math.round(value));
  animateMetric("tapeDown", overview.down_count, (value) => Math.round(value));
  setText("tapeLimit", `${overview.limit_up_count} / 跌停 ${overview.limit_down_count}`);
  animateMetric("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
  animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)} 家`);
  animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)} 家`);
  animateMetric("brokenMetric", overview.broken_count, (value) => `${Math.round(value)} 家`);
  animateMetric("sealRateMetric", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`);
  animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
  setText("dataDateMetric", dashboardDataTimestamp(meta));
  animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value));
  setText("sentimentText", sentimentLabel(overview.sentiment_score));
  updateSentimentGauge(overview.sentiment_score);
  setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`);

  renderLimitTable();
  renderLadderMini(ladders || []);
  renderSectorMini(sectors || []);
  renderBrokenTable(state.dashboard.broken || []);
  renderDownTable(state.dashboard.down_limits || []);
  renderYesterdayTable(state.dashboard.yesterday_limits || []);
  renderPerformance(state.dashboard.limit_performance || []);
  renderLadderBoard(ladders || []);
  renderRotationMembers();
}

async function loadSentimentHistory(force = false) {
  if (!state.dashboard || state.sentimentLoading) return;
  const key = `${elements.tradeDate.value}:${state.sentimentRange}`;
  if (!force && state.sentimentHistoryKey === key && state.sentimentHistory) {
    renderSentimentHistory();
    return;
  }
  state.sentimentLoading = true;
  const notice = document.querySelector("#sentimentHistoryNotice");
  notice.hidden = true;
  try {
    const query = new URLSearchParams({
      trade_date: elements.tradeDate.value,
      limit: String(state.sentimentRange),
    });
    state.sentimentHistory = await apiRequest(`/api/sentiment/history?${query}`);
    state.sentimentHistoryKey = key;
    renderSentimentHistory();
  } catch (error) {
    notice.textContent = error.message || "情绪周期数据加载失败";
    notice.hidden = false;
    showToast(notice.textContent);
  } finally {
    state.sentimentLoading = false;
  }
}

function renderSentimentHistory() {
  const payload = state.sentimentHistory;
  if (!payload) return;
  const rows = payload.rows || [];
  const latest = rows[rows.length - 1];
  const body = document.querySelector("#sentimentHistoryBody");
  const empty = document.querySelector("#sentimentHistoryEmpty");
  empty.hidden = rows.length > 0;
  body.innerHTML = [...rows].reverse().map((row) => {
    return `
      <tr class="${row.trade_date === latest?.trade_date ? "latest-row" : ""}">
        <td class="sentiment-date-cell">${escapeHtml(displayCompactDate(row.trade_date))}</td>
        <td class="number sentiment-score-cell ${sentimentScoreClass(row.score)}">${number(row.score)}</td>
        <td><span class="sentiment-phase-badge ${sentimentPhaseClass(row.phase)}">${escapeHtml(row.phase)}</span></td>
        <td><span class="sentiment-direction ${trendClass(row.direction)}">${escapeHtml(row.direction)}</span></td>
        <td class="number">${number(row.limit_up_count)}</td>
        <td class="number">${number(row.first_board_count)}</td>
        <td class="number">${number(row.second_board_count)}</td>
        <td class="number">${number(row.three_plus_count)}</td>
        <td class="number">${number(row.max_height)}板</td>
        <td class="number">${number(row.broken_count)}</td>
        <td class="number">${number(row.limit_down_count)}</td>
        <td class="number">${number(row.previous_limit_count)}</td>
        <td class="number">${number(row.previous_positive_count)}</td>
        <td class="number">${formatNumber(row.previous_positive_rate, 1)}%</td>
      </tr>
    `;
  }).join("");

  if (!latest) {
    setText("sentimentHistoryDateRange", "暂无历史数据");
    return;
  }
  setText(
    "sentimentHistoryDateRange",
    `${displayCompactDate(rows[0].trade_date)} 至 ${displayCompactDate(latest.trade_date)}`,
  );
  setText("sentimentCycleScore", number(latest.score));
  setText("sentimentCycleLabel", latest.label);
  setText("sentimentCycleDate", displayCompactDate(latest.trade_date));
  setText("sentimentCyclePhase", latest.phase);
  setText("sentimentCycleDirection", latest.direction);
  const dayChange = number(latest.day_change);
  const confidence = sentimentPhaseConfidence(latest);
  setText("sentimentPhaseConfidence", `置信度 ${confidence}%`);
  setText("sentimentDayChange", `${dayChange > 0 ? "+" : ""}${formatNumber(dayChange, 1)}`);
  setText("sentimentSealRate", `${formatNumber(latest.seal_rate, 1)}%`);
  setText("sentimentLimitUp", number(latest.limit_up_count));
  setText("sentimentBroken", number(latest.broken_count));
  setText("sentimentPhaseAdvice", sentimentPhaseAdvice(latest.phase));
  setText("sentimentCurrentTag", `当前 ${number(latest.score)} · ${latest.phase}`);
  setText("sentimentComponentSummary", `五维加权 → 温度 ${number(latest.score)}`);
  setText("sentimentPeriodNote", `近 ${state.sentimentRange} 个交易日，当前展示 ${rows.length} 日`);
  const changeElement = document.querySelector("#sentimentDayChange");
  changeElement.className = changeClass(dayChange);
  setText("sentimentPreviousPositive", `${number(latest.previous_positive_count)} / ${number(latest.previous_limit_count)} 只`);
  setText("sentimentPreviousAverage", `红盘率 ${formatNumber(latest.previous_positive_rate, 1)}% · 平均 ${signed(latest.average_previous_change)}%`);
  setText("sentimentHistoryDays", `${number(payload.available_days)} 个交易日`);
  setText("sentimentNormalization", `${latest.normalization} · 当前展示 ${rows.length} 日`);
  const marker = document.querySelector("#sentimentCycleScoreMarker");
  marker.className = `sentiment-current-phase-badge ${sentimentPhaseClass(latest.phase)}`;
  document.querySelector("#sentimentComponentList").innerHTML = Object.values(latest.components || {}).map((item) => `
    <article class="sentiment-component-item">
      <div class="sentiment-component-main">
        <strong>${escapeHtml(item.label)}</strong>
        <div class="sentiment-component-track" aria-hidden="true"><i data-component-score="${clamp(item.score, 0, 100)}" style="width:0%"></i></div>
        <b>${formatNumber(item.score, 1)} <em>× ${number(item.weight)}%</em></b>
      </div>
      <small>${escapeHtml(item.summary)}</small>
    </article>
  `).join("");
  requestAnimationFrame(() => {
    animateSentimentComponents();
    animateSentimentTrendChart(rows);
    bindSentimentChartTooltip(rows);
  });
  animateRows(body);
}

function animateSentimentComponents() {
  document.querySelectorAll("#sentimentComponentList [data-component-score]").forEach((bar, index) => {
    const width = `${number(bar.dataset.componentScore)}%`;
    if (!motionEnabled()) {
      bar.style.width = width;
      return;
    }
    setTimeout(() => { bar.style.width = width; }, index * 70);
  });
}

function animateSentimentTrendChart(rows) {
  if (sentimentChartAnimationFrame) cancelAnimationFrame(sentimentChartAnimationFrame);
  if (!motionEnabled()) {
    drawSentimentTrendChart(rows, 1);
    return;
  }
  const startedAt = performance.now();
  const duration = 780;
  const frame = (now) => {
    const rawProgress = Math.min(1, (now - startedAt) / duration);
    const progress = 1 - (1 - rawProgress) ** 3;
    drawSentimentTrendChart(rows, progress);
    if (rawProgress < 1) sentimentChartAnimationFrame = requestAnimationFrame(frame);
    else sentimentChartAnimationFrame = null;
  };
  sentimentChartAnimationFrame = requestAnimationFrame(frame);
}

function drawSentimentTrendChart(rows, progress = 1) {
  const canvas = document.querySelector("#sentimentTrendChart");
  if (!canvas || !rows.length || state.activeView !== "sentimentCycleView") return;
  const rect = canvas.getBoundingClientRect();
  if (!rect.width) return;
  const width = Math.max(320, rect.width);
  const height = Math.max(220, rect.height);
  const ratio = window.devicePixelRatio || 1;
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  const padding = { top: 18, right: 18, bottom: 34, left: 42 };
  const chartWidth = width - padding.left - padding.right;
  const chartHeight = height - padding.top - padding.bottom;
  const x = (index) => padding.left + (rows.length === 1 ? chartWidth / 2 : index / (rows.length - 1) * chartWidth);
  const y = (score) => padding.top + (100 - clamp(score, 0, 100)) / 100 * chartHeight;

  context.font = '10px "Microsoft YaHei UI", sans-serif';
  context.textAlign = "right";
  context.textBaseline = "middle";
  for (let score = 0; score <= 100; score += 20) {
    const lineY = y(score);
    context.strokeStyle = score === 40 || score === 80 ? palette.zero : palette.grid;
    context.lineWidth = 1;
    context.beginPath();
    context.moveTo(padding.left, lineY);
    context.lineTo(width - padding.right, lineY);
    context.stroke();
    context.fillStyle = palette.axis;
    context.fillText(String(score), padding.left - 8, lineY);
  }

  context.save();
  context.beginPath();
  context.rect(padding.left - 6, padding.top - 8, (chartWidth + 12) * clamp(progress, 0, 1), chartHeight + 18);
  context.clip();

  const finalPhase = rows[rows.length - 1]?.phase;
  let phaseStart = rows.length - 1;
  while (phaseStart > 0 && rows[phaseStart - 1]?.phase === finalPhase) phaseStart -= 1;
  if (["退潮", "冰点"].includes(finalPhase)) {
    const startX = phaseStart === 0 ? padding.left : (x(phaseStart - 1) + x(phaseStart)) / 2;
    context.fillStyle = palette.alertArea;
    context.fillRect(startX, padding.top, width - padding.right - startX, chartHeight);
    context.fillStyle = palette.up;
    context.font = '10px "Microsoft YaHei UI", sans-serif';
    context.textAlign = "center";
    context.textBaseline = "top";
    context.fillText(finalPhase, (startX + width - padding.right) / 2, padding.top + 4);
  }

  const movingAverage = rows.map((_row, index) => {
    const start = Math.max(0, index - 4);
    const sample = rows.slice(start, index + 1);
    return sample.reduce((sum, item) => sum + number(item.score), 0) / sample.length;
  });
  context.beginPath();
  movingAverage.forEach((score, index) => {
    if (index === 0) context.moveTo(x(index), y(score));
    else context.lineTo(x(index), y(score));
  });
  context.strokeStyle = palette.movingAverage;
  context.lineWidth = 1.5;
  context.setLineDash([5, 4]);
  context.stroke();
  context.setLineDash([]);

  context.beginPath();
  rows.forEach((row, index) => {
    const pointX = x(index);
    const pointY = y(row.score);
    if (index === 0) context.moveTo(pointX, pointY);
    else context.lineTo(pointX, pointY);
  });
  context.lineTo(x(rows.length - 1), padding.top + chartHeight);
  context.lineTo(x(0), padding.top + chartHeight);
  context.closePath();
  context.fillStyle = palette.area;
  context.fill();

  context.beginPath();
  rows.forEach((row, index) => {
    const pointX = x(index);
    const pointY = y(row.score);
    if (index === 0) context.moveTo(pointX, pointY);
    else context.lineTo(pointX, pointY);
  });
  context.strokeStyle = palette.line;
  context.lineWidth = 2.5;
  context.lineJoin = "round";
  context.lineCap = "round";
  context.stroke();

  rows.forEach((row, index) => {
    context.beginPath();
    context.arc(x(index), y(row.score), index === rows.length - 1 ? 4.5 : 3, 0, Math.PI * 2);
    context.fillStyle = ["退潮", "冰点"].includes(row.phase) ? palette.up : row.phase === "修复" ? palette.repair : palette.line;
    context.fill();
    context.strokeStyle = palette.background;
    context.lineWidth = 1.5;
    context.stroke();
  });
  context.restore();

  const labelStep = Math.max(1, Math.ceil(rows.length / 6));
  context.textAlign = "center";
  context.textBaseline = "top";
  context.fillStyle = palette.axis;
  rows.forEach((row, index) => {
    if (index % labelStep !== 0 && index !== rows.length - 1) return;
    const dateText = displayCompactDate(row.trade_date).slice(5);
    context.fillText(dateText, x(index), height - padding.bottom + 10);
  });
}

function bindSentimentChartTooltip(rows) {
  const canvas = document.querySelector("#sentimentTrendChart");
  const tooltip = document.querySelector("#sentimentChartTooltip");
  if (!canvas || !tooltip || !rows.length) return;
  canvas.onmousemove = (event) => {
    const rect = canvas.getBoundingClientRect();
    const padding = { left: 42, right: 18 };
    const chartWidth = Math.max(1, rect.width - padding.left - padding.right);
    const relativeX = clamp(event.clientX - rect.left - padding.left, 0, chartWidth);
    const index = rows.length === 1 ? 0 : Math.round(relativeX / chartWidth * (rows.length - 1));
    const row = rows[index];
    tooltip.innerHTML = `${escapeHtml(displayCompactDate(row.trade_date))} · 温度 <b>${number(row.score)}</b> · ${escapeHtml(row.phase)}`;
    tooltip.hidden = false;
    const targetLeft = padding.left + (rows.length === 1 ? chartWidth / 2 : index / (rows.length - 1) * chartWidth);
    tooltip.style.left = `${clamp(targetLeft + 10, 8, rect.width - tooltip.offsetWidth - 8)}px`;
    tooltip.style.top = `${clamp(event.clientY - rect.top - 34, 8, rect.height - 34)}px`;
  };
  canvas.onmouseleave = () => { tooltip.hidden = true; };
}

function sentimentScoreClass(score) {
  const value = number(score);
  return value >= 60 ? "score-strong" : value < 40 ? "score-weak" : "score-neutral";
}

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

function sentimentPhaseConfidence(row) {
  const explicit = number(row?.confidence || row?.phase_confidence);
  if (explicit > 0) return Math.round(clamp(explicit, 0, 100));
  const historyEvidence = Math.min(12, number(row?.history_days) * 0.6);
  const movementEvidence = Math.min(18, Math.abs(number(row?.day_change)) * 0.8);
  return Math.round(clamp(62 + historyEvidence + movementEvidence, 60, 92));
}

function sentimentPhaseAdvice(phase) {
  return {
    "冰点": "情绪处于极弱区，先观察风险释放，允许没有候选结果。",
    "修复": "风险开始收敛，关注率先转强的核心，小仓验证修复强度。",
    "发酵": "主线与梯队正在形成，优先跟随核心，避免偏离主线。",
    "高潮": "情绪与一致性已处高位，聚焦核心并主动降低后排暴露。",
    "分化": "强弱开始分层，关注承接与回流，淘汰失去辨识度的方向。",
    "退潮": "情绪指标继续走弱。",
  }[phase] || "市场结构尚未形成清晰阶段，保持观察并等待确认。";
}

function getVisibleStocks() {
  if (!state.dashboard) return [];
  let rows = [...(state.dashboard.limits || [])];
  if (state.filter === "1") rows = rows.filter((row) => number(row.streak) === 1);
  if (state.filter === "2") rows = rows.filter((row) => number(row.streak) === 2);
  if (state.filter === "3") rows = rows.filter((row) => number(row.streak) >= 3);
  if (state.query) {
    rows = rows.filter((row) => {
      const haystack = `${row.code} ${row.name} ${row.sector} ${row.reason}`.toLowerCase();
      return haystack.includes(state.query);
    });
  }
  return rows.sort((left, right) => compareRows(left, right));
}

function renderLimitTable() {
  if (!state.dashboard) return;
  const rows = getVisibleStocks();
  const allRows = state.dashboard.limits || [];
  const body = document.querySelector("#limitTableBody");
  body.innerHTML = rows.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}">
      <td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num"><span class="pool-streak-tag tag red">${streakLabel(row.streak)}</span></td>
      <td class="number num up">${signed(row.change)}</td>
      <td class="number num">${formatNumber(row.price, 2)}</td>
      <td>${escapeHtml(row.sector || "其他")}</td>
      <td class="number num muted">${escapeHtml(row.first_time || "")}</td>
      <td class="number num muted">${escapeHtml(row.last_time || "")}</td>
      <td class="number num">${limitOpenState(row)}</td>
      <td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
      <td class="number num">${formatNumber(row.amount_billion, 2)}</td>
      <td class="number num">${formatLimitSealAmount(row.seal_amount_million)}</td>
      <td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
    </tr>
  `).join("");
  bindStockRows(body);
  setText("resultCount", `${rows.length} 只`);
  setText("limitPoolSubtitle", `${allRows.length} 只 · 数据日期 ${displayCompactDate(state.dashboard.meta?.trade_date || elements.tradeDate.value)}`);
  setText("limitAllCount", allRows.length);
  setText("limitFirstCount", allRows.filter((row) => number(row.streak) === 1).length);
  setText("limitSecondCount", allRows.filter((row) => number(row.streak) === 2).length);
  setText("limitThreePlusCount", allRows.filter((row) => number(row.streak) >= 3).length);
  document.querySelector("#emptyState").hidden = rows.length !== 0;
  updateSortHeaders();
}

function limitOpenState(row) {
  const openTimes = number(row.open_times);
  const firstTime = String(row.first_time || "");
  if (firstTime.startsWith("09:25") && openTimes === 0) return '<span class="pool-state-tag one-word">一字</span>';
  if (openTimes >= 6) return `<span class="pool-state-tag broken">烂板×${openTimes}</span>`;
  return String(openTimes);
}

function formatLimitSealAmount(value) {
  const amount = number(value);
  if (!amount) return "";
  return Math.round(amount).toLocaleString("zh-CN");
}

function renderBrokenTable(rows) {
  const visibleRows = getVisibleBrokenRows(rows);
  setText("brokenCount", `${rows.length} 只`);
  setText("brokenMeta", ` · 触及涨停后未能封住 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
  const body = document.querySelector("#brokenTableBody");
  body.innerHTML = visibleRows.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}">
      <td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num ${changeClass(row.change)}" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
      <td class="number num broken-limit-gap" data-sort-value="${row.limitGap}">${formatNumber(row.limitGap, 2)}</td>
      <td class="number num">${formatNumber(row.price, 2)}</td>
      <td>${escapeHtml(row.sector || "其他")}</td>
      <td class="number num muted">${escapeHtml(row.first_time || "")}</td>
      <td class="number num" data-sort-value="${number(row.open_times)}">${brokenOpenState(row)}</td>
      <td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
      <td class="number num">${formatNumber(row.amount_billion, 2)}</td>
      <td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
    </tr>
  `).join("");
  bindStockRows(body);
  document.querySelector("#brokenEmptyState").hidden = visibleRows.length !== 0;
  updateBrokenSortHeaders();
}

function getVisibleBrokenRows(rows = state.dashboard?.broken || []) {
  let visibleRows = rows.map((row) => ({ ...row, limitGap: brokenLimitGap(row) }));
  if (state.brokenQuery) {
    visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.brokenQuery));
  }
  if (!state.brokenSortKey) return visibleRows;
  return visibleRows.sort((left, right) => {
    const result = number(left[state.brokenSortKey]) - number(right[state.brokenSortKey]);
    return state.brokenSortDirection === "asc" ? result : -result;
  });
}

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

function brokenLimitGap(row) {
  return Math.max(0, brokenLimitRate(row) - number(row.change));
}

function brokenOpenState(row) {
  const openTimes = number(row.open_times);
  return openTimes >= 6
    ? `<span class="broken-repeat-tag">反复炸 ×${openTimes}</span>`
    : String(openTimes);
}

function changeBrokenSort(key) {
  if (state.brokenSortKey === key) state.brokenSortDirection = state.brokenSortDirection === "asc" ? "desc" : "asc";
  else {
    state.brokenSortKey = key;
    state.brokenSortDirection = "desc";
  }
  renderBrokenTable(state.dashboard?.broken || []);
}

function updateBrokenSortHeaders() {
  document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
    header.classList.remove("sort-asc", "sort-desc", "sorted");
    header.setAttribute("aria-sort", "none");
    if (header.dataset.brokenSort === state.brokenSortKey) {
      header.classList.add(state.brokenSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
      header.setAttribute("aria-sort", state.brokenSortDirection === "asc" ? "ascending" : "descending");
    }
    const arrow = header.querySelector(".arr");
    if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.brokenSortDirection === "asc" ? "▲" : "▼") : "↕";
  });
}

function renderDownTable(rows) {
  const visibleRows = getVisibleDownRows(rows);
  setText("downCount", `${rows.length} 只`);
  setText("downMeta", ` · 观察退潮、高位风险与亏钱效应 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
  renderDownSectorCluster(rows);
  const body = document.querySelector("#downTableBody");
  body.innerHTML = visibleRows.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}">
      <td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num down" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
      <td class="number num">${formatNumber(row.price, 2)}</td>
      <td>${escapeHtml(row.sector || "其他")}</td>
      <td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
      <td class="number num">${formatNumber(row.amount_billion, 2)}</td>
      <td class="number num">${number(row.streak) > 0 ? number(row.streak) : ""}</td>
      <td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
    </tr>
  `).join("");
  bindStockRows(body);
  document.querySelector("#downEmptyState").hidden = visibleRows.length !== 0;
  updateDownSortHeaders();
}

function getVisibleDownRows(rows = state.dashboard?.down_limits || []) {
  let visibleRows = [...rows];
  if (state.downQuery) {
    visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.downQuery));
  }
  if (!state.downSortKey) return visibleRows;
  return visibleRows.sort((left, right) => {
    const result = number(left[state.downSortKey]) - number(right[state.downSortKey]);
    return state.downSortDirection === "asc" ? result : -result;
  });
}

function renderDownSectorCluster(rows) {
  const counts = new Map();
  rows.forEach((row) => {
    const sector = String(row.sector || "其他").trim() || "其他";
    if (sector === "其他") return;
    counts.set(sector, (counts.get(sector) || 0) + 1);
  });
  const cluster = [...counts.entries()].sort((left, right) => right[1] - left[1])[0];
  const element = document.querySelector("#downSectorCluster");
  element.hidden = !cluster || cluster[1] < 2;
  element.textContent = cluster && cluster[1] >= 2 ? `${cluster[0]}集中跌停 ×${cluster[1]}` : "";
}

function changeDownSort(key) {
  if (state.downSortKey === key) state.downSortDirection = state.downSortDirection === "asc" ? "desc" : "asc";
  else {
    state.downSortKey = key;
    state.downSortDirection = "asc";
  }
  renderDownTable(state.dashboard?.down_limits || []);
}

function updateDownSortHeaders() {
  document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
    header.classList.remove("sort-asc", "sort-desc", "sorted");
    header.setAttribute("aria-sort", "none");
    if (header.dataset.downSort === state.downSortKey) {
      header.classList.add(state.downSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
      header.setAttribute("aria-sort", state.downSortDirection === "asc" ? "ascending" : "descending");
    }
    const arrow = header.querySelector(".arr");
    if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.downSortDirection === "asc" ? "▲" : "▼") : "↕";
  });
}

function renderYesterdayTable(rows) {
  const visibleRows = getVisibleYesterdayRows(rows);
  const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
  const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
  setText("yesterdayCount", `${rows.length} 只`);
  setText("yesterdayMeta", ` · 昨日 ${previousDate} → 今日 ${currentDate}`);
  renderYesterdaySummary(rows);
  const body = document.querySelector("#yesterdayTableBody");
  body.innerHTML = visibleRows.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}">
      <td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num">${number(row.prior_streak)}</td>
      <td class="number num ${changeClass(row.current_change)}">${signed(row.current_change)}</td>
      <td><span class="yesterday-outcome-tag ${yesterdayOutcomeClass(row.outcome)}">${escapeHtml(row.outcome)}</span></td>
      <td class="number num">${number(row.current_streak) ? `<span class="yesterday-height-tag">${number(row.current_streak)}</span>` : ""}</td>
      <td>${escapeHtml(row.sector || "其他")}</td>
      <td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
    </tr>
  `).join("");
  bindStockRows(body);
  document.querySelector("#yesterdayEmptyState").hidden = visibleRows.length !== 0;
  updateYesterdayControls();
}

function getVisibleYesterdayRows(rows = state.dashboard?.yesterday_limits || []) {
  let visibleRows = rows.filter((row) => {
    if (state.yesterdayFilter === "advance") return row.outcome === "晋级";
    if (state.yesterdayFilter === "positive") return number(row.current_change) > 0;
    if (state.yesterdayFilter === "fail") return row.outcome === "断板";
    if (state.yesterdayFilter === "risk") return ["炸板", "跌停"].includes(row.outcome);
    return true;
  });
  if (state.yesterdayQuery) {
    visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.yesterdayQuery));
  }
  if (!state.yesterdaySortKey) return visibleRows;
  return visibleRows.sort((left, right) => {
    const result = number(left[state.yesterdaySortKey]) - number(right[state.yesterdaySortKey]);
    return state.yesterdaySortDirection === "asc" ? result : -result;
  });
}

function renderYesterdaySummary(rows) {
  const total = rows.length;
  const advance = rows.filter((row) => row.outcome === "晋级").length;
  const positive = rows.filter((row) => number(row.current_change) > 0).length;
  const fail = rows.filter((row) => row.outcome === "断板").length;
  const risk = rows.filter((row) => ["炸板", "跌停"].includes(row.outcome)).length;
  const rate = (value) => total ? value / total * 100 : 0;
  setText("yesterdayAllCount", total);
  setText("yesterdayAdvanceCount", advance);
  setText("yesterdayAdvanceRate", `晋级率 ${formatNumber(rate(advance), 1)}%`);
  setText("yesterdayPositiveCount", positive);
  setText("yesterdayPositiveRate", `兑现率 ${formatNumber(rate(positive), 1)}%`);
  setText("yesterdayFailCount", fail);
  setText("yesterdayFailRate", `占 ${formatNumber(rate(fail), 1)}%`);
  setText("yesterdayRiskCount", risk);
  setText("yesterdayRiskRate", `亏钱效应 ${formatNumber(rate(risk), 1)}%`);
}

function yesterdayOutcomeClass(outcome) {
  return { "晋级": "advance", "断板": "fail", "炸板": "broken", "跌停": "down" }[outcome] || "fail";
}

function changeYesterdaySort(key) {
  if (state.yesterdaySortKey === key) state.yesterdaySortDirection = state.yesterdaySortDirection === "asc" ? "desc" : "asc";
  else {
    state.yesterdaySortKey = key;
    state.yesterdaySortDirection = "desc";
  }
  renderYesterdayTable(state.dashboard?.yesterday_limits || []);
}

function updateYesterdayControls() {
  document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
    const active = button.dataset.yesterdayFilter === state.yesterdayFilter;
    button.classList.toggle("active", active);
    button.setAttribute("aria-pressed", String(active));
  });
  document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
    header.classList.remove("sort-asc", "sort-desc", "sorted");
    header.setAttribute("aria-sort", "none");
    if (header.dataset.yesterdaySort === state.yesterdaySortKey) {
      header.classList.add(state.yesterdaySortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
      header.setAttribute("aria-sort", state.yesterdaySortDirection === "asc" ? "ascending" : "descending");
    }
    const arrow = header.querySelector(".arr");
    if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.yesterdaySortDirection === "asc" ? "▲" : "▼") : "↕";
  });
}

function renderPerformance(rows) {
  rows = normalizePerformanceRows(rows);
  const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
  const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
  setText("performanceDateRange", `昨日 ${previousDate} → 今日 ${currentDate}`);
  document.querySelector("#performanceCards").innerHTML = rows.map((row) => `
    <article class="performance-stage-card" title="收红 ${formatNumber(row.positive_rate, 1)}% · 平均涨幅 ${signed(row.average_change)}%"
      aria-label="${escapeHtml(row.label)}，晋级率 ${formatNumber(row.advance_rate, 1)}%，晋级 ${number(row.advanced)} 只，共 ${number(row.count)} 只，收红率 ${formatNumber(row.positive_rate, 1)}%，平均涨幅 ${signed(row.average_change)}%">
      <div class="performance-stage-label"><span>${escapeHtml(row.label)} → 今日</span><i class="performance-status-tag ${performanceRateState(row.advance_rate).className}">${performanceRateState(row.advance_rate).label}</i></div>
      <strong class="performance-stage-rate ${performanceRateState(row.advance_rate).className}">${formatNumber(row.advance_rate, 1)}%</strong>
      <span class="performance-stage-count">晋级 ${number(row.advanced)} / 共 ${number(row.count)} 只</span>
      <div class="performance-stage-track" aria-hidden="true"><i class="${performanceRateState(row.advance_rate).className}" style="width:${Math.max(number(row.advance_rate), number(row.advance_rate) > 0 ? 2 : 0)}%"></i></div>
    </article>
  `).join("") || '<div class="performance-empty-state">暂无昨日涨停统计</div>';
  renderPerformanceConclusion(rows);
  renderMarketBreadth(state.dashboard?.overview || {});
}

function normalizePerformanceRows(rows) {
  const groups = new Map();
  (rows || []).forEach((row) => {
    const level = Math.max(1, number(row.level));
    const displayLevel = Math.min(level, 5);
    const group = groups.get(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.set(displayLevel, group);
  });
  return [...groups.values()]
    .sort((left, right) => right.level - left.level)
    .map((group) => ({
      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 performanceRateState(rate) {
  const value = number(rate);
  if (value === 0) return { label: "失效", className: "is-neutral" };
  if (value < 20) return { label: "危险", className: "is-warning" };
  return { label: "活跃", className: "is-active" };
}

function renderPerformanceConclusion(rows) {
  const container = document.querySelector("#performanceConclusion");
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">暂无昨日梯队数据，暂不生成结论</div>';
    return;
  }
  const sorted = [...rows].sort((left, right) => number(right.level) - number(left.level));
  const highRows = sorted.filter((row) => number(row.level) >= 4);
  const highAdvanced = highRows.reduce((total, row) => total + number(row.advanced), 0);
  const highSamples = highRows.map((row) => escapeHtml(row.label)).join("、");
  const strongest = [...rows].sort((left, right) => (
    number(right.advance_rate) - number(left.advance_rate) || number(right.level) - number(left.level)
  ))[0];
  const firstBoard = rows.find((row) => number(row.level) === 1);
  const overview = state.dashboard?.overview || {};
  const phase = overview.sentiment_phase || "观察";
  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 highText = highRows.length
    ? `高位晋级率<b class="${highAdvanced ? "up" : "is-neutral"}">${highAdvanced ? "仍有承接" : "全线失效"}</b>：${highSamples}${highAdvanced ? `共晋级 ${highAdvanced} 只` : "今日均未晋级"}；`
    : "高位梯队暂无昨日样本，空间信号仍待确认；";
  const strongestText = strongest
    ? `<b>${escapeHtml(strongest.label)}</b>晋级率最高，为 <b class="up">${formatNumber(strongest.advance_rate, 1)}%</b>（${number(strongest.advanced)} 只晋级 / 共 ${number(strongest.count)} 只）；`
    : "暂无相对占优梯队；";
  const firstBoardText = firstBoard
    ? `首板基数 ${number(firstBoard.count)} 只，晋级率 <b class="${performanceRateState(firstBoard.advance_rate).className}">${formatNumber(firstBoard.advance_rate, 1)}%</b>，低位接力${number(firstBoard.advance_rate) < 20 ? "胜率偏低" : "仍有活跃度"}；`
    : "首板梯队暂无有效样本；";
  container.innerHTML = `
    <div>· ${highText}</div>
    <div>· ${strongestText}</div>
    <div>· ${firstBoardText}</div>
    <div>· 结论：<b>${stance}</b>，当前情绪周期「${escapeHtml(phase)}」。</div>
  `;
}

function renderMarketBreadth(overview) {
  const up = number(overview.up_count);
  const down = number(overview.down_count);
  const flat = Math.max(0, number(overview.flat_count));
  const total = Math.max(1, up + down + flat);
  const upRate = up / total * 100;
  const flatRate = flat / total * 100;
  const downRate = down / total * 100;
  const panel = document.querySelector(".market-breadth-panel");
  panel.classList.remove("breadth-enter");
  void panel.offsetWidth;
  panel.classList.add("breadth-enter");
  setText("breadthDataTime", dashboardDataTimestamp(state.dashboard?.meta || {}));
  animateMetric("breadthRatio", upRate, (value) => `${formatNumber(value, 1)}%`);
  animateMetric("breadthUpCount", up, (value) => formatNumber(Math.round(value)));
  animateMetric("breadthDownCount", down, (value) => formatNumber(Math.round(value)));
  setText("breadthUpLegend", `${formatNumber(up)}（${formatNumber(upRate, 1)}%）`);
  setText("breadthFlatLegend", `${formatNumber(flat)}（${formatNumber(flatRate, 1)}%）`);
  setText("breadthDownLegend", `${formatNumber(down)}（${formatNumber(downRate, 1)}%）`);
  document.querySelector("#breadthFlatLegendItem").hidden = flat === 0;
  const limitUp = number(overview.limit_up_count);
  const limitDown = number(overview.limit_down_count);
  const breadthLabel = upRate < 20 ? "宽度极差" : upRate < 40 ? "宽度偏弱" : upRate < 55 ? "宽度均衡" : "宽度偏强";
  setText("breadthWarning", `△ ${breadthLabel}，涨跌停 ${limitUp}:${limitDown}`);
  const bars = [
    ["breadthUpBar", upRate],
    ["breadthFlatBar", flatRate],
    ["breadthDownBar", downRate],
  ];
  bars.forEach(([id, width]) => {
    const bar = document.getElementById(id);
    const targetWidth = `${Math.max(width, width > 0 ? 0.8 : 0)}%`;
    bar.style.transition = "none";
    bar.style.width = "0%";
    requestAnimationFrame(() => requestAnimationFrame(() => {
      bar.style.transition = "width 760ms var(--ease-out)";
      bar.style.width = targetWidth;
    }));
    bar.title = `${formatNumber(width, 1)}%`;
  });
}

async function loadRotationHistory(force = false) {
  if (!state.dashboard || state.rotationLoading) return;
  const key = `${elements.tradeDate.value}:9`;
  if (!force && state.rotationHistoryKey === key && state.rotationHistory) {
    renderRotationHistory();
    return;
  }
  state.rotationLoading = true;
  const container = document.querySelector("#rotationHistory");
  renderEmptyState(container, "正在读取轮动历史");
  try {
    const query = new URLSearchParams({
      trade_date: elements.tradeDate.value,
    });
    state.rotationHistory = await apiRequest(`/api/rotation/history?${query}`);
    state.rotationHistoryKey = key;
    renderRotationHistory();
  } catch (error) {
    renderEmptyState(container, error.message || "轮动历史加载失败");
    showToast(error.message || "轮动历史加载失败");
  } finally {
    state.rotationLoading = false;
  }
}

function renderRotationHistory() {
  const rows = state.rotationHistory?.rows || [];
  const selected = state.rotationSelectedSector;
  const container = document.querySelector("#rotationHistory");
  const tracker = document.querySelector("#rotationTracker");
  if (!rows.length) {
    renderEmptyState(container, "尚无连续交易日的板块数据");
    setText("rotationHistoryRange", "暂无轮动历史");
    tracker.hidden = true;
    return;
  }
  const chronological = [...rows]
    .sort((left, right) => String(left.trade_date).localeCompare(String(right.trade_date)))
    .slice(-9);
  const displayRows = state.rotationOrder === "latest" ? [...chronological].reverse() : chronological;
  document.querySelectorAll("[data-rotation-order]").forEach((button) => {
    button.classList.toggle("active", button.dataset.rotationOrder === state.rotationOrder);
  });
  setText(
    "rotationHistoryRange",
    `最近 ${chronological.length} 个交易日 · ${displayCompactDate(chronological[0].trade_date)} → ${displayCompactDate(chronological[chronological.length - 1].trade_date)} · ${state.rotationOrder === "latest" ? "由近到远，左侧为最新交易日" : "由远到近，右侧为最新交易日"}`,
  );
  setText("rotationSelectionHint", selected ? `已联动高亮 ${selected}` : "点击任意板块追踪其连续性");
  if (selected) {
    const sequence = displayRows.map((day) => {
      const sector = (day.sectors || []).find((item) => item.name === selected);
      return { tradeDate: day.trade_date, sector };
    });
    const appearances = sequence.filter((item) => item.sector);
    const bestRank = appearances.length ? Math.min(...appearances.map((item) => number(item.sector.rank))) : 0;
    tracker.hidden = false;
    const continuity = appearances.length >= 3 ? "主线候选" : appearances.length === 1 ? "单日异动，持续性待验证" : "间断活跃";
    tracker.innerHTML = `
      <div class="rotation-tracker-copy"><strong>${escapeHtml(selected)}</strong><span>近 9 日在榜 <b>${appearances.length}</b> 天 · 最高排名 <b>#${bestRank || "--"}</b> · ${continuity}</span></div>
      <div class="rotation-tracker-spark" aria-label="${escapeHtml(selected)}九日强度轨迹">
        ${sequence.map((item) => item.sector
          ? `<span style="--spark-height:${Math.max(18, clamp(number(item.sector.strength), 0, 100))}%" title="${escapeHtml(displayCompactDate(item.tradeDate))} · 第 ${number(item.sector.rank)} 名 · 强度 ${formatNumber(item.sector.strength, 0)}"><i></i><small>#${number(item.sector.rank)}</small></span>`
          : `<span class="missing" title="${escapeHtml(displayCompactDate(item.tradeDate))} · 未上榜"><i></i><small>--</small></span>`).join("")}
      </div>
      <button class="rotation-track-cancel" type="button">取消追踪</button>`;
    tracker.querySelector(".rotation-track-cancel").addEventListener("click", () => {
      state.rotationSelectedSector = "";
      state.rotationSelectedDate = "";
      renderRotationHistory();
      loadRotationMembers("");
    });
  } else {
    tracker.hidden = true;
    tracker.innerHTML = "";
  }
  container.classList.toggle("tracking", Boolean(selected));
  const latestTradeDate = chronological[chronological.length - 1].trade_date;
  container.innerHTML = displayRows.map((day) => {
    const hasSelected = selected && (day.sectors || []).some((sector) => sector.name === selected);
    return `
      <article class="rotation-day ${selected ? "has-selection" : ""} ${hasSelected ? "selected-day" : ""} ${day.trade_date === latestTradeDate ? "latest-day" : ""}">
        <header><time>${escapeHtml(displayCompactDate(day.trade_date).slice(5))}</time><span>${(day.sectors || []).length} 个热点</span></header>
        <div class="rotation-day-sectors">${(day.sectors || []).map((sector) => {
          const strength = clamp(number(sector.strength), 0, 100);
          const heatClass = strength >= 90 ? "heat-strong" : strength >= 70 ? "heat-warm" : "heat-mild";
          return `
          <button type="button" class="rotation-sector-chip ${heatClass} ${selected === sector.name ? "selected" : ""}" data-rotation-sector="${escapeHtml(sector.name)}" data-rotation-date="${escapeHtml(day.trade_date)}">
            <span class="rotation-rank rank-${Math.min(number(sector.rank), 4)}">${number(sector.rank)}</span><strong>${escapeHtml(sector.name)}</strong><small><b>${number(sector.count)}</b> 家 · ${formatNumber(sector.strength, 0)}</small>
            <span class="rotation-cell-tooltip">${escapeHtml(displayCompactDate(day.trade_date).slice(5))} · 第 ${number(sector.rank)} 名 · 涨停 ${number(sector.count)} 家 · 强度 ${formatNumber(sector.strength, 0)}</span>
          </button>`;
        }).join("")}</div>
      </article>`;
  }).join("");
  container.querySelectorAll("[data-rotation-sector]").forEach((button) => {
    button.addEventListener("click", () => {
      const clickedSector = button.dataset.rotationSector;
      const clickedDate = button.dataset.rotationDate;
      const isSameSelection = clickedSector === state.rotationSelectedSector
        && clickedDate === state.rotationSelectedDate;
      state.rotationSelectedSector = isSameSelection ? "" : clickedSector;
      state.rotationSelectedDate = isSameSelection ? "" : clickedDate;
      renderRotationHistory();
      loadRotationMembers(state.rotationSelectedSector);
    });
  });
}

async function loadRotationMembers(sector, force = false) {
  if (!sector) {
    state.rotationMembers = null;
    state.rotationMembersKey = "";
    renderRotationMembers();
    return;
  }
  const memberDate = state.rotationSelectedDate || elements.tradeDate.value;
  const key = `${memberDate}:${sector}`;
  if (!force && state.rotationMembersKey === key && state.rotationMembers) {
    renderRotationMembers();
    return;
  }
  state.rotationMembersLoading = true;
  renderRotationMembers();
  try {
    const query = new URLSearchParams({ trade_date: memberDate, sector });
    state.rotationMembers = await apiRequest(`/api/rotation/members?${query}`);
    state.rotationMembersKey = key;
  } catch (error) {
    state.rotationMembers = { error: error.message || "成分股加载失败", rows: [] };
    state.rotationMembersKey = key;
  } finally {
    state.rotationMembersLoading = false;
    renderRotationMembers();
  }
}

function renderRotationMembers() {
  const body = document.querySelector("#rotationTableBody");
  const empty = document.querySelector("#rotationMembersEmpty");
  if (state.rotationMembersLoading) {
    body.innerHTML = "";
    empty.textContent = `正在核验${state.rotationSelectedSector}成分股`;
    empty.hidden = false;
    return;
  }
  const payload = state.rotationMembers;
  const rows = payload?.rows || [];
  if (!state.rotationSelectedSector || !payload || payload.error || !rows.length) {
    body.innerHTML = "";
    empty.textContent = payload?.error || (state.rotationSelectedSector ? "该板块暂无可用成分行情" : "点击上方任意板块查看成分股");
    empty.hidden = false;
    setText("rotationDetailTitle", "板块成分股");
    setText("rotationDetailMeta", state.rotationSelectedSector || "--");
    return;
  }
  empty.hidden = true;
  setText("rotationDetailTitle", `${payload.meta?.sector_name || state.rotationSelectedSector}成分股`);
  setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)} 只`);
  body.innerHTML = rows.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}"><td class="number num muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
      <td class="number num ${row.quoted ? changeClass(row.change) : "muted"}" data-sort-value="${row.quoted ? number(row.change) : -999}">${row.quoted ? signed(row.change) : ""}</td>
      <td class="number num">${row.quoted ? formatNumber(row.open, 2) : ""}</td><td class="number num">${row.quoted ? formatNumber(row.close, 2) : ""}</td>
      <td class="number num" data-sort-value="${number(row.amount_billion)}">${row.quoted ? formatNumber(row.amount_billion, 2) : ""}</td><td>${row.quoted ? "正常交易" : "当日无行情"}</td></tr>
  `).join("");
  animateRows(body);
  bindStockRows(body);
}

function renderLadderMini(ladders) {
  const container = document.querySelector("#ladderMini");
  const highest = ladders.length ? Math.max(...ladders.map((item) => number(item.level))) : 0;
  setText("maxHeight", highest ? `最高 ${highest} 板` : "暂无");
  container.innerHTML = ladders.slice(0, 5).map((group) => {
    const allNames = group.stocks.map((stock) => stock.name).filter(Boolean);
    const visibleNames = allNames.slice(0, 3).join("、");
    const suffix = allNames.length > 3 ? ` <em>等 ${number(group.count)} 只</em>` : "";
    return `<div class="pool-side-group">
      <div><strong>${escapeHtml(group.label)}</strong><small>${number(group.count)} 只</small></div>
      <p title="${escapeHtml(allNames.join("、"))}">${escapeHtml(visibleNames || "--")}${suffix}</p>
    </div>`;
  }).join("") || emptyStateHtml("暂无梯队数据");
}

function renderSectorMini(sectors) {
  document.querySelector("#sectorMini").innerHTML = sectors.slice(0, 7).map((sector) => `
    <div class="pool-hot-row"><strong title="${escapeHtml(sector.name)}">${escapeHtml(sector.name)}</strong><span>${number(sector.count)}</span></div>
  `).join("") || emptyStateHtml("暂无板块数据");
}

function renderLadderBoard(ladders) {
  const container = document.querySelector("#ladderBoard");
  const insights = document.querySelector("#ladderInsights");
  const ordered = [...ladders].sort((left, right) => number(right.level) - number(left.level));
  const maxLevel = ordered.length ? Math.max(...ordered.map((group) => number(group.level))) : 0;
  const topVisibleLevel = Math.max(5, maxLevel);
  const groupMap = new Map(ordered.map((group) => [number(group.level), group]));
  const displayGroups = Array.from({ length: topVisibleLevel }, (_, index) => {
    const level = topVisibleLevel - index;
    return groupMap.get(level) || { level, label: level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}板`, count: 0, stocks: [] };
  });
  const total = ordered.reduce((sum, group) => sum + number(group.count), 0);
  const spaceStocks = ordered.find((group) => number(group.level) === maxLevel)?.stocks || [];
  const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
  const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
  setText("ladderDateRange", `数据日期 ${currentDate}`);
  container.innerHTML = displayGroups.map((group) => {
    const level = number(group.level);
    const limit = level === 1 || level === 2 ? 8 : 99;
    const expanded = state.expandedLadderLevels.has(level);
    const groupStocks = [...(group.stocks || [])].sort((left, right) => {
      if (state.ladderSortMode === "open") {
        return number(left.open_times) - number(right.open_times)
          || String(left.first_time || "99:99:99").localeCompare(String(right.first_time || "99:99:99"));
      }
      return String(left.first_time || "99:99:99").localeCompare(String(right.first_time || "99:99:99"));
    });
    const stocks = expanded ? groupStocks : groupStocks.slice(0, limit);
    const remaining = Math.max(0, groupStocks.length - stocks.length);
    const label = group.label || (level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}板`);
    const color = { 1: "#2563eb", 2: "#16a34a", 3: "#d97706", 4: "#e04536" }[level] || "#9ca3af";
    return `
      <section class="market-ladder-tier ${number(group.count) ? "" : "is-gap"}" data-ladder-level-card="${level}">
        <div class="market-ladder-label" style="--tier-color:${color}"><div class="market-ladder-level"><span class="market-ladder-dot"></span>${escapeHtml(label)}</div><div class="market-ladder-count">${number(group.count)} 只</div>${number(group.count) && level > 1 ? `<div class="market-ladder-rate">${escapeHtml(label)} · <b>${formatNumber(number(group.count) / Math.max(number(groupMap.get(level - 1)?.count), 1) * 100, 1)}%</b></div>` : ""}</div>
        <div class="market-ladder-stocks">${stocks.length ? stocks.map((stock) => {
          const onePrice = String(stock.first_time || "").startsWith("09:25") && number(stock.open_times) === 0;
          const broken = number(stock.open_times) >= 6;
          const amount = number(stock.seal_amount_million) ? `封单 ${formatNumber(stock.seal_amount_million, 0)} 万` : `成交 ${formatNumber(stock.amount_billion, 1)} 亿`;
          return `<button type="button" class="market-ladder-stock" data-code="${escapeHtml(stock.code)}" aria-label="查看 ${escapeHtml(stock.name)} ${escapeHtml(stock.code)}详情">
            <span class="market-ladder-stock-first"><strong>${escapeHtml(stock.name)}</strong><small class="stock-code">${escapeHtml(stock.code)}</small><span class="market-ladder-tags">${onePrice ? '<em class="market-ladder-tag one-price">一字</em>' : ""}${broken ? `<em class="market-ladder-tag broken">烂板×${number(stock.open_times)}</em>` : ""}</span></span>
            <span class="market-ladder-stock-second"><b>${escapeHtml(stock.sector || stock.reason || "其他")}</b><small>${stock.first_time && stock.first_time !== "--" ? escapeHtml(stock.first_time) : "时间待校正"}</small><small>${amount}</small></span>
          </button>`;
        }).join("") : `<div class="market-ladder-gap-note">${level >= maxLevel ? `断层 · ${escapeHtml(label)}及以上空缺` : "该层暂时空缺"}</div>`}${groupStocks.length > limit ? `<button class="market-ladder-more" type="button" data-ladder-level="${level}">${expanded ? "收起" : `展开剩余 ${remaining} 只`}<i data-lucide="chevron-${expanded ? "up" : "down"}"></i></button>` : ""}</div>
      </section>`;
  }).join("");
  const structureRows = displayGroups.filter((group) => number(group.count) || number(group.level) <= maxLevel + 1);
  const maxCount = Math.max(1, ...structureRows.map((group) => number(group.count)));
  const rateRows = (state.dashboard?.limit_performance || []).map((row) => ({
    label: `${row.label || (number(row.level) === 1 ? "昨日首板" : `昨日${number(row.level)}板`)} → 今日`,
    value: clamp(number(row.advance_rate), 0, 100),
  }));
  const previousMax = Math.max(0, ...(state.dashboard?.yesterday_limits || []).map((row) => number(row.prior_streak)));
  const spaceChange = previousMax && maxLevel < previousMax ? `较昨日 ${previousMax} 板 ↓ 空间压缩` : previousMax && maxLevel > previousMax ? `较昨日 ${previousMax} 板 ↑ 高度抬升` : "高度与昨日接近";
  const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度，重点观察承接而非单看高度。" : maxLevel >= 3 ? "空间位于中段，梯队延续性比绝对高度更重要。" : "高度受到压缩，先观察首板向二板的结构修复。";
  const strongestGroup = structureRows.reduce((best, group) => number(group.count) > number(best?.count) ? group : best, structureRows[0]);
  insights.innerHTML = `
    <section class="market-ladder-insight-card market-ladder-apex-card"><header><h3>空间板</h3><span>市场高度</span></header><div class="market-ladder-apex"><div><strong>${maxLevel ? `${maxLevel} 板` : "--"}</strong><em>${escapeHtml(spaceChange)}</em></div><p>${spaceStocks.length ? spaceStocks.map((stock) => `<b>${escapeHtml(stock.name)}</b>（${escapeHtml(stock.sector || "其他")}）`).join(" · ") : "暂无空间板"}</p></div><p>${spaceNote}</p></section>
    <section class="market-ladder-insight-card"><header><h3>梯队结构</h3><span>完整度</span></header><div class="market-ladder-pyramid">${structureRows.map((group) => `<div class="market-ladder-pyramid-row ${number(group.count) ? "" : "is-gap"}"><span>${escapeHtml(group.label || `${number(group.level)}板`)}</span><i><b style="width:${Math.max(number(group.count) ? 8 : 100, number(group.count) / maxCount * 100)}%"></b></i><strong>${number(group.count) ? `${number(group.count)} 只` : "断层"}</strong></div>`).join("")}</div><p>断层越少，梯队从低位向高位传导越连贯。当前腰部为 <b>${escapeHtml(strongestGroup?.label || "--")}</b>。</p></section>
    <section class="market-ladder-insight-card"><header><h3>晋级率参考</h3><span>昨日梯队 → 今日</span></header><div class="market-ladder-rate-list">${rateRows.length ? rateRows.map((row) => `<div><span>${escapeHtml(row.label)}</span><i><b class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}" style="width:${Math.max(row.value, row.value > 0 ? 2 : 0)}%"></b></i><strong class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}">${formatNumber(row.value, 1)}%</strong></div>`).join("") : '<div class="empty-state">暂无可比梯队</div>'}</div><small class="market-ladder-source">数据来自“涨停表现”页 · 昨日梯队样本</small></section>`;
  container.querySelectorAll("[data-ladder-level]").forEach((button) => {
    button.addEventListener("click", () => {
      const level = number(button.dataset.ladderLevel);
      if (state.expandedLadderLevels.has(level)) state.expandedLadderLevels.delete(level);
      else state.expandedLadderLevels.add(level);
      renderLadderBoard(state.dashboard?.ladders || []);
    });
  });
  bindStockRows(container);
  refreshIcons();
}

async function loadAuctionCenter(force = false) {
  if (state.auctionLoading) return;
  state.auctionLoading = true;
  const button = document.querySelector("#auctionRefreshButton");
  button.disabled = true;
  setText("auctionDateLabel", "正在读取竞价数据");
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    if (force) query.set("force", "1");
    state.auctionData = await apiRequest(`/api/auction?${query}`);
    renderAuctionCenter();
    scheduleAuctionTransition(state.auctionData.meta || {});
  } catch (error) {
    document.querySelector("#auctionSummary").innerHTML = "";
    document.querySelector("#auctionThemeCarry").innerHTML = "";
    document.querySelector("#auctionNewThemes").innerHTML = "";
    document.querySelector("#auctionAmountTrend").innerHTML = "";
    document.querySelector("#auctionAmountCompare").innerHTML = "";
    document.querySelector("#auctionTableBody").innerHTML = "";
    document.querySelector("#auctionEmpty").hidden = false;
    setText("auctionDateLabel", error.message || "竞价数据暂不可用");
    showToast(error.message || "竞价数据加载失败");
  } finally {
    state.auctionLoading = false;
    button.disabled = false;
  }
}

function renderAuctionCenter() {
  const payload = state.auctionData;
  if (!payload) return;
  const summary = payload.summary || {};
  renderAuctionPhase(payload.meta || {});
  setText(
    "auctionDateLabel",
    `${payload.meta?.carried_forward ? "最近有效竞价" : "竞价日期"} ${payload.meta?.trade_date || "--"}`,
  );
  document.querySelector("#auctionSummary").innerHTML = [
    ["竞价覆盖", `${formatNumber(summary.stock_count, 0)} 只`, ""],
    ["重点异动", `${formatNumber(summary.focus_count, 0)} 只`, "up"],
    ["竞价一字", `${formatNumber(summary.one_price_count, 0)} 只`, ""],
    ["竞价成交额", `${formatNumber(summary.amount_billion, 2)} 亿`, ""],
  ].map(([label, value, tone]) => `<div><span>${label}</span><strong class="${tone}">${value}</strong></div>`).join("");
  setText("auctionFocusCount", number(summary.focus_count));
  setText("auctionAllCount", number(summary.candidate_count));
  setText("auctionOnePriceCount", number(summary.one_price_count));
  setText("auctionWatchlistCount", number(payload.watchlist_rows?.length));
  renderAuctionInsights(payload);
  renderAuctionTable();
}

function renderAuctionInsights(payload) {
  const themes = payload.themes || {};
  const carry = themes.carry || [];
  const tone = { "强承接": "strong", "有承接": "steady", "分歧": "mixed", "承接弱": "weak" };
  setText("auctionThemeBaseline", `基于 ${payload.candidate_meta?.baseline_date || "--"}`);
  document.querySelector("#auctionThemeCarry").innerHTML = carry.length
    ? carry.map((item) => `
      <div class="auction-theme-row">
        <strong class="auction-theme-name">${escapeHtml(item.name)}</strong>
        <span class="auction-theme-info">${escapeHtml(item.leader || "--")} · 昨日 ${number(item.prior_limit_count)} 只涨停</span>
        <span class="auction-theme-status ${tone[item.status] || "mixed"}">${escapeHtml(item.status)}</span>
        <span class="auction-theme-median ${item.median_change == null ? "" : changeClass(item.median_change)}">${item.median_change == null ? "暂无有效候选" : `${signed(item.median_change)}%`}<small>中位</small></span>
      </div>`).join("")
    : '<div class="auction-inline-empty">暂无昨日强势题材基线</div>';

  const newThemes = themes.new_themes || [];
  document.querySelector("#auctionNewThemes").innerHTML = newThemes.length
    ? newThemes.map((item) => `<span title="${escapeHtml((item.leaders || []).join("、"))}">${escapeHtml(item.name)} <strong>${number(item.stock_count)}</strong></span>`).join("")
    : '<small>尚未形成多股共振的新线索</small>';

  const history = payload.amount_history || [];
  const maximum = Math.max(...history.map((item) => number(item.amount_billion)), 1);
  const priorFive = history.slice(Math.max(0, history.length - 6), Math.max(0, history.length - 1));
  const fiveDayAverage = priorFive.length
    ? priorFive.reduce((sum, item) => sum + number(item.amount_billion), 0) / priorFive.length
    : null;
  document.querySelector("#auctionAmountTrend").innerHTML = history.length
    ? history.map((item, index) => {
      const height = Math.max(8, number(item.amount_billion) / maximum * 100);
      const current = index === history.length - 1 ? " current" : "";
      return `<div class="auction-amount-day${current}" title="${escapeHtml(item.trade_date)} · ${formatNumber(item.amount_billion, 2)} 亿 · ${number(item.stock_count)} 只">
        <span style="height:${height.toFixed(1)}%"></span><small>${escapeHtml(String(item.trade_date || "").slice(5))}</small>
      </div>`;
    }).join("") + (fiveDayAverage === null ? "" : `<div class="auction-amount-average" style="bottom:${(20 + Math.min(fiveDayAverage / maximum, 1) * 82).toFixed(1)}px"><small>5日均 ${formatNumber(fiveDayAverage, 1)}</small></div>`)
    : '<div class="auction-inline-empty">历史竞价量能尚未形成</div>';
  setText("auctionAmountValue", `${formatNumber(payload.summary?.amount_billion, 2)} 亿`);
  const comparison = [
    ["较昨日", payload.summary?.amount_change_previous],
    ["较5日均值", payload.summary?.amount_change_5d],
  ];
  document.querySelector("#auctionAmountCompare").innerHTML = comparison.map(([label, value]) => `
    <span>${label}<strong class="${value == null ? "" : changeClass(value)}">${value == null ? "--" : `${signed(value)}%`}</strong></span>
  `).join("");
}

function renderAuctionPhase(meta) {
  const phase = meta.phase || "archive";
  const available = Boolean(meta.available);
  const copy = {
    pending: ["竞价尚未开始", "9:15 进入观察期，9:25 读取最终竞价结果。", "下一阶段 09:15"],
    observing: ["竞价观察期", "此阶段先观察盘前变化，系统将在 9:25 自动读取最终结果。", "09:25 定格"],
    selection: available
      ? ["竞价筛选窗口", "最终竞价结果已经定格，请在 9:30 前完成筛选。", "有效至 09:30"]
      : ["等待最终竞价", "9:25 数据尚未到达，系统正在自动重试。", "即将更新"],
    finalized: ["今日竞价已定格", "9:30 后停止更新，仅保留用于复盘、回测与智能选股。", "已冻结"],
    archive: ["历史竞价归档", "当前展示所选交易日的最终竞价结果。", "归档数据"],
  }[phase] || ["竞价状态", "当前竞价状态待确认。", "--"];
  const notice = document.querySelector("#auctionPhaseNotice");
  notice.dataset.phase = phase;
  setText("auctionPhaseTitle", copy[0]);
  setText("auctionPhaseDetail", copy[1]);
  setText("auctionPhaseTime", copy[2]);
  const refresh = document.querySelector("#auctionRefreshButton");
  refresh.hidden = phase !== "selection";
  refresh.disabled = state.auctionLoading;
}

function clearAuctionTimer() {
  if (state.auctionTimer) clearTimeout(state.auctionTimer);
  state.auctionTimer = null;
}

function scheduleAuctionTransition(meta) {
  clearAuctionTimer();
  if (state.activeView !== "auctionView") return;
  let delay = 0;
  if (["selection", "finalized"].includes(meta.phase) && !meta.available) {
    delay = 10_000;
  } else if (meta.next_transition_at) {
    const transitionAt = new Date(meta.next_transition_at).getTime();
    if (Number.isFinite(transitionAt)) delay = Math.max(800, transitionAt - Date.now() + 500);
  }
  if (!delay) return;
  state.auctionTimer = setTimeout(() => {
    state.auctionTimer = null;
    if (state.activeView === "auctionView") loadAuctionCenter(true);
  }, Math.min(delay, 2_147_000_000));
}

function renderAuctionTable() {
  const rows = currentAuctionRows();
  const columns = auctionColumns();
  const head = document.querySelector("#auctionTableHead");
  head.innerHTML = columns.map((column) => {
    const sorted = column.sortKey === state.auctionSortKey;
    const arrow = !column.sortKey ? "" : `<span class="arr">${sorted ? (state.auctionSortDirection === "desc" ? "▼" : "▲") : "↕"}</span>`;
    return `<th class="${column.numeric ? "number num " : ""}${column.sortKey ? "sortable " : ""}${sorted ? "sorted" : ""}"${column.sortKey ? ` data-auction-sort="${column.sortKey}"` : ""}>${column.label}${arrow}</th>`;
  }).join("");
  const body = document.querySelector("#auctionTableBody");
  body.innerHTML = rows.map((row) => `<tr data-code="${escapeHtml(row.code)}">${columns.map((column) => renderAuctionCell(row, column.key)).join("")}</tr>`).join("");
  bindStockRows(body);
  const datasetCopy = {
    focus: ["重点异动", "优先查看市场核心与显著预期差"],
    onePrice: ["竞价一字", "竞价封于当日真实涨停价，不参与普通异动评分"],
    watchlist: ["我的自选", "仅展示当前账号关注标的的竞价反馈"],
    all: ["全部候选", "昨日涨停、炸板与热榜前20候选"],
  }[state.auctionDataset] || ["竞价异动", ""];
  setText("auctionWorkspaceTitle", datasetCopy[0]);
  setText("auctionWorkspaceSubtitle", datasetCopy[1]);
  document.querySelector("#auctionExpectationControls").hidden = state.auctionDataset === "onePrice";
  const empty = document.querySelector("#auctionEmpty");
  const phase = state.auctionData?.meta?.phase || "archive";
  empty.textContent = phase === "selection" && !state.auctionData?.meta?.available
    ? "正在等待 9:25 最终竞价数据"
    : state.auctionDataset === "watchlist"
      ? "当前账号还没有可观察的自选股"
      : state.auctionDataset === "onePrice"
        ? "当前没有竞价封于涨停价的股票"
        : "没有符合条件的竞价候选";
  empty.hidden = rows.length > 0;
}

function currentAuctionRows() {
  const datasets = {
    focus: state.auctionData?.focus_rows || [],
    onePrice: state.auctionData?.one_price_rows || [],
    watchlist: state.auctionData?.watchlist_rows || [],
    all: state.auctionData?.rows || [],
  };
  let rows = [...(datasets[state.auctionDataset] || [])];
  const filter = state.auctionFilter;
  const labels = { above: "超预期", matched: "符合预期", below: "低于预期" };
  if (labels[filter]) rows = rows.filter((item) => item.expectation === labels[filter]);
  if (state.auctionQuery) {
    rows = rows.filter((item) => `${item.code} ${item.name} ${item.sector}`.toLocaleLowerCase("zh-CN").includes(state.auctionQuery));
  }
  const key = state.auctionSortKey;
  const direction = state.auctionSortDirection === "asc" ? 1 : -1;
  if (key) {
    rows.sort((left, right) => {
      const leftValue = left[key];
      const rightValue = right[key];
      if (leftValue == null && rightValue == null) return 0;
      if (leftValue == null) return 1;
      if (rightValue == null) return -1;
      const result = typeof leftValue === "number" || typeof rightValue === "number"
        ? number(leftValue) - number(rightValue)
        : String(leftValue).localeCompare(String(rightValue), "zh-CN", { numeric: true });
      return result * direction;
    });
  }
  return rows.slice(0, 300);
}

function auctionColumns() {
  const base = [
    { key: "stock", label: "股票" },
    { key: "context", label: "方向与来源" },
    { key: "identity", label: "市场身份" },
  ];
  const metrics = [
    { key: "score", label: "关注分", numeric: true, sortKey: "attention_score" },
    { key: "expectation", label: "预期判断" },
    { key: "change", label: "竞价涨幅（%）", numeric: true, sortKey: "change" },
    { key: "amount", label: "竞价额（百万）", numeric: true, sortKey: "amount_million" },
    { key: "volume", label: "量比", numeric: true, sortKey: "volume_ratio" },
  ];
  return state.auctionDataset === "onePrice" ? [...base, ...metrics.slice(2)] : [...base, ...metrics];
}

function renderAuctionCell(row, key) {
  const unavailable = row.available === false;
  const onePrice = Boolean(row.is_one_price);
  const expectationTone = { "超预期": "above", "符合预期": "matched", "低于预期": "below" };
  if (key === "stock") return `<td><span class="auction-stock-cell-v2"><strong class="sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>`;
  if (key === "context") return `<td><span class="auction-context-cell-v2"><strong>${escapeHtml(row.sector || "其他")}</strong>${renderAuctionSources(row.source_label || (state.auctionDataset === "watchlist" ? "我的自选" : "全市场"))}</span></td>`;
  if (key === "identity") return `<td>${renderAuctionCoreTags(row.core_tags)}</td>`;
  if (unavailable) return key === "expectation"
    ? '<td><span class="table-muted">暂无竞价</span></td>'
    : `<td class="${["score", "change", "amount", "volume"].includes(key) ? "number num" : ""}"></td>`;
  if (key === "score") return `<td class="number num auction-score">${onePrice ? "" : formatNumber(row.attention_score, 1)}</td>`;
  if (key === "expectation") {
    const tag = onePrice
      ? '<span class="auction-one-price-tag">竞价一字</span>'
      : `<span class="auction-expectation ${expectationTone[row.expectation] || "matched"}">${escapeHtml(row.expectation || "符合预期")}</span>`;
    return `<td>${tag}</td>`;
  }
  if (key === "change") return `<td class="number num ${changeClass(row.change)}">${signed(row.change)}</td>`;
  if (key === "amount") return `<td class="number num">${formatNumber(row.amount_million, 2)}</td>`;
  if (key === "volume") return `<td class="number num auction-volume-ratio">${formatNumber(row.volume_ratio, 2)}</td>`;
  return "<td></td>";
}

function renderAuctionSources(value) {
  const sources = String(value || "").split(/[·、/]/).map((item) => item.trim()).filter(Boolean).slice(0, 3);
  return `<small class="auction-source-tags-v2">${sources.map((source) => `<b>${escapeHtml(source)}</b>`).join("")}</small>`;
}

function renderAuctionCoreTags(tags) {
  const values = Array.isArray(tags) ? tags : [];
  return values.length
    ? `<span class="auction-core-tags">${values.slice(0, 2).map((tag) => `<b>${escapeHtml(tag)}</b>`).join("")}</span>`
    : '<span class="auction-identity-empty" aria-label="无市场身份"></span>';
}

function exportAuctionRows() {
  const rows = currentAuctionRows();
  exportRows("集合竞价", rows, [
    ["股票代码", "code"], ["股票名称", "name"], ["行业", "sector"], ["来源", "source_label"],
    ["市场身份", "core_tags"], ["关注分", "attention_score"], ["预期判断", "expectation"],
    ["竞价涨幅%", "change"], ["竞价额百万", "amount_million"], ["量比", "volume_ratio"],
  ]);
}

async function loadThemeLibrary(force = false) {
  if (state.themeLoading) return;
  state.themeLoading = true;
  const button = document.querySelector("#themeRefreshButton");
  button.disabled = true;
  setText("themeDateLabel", "正在整理题材库");
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    if (force) query.set("force", "1");
    state.themeLibrary = await apiRequest(`/api/themes?${query}`);
    renderThemeLibrary();
    const available = (state.themeLibrary.items || []).some((item) => item.code === state.selectedThemeCode);
    if (!available) state.selectedThemeCode = "";
    const initialCode = state.selectedThemeCode || state.themeLibrary.items?.[0]?.code || "";
    if (initialCode) await selectTheme(initialCode, true);
  } catch (error) {
    setText("themeDateLabel", error.message || "题材数据暂不可用");
    renderEmptyState("themeDirectory", error.message || "题材数据加载失败");
    showToast(error.message || "题材数据加载失败");
  } finally {
    state.themeLoading = false;
    button.disabled = false;
  }
}

function renderThemeLibrary() {
  const payload = state.themeLibrary;
  if (!payload) return;
  const summary = payload.summary || {};
  setText("themeDateLabel", `${payload.meta?.carried_forward ? "最近有效行情" : "行情日期"} ${payload.meta?.trade_date || "--"}`);
  document.querySelector("#themeSummary").innerHTML = [
    ["收录题材", number(summary.theme_count), "个", ""],
    ["当日上涨", number(summary.up_count), "个", "up"],
    ["当日下跌", number(summary.down_count), "个", "down"],
    ["人气题材", number(summary.hot_count), "个", "warning"],
  ].map(([label, value, unit, tone]) => `<div><span>${label}</span><strong class="${tone}">${value}<small>${unit}</small></strong></div>`).join("");
  renderThemeDirectory();
}

function renderThemeDirectory() {
  let items = [...(state.themeLibrary?.items || [])];
  if (state.themeQuery) {
    items = items.filter((item) => `${item.code} ${item.name}`.toLocaleLowerCase("zh-CN").includes(state.themeQuery));
  }
  setText("themeResultCount", `${items.length} 个`);
  document.querySelector("#themeDirectory").innerHTML = items.map((item, index) => {
    const active = item.code === state.selectedThemeCode;
    return `
    <button type="button" class="theme-directory-item-v2 ${active ? "active" : ""}" data-theme-code="${escapeHtml(item.code)}" aria-pressed="${active}">
      <span class="theme-rank-v2">${index + 1}</span>
      <span class="theme-directory-copy-v2"><strong class="market-preview-trigger" data-market-preview-type="theme" data-market-preview-id="${escapeHtml(item.code)}" title="悬停预览题材行情">${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 ? `${signed(item.change)}%` : "--"}</b>
    </button>`;
  }).join("") || emptyStateHtml("没有匹配的题材");
}

async function selectTheme(code, keepSelection = false) {
  if (!code) return;
  state.selectedThemeCode = code;
  if (!keepSelection) renderThemeDirectory();
  document.querySelector("#themeDetailEmpty").hidden = false;
  document.querySelector("#themeDetailContent").hidden = true;
  setText("themeDetailEmpty", "正在读取题材详情");
  try {
    const query = new URLSearchParams({ code, trade_date: elements.tradeDate.value });
    state.themeDetail = await apiRequest(`/api/themes/detail?${query}`);
    renderThemeDetail();
  } catch (error) {
    setText("themeDetailEmpty", error.message || "题材详情加载失败");
    showToast(error.message || "题材详情加载失败");
  }
}

function renderThemeDetail() {
  const payload = state.themeDetail;
  if (!payload) return;
  const theme = payload.theme || {};
  const summary = payload.summary || {};
  document.querySelector("#themeDetailEmpty").hidden = true;
  document.querySelector("#themeDetailContent").hidden = false;
  setText("themeDetailName", theme.name || "--");
  setText("themeDetailCode", `${theme.code || "--"} · ${payload.meta?.trade_date || "--"}`);
  setText("themeDetailChange", `${signed(theme.change)}%`);
  document.querySelector("#themeDetailChange").className = changeClass(theme.change);
  document.querySelector("#themeDetailMetrics").innerHTML = [
    ["成分股", `${number(summary.member_count)} 只`, ""],
    ["有行情", `${number(summary.quoted_count)} 只`, ""],
    ["上涨", `${number(summary.up_count)} 只`, "up"],
    ["下跌", `${number(summary.down_count)} 只`, "down"],
    ["换手率", `${formatNumber(theme.turnover_rate, 2)}%`, ""],
  ].map(([label, value, tone]) => `<div><span>${label}</span><strong class="${tone}">${value}</strong></div>`).join("");
  setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`);
  const body = document.querySelector("#themeMemberTableBody");
  body.innerHTML = (payload.members || []).map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}"><td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num ${changeClass(row.change)}">${row.has_quote ? signed(row.change) : ""}</td>
      <td class="number num">${row.has_quote ? formatNumber(row.price, 2) : ""}</td><td class="number num">${row.has_quote ? formatNumber(row.amount_billion, 2) : ""}</td></tr>`).join("");
  bindStockRows(body);
  renderThemeDirectory();
}

async function loadPopularity(force = false) {
  if (state.popularityLoading) return;
  state.popularityLoading = true;
  const button = document.querySelector("#popularityRefreshButton");
  button.disabled = true;
  setText("popularityDateLabel", "正在读取人气榜");
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    if (force) query.set("force", "1");
    state.popularityData = await apiRequest(`/api/popularity?${query}`);
    renderPopularity();
  } catch (error) {
    setText("popularityDateLabel", error.message || "人气榜暂不可用");
    document.querySelector("#popularityTableBody").innerHTML = "";
    document.querySelector("#popularityEmpty").hidden = false;
    showToast(error.message || "人气榜加载失败");
  } finally {
    state.popularityLoading = false;
    button.disabled = false;
  }
}

function renderPopularity() {
  const payload = state.popularityData;
  if (!payload) return;
  const summary = payload.summary || {};
  setText("popularityDateLabel", `${payload.meta?.carried_forward ? "最近有效榜单" : "榜单日期"} ${payload.meta?.trade_date || "--"}`);
  const topNames = (rows) => (rows || []).slice(0, 3).map((item) => item.name).filter(Boolean).join(" · ") || "--";
  document.querySelector("#popularitySummary").innerHTML = [
    ["同花顺热度 Top3", topNames(payload.ths), `共 ${number(summary.ths_count)} 只上榜`],
    ["东方财富热度 Top3", topNames(payload.dc), `共 ${number(summary.dc_count)} 只上榜`],
    ["双榜共识", `${number(summary.dual_count)} 只`, "同时进入两榜，共识度更高"],
  ].map(([label, value, detail], index) => `<article class="${index === 2 ? "consensus" : ""}"><span>${label}</span><strong>${escapeHtml(value)}</strong><small>${escapeHtml(detail)}</small></article>`).join("");
  renderPopularityTable();
}

function renderPopularityTable() {
  const source = state.popularitySource;
  let rows = [...(state.popularityData?.[source] || [])];
  if (state.popularityQuery) {
    rows = rows.filter((item) => `${item.code} ${item.name} ${(item.concepts || []).join(" ")}`.toLocaleLowerCase("zh-CN").includes(state.popularityQuery));
  }
  const combined = source === "combined";
  const sourceName = source === "ths" ? "同花顺" : source === "dc" ? "东方财富" : "双榜综合";
  setText("popularityTableTitle", `${sourceName}榜`);
  setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
  const headers = [
    ["排名", "number num"], ["股票", ""], ["最新价（元）", "number num"], ["涨跌幅（%）", "number num"],
    ...(source !== "dc" ? [["同花顺", "number num"]] : []),
    ...(source !== "ths" ? [["东方财富", "number num"]] : []),
    ["排名变化", "number num"], ["热门概念", ""], ...(!combined ? [["榜单状态", ""]] : []),
  ];
  document.querySelector("#popularityTableHead").innerHTML = headers.map(([label, className]) => `<th scope="col" class="${className}">${label}</th>`).join("");
  const body = document.querySelector("#popularityTableBody");
  body.innerHTML = rows.map((row, index) => {
    const thsRank = source === "ths" ? row.rank : row.ths_rank;
    const dcRank = source === "dc" ? row.rank : row.dc_rank;
    const move = row.rank_change;
    const movement = move === null || move === undefined ? "新" : number(move) > 0 ? `↑${number(move)}` : number(move) < 0 ? `↓${Math.abs(number(move))}` : "持平";
    return `<tr data-code="${escapeHtml(row.code)}">
      <td class="number num popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
      <td><div class="popularity-stock-v2"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><span class="stock-code scode">${escapeHtml(row.code)}</span></div></td>
      <td class="number num">${row.price == null ? "" : formatNumber(row.price, 2)}</td>
      <td class="number num ${row.change == null ? "" : changeClass(row.change)}">${row.change == null ? "" : signed(row.change)}</td>
      ${source !== "dc" ? `<td class="number num popularity-list-rank-v2">${thsRank ? number(thsRank) : ""}</td>` : ""}
      ${source !== "ths" ? `<td class="number num popularity-list-rank-v2">${dcRank ? number(dcRank) : ""}</td>` : ""}
      <td class="number num popularity-movement-v2 ${number(move) > 0 ? "up" : number(move) < 0 ? "down" : ""}">${movement}</td>
      <td class="popularity-concepts-v2" title="${escapeHtml((row.concepts || []).join("、"))}">${escapeHtml((row.concepts || []).slice(0, 3).join("、"))}</td>
      ${!combined ? `<td><span class="popularity-source-tag-v2 ${row.dual_source ? "dual" : ""}">${row.dual_source ? "双榜共识" : "单榜入选"}</span></td>` : ""}
    </tr>`;
  }).join("");
  bindStockRows(body);
  markAutoSortableHeaders(body.closest("table"));
  document.querySelector("#popularityEmpty").hidden = rows.length > 0;
}

function selectDragonViewMode(mode) {
  state.dragonViewMode = mode === "profiles" ? "profiles" : "daily";
  document.querySelectorAll("[data-dragon-view-mode]").forEach((button) => {
    const active = button.dataset.dragonViewMode === state.dragonViewMode;
    button.classList.toggle("active", active);
    button.setAttribute("aria-pressed", String(active));
  });
  if (state.dragonViewMode === "profiles") {
    document.querySelector("#dragonDailyContent").hidden = true;
    document.querySelector("#dragonEmptyState").hidden = true;
    document.querySelector("#dragonProfilesContent").hidden = false;
    if (state.hotMoneyProfiles) renderHotMoneyProfiles();
    else loadHotMoneyProfiles();
  } else {
    document.querySelector("#dragonProfilesContent").hidden = true;
    if (state.dragonTiger) renderDragonTiger();
    else loadDragonTiger();
  }
}

async function loadHotMoneyProfiles(force = false) {
  if (!force && state.hotMoneyProfiles) {
    renderHotMoneyProfiles();
    return;
  }
  setStatus("正在加载游资档案");
  try {
    const query = new URLSearchParams();
    if (force) query.set("force", "1");
    const suffix = query.size ? `?${query}` : "";
    state.hotMoneyProfiles = await apiRequest(`/api/dragon-tiger/profiles${suffix}`);
    renderHotMoneyProfiles();
    const count = number(state.hotMoneyProfiles.summary?.profile_count);
    setStatus(`游资档案已加载 · 共 ${count} 位`);
  } catch (error) {
    showToast(error.message || "游资档案加载失败");
    setStatus("游资档案加载失败");
  }
}

function renderHotMoneyProfiles() {
  const payload = state.hotMoneyProfiles;
  if (!payload) return;
  const profiles = payload.profiles || [];
  const summary = payload.summary || {};
  const query = state.hotMoneyProfileQuery;
  const visible = profiles.filter((profile) => {
    if (!query) return true;
    return [profile.name, profile.description, ...(profile.organizations || [])]
      .join(" ")
      .toLocaleLowerCase("zh-CN")
      .includes(query);
  });
  if (!visible.some((profile) => profile.id === state.selectedHotMoneyProfileId)) {
    state.selectedHotMoneyProfileId = visible[0]?.id || "";
  }
  const selected = visible.find((profile) => profile.id === state.selectedHotMoneyProfileId) || null;

  setText("dragonDateLabel", `收录 ${number(summary.profile_count)} 位`);
  setText("hotMoneyProfileResultCount", query ? `${visible.length} / ${profiles.length} 位` : `${profiles.length} 位`);
  document.querySelector("#hotMoneyProfileSummary").innerHTML = [
    ["收录游资", number(summary.profile_count)],
    ["已有简介", number(summary.described_count)],
    ["关联席位", number(summary.organization_count)],
  ].map(([label, value]) => `<span><small>${label}</small><strong>${value}</strong></span>`).join("");

  const list = document.querySelector("#hotMoneyProfileList");
  list.innerHTML = visible.length ? visible.map((profile, index) => `
    <button class="hot-money-profile-row-v2 ${profile.id === state.selectedHotMoneyProfileId ? "selected" : ""}"
      type="button" role="option" aria-selected="${profile.id === state.selectedHotMoneyProfileId}"
      data-hot-money-profile="${escapeHtml(profile.id)}">
      <span class="hot-money-profile-index-v2">${String(index + 1).padStart(2, "0")}</span>
      <span class="hot-money-profile-monogram-v2">${escapeHtml(profile.name.slice(0, 2))}</span>
      <span class="hot-money-profile-row-copy-v2">
        <strong>${escapeHtml(profile.name)}</strong>
        <small>${escapeHtml(profile.description || "暂未收录简介")}</small>
      </span>
      <span class="hot-money-profile-seat-count-v2">${number(profile.organization_count)} 席</span>
    </button>`).join("") : `
      <div class="hot-money-profile-list-empty-v2">
        <i data-lucide="search-x" aria-hidden="true"></i>
        <span>${profiles.length ? "没有符合条件的游资档案" : "游资名录暂不可用"}</span>
      </div>`;

  const detail = document.querySelector("#hotMoneyProfileDetail");
  if (!selected) {
    detail.innerHTML = `
      <div class="hot-money-profile-empty-v2">
        <i data-lucide="contact" aria-hidden="true"></i>
        <strong>${profiles.length ? "选择一位游资查看档案" : "暂无可展示的游资档案"}</strong>
      </div>`;
  } else {
    const organizations = selected.organizations || [];
    detail.innerHTML = `
      <header class="hot-money-profile-detail-head-v2">
        <span class="hot-money-profile-avatar-v2">${escapeHtml(selected.name.slice(0, 2))}</span>
        <div>
          <small>游资档案</small>
          <h3>${escapeHtml(selected.name)}</h3>
          <span>${organizations.length ? `关联 ${organizations.length} 个公开席位` : "暂无关联席位"}</span>
        </div>
      </header>
      <section class="hot-money-profile-section-v2">
        <h4>人物简介</h4>
        <p class="${selected.description ? "" : "is-empty"}">${escapeHtml(selected.description || "名录暂未收录该游资的公开简介。")}</p>
      </section>
      <section class="hot-money-profile-section-v2 hot-money-profile-org-section-v2">
        <div class="hot-money-profile-section-title-v2">
          <h4>关联营业部</h4>
          <span>${organizations.length} 个</span>
        </div>
        <div class="hot-money-profile-organizations-v2">
          ${organizations.length ? organizations.map((organization) => `
            <span><i data-lucide="building-2" aria-hidden="true"></i>${escapeHtml(organization)}</span>
          `).join("") : '<p class="is-empty">名录暂未收录关联营业部。</p>'}
        </div>
      </section>
      ${payload.meta?.notice ? `<p class="hot-money-profile-notice-v2">${escapeHtml(payload.meta.notice)}</p>` : ""}`;
  }
  refreshIcons();
}

async function loadDragonTiger(force = false) {
  const requestedDate = elements.tradeDate.value;
  if (
    !force
    && ["success", "empty", "partial", "unavailable"].includes(state.dragonTiger?.meta?.status)
    && (state.dragonTiger?.meta?.requested_date || state.dragonTiger?.meta?.trade_date) === requestedDate
  ) {
    renderDragonTiger();
    return;
  }
  setStatus("正在加载龙虎榜");
  try {
    const query = new URLSearchParams({ trade_date: requestedDate });
    if (force) query.set("force", "1");
    const payload = await apiRequest(`/api/dragon-tiger?${query}`);
    state.dragonTiger = payload;
    renderDragonTiger();
    const statusLabel = payload.meta.status === "error"
      ? "龙虎榜数据暂不可用"
      : payload.meta.status === "empty"
        ? "当日暂无公开游资明细"
        : payload.meta.status === "partial"
          ? "当日有龙虎榜，暂无命名游资明细"
        : payload.meta.status === "unavailable" ? "龙虎榜数据暂不可用" : "龙虎榜明细";
    setStatus(`${statusLabel} · 龙虎榜已加载`);
  } catch (error) {
    showToast(error.message || "龙虎榜加载失败");
    setStatus("龙虎榜加载失败");
  }
}

function renderDragonTiger() {
  const payload = state.dragonTiger;
  if (!payload) return;
  const summary = payload.summary || {};
  if (state.dragonViewMode === "daily") setText("dragonDateLabel", `数据日期 ${payload.meta.trade_date}`);
  const status = payload.meta?.status || "empty";
  const hasRecognizedTraders = (payload.traders || []).some((item) => item.identity_type === "trader" && item.recognized !== false);
  const showEmptyState = !hasRecognizedTraders
    && !(payload.unclassified_seats || []).length
    && ["empty", "error", "unavailable"].includes(status);
  const dailyVisible = state.dragonViewMode === "daily";
  document.querySelector("#dragonProfilesContent").hidden = dailyVisible;
  document.querySelector("#dragonEmptyState").hidden = !dailyVisible || !showEmptyState;
  document.querySelector("#dragonDailyContent").hidden = !dailyVisible || showEmptyState;
  if (showEmptyState) {
    const unavailable = ["error", "unavailable"].includes(status);
    setText("dragonEmptyTitle", unavailable ? "龙虎榜数据暂不可用" : `${payload.meta?.trade_date || "该交易日"} 暂无龙虎榜明细`);
    setText("dragonEmptyDescription", unavailable
      ? "当前数据暂未完成更新，可稍后重新检查或查看前一交易日。"
      : "龙虎榜明细通常在交易日盘后陆续披露，可稍后刷新或查看前一交易日。");
  }
  document.querySelector("#dragonSummary").innerHTML = [
    ["上榜游资", `${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(([label, value, className]) => `<div class="dragon-metric"><span>${label}</span><strong class="${className}">${value}</strong></div>`).join("");

  renderDragonTraderList();
  renderUnclassifiedSeats();
}

function renderDragonTraderList() {
  const payload = state.dragonTiger;
  if (!payload) return;
  let traders = [...(payload.traders || [])].filter((item) => item.identity_type === "trader" && item.recognized !== false);
  if (state.dragonFilter === "buy") traders = traders.filter((item) => number(item.net_buy_million) > 0);
  if (state.dragonFilter === "sell") traders = traders.filter((item) => number(item.net_buy_million) < 0);
  if (state.dragonFilter === "unclassified") traders = [];
  if (state.dragonQuery) {
    traders = traders.filter((item) => {
      const searchable = [
        item.name,
        ...(item.operations || []).flatMap((operation) => [operation.code, operation.name, operation.seat_name]),
      ].join(" ").toLowerCase();
      return searchable.includes(state.dragonQuery);
    });
  }

  const container = document.querySelector("#dragonTraderList");
  let emptyMessage = "没有符合当前条件的游资操作";
  if (!Array.isArray(payload.traders)) emptyMessage = "龙虎榜数据格式暂不可用，请稍后重试";
  else if (["error", "unavailable"].includes(payload.meta?.status)) emptyMessage = "龙虎榜数据暂不可用，请稍后重试";
  else if (payload.meta?.status === "empty") emptyMessage = "该交易日暂无游资每日明细";
  else if (payload.meta?.status === "partial") emptyMessage = `当日有 ${number(payload.summary?.official_stock_count)} 只股票上榜，但暂无可识别的游资明细`;
  if (!traders.some((item) => item.id === state.selectedDragonTraderId)) {
    state.selectedDragonTraderId = traders[0]?.id || "";
  }
  const cardMarkup = traders.map((trader, index) => {
    const description = trader.description || `${number(trader.stock_count)} 只股票，${number(trader.operation_count)} 笔操作`;
    return `
      <article class="dragon-trader-card dealing ${trader.id === state.selectedDragonTraderId ? "selected" : ""}" data-dragon-card="${escapeHtml(trader.id)}" aria-hidden="true" style="--deal-delay:${Math.min(index * 38, 650)}ms">
        <span class="dragon-card-rank">${String(index + 1).padStart(2, "0")}</span>
        <span class="dragon-card-monogram">${escapeHtml(trader.name.slice(0, 2))}</span>
        <span class="dragon-card-copy"><strong>${escapeHtml(trader.name)}</strong><q title="${escapeHtml(description)}">${escapeHtml(description)}</q></span>
        <span class="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>
      </article>`;
  }).join("");
  const hitZoneMarkup = traders.map((trader) => `
    <button type="button" class="dragon-card-hit-zone" data-dragon-trader="${escapeHtml(trader.id)}" aria-label="查看 ${escapeHtml(trader.name)} 当日操作" aria-pressed="${trader.id === state.selectedDragonTraderId}"></button>
  `).join("");
  container.innerHTML = traders.length
    ? `${cardMarkup}<div class="dragon-card-hit-layer">${hitZoneMarkup}</div>`
    : emptyStateHtml(state.dragonFilter === "unclassified" ? "待归类席位请在下方管理" : emptyMessage, { className: "dragon-empty" });
  container.querySelectorAll("[data-dragon-card]").forEach((card) => {
    card.addEventListener("animationend", () => card.classList.remove("dealing"), { once: true });
  });
  container.querySelectorAll("[data-dragon-trader]").forEach((hitZone) => {
    const setHovered = (hovered) => {
      container.querySelector(`[data-dragon-card="${CSS.escape(hitZone.dataset.dragonTrader)}"]`)?.classList.toggle("hovered", hovered);
    };
    hitZone.addEventListener("pointerenter", () => setHovered(true));
    hitZone.addEventListener("pointerleave", () => setHovered(false));
    hitZone.addEventListener("focus", () => setHovered(true));
    hitZone.addEventListener("blur", () => setHovered(false));
    hitZone.addEventListener("click", () => {
      state.selectedDragonTraderId = hitZone.dataset.dragonTrader;
      container.querySelectorAll("[data-dragon-card]").forEach((card) => {
        card.classList.toggle("selected", card.dataset.dragonCard === state.selectedDragonTraderId);
      });
      container.querySelectorAll("[data-dragon-trader]").forEach((item) => {
        item.setAttribute("aria-pressed", String(item.dataset.dragonTrader === state.selectedDragonTraderId));
      });
      renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId));
    });
  });
  requestAnimationFrame(() => layoutDragonCards(container));
  renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId));
}

function layoutDragonCards(container = document.querySelector("#dragonTraderList")) {
  if (!container) return;
  const cards = [...container.querySelectorAll(".dragon-trader-card")];
  const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")];
  if (!cards.length) return;
  const compact = window.innerWidth <= 720;
  const cardWidth = compact ? 148 : 176;
  const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72));
  const spread = Math.min(available - cardWidth, compact ? 310 : 1050);
  const step = cards.length > 1 ? Math.min(cardWidth + 14, spread / (cards.length - 1)) : 0;
  const center = (cards.length - 1) / 2;
  container.style.setProperty("--dragon-card-width", `${cardWidth}px`);
  cards.forEach((card, index) => {
    const x = (index - center) * step;
    card.style.setProperty("--card-x", `${x.toFixed(2)}px`);
    card.style.setProperty("--card-rotation", "0deg");
    card.style.setProperty("--card-y", "0px");
    card.style.zIndex = String(index + 1);
    const hitZone = hitZones[index];
    if (hitZone) {
      const zoneWidth = index === cards.length - 1 ? cardWidth : Math.max(18, step);
      hitZone.style.left = `calc(50% + ${(x - cardWidth / 2).toFixed(2)}px)`;
      hitZone.style.width = `${zoneWidth.toFixed(2)}px`;
    }
  });
}

function renderDragonTraderDetail(trader) {
  const container = document.querySelector("#dragonTraderDetail");
  if (!trader) {
    container.hidden = true;
    renderEmptyState(container, "选择一位游资查看操作明细", { className: "dragon-empty" });
    return;
  }
  container.hidden = false;
  container.innerHTML = `
    <header class="dragon-detail-header">
      <div><span>当日操作明细</span><h3>${escapeHtml(trader.name)}</h3><p>${escapeHtml(trader.description || "按当日公开龙虎榜席位汇总")}</p></div>
      <dl><div><dt>买入</dt><dd class="up">${formatMoneyMillion(trader.buy_million)}</dd></div><div><dt>卖出</dt><dd class="down">${formatMoneyMillion(trader.sell_million)}</dd></div><div><dt>净额</dt><dd class="${changeClass(trader.net_buy_million)}">${formatMoneyMillion(trader.net_buy_million)}</dd></div></dl>
    </header>
    <div class="trader-operations table-frame tbl-wrap">
      <table class="data-table tbl dragon-operation-table">
        <colgroup><col class="dragon-col-index"><col class="dragon-col-stock"><col class="dragon-col-direction"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-seat"><col class="dragon-col-reason"></colgroup>
        <thead><tr><th class="row-number num">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅（%）</th><th class="number num">买入（百万）</th><th class="number num">卖出（百万）</th><th class="number num">净额（百万）</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
        <tbody>${(trader.operations || []).map((operation, index) => `
          <tr data-code="${escapeHtml(operation.code)}">
            <td class="row-number num">${index + 1}</td>
            <td><strong class="sname">${escapeHtml(operation.name)}</strong><span class="scode">${escapeHtml(operation.code)}</span></td>
            <td><span class="direction-label ${changeClass(operation.net_buy_million)}">${escapeHtml(operation.direction)}</span></td>
            <td class="number num ${operation.change == null ? "" : changeClass(operation.change)}">${operation.change == null ? "" : signed(operation.change)}</td>
            <td class="number num">${operation.buy_million == null ? "" : formatNumber(operation.buy_million, 2)}</td>
            <td class="number num">${operation.sell_million == null ? "" : formatNumber(operation.sell_million, 2)}</td>
            <td class="number num ${operation.net_buy_million == null ? "" : changeClass(operation.net_buy_million)}">${operation.net_buy_million == null ? "" : signed(operation.net_buy_million)}</td>
            <td class="seat-cell" title="${escapeHtml(operation.seat_name)}">${escapeHtml(operation.seat_name)}</td>
            <td class="reason-column" title="${escapeHtml([operation.tag, operation.reason].filter((item) => item && item !== "--").join(" · "))}">${escapeHtml(operation.tag && operation.tag !== "--" ? operation.tag : operation.reason && operation.reason !== "--" ? operation.reason : "")}</td>
          </tr>`).join("")}</tbody>
      </table>
    </div>`;
  bindStockRows(container);
  markAutoSortableHeaders(container);
}

function renderUnclassifiedSeats() {
  const seats = state.dragonTiger?.unclassified_seats || [];
  const canManage = state.user?.role === "admin";
  document.querySelector("#dragonUnclassifiedSection").hidden = !canManage || seats.length === 0;
  document.querySelector("#dragonUnclassifiedFilter").hidden = !canManage || seats.length === 0;
  if (!seats.length && state.dragonFilter === "unclassified") {
    state.dragonFilter = "all";
    document.querySelectorAll("[data-dragon-filter]").forEach((button) => {
      button.classList.toggle("active", button.dataset.dragonFilter === "all");
    });
    renderDragonTraderList();
  }
  setText("unclassifiedCount", `${seats.length} 个`);
  const list = document.querySelector("#unclassifiedSeatList");
  list.innerHTML = seats.map((seat, index) => `
    <form class="unclassified-seat-row" data-unclassified-index="${index}">
      <span class="unclassified-seat-name" title="${escapeHtml(seat.seat_name)}">${escapeHtml(seat.seat_name)}</span>
      <span class="unclassified-seat-stats">${number(seat.operation_count)} 笔 · ${number(seat.stock_count)} 股</span>
      <strong class="${changeClass(seat.net_buy_million)}">${formatMoneyMillion(seat.net_buy_million)}</strong>
      <input type="text" maxlength="50" placeholder="输入游资名" aria-label="${escapeHtml(seat.seat_name)}的游资名" required>
      <button class="button" type="submit">归类</button>
    </form>
  `).join("") || emptyStateHtml("当前席位均已归类");
  list.querySelectorAll(".unclassified-seat-row").forEach((form) => {
    form.addEventListener("submit", saveSeatAlias);
  });
}

function dragonIdentityLabel(type) {
  return { trader: "游资", institution: "机构", channel: "通道", unclassified: "待归类" }[type] || "席位";
}

async function saveSeatAlias(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const seat = state.dragonTiger?.unclassified_seats?.[number(form.dataset.unclassifiedIndex)];
  const alias = form.querySelector("input").value.trim();
  if (!seat || !alias) {
    showToast("请输入游资名");
    return;
  }
  const button = form.querySelector("button");
  button.disabled = true;
  try {
    await apiRequest("/api/seat-aliases", "POST", { seat_name: seat.seat_name, alias });
    state.dragonTiger = null;
    await loadDragonTiger();
    showToast(`已将席位归类为 ${alias}`);
  } catch (error) {
    showToast(error.message);
    button.disabled = false;
  }
}

async function loadReviewWorkspace() {
  try {
    const [watchlistPayload, notesPayload, tradesPayload] = await Promise.all([
      apiRequest(`/api/watchlist?trade_date=${encodeURIComponent(elements.tradeDate.value)}`),
      apiRequest("/api/notes?scope=daily"),
      apiRequest("/api/trades"),
    ]);
    state.watchlist = watchlistPayload.items || [];
    state.notes = notesPayload.items || [];
    state.tradeEntries = tradesPayload.items || [];
    state.tradeSummary = tradesPayload.summary || {};
    setText("reviewDataDate", displayCompactDate(elements.tradeDate.value));
    renderWatchlist();
    renderNotesHistory(state.notes, document.querySelector("#notesHistory"), false);
    setText("notesCount", `${state.notes.length} 条`);
    renderTradeLog();
    populateJournalForm();
  } catch (error) {
    showToast(error.message || "我的复盘加载失败");
  }
}

function renderWatchlist() {
  setText("watchlistCount", `${state.watchlist.length} 只`);
  const body = document.querySelector("#watchlistTableBody");
  body.innerHTML = state.watchlist.map((item) => `
    <tr data-code="${escapeHtml(item.code)}"><td><span class="review-watch-mark ${escapeHtml(item.color)}" title="${escapeHtml(item.color)}">★</span></td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(item.name)}</strong><small class="stock-code scode">${escapeHtml(item.code)}</small></span></td>
      <td>${escapeHtml(item.sector || "其他")}</td>
      <td class="number num ${item.change == null ? "" : changeClass(item.change)}">${formatWatchMetric(item.change)}</td>
      <td class="number num ${item.return_5d == null ? "" : changeClass(item.return_5d)}">${formatWatchMetric(item.return_5d)}</td>
      <td class="number num"><strong class="watch-attention-score">${item.attention_score == null ? "" : formatNumber(item.attention_score, 1)}</strong></td>
      <td><span class="watch-remark" title="${escapeHtml(item.remark || "尚未填写跟踪备注")}">${escapeHtml(item.remark || "尚未填写")}</span></td>
      <td><span class="review-row-actions"><button class="table-action" type="button" data-watch-remark="${escapeHtml(item.code)}">备注</button>
      <button class="table-action down" type="button" data-watch-delete="${escapeHtml(item.code)}" aria-label="移除 ${escapeHtml(item.name)}">移除</button></span></td></tr>
  `).join("");
  document.querySelector("#watchlistEmpty").hidden = state.watchlist.length > 0;
  body.querySelectorAll("[data-watch-remark]").forEach((button) => {
    button.addEventListener("click", () => {
      const item = state.watchlist.find((row) => row.code === button.dataset.watchRemark);
      openWatchlistDialog(item);
    });
  });
  body.querySelectorAll("[data-watch-delete]").forEach((button) => {
    button.addEventListener("click", () => removeWatchlist(button.dataset.watchDelete));
  });
  bindStockRows(body);
}

function formatWatchMetric(value) {
  if (value == null || !Number.isFinite(Number(value))) return "";
  return signed(value);
}

function openWatchlistDialog(item = null) {
  clearTimeout(watchlistSearchTimer);
  state.watchlistSelection = item ? {
    code: item.code,
    name: item.name,
    sector: item.sector || "其他",
    color: item.color || "red",
  } : null;
  state.watchlistSearchResults = [];
  setText("watchlistDialogTitle", item ? "编辑跟踪备注" : "添加自选");
  document.querySelector("#watchlistRemark").value = item?.remark || "";
  document.querySelector("#watchlistSearchInput").value = "";
  document.querySelector("#watchlistSearchResults").innerHTML = "";
  syncWatchlistSelection(Boolean(item));
  openModalDialog(elements.watchlistDialog);
  requestAnimationFrame(() => (item ? document.querySelector("#watchlistRemark") : document.querySelector("#watchlistSearchInput")).focus());
}

function closeWatchlistDialog() {
  clearTimeout(watchlistSearchTimer);
  if (elements.watchlistDialog.open) elements.watchlistDialog.close();
}

function clearWatchlistSelection() {
  state.watchlistSelection = null;
  syncWatchlistSelection(false);
  document.querySelector("#watchlistSearchInput").focus();
}

function syncWatchlistSelection(editing = false) {
  const item = state.watchlistSelection;
  document.querySelector("#watchlistSearchField").hidden = Boolean(item);
  document.querySelector("#watchlistSelection").hidden = !item;
  document.querySelector("#changeWatchlistSelection").hidden = editing;
  document.querySelector("#saveWatchlist").disabled = !item;
  if (!item) return;
  setText("watchlistSelectionName", item.name || "--");
  setText("watchlistSelectionCode", item.code || "--");
  setText("watchlistSelectionSector", item.sector || "其他");
  refreshIcons();
}

function scheduleWatchlistSearch() {
  clearTimeout(watchlistSearchTimer);
  const query = document.querySelector("#watchlistSearchInput").value.trim();
  if (!query) {
    document.querySelector("#watchlistSearchResults").innerHTML = "";
    return;
  }
  document.querySelector("#watchlistSearchResults").innerHTML = '<div class="watchlist-search-status">正在查找股票</div>';
  watchlistSearchTimer = setTimeout(() => runWatchlistSearch(query), 160);
}

async function runWatchlistSearch(query) {
  const sequence = ++state.watchlistSearchRequestSequence;
  try {
    const params = new URLSearchParams({ q: query, trade_date: elements.tradeDate.value });
    const payload = await apiRequest(`/api/search?${params}`);
    if (sequence !== state.watchlistSearchRequestSequence) return;
    state.watchlistSearchResults = payload.groups?.stocks || [];
    document.querySelector("#watchlistSearchResults").innerHTML = state.watchlistSearchResults.map((item, index) => `
      <button type="button" data-watchlist-result="${index}"><span><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.industry || "其他")}</small></span><b>${escapeHtml(item.code)}</b></button>
    `).join("") || '<div class="watchlist-search-status">没有找到匹配的股票</div>';
  } catch (error) {
    document.querySelector("#watchlistSearchResults").innerHTML = `<div class="watchlist-search-status">${escapeHtml(error.message || "搜索失败")}</div>`;
  }
}

function handleWatchlistSearchResult(event) {
  const button = event.target.closest("[data-watchlist-result]");
  if (!button) return;
  const item = state.watchlistSearchResults[number(button.dataset.watchlistResult)];
  if (!item) return;
  state.watchlistSelection = {
    code: item.code,
    name: item.name,
    sector: item.industry || "其他",
    color: "red",
  };
  syncWatchlistSelection(false);
}

async function saveWatchlistFromDialog(event) {
  event.preventDefault();
  const item = state.watchlistSelection;
  if (!item) return;
  const button = document.querySelector("#saveWatchlist");
  button.disabled = true;
  try {
    await apiRequest("/api/watchlist", "POST", {
      code: item.code,
      name: item.name,
      sector: item.sector || "其他",
      color: item.color || "red",
      remark: document.querySelector("#watchlistRemark").value.trim(),
    });
    closeWatchlistDialog();
    await loadReviewWorkspace();
    showToast(state.watchlist.some((row) => row.code === item.code) ? "自选跟踪已保存" : "已加入自选");
  } catch (error) {
    showToast(error.message || "自选保存失败");
    button.disabled = false;
  }
}

async function toggleActiveWatchlist() {
  const stock = state.activeStock;
  if (!stock?.code) return;
  const isWatched = Boolean(state.stockDetail?.stock?.watchlist || state.watchlist.some((item) => item.code === stock.code));
  try {
    if (isWatched) {
      await apiRequest(`/api/watchlist/${stock.code}`, "DELETE");
      state.watchlist = state.watchlist.filter((item) => item.code !== stock.code);
      if (state.stockDetail?.stock) state.stockDetail.stock.watchlist = null;
      showToast("已移出自选");
    } else {
      const payload = await apiRequest("/api/watchlist", "POST", {
        code: stock.code,
        name: stock.name || "--",
        sector: stock.sector || "其他",
        color: "red",
      });
      state.watchlist = payload.items || state.watchlist;
      if (state.stockDetail?.stock) state.stockDetail.stock.watchlist = state.watchlist.find((item) => item.code === stock.code);
      showToast("已加入自选");
    }
    updateWatchButton();
    renderWatchlist();
  } catch (error) {
    showToast(error.message);
  }
}

function updateWatchButton() {
  const code = state.activeStock?.code;
  const watched = Boolean(state.stockDetail?.stock?.watchlist || state.watchlist.some((item) => item.code === code));
  setText("watchStockButton", watched ? "移出自选" : "加入自选");
}

async function removeWatchlist(code) {
  try {
    await apiRequest(`/api/watchlist/${code}`, "DELETE");
    state.watchlist = state.watchlist.filter((item) => item.code !== code);
    renderWatchlist();
    showToast("已移出自选");
  } catch (error) {
    showToast(error.message);
  }
}

async function saveJournal(event) {
  event.preventDefault();
  try {
    await apiRequest("/api/notes", "POST", {
      trade_date: document.querySelector("#journalDate").value,
      id: state.editingDailyNoteId || undefined,
      summary: document.querySelector("#journalSummary").value,
      content: document.querySelector("#journalContent").value,
      plan: document.querySelector("#journalPlan").value,
    });
    await loadReviewWorkspace();
    showToast("每日复盘已保存");
  } catch (error) {
    showToast(error.message);
  }
}

function populateJournalForm() {
  const selectedDate = document.querySelector("#journalDate").value.replaceAll("-", "");
  const note = state.notes.find((item) => String(item.trade_date).replaceAll("-", "") === selectedDate);
  state.editingDailyNoteId = number(note?.id);
  document.querySelector("#journalSummary").value = note?.summary || "";
  document.querySelector("#journalContent").value = note?.content || "";
  document.querySelector("#journalPlan").value = note?.plan || "";
}

function openTradeLogDialog() {
  resetTradeLogForm();
  openModalDialog(elements.tradeLogDialog);
  requestAnimationFrame(() => document.querySelector("#tradeLogCode").focus());
}

function closeTradeLogDialog() {
  if (elements.tradeLogDialog.open) elements.tradeLogDialog.close();
  else resetTradeLogForm();
}

async function saveTradeLog(event) {
  event.preventDefault();
  const button = document.querySelector("#saveTradeLog");
  button.disabled = true;
  try {
    const payload = await apiRequest("/api/trades", "POST", {
      id: state.editingTradeId || undefined,
      trade_date: document.querySelector("#tradeLogDate").value,
      code: document.querySelector("#tradeLogCode").value.trim(),
      name: document.querySelector("#tradeLogName").value.trim(),
      action: document.querySelector("#tradeLogAction").value,
      price: document.querySelector("#tradeLogPrice").value,
      quantity: document.querySelector("#tradeLogQuantity").value,
      position_pct: document.querySelector("#tradeLogPosition").value,
      pnl_amount: document.querySelector("#tradeLogPnlAmount").value,
      pnl_pct: document.querySelector("#tradeLogPnlPct").value,
      emotion: document.querySelector("#tradeLogEmotion").value,
      tags: document.querySelector("#tradeLogTags").value,
      thesis: document.querySelector("#tradeLogThesis").value,
      execution: document.querySelector("#tradeLogExecution").value,
    });
    state.tradeEntries = payload.items || [];
    state.tradeSummary = payload.summary || {};
    renderTradeLog();
    closeTradeLogDialog();
    showToast("交易记录已保存");
  } catch (error) {
    showToast(error.message || "交易记录保存失败");
  } finally {
    button.disabled = false;
  }
}

function resetTradeLogForm() {
  state.editingTradeId = 0;
  document.querySelector("#tradeLogForm").reset();
  document.querySelector("#tradeLogDate").value = elements.tradeDate.value || todayString();
  document.querySelector("#tradeLogQuantity").value = "0";
  document.querySelector("#tradeLogPosition").value = "0";
  setText("tradeLogDialogTitle", "交易日志");
  setText("saveTradeLog", "保存交易");
}

function editTradeLog(id) {
  const item = state.tradeEntries.find((entry) => number(entry.id) === id);
  if (!item) return;
  state.editingTradeId = id;
  document.querySelector("#tradeLogDate").value = displayCompactDate(item.trade_date);
  document.querySelector("#tradeLogCode").value = item.code;
  document.querySelector("#tradeLogName").value = item.name;
  document.querySelector("#tradeLogAction").value = item.action;
  document.querySelector("#tradeLogPrice").value = item.price;
  document.querySelector("#tradeLogQuantity").value = item.quantity;
  document.querySelector("#tradeLogPosition").value = item.position_pct;
  document.querySelector("#tradeLogPnlAmount").value = item.pnl_amount ?? "";
  document.querySelector("#tradeLogPnlPct").value = item.pnl_pct ?? "";
  document.querySelector("#tradeLogEmotion").value = item.emotion;
  document.querySelector("#tradeLogTags").value = (item.tags || []).join(", ");
  document.querySelector("#tradeLogThesis").value = item.thesis || "";
  document.querySelector("#tradeLogExecution").value = item.execution || "";
  setText("tradeLogDialogTitle", "编辑交易日志");
  setText("saveTradeLog", "保存修改");
  openModalDialog(elements.tradeLogDialog);
  requestAnimationFrame(() => document.querySelector("#tradeLogCode").focus());
}

async function handleTradeLogAction(event) {
  const button = event.target.closest("[data-trade-action]");
  if (!button) return;
  const id = number(button.dataset.tradeId);
  if (button.dataset.tradeAction === "edit") {
    editTradeLog(id);
    return;
  }
  if (!window.confirm("确定删除这条交易记录吗？")) return;
  try {
    const payload = await apiRequest(`/api/trades/${id}`, "DELETE");
    state.tradeEntries = payload.items || [];
    state.tradeSummary = payload.summary || {};
    if (state.editingTradeId === id) resetTradeLogForm();
    renderTradeLog();
    showToast("交易记录已删除");
  } catch (error) {
    showToast(error.message || "交易记录删除失败");
  }
}

function renderTradeLog() {
  const summary = state.tradeSummary || {};
  setText("tradeLogCount", `${state.tradeEntries.length} 条`);
  document.querySelector("#tradeLogSummary").innerHTML = [
    ["记录", `${number(summary.total)} 条`],
    ["已实现", `${number(summary.realized)} 条`],
    ["胜率", summary.win_rate == null ? "--" : `${formatNumber(summary.win_rate, 1)}%`],
    ["累计盈亏", summary.pnl_amount == null ? "--" : `${number(summary.pnl_amount) > 0 ? "+" : ""}${formatNumber(summary.pnl_amount, 2)}`],
    ["平均仓位", summary.average_position == null ? "--" : `${formatNumber(summary.average_position, 1)}%`],
  ].map(([label, value]) => `<div><span>${label}</span><strong>${value}</strong></div>`).join("");
  document.querySelector("#tradeLogEmpty").hidden = state.tradeEntries.length > 0;
  document.querySelector("#tradeLogTableBody").innerHTML = state.tradeEntries.map((item) => `
    <tr data-code="${escapeHtml(item.code)}">
      <td>${displayCompactDate(item.trade_date)}</td>
      <td><span class="stock-cell"><strong class="sname">${escapeHtml(item.name)}</strong><small class="stock-code scode">${escapeHtml(item.code)}</small></span></td>
      <td><span class="trade-action trade-action-${escapeHtml(item.action)}">${escapeHtml(item.action_label)}</span></td>
      <td class="number num">${item.position_pct == null ? "" : formatNumber(item.position_pct, 1)}</td>
      <td class="number num ${item.pnl_pct == null ? "" : changeClass(item.pnl_pct)}">${item.pnl_pct == null ? "" : signed(item.pnl_pct)}</td>
      <td class="number num ${item.pnl_amount == null ? "" : changeClass(item.pnl_amount)}">${item.pnl_amount == null ? "" : signed(item.pnl_amount)}</td>
      <td><span class="trade-emotion">${escapeHtml(item.emotion_label)}</span><div class="trade-tags">${(item.tags || []).map((tag) => `<em>${escapeHtml(tag)}</em>`).join("")}</div></td>
      <td class="trade-copy" title="交易逻辑：${escapeHtml(item.thesis || "")}；执行复核：${escapeHtml(item.execution || "")}"><strong>${escapeHtml(item.thesis || "")}</strong><small>${escapeHtml(item.execution || "尚未填写执行复核")}</small></td>
      <td><div class="trade-row-actions"><button class="table-action" type="button" data-trade-action="edit" data-trade-id="${number(item.id)}">编辑</button><button class="table-action down" type="button" data-trade-action="delete" data-trade-id="${number(item.id)}">删除</button></div></td>
    </tr>
  `).join("");
  bindStockRows(document.querySelector("#tradeLogTableBody"));
}

async function saveStockNote(event) {
  event.preventDefault();
  if (!state.activeStock?.code) return;
  try {
    await apiRequest("/api/notes", "POST", {
      code: state.activeStock.code,
      stock_name: state.activeStock.name || "--",
      trade_date: elements.tradeDate.value,
      content: document.querySelector("#stockNoteContent").value,
      plan: document.querySelector("#stockNotePlan").value,
    });
    document.querySelector("#stockNoteContent").value = "";
    document.querySelector("#stockNotePlan").value = "";
    const payload = await apiRequest(`/api/notes?scope=stock&code=${encodeURIComponent(state.activeStock.code)}`);
    state.stockDetail.notes = payload.items || [];
    renderStockNotes(state.stockDetail.notes);
    showToast("个股笔记已保存");
  } catch (error) {
    showToast(error.message);
  }
}

async function saveReasonOverride(event) {
  event.preventDefault();
  if (!state.activeStock?.code) return;
  const reason = document.querySelector("#reasonInput").value.trim();
  try {
    await apiRequest("/api/reasons", "POST", {
      trade_date: elements.tradeDate.value,
      code: state.activeStock.code,
      reason,
    });
    state.activeStock.reason = reason;
    for (const key of ["limits", "broken", "down_limits"]) {
      const row = state.dashboard?.[key]?.find((item) => item.code === state.activeStock.code);
      if (row) row.reason = reason;
    }
    setText("detailReason", reason);
    renderDashboard();
    showToast("事件逻辑已修订");
  } catch (error) {
    showToast(error.message);
  }
}

function renderMoneyflow(flow) {
  for (const [id, value] of [["flowNet", flow.net_million], ["flowLarge", flow.large_million], ["flowMedium", flow.medium_million], ["flowSmall", flow.small_million]]) {
    const element = document.getElementById(id);
    element.textContent = formatMoneyMillion(value);
    element.className = changeClass(value);
  }
}

function renderStockNotes(notes) {
  renderNotesHistory(notes, document.querySelector("#stockNotes"), true);
}

function renderNotesHistory(notes, container, compact) {
  container.innerHTML = notes.map((note) => `
    <article class="note-row">
      <div><time>${displayCompactDate(note.trade_date)}</time>${note.stock_name ? `<small>${escapeHtml(note.stock_name)}</small>` : ""}</div>
      ${!compact ? `<div class="note-block note-summary"><strong>盘面</strong><p>${escapeHtml(note.summary || "--")}</p></div>` : ""}
      <div class="note-block"><strong>复盘</strong><p>${escapeHtml(note.content || "--")}</p></div>
      <div class="note-block"><strong>计划</strong><p>${escapeHtml(note.plan || "--")}</p></div>
      <button class="table-action down" type="button" data-note-delete="${number(note.id)}">删除</button>
    </article>
  `).join("") || emptyStateHtml("暂无复盘记录");
  container.querySelectorAll("[data-note-delete]").forEach((button) => {
    button.addEventListener("click", () => deleteNote(number(button.dataset.noteDelete), compact));
  });
}

async function deleteNote(noteId, compact) {
  try {
    await apiRequest(`/api/notes/${noteId}`, "DELETE");
    if (compact && state.activeStock) {
      state.stockDetail.notes = state.stockDetail.notes.filter((note) => number(note.id) !== noteId);
      renderStockNotes(state.stockDetail.notes);
    } else {
      await loadReviewWorkspace();
    }
    showToast("笔记已删除");
  } catch (error) {
    showToast(error.message);
  }
}

async function backfillData() {
  const button = document.querySelector("#backfillButton");
  button.disabled = true;
  setLoading(true, "正在回补历史交易日");
  try {
    const payload = await apiRequest("/api/backfill", "POST", {
      start_date: document.querySelector("#backfillStart").value,
      end_date: document.querySelector("#backfillEnd").value,
    });
    showToast(`历史回补完成，共处理 ${payload.results.length} 个工作日`);
    await openAdminSettings(true);
  } catch (error) {
    showToast(error.message);
  } finally {
    setLoading(false);
    button.disabled = false;
  }
}

function screenerStrategyKey(strategyId, strategyName) {
  return strategyId != null && strategyId !== 0 ? `id:${strategyId}` : `name:${strategyName || ""}`;
}

function currentScreenerStrategy(mode) {
  if (mode === "curated") return activeCuratedStrategy();
  if (mode === "smart") return state.selectedStrategy;
  return null;
}

function screenerResultContext(mode, result, { regime, strategyId = null, strategyName = "" } = {}) {
  const normalizedMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
  return {
    mode: normalizedMode,
    regime: regime || result?.meta?.regime || state.selectedRegime,
    strategyName: strategyName || result?.meta?.strategy_name || "",
    strategyKey: screenerStrategyKey(
      strategyId,
      strategyName || result?.meta?.strategy_name || "",
    ),
  };
}

function screenerResultKey(context) {
  if (!context) return "";
  if (context.mode === "quant") return "quant";
  if (context.mode === "curated") return JSON.stringify(["curated", context.strategyKey]);
  return JSON.stringify(["smart", context.regime, context.strategyKey]);
}

function selectedScreenerResultKey(mode) {
  const normalizedMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
  if (normalizedMode === "quant") return "quant";
  const strategy = currentScreenerStrategy(mode);
  if (!strategy) return "";
  return screenerResultKey(screenerResultContext(normalizedMode, null, {
    regime: state.selectedRegime,
    strategyId: strategy.id,
    strategyName: strategy.name,
  }));
}

function activeScreenerResultEntry(mode = state.screenerMode) {
  const key = selectedScreenerResultKey(mode);
  return key ? state.screenerResultStore[key] || null : null;
}

function screenerResultMatchesSelection(mode) {
  return Boolean(activeScreenerResultEntry(mode));
}

function activeScreenerResult(mode = state.screenerMode) {
  return activeScreenerResultEntry(mode)?.result || null;
}

function activeScreenerResultContext(mode = state.screenerMode) {
  return activeScreenerResultEntry(mode)?.context || null;
}

function storeScreenerResult(
  mode,
  result,
  { regime, strategyId = null, strategyName = "" } = {},
  updateLatest = true,
) {
  const normalizedMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
  const context = result
    ? screenerResultContext(normalizedMode, result, { regime, strategyId, strategyName })
    : null;
  const key = screenerResultKey(context);
  if (key && result) state.screenerResultStore[key] = { result, context };
  if (updateLatest) {
    state.screenerResults[normalizedMode] = result || null;
    state.screenerResultContexts[normalizedMode] = context;
  }
}

function setScreenerResult(mode, result, options = {}) {
  storeScreenerResult(mode, result, options, true);
}

function applyScreenerSetup(payload, requestKey) {
  const dateChanged = Boolean(state.screenerSetupKey && state.screenerSetupKey !== requestKey);
  if (dateChanged) {
    state.screenerResults = { smart: null, curated: null, quant: null };
    state.screenerResultContexts = { smart: null, curated: null, quant: null };
    state.screenerResultStore = {};
  }
  state.screenerSetup = payload;
  state.screenerSetupKey = requestKey;

  const latestResults = { ...(payload.latest_results || {}) };
  if (!latestResults.smart && payload.latest_result) latestResults.smart = payload.latest_result;
  const smartLatestMeta = latestResults.smart?.meta || {};
  const curatedLatestMeta = latestResults.curated?.meta || {};
  state.selectedRegime = payload.regime.id;

  const selectedId = state.selectedStrategy?.id;
  const smartStrategies = payload.strategies.filter((item) => item.formula?.meta?.library !== "curated");
  const curatedStrategies = payload.strategies.filter((item) => item.formula?.meta?.library === "curated");
  const latestSmartStrategy = smartStrategies.find((item) => item.name === smartLatestMeta.strategy_name);
  state.selectedStrategy = smartStrategies.find((item) => item.id === selectedId)
    || latestSmartStrategy
    || smartStrategies.find((item) => item.regimes.includes(state.selectedRegime))
    || smartStrategies[0]
    || null;

  if (!curatedStrategies.some((item) => item.id === state.selectedCuratedStrategyId) || dateChanged) {
    state.selectedCuratedStrategyId = curatedStrategies.find(
      (item) => item.name === curatedLatestMeta.strategy_name,
    )?.id || curatedStrategies[0]?.id || 0;
  }

  for (const result of [...(payload.recent_results || [])].reverse()) {
    const mode = ["smart", "curated", "quant"].includes(result.meta?.mode)
      ? result.meta.mode
      : "smart";
    const strategies = mode === "curated" ? curatedStrategies : smartStrategies;
    const strategy = strategies.find((item) => item.name === result.meta?.strategy_name);
    storeScreenerResult(mode, result, {
      regime: result.meta?.regime || payload.regime.id,
      strategyId: strategy?.id,
      strategyName: result.meta?.strategy_name || strategy?.name || "",
    }, false);
  }

  for (const mode of ["smart", "curated", "quant"]) {
    if (state.screenerResults[mode] || !latestResults[mode]) continue;
    const result = latestResults[mode];
    const meta = result.meta || {};
    const strategy = mode === "curated"
      ? curatedStrategies.find((item) => item.name === meta.strategy_name)
      : mode === "smart"
        ? smartStrategies.find((item) => item.name === meta.strategy_name)
        : null;
    setScreenerResult(mode, result, {
      regime: meta.regime || payload.regime.id,
      strategyId: strategy?.id,
      strategyName: meta.strategy_name || strategy?.name || "",
    });
  }
  if (!state.quantScores.length) resetQuantBuilder(false);
  renderScreenerSetup();
  renderScreenerResult();
}

async function loadScreenerSetup(force = false) {
  const requestKey = elements.tradeDate.value.replaceAll("-", "");
  if (!force && state.screenerSetup && state.screenerSetupKey === requestKey) {
    renderScreenerSetup();
    renderScreenerResult();
    return state.screenerSetup;
  }
  if (!force && state.screenerSetupPromise && state.screenerSetupRequestKey === requestKey) {
    return state.screenerSetupPromise;
  }
  const request = (async () => {
    try {
      const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
      const payload = await apiRequest(`/api/screener/setup?${query}`);
      applyScreenerSetup(payload, requestKey);
      await loadScreenerTracking();
      return payload;
    } catch (error) {
      showToast(error.message || "选股配置加载失败");
      return null;
    } finally {
      if (state.screenerSetupPromise === request) {
        state.screenerSetupPromise = null;
        state.screenerSetupRequestKey = "";
      }
    }
  })();
  state.screenerSetupRequestKey = requestKey;
  state.screenerSetupPromise = request;
  return request;
}

function renderScreenerSetup() {
  const setup = state.screenerSetup;
  if (!setup) return;
  setText("screenerDateLabel", `数据日期 ${displayCompactDate(setup.trade_date)}`);
  setText("regimeLabel", setup.regime.label);
  setText("regimeConfidence", `置信度 ${formatNumber(setup.regime.confidence, 0)}%`);
  setText("regimeStepStatus", `${setup.regime.label} · 置信度 ${formatNumber(setup.regime.confidence, 0)}%`);
  setText("regimeReason", setup.regime.reason);
  const evidence = (setup.regime.evidence || []).filter(Boolean);
  if (!evidence.some((item) => String(item).includes("情绪温度"))) {
    const temperature = formatNumber(state.dashboard?.overview?.sentiment_score, 0);
    const direction = state.dashboard?.overview?.sentiment_direction;
    evidence.unshift(`情绪温度 ${temperature}${direction ? `，较前一交易日${direction}` : ""}`);
  }
  document.querySelector("#regimeEvidenceList").textContent = evidence.join(" · ");
  setText("factorDateCount", `${number(setup.factor_data.date_count)} 日`);
  setText("factorDateRange", setup.factor_data.ready
    ? `${displayCompactDate(setup.factor_data.start_date)} 至 ${displayCompactDate(setup.factor_data.end_date)} · 竞价 ${number(setup.factor_data.auction_date_count)} 日`
    : "尚未达到 21 个交易日");
  setText("factorTaskStatus", setup.factor_data.ready ? `已就绪 · ${number(setup.factor_data.date_count)} 日` : "需要同步");
  setText("compilerStatus", "策略生成已就绪");
  setText(
    "screenerRunStatus",
    activeScreenerResult("smart") ? `已有结果 · ${(activeScreenerResult("smart").candidates || []).length} 只` : "等待执行",
  );
  setText("strategyCount", `${setup.strategies.filter((item) => item.formula?.meta?.library !== "curated").length} 套`);
  updateBacktestTaskStatus();
  selectScreenerMobileView(state.screenerMobileView);

  const selector = document.querySelector("#regimeSelector");
  selector.innerHTML = setup.regimes.map((item) => `
    <span class="regime-option ${item.id === state.selectedRegime ? "active" : ""}">${escapeHtml(item.label)}</span>
  `).join("");
  renderStrategyList();
  renderStrategySummary();
  renderScreenerMode();
  renderCuratedStrategyLibrary();
  renderQuantBuilder();
  renderScreenerProgress();
}

function renderStrategySummary() {
  const strategy = state.selectedStrategy;
  setText("activeStrategyHeading", strategy?.name || "--");
  setText("activeStrategyEditorHeading", strategy?.name || "--");
  setText("activeStrategyDescription", strategy?.description || "等待匹配当前市场阶段的策略。");
  setText("strategyStepStatus", strategy?.name || "等待匹配");
  document.querySelector("#activeStrategyRegimes").innerHTML = strategy
    ? `${strategy.regimes.map((item) => `<b>${escapeHtml(regimeLabel(item))}</b>`).join("")}<b class="neutral">${strategy.builtin ? "内置" : "自定义"}</b>`
    : "";
}

function selectScreenerMode(mode) {
  state.screenerMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
  localStorage.setItem("xiaobaiScreenerMode", state.screenerMode);
  state.screenerMobileView = "strategy";
  renderScreenerMode();
  selectScreenerMobileView("strategy");
}

function renderScreenerMode() {
  const mode = state.screenerMode || "smart";
  document.querySelectorAll("[data-screener-mode]").forEach((button) => {
    const active = button.dataset.screenerMode === mode;
    button.classList.toggle("active", active);
    button.setAttribute("aria-selected", String(active));
  });
  document.querySelectorAll("[data-screener-panel]").forEach((panel) => {
    panel.hidden = panel.dataset.screenerPanel !== mode;
  });
  const results = document.querySelector("#screenerView .screener-results-view");
  const resultsSlot = document.querySelector(`[data-screener-results-slot="${mode}"]`);
  if (results && resultsSlot && results.parentElement !== resultsSlot) resultsSlot.append(results);
  const titles = { smart: "盘后候选结果", curated: "策略候选结果", quant: "自定义选股结果" };
  setText("screenerResultTitle", titles[mode]);
  renderScreenerResult();
}

function curatedStrategies() {
  return (state.screenerSetup?.strategies || []).filter((item) => item.formula?.meta?.library === "curated");
}

function activeCuratedStrategy() {
  const strategies = curatedStrategies();
  return strategies.find((item) => item.id === state.selectedCuratedStrategyId) || strategies[0] || null;
}

function curatedStrategySchool(strategy) {
  const category = String(strategy?.formula?.meta?.category || "");
  if (["红利价值", "质量价值", "现金流价值", "成长质量", "小盘质量"].includes(category)) return "基本面";
  if (["行业轮动", "形态突破", "趋势追踪"].includes(category)) return "趋势";
  if (["短线竞价", "连板接力", "低吸反核"].includes(category)) return "短线";
  if (["动量反转"].includes(category)) return "动量";
  if (["元策略", "多因子"].includes(category)) return "量化";
  if (["业绩事件", "热度观察"].includes(category)) return "事件";
  if (["资金席位"].includes(category)) return "资金";
  if (/红利|价值|质量|成长|财务|现金流/.test(category)) return "基本面";
  if (/趋势|轮动|突破/.test(category)) return "趋势";
  if (/竞价|连板|龙头|反核|首阴|反包|打板/.test(category)) return "短线";
  if (/动量|反转/.test(category)) return "动量";
  if (/因子|量化|元策略/.test(category)) return "量化";
  if (/事件|热度|公告|业绩/.test(category)) return "事件";
  if (/席位|资金/.test(category)) return "资金";
  return "其他";
}

function curatedSchoolIcon(school) {
  return {
    基本面: "circle-dollar-sign", 趋势: "trending-up", 短线: "zap",
    动量: "refresh-cw", 量化: "binary", 事件: "calendar-clock", 资金: "landmark", 其他: "boxes",
  }[school] || "boxes";
}

function curatedStrategyRunState(strategy, result) {
  const missingData = strategy?.missing_data || [];
  if (!strategy?.data_ready || missingData.length) {
    return { label: "数据不足", className: "missing", verifiedEmpty: false };
  }
  if (!result) return { label: "等待盘后", className: "pending", verifiedEmpty: false };
  const count = (result.candidates || []).length;
  if (count) return { label: `${count} 只候选`, className: "ready", verifiedEmpty: false };
  return { label: "暂无信号", className: "quiet", verifiedEmpty: true };
}

function renderCuratedStrategyLibrary() {
  if (!state.screenerSetup) return;
  const strategies = curatedStrategies();
  const categories = ["全部", ...new Set(strategies.map((item) => item.formula?.meta?.category || "其他"))];
  const schools = ["全部", "基本面", "趋势", "短线", "动量", "量化", "事件", "资金"];
  if (!categories.includes(state.curatedCategory)) state.curatedCategory = "全部";
  if (!schools.includes(state.curatedSchool)) state.curatedSchool = "全部";
  setText("curatedStrategyCount", `${strategies.length} 套`);
  const categorySelect = document.querySelector("#curatedCategoryFilter");
  categorySelect.innerHTML = categories.map((category) => `
    <option value="${escapeHtml(category)}" ${category === state.curatedCategory ? "selected" : ""}>${escapeHtml(category)}</option>
  `).join("");
  document.querySelector("#curatedSchoolFilters").innerHTML = schools.map((school) => {
    const count = school === "全部" ? strategies.length : strategies.filter((item) => curatedStrategySchool(item) === school).length;
    return `<button class="${school === state.curatedSchool ? "active" : ""}" type="button" data-curated-school="${escapeHtml(school)}" aria-pressed="${school === state.curatedSchool}">${escapeHtml(school)}<small>${count}</small></button>`;
  }).join("");
  document.querySelectorAll("[data-curated-view]").forEach((button) => {
    const active = button.dataset.curatedView === state.curatedViewMode;
    button.classList.toggle("active", active);
    button.setAttribute("aria-pressed", String(active));
  });
  const query = state.curatedQuery;
  const visible = strategies.filter((item) => {
    const meta = item.formula?.meta || {};
    const categoryMatch = state.curatedCategory === "全部" || meta.category === state.curatedCategory;
    const school = curatedStrategySchool(item);
    const schoolMatch = state.curatedSchool === "全部" || school === state.curatedSchool;
    const queryMatch = !query || `${item.name} ${item.description} ${meta.category} ${school} ${meta.suitable_environment} ${meta.failure_risk}`.toLocaleLowerCase("zh-CN").includes(query);
    return categoryMatch && schoolMatch && queryMatch;
  });
  const list = document.querySelector("#curatedStrategyList");
  list.classList.toggle("is-grid", state.curatedViewMode === "grid");
  list.innerHTML = visible.length ? visible.map((strategy) => {
    const meta = strategy.formula?.meta || {};
    const school = curatedStrategySchool(strategy);
    const rank = strategies.findIndex((item) => item.id === strategy.id) + 1;
    const resultKey = screenerResultKey(screenerResultContext("curated", null, {
      regime: strategy.regimes[0] || state.selectedRegime,
      strategyId: strategy.id,
      strategyName: strategy.name,
    }));
    const result = state.screenerResultStore[resultKey]?.result;
    const runState = curatedStrategyRunState(strategy, result);
    return `<article class="curated-strategy-card ${strategy.id === activeCuratedStrategy()?.id ? "active" : ""}" data-curated-strategy="${strategy.id}">
      <span class="curated-strategy-icon" aria-hidden="true"><i data-lucide="${curatedSchoolIcon(school)}"></i></span>
      <span class="curated-card-head"><i class="curated-strategy-rank">${String(rank).padStart(2, "0")}</i><span><strong>${escapeHtml(strategy.name)}</strong><small>${escapeHtml(school)} · ${escapeHtml(meta.category || "策略")}</small></span><em class="curated-card-result ${runState.className}">${escapeHtml(runState.label)}</em></span>
      <span class="curated-card-tags"><em>${escapeHtml(meta.quality || "--")}</em><em>${escapeHtml(meta.frequency || "--")}</em><em>风险 ${escapeHtml(meta.risk || "--")}</em></span>
    </article>`;
  }).join("") : emptyStateHtml("没有符合条件的策略");
  renderCuratedStrategyDetail();
}

function renderCuratedStrategyDetail() {
  const strategy = activeCuratedStrategy();
  if (!strategy) return;
  const formula = strategy.formula || {};
  const meta = formula.meta || {};
  const result = activeScreenerResult("curated");
  const resultMeta = result?.meta || {};
  const health = resultMeta.health || {};
  const runState = curatedStrategyRunState(strategy, result);
  setText("curatedStrategyCategory", meta.category || "精选策略");
  setText("curatedStrategyName", strategy.name);
  setText("curatedStrategyDescription", strategy.description);
  document.querySelector("#curatedStrategyBadges").innerHTML = [
    `质量 ${meta.quality || "--"}`, meta.frequency || "--", `风险 ${meta.risk || "--"}`,
    meta.data_group || "行情因子",
  ].map((value) => `<span>${escapeHtml(value)}</span>`).join("");
  setText("curatedSuitableEnvironment", meta.suitable_environment || "以策略条件为准");
  setText("curatedFailureRisk", meta.failure_risk || "策略可能随市场结构变化而失效");
  const filters = formula.filters || [];
  setText("curatedFilterCount", `${filters.length} 项`);
  document.querySelector("#curatedFilterList").innerHTML = filters.map((item) => `
    <div class="curated-rule-row"><span>${escapeHtml(factorLabel(item.field))}</span><strong>${escapeHtml(formatRuleValue(item))}</strong></div>
  `).join("");
  const scores = formula.score || [];
  const total = scores.reduce((sum, item) => sum + number(item.weight), 0) || 1;
  setText("curatedWeightTotal", `${formatNumber(total * 100, 0)}%`);
  document.querySelector("#curatedScoreList").innerHTML = scores.map((item) => {
    const percent = number(item.weight) / total * 100;
    return `<div class="curated-score-row"><span>${escapeHtml(factorLabel(item.field))}</span><span class="curated-score-track"><i style="width:${Math.min(100, percent)}%"></i></span><strong>${formatNumber(percent, 0)}%</strong></div>`;
  }).join("");
  const candidateCount = (result?.candidates || []).length;
  const statusLabel = runState.className === "ready" ? "运行正常" : runState.label;
  const statusClass = runState.className;
  let updatedLabel = "--";
  if (resultMeta.updated_at) {
    const updated = new Date(resultMeta.updated_at);
    if (!Number.isNaN(updated.getTime())) {
      updatedLabel = `${String(updated.getMonth() + 1).padStart(2, "0")}-${String(updated.getDate()).padStart(2, "0")} ${updated.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false })}`;
    }
  }
  document.querySelector("#curatedHealthMetrics").innerHTML = [
    ["运行状态", statusLabel, statusClass],
    ["当日信号", result ? `${candidateCount} 只` : "--", ""],
    ["字段覆盖", health.coverage != null ? `${formatNumber(health.coverage, 1)}%` : strategy.data_ready ? "数据已就绪" : "--", ""],
    ["最近更新", updatedLabel, ""],
  ].map(([label, value, className]) => `<div><span>${escapeHtml(label)}</span><strong class="${className}">${escapeHtml(value)}</strong></div>`).join("");
  const status = document.querySelector("#curatedDataStatus");
  status.classList.toggle("missing", statusClass === "missing");
  status.innerHTML = strategy.data_ready
    ? `<i data-lucide="${runState.verifiedEmpty ? "circle-check" : "database"}"></i><span><strong>${runState.verifiedEmpty ? "本日暂无信号" : "盘后自动更新"}</strong><small>${runState.verifiedEmpty ? `必需数据已完整，本日没有股票同时满足 ${filters.length} 项准入条件` : result ? health.required_field_count != null ? `已核验 ${number(health.required_field_count)} 项因子 · ${number(health.complete_rows)} 只股票` : "盘后定格结果已载入" : "等待当日行情定格后生成"}</small></span>`
    : `<i data-lucide="circle-alert"></i><span><strong>数据尚未完备</strong><small>${escapeHtml((strategy.missing_data || []).join("、") || "等待后台同步")}</small></span>`;
  refreshIcons();
}

function factorLabel(field) {
  return state.screenerSetup?.factor_fields?.find((item) => item.id === field)?.label || field;
}

function formatRuleValue(item) {
  const operator = { between: "介于", ">=": "不低于", "<=": "不高于", ">": "高于", "<": "低于", "==": "等于" }[item.op] || item.op;
  const value = Array.isArray(item.value) ? item.value.join(" ～ ") : item.value;
  return `${operator} ${value}`;
}

function groupedFactorOptions(selected = "") {
  return (state.screenerSetup?.factor_groups || []).map((group) => `
    <optgroup label="${escapeHtml(group.name)}">${group.fields.map((field) => `<option value="${escapeHtml(field.id)}" ${field.id === selected ? "selected" : ""}>${escapeHtml(field.label)}</option>`).join("")}</optgroup>
  `).join("");
}

function quantId() {
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function resetQuantBuilder(render = true) {
  state.quantFilters = [
    { id: quantId(), field: "amount_billion", op: ">=", value: "1" },
    { id: quantId(), field: "above_ma20", op: "==", value: "1" },
  ];
  state.quantScores = [
    { id: quantId(), field: "relative_strength", weight: 30, direction: "desc" },
    { id: quantId(), field: "sector_strength", weight: 25, direction: "desc" },
    { id: quantId(), field: "volume_ratio_5d", weight: 20, direction: "desc" },
    { id: quantId(), field: "amount_billion", weight: 15, direction: "desc" },
    { id: quantId(), field: "volatility_10d", weight: 10, direction: "asc" },
  ];
  if (render) renderQuantBuilder();
}

function addQuantFilter() {
  const used = new Set(state.quantFilters.map((item) => item.field));
  const field = state.screenerSetup.factor_fields.find((item) => !used.has(item.id))?.id || "pct_chg";
  state.quantFilters.push({ id: quantId(), field, op: ">=", value: "0" });
  renderQuantBuilder();
}

function addQuantScore() {
  const used = new Set(state.quantScores.map((item) => item.field));
  const field = state.screenerSetup.factor_fields.find((item) => !used.has(item.id))?.id || "pct_chg";
  state.quantScores.push({ id: quantId(), field, weight: 10, direction: "desc" });
  renderQuantBuilder();
}

function renderQuantBuilder() {
  if (!state.screenerSetup) return;
  document.querySelector("#quantFilterRows").innerHTML = state.quantFilters.map((item) => `
    <div class="quant-rule-row quant-filter-row" data-quant-filter="${item.id}">
      <select aria-label="过滤因子" data-quant-key="field">${groupedFactorOptions(item.field)}</select>
      <select aria-label="比较方式" data-quant-key="op">${(state.screenerSetup.operators || []).map((op) => `<option value="${op}" ${op === item.op ? "selected" : ""}>${escapeHtml({ between: "区间", ">=": "≥", "<=": "≤", ">": ">", "<": "<", "==": "=" }[op] || op)}</option>`).join("")}</select>
      <input class="quant-value-input" data-quant-key="value" aria-label="条件值" type="text" value="${escapeHtml(item.value)}" placeholder="区间用逗号分隔">
      <button class="quant-remove-button" type="button" data-quant-action="remove" aria-label="删除过滤条件"><i data-lucide="trash-2"></i></button>
    </div>
  `).join("");
  document.querySelector("#quantScoreRows").innerHTML = state.quantScores.map((item) => `
    <div class="quant-rule-row quant-score-row" data-quant-score="${item.id}">
      <span class="quant-factor-identity"><i aria-hidden="true">✓</i><span><select aria-label="评分因子" data-quant-key="field">${groupedFactorOptions(item.field)}</select><button type="button" data-quant-action="direction" data-direction="${item.direction === "desc" ? "asc" : "desc"}">${item.direction === "desc" ? "数值越高越优" : "数值越低越优"}</button></span></span>
      <label class="quant-weight-control">
        <span class="visually-hidden">权重百分比</span>
        <input data-quant-key="weight" aria-label="权重百分比" type="range" min="1" max="100" step="1" value="${number(item.weight)}">
        <output>${number(item.weight)}%</output>
      </label>
      <button class="quant-remove-button" type="button" data-quant-action="remove" aria-label="删除评分因子"><i data-lucide="trash-2"></i></button>
    </div>
  `).join("");
  renderQuantSummary();
  refreshIcons();
}

function handleQuantBuilderInput(event) {
  const row = event.target.closest("[data-quant-filter], [data-quant-score]");
  const key = event.target.dataset.quantKey;
  if (!row || !key) return;
  const collection = row.dataset.quantFilter ? state.quantFilters : state.quantScores;
  const id = row.dataset.quantFilter || row.dataset.quantScore;
  const item = collection.find((entry) => entry.id === id);
  if (!item) return;
  item[key] = key === "weight" ? number(event.target.value) : event.target.value;
  if (key === "weight") {
    const output = event.target.closest(".quant-weight-control")?.querySelector("output");
    if (output) output.textContent = `${number(event.target.value)}%`;
  }
  renderQuantSummary();
}

function handleQuantBuilderClick(event) {
  const button = event.target.closest("[data-quant-action]");
  if (!button) return;
  const row = button.closest("[data-quant-filter], [data-quant-score]");
  const isFilter = Boolean(row?.dataset.quantFilter);
  const id = row?.dataset.quantFilter || row?.dataset.quantScore;
  const collection = isFilter ? state.quantFilters : state.quantScores;
  const item = collection.find((entry) => entry.id === id);
  if (button.dataset.quantAction === "remove") {
    if (!isFilter && collection.length <= 1) {
      showToast("至少保留一个评分因子");
      return;
    }
    const index = collection.findIndex((entry) => entry.id === id);
    if (index >= 0) collection.splice(index, 1);
    renderQuantBuilder();
  } else if (button.dataset.quantAction === "direction" && item) {
    item.direction = button.dataset.direction;
    renderQuantBuilder();
  }
}

function buildQuantFormula() {
  const filters = state.quantFilters.map((item) => {
    let value;
    if (item.op === "between") {
      value = String(item.value).split(/[,，~～]/).map((part) => Number(part.trim()));
      if (value.length !== 2 || value.some((part) => !Number.isFinite(part))) throw new Error(`${factorLabel(item.field)}需要两个有效区间值`);
      if (value[0] > value[1]) value.reverse();
    } else {
      value = Number(item.value);
      if (!Number.isFinite(value)) throw new Error(`${factorLabel(item.field)}的条件值无效`);
    }
    return { field: item.field, op: item.op, value };
  });
  const score = state.quantScores.map((item) => {
    const weight = number(item.weight) / 100;
    if (weight <= 0 || weight > 1) throw new Error(`${factorLabel(item.field)}的权重应为1%至100%`);
    return { field: item.field, weight, direction: item.direction };
  });
  return {
    meta: { library: "custom", category: "量化公式", frequency: "按需", risk: "自定义", data_group: "组合因子" },
    universe: {
      exclude_st: document.querySelector("#quantExcludeSt").checked,
      listed_days_min: Math.max(0, Math.min(5000, number(document.querySelector("#quantListedDays").value))),
    },
    filters,
    score,
    limit: Math.max(1, Math.min(50, number(document.querySelector("#quantLimit").value))),
    min_score: Math.max(0, Math.min(1, number(document.querySelector("#quantMinScore").value) / 100)),
  };
}

function formulaMissingData(formula) {
  const health = state.screenerSetup?.factor_data?.health || {};
  const fields = new Set([...(formula.filters || []), ...(formula.score || [])].map((item) => item.field));
  const missing = [];
  if (!state.screenerSetup?.factor_data?.ready) missing.push("基础行情");
  if (["pe_ttm", "pb", "ps_ttm", "dividend_yield_ttm", "total_mv_billion"].some((field) => fields.has(field)) && !health.valuation) missing.push("估值数据");
  if (["roe", "roa", "roic", "gross_margin", "netprofit_yoy", "revenue_yoy", "ocf_to_opincome"].some((field) => fields.has(field)) && !health.fundamental) missing.push("财务质量");
  if (fields.has("dividend_years") && !health.dividend_history) missing.push("历年分红");
  if (["auction_change", "auction_amount_million", "auction_turnover_rate", "auction_volume_ratio"].some((field) => fields.has(field)) && !health.auction) missing.push("竞价数据");
  return missing;
}

function renderQuantSummary() {
  if (!state.screenerSetup) return;
  const total = state.quantScores.reduce((sum, item) => sum + number(item.weight), 0);
  setText("quantWeightTotal", `${formatNumber(total, 0)}%`);
  const bar = document.querySelector("#quantWeightBar");
  bar.style.width = `${Math.min(100, total)}%`;
  bar.style.background = Math.abs(total - 100) < 0.01 ? "#2563eb" : "#d97706";
  const message = document.querySelector("#quantValidationMessage");
  try {
    const formula = buildQuantFormula();
    const missing = formulaMissingData(formula);
    message.classList.toggle("error", Boolean(missing.length));
    message.textContent = missing.length ? `需要先同步：${missing.join("、")}` : "公式有效，可执行并生成逐股贡献解释。";
    document.querySelector("#quantRunButton").disabled = Boolean(missing.length);
  } catch (error) {
    message.classList.add("error");
    message.textContent = error.message;
    document.querySelector("#quantRunButton").disabled = true;
  }
}

function renderScreenerProgress() {
  const hasSetup = Boolean(state.screenerSetup?.regime);
  const hasStrategy = Boolean(state.selectedStrategy);
  const hasResult = Boolean(activeScreenerResult("smart"));
  const states = {
    regime: hasSetup ? "complete" : "current",
    strategy: hasStrategy ? "complete" : hasSetup ? "current" : "pending",
    run: state.screenerRunning ? "current" : hasResult ? "complete" : hasStrategy ? "current" : "pending",
    result: hasResult ? "current" : "pending",
  };
  const steps = [...document.querySelectorAll("[data-screener-step]")];
  steps.forEach((step, index) => {
    const status = states[step.dataset.screenerStep] || "pending";
    step.dataset.state = status;
    if (status === "current") step.setAttribute("aria-current", "step");
    else step.removeAttribute("aria-current");
    const line = step.nextElementSibling;
    if (line?.classList.contains("step-line")) line.classList.toggle("complete", status === "complete" && index < steps.length - 1);
  });
}

function openStrategyDrawer(target = "editor") {
  const drawer = document.querySelector("#strategyDrawer");
  openModalDialog(drawer);
  requestAnimationFrame(() => {
    const focusTarget = target === "library"
      ? document.querySelector("#strategyList .strategy-item.active") || document.querySelector("#strategyList .strategy-item")
      : document.querySelector("#strategyNameInput");
    focusTarget?.focus();
  });
}

function openCustomStrategyDrawer() {
  if (!state.customStrategyDraft) {
    state.customStrategyDraft = {
      id: null,
      builtin: false,
      name: "自定义选股策略",
      description: "",
      regimes: [state.selectedRegime],
      formula: buildQuantFormula(),
    };
  }
  populateStrategyEditor(state.customStrategyDraft);
  renderStrategyList();
  openStrategyDrawer("editor");
}

function selectScreenerMobileView(view) {
  state.screenerMobileView = view === "results" ? "results" : "strategy";
  const workspace = document.querySelector("#screenerView");
  workspace.classList.toggle("mobile-strategy", state.screenerMobileView === "strategy");
  workspace.classList.toggle("mobile-results", state.screenerMobileView === "results");
  document.querySelectorAll("[data-screener-mobile-view]").forEach((button) => {
    const active = button.dataset.screenerMobileView === state.screenerMobileView;
    button.classList.toggle("active", active);
    button.setAttribute("aria-selected", String(active));
  });
}

function updateBacktestTaskStatus() {
  setText("backtestTaskStatus", activeScreenerResult("smart") ? "结果已归档" : "等待盘后生成");
  renderScreenerProgress();
}

function selectRegime(regime) {
  state.selectedRegime = regime;
  const recommended = state.screenerSetup.strategies.find((item) => item.formula?.meta?.library !== "curated" && item.regimes.includes(regime));
  if (recommended) state.selectedStrategy = recommended;
  renderScreenerSetup();
}

function renderStrategyList() {
  const list = document.querySelector("#strategyList");
  const strategies = state.screenerSetup.strategies.filter((item) => !item.builtin && item.formula?.meta?.library !== "curated");
  list.innerHTML = strategies.map((strategy) => `
    <button type="button" class="strategy-item ${strategy.id === state.customStrategyDraft?.id ? "active" : ""}" data-strategy-id="${strategy.id}">
      <strong>${escapeHtml(strategy.name)}</strong><span>${escapeHtml(strategy.description || "--")}</span>
      <small>${strategy.regimes.map((item) => regimeLabel(item)).join(" / ")}</small>
    </button>
  `).join("") || emptyStateHtml("暂无已保存的自定义公式");
  list.querySelectorAll("[data-strategy-id]").forEach((button) => {
    button.addEventListener("click", () => {
      state.customStrategyDraft = state.screenerSetup.strategies.find((item) => item.id === number(button.dataset.strategyId));
      populateStrategyEditor(state.customStrategyDraft);
      renderStrategyList();
    });
  });
}

function populateStrategyEditor(strategy) {
  const deleteButton = document.querySelector("#deleteStrategyButton");
  deleteButton.hidden = !strategy?.id || Boolean(strategy.builtin);
  if (!strategy) {
    setText("activeStrategyEditorHeading", "--");
    return;
  }
  setText("activeStrategyEditorHeading", strategy.name || "未命名策略");
  document.querySelector("#strategyNameInput").value = strategy.name || "";
  document.querySelector("#strategyDescriptionInput").value = strategy.description || "";
  document.querySelector("#strategyPrompt").value = strategy.builtin ? strategy.description || "" : document.querySelector("#strategyPrompt").value;
  document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2);
}

async function compileStrategy() {
  const prompt = document.querySelector("#strategyPrompt").value.trim();
  const button = document.querySelector("#compileStrategyButton");
  button.disabled = true;
  setText("compilerStatus", "正在编译");
  setStatus("正在编译选股策略");
  try {
    const payload = await apiRequest("/api/screener/compile", "POST", {
      prompt,
      regime: state.selectedRegime,
    });
    const strategy = payload.strategy;
    state.customStrategyDraft = { ...strategy, id: null, builtin: false };
    document.querySelector("#deleteStrategyButton").hidden = true;
    document.querySelector("#strategyNameInput").value = strategy.name;
    document.querySelector("#strategyDescriptionInput").value = strategy.description;
    document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2);
    setText("compilerStatus", "策略生成完成");
    if (strategy.notice) showToast(strategy.notice);
    setStatus("选股策略已编译");
  } catch (error) {
    showToast(error.message);
    setStatus("策略编译失败");
    setText("compilerStatus", "编译失败");
  } finally {
    button.disabled = false;
  }
}

async function saveCurrentStrategy() {
  try {
    const formula = parseFormulaEditor();
    const payload = await apiRequest("/api/screener/strategies", "POST", {
      name: document.querySelector("#strategyNameInput").value,
      description: document.querySelector("#strategyDescriptionInput").value,
      regimes: [state.selectedRegime],
      formula,
    });
    state.screenerSetup.strategies = payload.strategies;
    state.customStrategyDraft = payload.strategies.find((item) => item.id === payload.id);
    renderStrategyList();
    populateStrategyEditor(state.customStrategyDraft);
    showToast("自定义策略已保存");
  } catch (error) {
    showToast(error.message);
  }
}

async function deleteCurrentStrategy() {
  const strategy = state.customStrategyDraft;
  if (!strategy?.id || strategy.builtin) {
    showToast("只能删除已保存的自定义策略");
    return;
  }
  if (!window.confirm(`确定删除策略“${strategy.name}”吗？此操作不可撤销。`)) return;

  const button = document.querySelector("#deleteStrategyButton");
  button.disabled = true;
  try {
    const payload = await apiRequest(`/api/screener/strategies/${strategy.id}`, "DELETE");
    state.screenerSetup.strategies = payload.strategies;
    state.customStrategyDraft = null;
    renderStrategyList();
    openCustomStrategyDrawer();
    showToast("自定义策略已删除");
  } catch (error) {
    showToast(error.message || "策略删除失败");
  } finally {
    button.disabled = false;
  }
}

async function runQuantStrategy() {
  let formula;
  try {
    formula = buildQuantFormula();
  } catch (error) {
    showToast(error.message);
    return;
  }
  await executeScreenerFormula({
    mode: "quant",
    formula,
    strategyName: "自定义选股公式",
    regime: state.selectedRegime,
    runBacktest: document.querySelector("#quantBacktestToggle").checked,
    button: document.querySelector("#quantRunButton"),
    loadingText: "正在执行自定义公式并计算因子贡献",
  });
}

function saveQuantAsStrategy() {
  try {
    const formula = buildQuantFormula();
    state.customStrategyDraft = {
      id: null,
      builtin: false,
      name: "自定义选股策略",
      description: "由自定义因子工作台生成，可在高级公式中继续调整。",
      regimes: [state.selectedRegime],
      formula,
    };
    populateStrategyEditor(state.customStrategyDraft);
    document.querySelector("#strategyPrompt").value = "自定义因子工作台生成的选股公式";
    openStrategyDrawer("editor");
  } catch (error) {
    showToast(error.message);
  }
}

async function executeScreenerFormula({ mode, formula, strategyName, strategyId = null, regime, runBacktest, button, loadingText }) {
  if (!state.screenerSetup?.factor_data?.ready) {
    showToast("请先同步至少 21 个交易日的因子数据");
    return;
  }
  const missing = formulaMissingData(formula);
  if (missing.length) {
    showToast(`请先同步${missing.join("、")}`);
    return;
  }
  const executionMode = ["smart", "curated", "quant"].includes(mode) ? mode : state.screenerMode;
  button.disabled = true;
  state.screenerRunning = true;
  state.screenerRunningMode = executionMode;
  if (executionMode === "smart") {
    setText("screenerRunStatus", "正在计算");
    setText("backtestTaskStatus", runBacktest ? "正在回测" : "本次不执行");
  }
  setLoading(true, loadingText, "screener");
  setStatus(`正在执行${strategyName}`);
  try {
    const payload = await apiRequest("/api/screener/run", "POST", {
      trade_date: elements.tradeDate.value,
      regime,
      strategy_name: strategyName,
      formula,
      mode: executionMode,
      run_backtest: runBacktest,
    });
    setScreenerResult(executionMode, payload.result, { regime, strategyId, strategyName });
    renderScreenerResult();
    if (executionMode === "smart") setText("screenerRunStatus", `完成 · ${payload.result.candidates.length} 只`);
    updateBacktestTaskStatus();
    if (window.innerWidth <= 720) selectScreenerMobileView("results");
    setStatus(`${strategyName}完成 · ${payload.result.candidates.length} 只候选`);
  } catch (error) {
    showToast(error.message);
    setStatus("选股执行失败");
    if (executionMode === "smart") setText("screenerRunStatus", "执行失败");
    updateBacktestTaskStatus();
  } finally {
    state.screenerRunning = false;
    state.screenerRunningMode = "";
    setLoading(false);
    button.disabled = false;
    updateBacktestTaskStatus();
  }
}

async function loadMentorSetup(force = false) {
  const requestedDate = elements.tradeDate.value.replaceAll("-", "");
  if (!force && state.mentorSetup?.requestedDate === requestedDate) {
    renderMentorWorkspace();
    return;
  }
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    const payload = await apiRequest(`/api/mentors/setup?${query}`);
    payload.requestedDate = requestedDate;
    if (!payload.preferences_configured) {
      payload.mentors.sort((first, second) => {
        if (Boolean(first.private) !== Boolean(second.private)) return first.private ? -1 : 1;
        return String(first.name || "").localeCompare(String(second.name || ""), "zh-CN");
      });
      payload.mentors.forEach((mentor, index) => { mentor.sort_order = index; });
    }
    state.mentorSetup = payload;
    const selectedExists = payload.mentors.some((item) => item.id === state.selectedMentorId);
    state.selectedMentorId = selectedExists ? state.selectedMentorId : payload.mentors[0]?.id || "";
    state.mentorMessages = await loadMentorMessages();
    renderMentorWorkspace();
  } catch (error) {
    showMentorNotice(error.message || "问师模块加载失败");
    showToast(error.message || "问师模块加载失败");
  }
}

function renderMentorWorkspace() {
  const setup = state.mentorSetup;
  if (!setup) return;
  const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null;
  setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
  setText("activeMentorName", selected?.name || "--");
  setText("mobileActiveMentorName", selected?.name || "选择思维模型");
  document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
  setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
  document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
    .map((item) => `<span>${escapeHtml(item)}</span>`).join("");
  renderMentorDirectory();
  renderMentorMessages();
}

function renderMentorDirectory() {
  const mentors = state.mentorSetup?.mentors || [];
  const query = state.mentorQuery;
  const filtered = mentors.filter((mentor) => {
    if (state.mentorSortMode) return true;
    if (state.mentorGrade !== "all" && mentor.evidence?.grade !== state.mentorGrade) return false;
    if (!query) return true;
    const haystack = [
      mentor.name,
      mentor.description,
      mentor.tagline,
      mentor.evidence?.label,
      mentor.evidence?.note,
      ...(mentor.focus || []),
    ].filter(Boolean).join(" ").toLocaleLowerCase("zh-CN");
    return haystack.includes(query);
  });
  setText("mentorCount", filtered.length === mentors.length ? `${mentors.length} 位` : `${filtered.length} / ${mentors.length} 位`);
  const sortToggle = document.querySelector("#mentorSortToggle");
  sortToggle.classList.toggle("active", state.mentorSortMode);
  sortToggle.setAttribute("aria-pressed", String(state.mentorSortMode));
  sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理";
  document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode;
  document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode;
  document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
    button.disabled = state.mentorSortMode;
  });
  const container = document.querySelector("#mentorList");
  container.classList.toggle("is-sorting", state.mentorSortMode);
  container.innerHTML = filtered.map((mentor) => {
    const group = mentors.filter((item) => Boolean(item.pinned) === Boolean(mentor.pinned));
    const groupIndex = group.findIndex((item) => item.id === mentor.id);
    return `
      <article class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""} ${mentor.pinned ? "is-pinned" : ""}"
        data-mentor-card="${escapeHtml(mentor.id)}" draggable="${state.mentorSortMode && !state.mentorSavingPreferences}">
        <button type="button" class="mentor-option-main" data-mentor-id="${escapeHtml(mentor.id)}" aria-pressed="${mentor.id === state.selectedMentorId}" ${state.mentorLoading ? "disabled" : ""}>
          <span class="mentor-option-copy">
            <span class="mentor-option-heading">
              <strong>${escapeHtml(mentor.name)}</strong>
              <span class="mentor-option-badges">${renderMentorBadges(mentor)}</span>
            </span>
            <em title="${escapeHtml(mentor.description || "")}">${escapeHtml(mentor.description || mentor.tagline || "思维模型")}</em>
            <span class="mentor-option-meta">
              ${mentor.evidence?.label ? `<span class="mentor-evidence-source" title="${escapeHtml(mentor.evidence?.note || "素材说明")}">${escapeHtml(mentor.evidence.label)}</span>` : ""}
              ${(mentor.focus || []).slice(0, 2).map((item) => `<span>#${escapeHtml(item)}</span>`).join("")}
            </span>
          </span>
        </button>
        <span class="mentor-option-tools">
          <button type="button" class="mentor-pin-button ${mentor.pinned ? "active" : ""}" data-mentor-pin="${escapeHtml(mentor.id)}"
            aria-label="${mentor.pinned ? "取消置顶" : "置顶"}${escapeHtml(mentor.name)}" title="${mentor.pinned ? "取消置顶" : "置顶"}" ${state.mentorSavingPreferences ? "disabled" : ""}>
            <i data-lucide="pin"></i>
          </button>
          ${state.mentorSortMode ? `
            <button type="button" class="mentor-order-button" data-mentor-move="up" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="上移${escapeHtml(mentor.name)}" title="上移" ${groupIndex <= 0 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-up"></i></button>
            <button type="button" class="mentor-order-button" data-mentor-move="down" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="下移${escapeHtml(mentor.name)}" title="下移" ${groupIndex >= group.length - 1 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-down"></i></button>
          ` : ""}
        </span>
      </article>
    `;
  }).join("");
  document.querySelector("#mentorListEmpty").hidden = filtered.length > 0;
  document.querySelectorAll("[data-mentor-id]").forEach((button) => {
    button.addEventListener("click", () => selectMentor(button.dataset.mentorId));
  });
  document.querySelectorAll("[data-mentor-pin]").forEach((button) => {
    button.addEventListener("click", () => toggleMentorPin(button.dataset.mentorPin));
  });
  document.querySelectorAll("[data-mentor-move]").forEach((button) => {
    button.addEventListener("click", () => moveMentor(button.dataset.mentorTarget, button.dataset.mentorMove));
  });
  document.querySelectorAll("[data-mentor-card]").forEach((card) => {
    card.addEventListener("dragstart", handleMentorDragStart);
    card.addEventListener("dragover", handleMentorDragOver);
    card.addEventListener("drop", handleMentorDrop);
    card.addEventListener("dragend", clearMentorDragState);
  });
  refreshIcons();
}

function toggleMentorSortMode() {
  state.mentorSortMode = !state.mentorSortMode;
  if (state.mentorSortMode) {
    state.mentorQuery = "";
    state.mentorGrade = "all";
    document.querySelector("#mentorSearchInput").value = "";
    document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
      button.classList.toggle("active", button.dataset.mentorGrade === "all");
    });
  }
  renderMentorDirectory();
}

async function toggleMentorPin(mentorId) {
  if (state.mentorSavingPreferences) return;
  const mentors = state.mentorSetup?.mentors || [];
  const index = mentors.findIndex((item) => item.id === mentorId);
  if (index < 0) return;
  const [mentor] = mentors.splice(index, 1);
  mentor.pinned = !mentor.pinned;
  if (mentor.pinned) {
    mentors.unshift(mentor);
  } else {
    const firstUnpinned = mentors.findIndex((item) => !item.pinned);
    mentors.splice(firstUnpinned < 0 ? mentors.length : firstUnpinned, 0, mentor);
  }
  normalizeMentorOrder();
  renderMentorWorkspace();
  await persistMentorPreferences();
}

async function moveMentor(mentorId, direction) {
  if (state.mentorSavingPreferences) return;
  const mentors = state.mentorSetup?.mentors || [];
  const index = mentors.findIndex((item) => item.id === mentorId);
  if (index < 0) return;
  const step = direction === "up" ? -1 : 1;
  const targetIndex = index + step;
  if (targetIndex < 0 || targetIndex >= mentors.length) return;
  if (Boolean(mentors[index].pinned) !== Boolean(mentors[targetIndex].pinned)) return;
  [mentors[index], mentors[targetIndex]] = [mentors[targetIndex], mentors[index]];
  normalizeMentorOrder();
  renderMentorDirectory();
  await persistMentorPreferences();
}

function handleMentorDragStart(event) {
  if (!state.mentorSortMode || state.mentorSavingPreferences) {
    event.preventDefault();
    return;
  }
  state.mentorDragId = event.currentTarget.dataset.mentorCard || "";
  event.dataTransfer.effectAllowed = "move";
  event.dataTransfer.setData("text/plain", state.mentorDragId);
  event.currentTarget.classList.add("is-dragging");
}

function handleMentorDragOver(event) {
  const source = state.mentorSetup?.mentors.find((item) => item.id === state.mentorDragId);
  const target = state.mentorSetup?.mentors.find((item) => item.id === event.currentTarget.dataset.mentorCard);
  if (!source || !target || Boolean(source.pinned) !== Boolean(target.pinned)) return;
  event.preventDefault();
  event.dataTransfer.dropEffect = "move";
  event.currentTarget.classList.add("is-drag-over");
}

async function handleMentorDrop(event) {
  event.preventDefault();
  const sourceId = state.mentorDragId || event.dataTransfer.getData("text/plain");
  const targetId = event.currentTarget.dataset.mentorCard || "";
  clearMentorDragState();
  if (!sourceId || !targetId || sourceId === targetId) return;
  const mentors = state.mentorSetup?.mentors || [];
  const sourceIndex = mentors.findIndex((item) => item.id === sourceId);
  const targetIndex = mentors.findIndex((item) => item.id === targetId);
  if (sourceIndex < 0 || targetIndex < 0) return;
  if (Boolean(mentors[sourceIndex].pinned) !== Boolean(mentors[targetIndex].pinned)) return;
  const [mentor] = mentors.splice(sourceIndex, 1);
  const insertionIndex = mentors.findIndex((item) => item.id === targetId);
  mentors.splice(insertionIndex, 0, mentor);
  normalizeMentorOrder();
  renderMentorDirectory();
  await persistMentorPreferences();
}

function clearMentorDragState() {
  state.mentorDragId = "";
  document.querySelectorAll(".mentor-option.is-dragging, .mentor-option.is-drag-over").forEach((item) => {
    item.classList.remove("is-dragging", "is-drag-over");
  });
}

function normalizeMentorOrder() {
  (state.mentorSetup?.mentors || []).forEach((mentor, index) => {
    mentor.sort_order = index;
  });
}

async function persistMentorPreferences() {
  const mentors = state.mentorSetup?.mentors || [];
  state.mentorSavingPreferences = true;
  renderMentorDirectory();
  try {
    await apiRequest("/api/mentors/preferences", "POST", {
      order: mentors.map((item) => item.id),
      pinned: mentors.filter((item) => item.pinned).map((item) => item.id),
    });
  } catch (error) {
    showToast(error.message || "问师顺序保存失败");
    await loadMentorSetup(true);
  } finally {
    state.mentorSavingPreferences = false;
    renderMentorDirectory();
  }
}

function renderMentorBadges(mentor, expanded = false) {
  const badges = [];
  if (mentor.private) {
    badges.push('<span class="mentor-badge private" title="仅管理员本人可见"><i data-lucide="lock-keyhole"></i>仅自己</span>');
  }
  const grade = mentor.evidence?.grade;
  if (grade) {
    badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(grade)}</span>`);
  }
  return badges.join("");
}

function toggleMentorDirectory(open) {
  const mobileOpen = Boolean(open) && window.innerWidth <= 720;
  state.mentorDirectoryOpen = mobileOpen;
  const sidebar = document.querySelector("#mentorView .mentor-sidebar");
  const backdrop = document.querySelector("#mentorDirectoryBackdrop");
  const toggle = document.querySelector("#mentorDirectoryToggle");
  sidebar.classList.toggle("is-open", mobileOpen);
  backdrop.hidden = !mobileOpen;
  toggle.setAttribute("aria-expanded", String(mobileOpen));
  document.body.classList.toggle("mentor-directory-open", mobileOpen);
  if (mobileOpen) requestAnimationFrame(() => document.querySelector("#mentorSearchInput").focus());
}

async function selectMentor(mentorId) {
  if (mentorId === state.selectedMentorId) {
    toggleMentorDirectory(false);
    return;
  }
  state.selectedMentorId = mentorId;
  state.mentorMessages = [];
  hideMentorNotice();
  renderMentorWorkspace();
  toggleMentorDirectory(false);
  state.mentorMessages = await loadMentorMessages();
  renderMentorMessages();
}

function renderMentorMessages() {
  const container = document.querySelector("#mentorMessages");
  const selected = state.mentorSetup?.mentors.find((item) => item.id === state.selectedMentorId);
  if (!state.mentorMessages.length && !state.mentorLoading) {
    container.innerHTML = `
      <div class="mentor-empty-state">
        <span class="mentor-empty-mark" aria-hidden="true"><i data-lucide="messages-square"></i></span>
        <strong>向「${escapeHtml(selected?.name || "问师")}」请教</strong>
        <p>${escapeHtml(selected?.tagline || selected?.description || "选择一个问题开始对话")}</p>
      </div>
    `;
    refreshIcons();
  } else {
    container.innerHTML = state.mentorMessages.map((message) => `
      <article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}">
        <div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
        <div class="mentor-message-content">${message.role === "assistant" ? formatMentorAnswer(message.content) : escapeHtml(message.content)}</div>
        ${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
        ${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
      </article>
    `).join("");
    if (state.mentorLoading && !state.mentorMessages.some((message) => message.streaming)) {
      container.insertAdjacentHTML("beforeend", `
        <article class="mentor-message assistant loading-message">
          <div class="mentor-message-label">${escapeHtml(selected?.name || "问师")}</div>
          <p>正在读取复盘数据并推演...</p>
        </article>
      `);
    }
  }
  document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading;
  document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
  document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
  document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
  requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
}

async function sendMentorQuestion(event) {
  event.preventDefault();
  if (state.mentorLoading || !state.selectedMentorId) return;
  const input = document.querySelector("#mentorQuestion");
  const question = input.value.trim();
  if (!question) return;
  const history = state.mentorMessages.slice(-6).map((item) => ({
    role: item.role,
    content: item.content.slice(0, 3500),
  }));
  state.mentorMessages.push({ role: "user", content: question });
  const responseMessage = { role: "assistant", content: "", streaming: true, meta: "" };
  state.mentorMessages.push(responseMessage);
  input.value = "";
  state.mentorLoading = true;
  state.mentorController = new AbortController();
  hideMentorNotice();
  renderMentorMessages();
  renderMentorDirectory();
  setStatus("问师正在读取复盘数据");
  try {
    await streamMentorRequest(
      {
        mentor_id: state.selectedMentorId,
        trade_date: elements.tradeDate.value,
        question,
        history,
      },
      state.mentorController.signal,
      (chunk) => {
        responseMessage.content += chunk;
        scheduleMentorRender();
      },
      (meta) => {
        responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
        if (meta.notice) showMentorNotice(meta.notice);
      },
    );
    responseMessage.streaming = false;
    setStatus("问师回答完成");
  } catch (error) {
    responseMessage.streaming = false;
    responseMessage.error = true;
    if (!responseMessage.content) {
      state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
    }
    showMentorNotice(error.message || "问师回答失败");
    showToast(error.message || "问师回答失败");
    setStatus("问师回答失败");
  } finally {
    state.mentorLoading = false;
    state.mentorController = null;
    renderMentorMessages();
    renderMentorDirectory();
    input.focus();
  }
}

let mentorRenderFrame = 0;

function scheduleMentorRender() {
  if (mentorRenderFrame) return;
  mentorRenderFrame = requestAnimationFrame(() => {
    mentorRenderFrame = 0;
    renderMentorMessages();
  });
}

async function streamMentorRequest(body, signal, onDelta, onMeta) {
  await window.XiaobaiAPI.streamNdjson("/api/mentors/chat", {
    method: "POST",
    body,
    signal,
    errorMessage: "问师暂不可用",
    onEvent: (event) => {
      if (event.type === "delta") onDelta(String(event.content || ""));
      if (event.type === "meta") onMeta(event);
    },
  });
}

function useMentorQuickPrompt(prompt) {
  const input = document.querySelector("#mentorQuestion");
  input.value = prompt || "";
  input.focus();
}

async function clearMentorConversation() {
  if (!state.mentorMessages.length || !window.confirm("确定清空当前老师的对话记录吗？")) return;
  try {
    const query = new URLSearchParams({
      mentor_id: state.selectedMentorId,
      trade_date: state.mentorSetup?.trade_date || elements.tradeDate.value,
    });
    await apiRequest(`/api/mentors/messages?${query}`, "DELETE");
    state.mentorMessages = [];
    hideMentorNotice();
    renderMentorMessages();
  } catch (error) {
    showToast(error.message || "对话记录清空失败");
  }
}

async function loadMentorMessages() {
  if (!state.selectedMentorId) return [];
  try {
    const query = new URLSearchParams({
      mentor_id: state.selectedMentorId,
      trade_date: state.mentorSetup?.trade_date || elements.tradeDate.value,
    });
    const payload = await apiRequest(`/api/mentors/messages?${query}`);
    return (payload.items || []).filter(
      (item) => ["user", "assistant"].includes(item?.role) && typeof item.content === "string",
    ).slice(-100);
  } catch (error) {
    showMentorNotice(error.message || "对话记录加载失败");
    return [];
  }
}

function showMentorNotice(message) {
  const notice = document.querySelector("#mentorNotice");
  notice.textContent = message;
  notice.hidden = false;
}

function hideMentorNotice() {
  document.querySelector("#mentorNotice").hidden = true;
}

function formatMentorAnswer(content) {
  const blocks = [];
  let listType = "";
  let listItems = [];
  const flushList = () => {
    if (!listItems.length) return;
    blocks.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `<li>${item}</li>`).join("")}</${listType}>`);
    listItems = [];
    listType = "";
  };
  String(content || "").replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").split("\n").forEach((rawLine) => {
    const line = rawLine.trim();
    if (!line) {
      flushList();
      return;
    }
    const heading = line.match(/^#{1,3}\s+(.+)$/);
    const bullet = line.match(/^[-*]\s+(.+)$/);
    const ordered = line.match(/^\d+[.、]\s*(.+)$/);
    if (heading) {
      flushList();
      blocks.push(`<strong class="mentor-answer-heading">${formatMentorInline(escapeHtml(heading[1]))}</strong>`);
    } else if (/^-{3,}$/.test(line)) {
      flushList();
      blocks.push('<span class="mentor-answer-rule"></span>');
    } else if (line.startsWith("> ")) {
      flushList();
      blocks.push(`<span class="mentor-answer-quote">${formatMentorInline(escapeHtml(line.slice(2)))}</span>`);
    } else if (bullet || ordered) {
      const nextType = bullet ? "ul" : "ol";
      if (listType && listType !== nextType) flushList();
      listType = nextType;
      listItems.push(formatMentorInline(escapeHtml((bullet || ordered)[1])));
    } else {
      flushList();
      blocks.push(`<p class="mentor-answer-paragraph">${formatMentorInline(escapeHtml(line))}</p>`);
    }
  });
  flushList();
  return blocks.join("");
}

function formatMentorInline(content) {
  return content.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
}

async function loadHeavenSetup(force = false, sector = "", stockCode = "") {
  const calendarDate = document.querySelector("#qiObservationDate")?.value || elements.tradeDate.value;
  const requestedDate = calendarDate.replaceAll("-", "");
  const manualData = state.heavenManualData;
  const calibrationKey = manualData ? JSON.stringify(manualData) : "auto";
  const requestedKey = `${requestedDate}:${sector}:${stockCode}:${calibrationKey}`;
  if (!force && state.heavenSetup?.requestedKey === requestedKey) {
    renderHeavenWorkspace();
    return;
  }
  const requestSequence = ++state.heavenRequestSequence;
  const heavenView = document.querySelector("#heavenView");
  const loadButton = document.querySelector("#loadHeavenSelectionButton");
  const calibrationButtons = [
    document.querySelector("#applyHeavenCalibrationButton"),
    document.querySelector("#resetHeavenCalibrationButton"),
  ].filter(Boolean);
  cancelHeavenPerformance();
  heavenView?.classList.add("heaven-data-loading");
  if (loadButton) loadButton.disabled = true;
  calibrationButtons.forEach((button) => { button.disabled = true; });
  try {
    if (state.heavenSetup?.requestedKey && state.heavenSetup.requestedKey !== requestedKey) {
      state.personalField = null;
    }
    const query = new URLSearchParams({
      trade_date: calendarDate,
    });
    if (sector) query.set("sector", sector);
    if (stockCode) query.set("stock_code", stockCode);
    if (manualData) query.set("manual_data", JSON.stringify(manualData));
    const payload = await apiRequest(`/api/heaven/setup?${query}`);
    if (
      requestSequence !== state.heavenRequestSequence
      || calendarDate !== document.querySelector("#qiObservationDate")?.value
    ) return;
    const previousFocus = state.heavenSetup
      ? `${state.heavenSetup.chart?.sector || ""}:${state.heavenSetup.chart?.stock?.code || ""}`
      : "";
    payload.requestedDate = requestedDate;
    payload.requestedKey = requestedKey;
    state.heavenSetup = payload;
    state.heavenInterpretations.fortune = payload.daily_fortune_reading || "";
    state.heavenManualData = Object.keys(payload.chart?.manual_data || {}).length
      ? payload.chart.manual_data
      : null;
    state.personalField = payload.personal_profile || null;
    state.heavenPerformanceKey = `${requestedKey}:${requestSequence}`;
    state.heavenPerformancePanels = new Set();
    state.heavenPerformanceActive = "";
    const nextFocus = `${payload.chart?.sector || ""}:${payload.chart?.stock?.code || ""}`;
    if (previousFocus && previousFocus !== nextFocus) state.heavenInterpretations.trend = "";
    hideHeavenNotice();
    renderHeavenWorkspace();
    if (payload.chart.selection_notice) showHeavenNotice(payload.chart.selection_notice);
  } catch (error) {
    if (requestSequence !== state.heavenRequestSequence) return;
    showHeavenNotice(error.message || "问天数据加载失败");
    showToast(error.message || "问天数据加载失败");
  } finally {
    if (requestSequence === state.heavenRequestSequence) {
      heavenView?.classList.remove("heaven-data-loading");
      if (loadButton) loadButton.disabled = false;
      calibrationButtons.forEach((button) => { button.disabled = false; });
    }
  }
}

function loadHeavenSelection() {
  const stockCode = document.querySelector("#heavenStockInput").value.trim();
  state.heavenManualData = null;
  loadHeavenSetup(true, "", stockCode);
}

function applyHeavenCalibration(event) {
  event.preventDefault();
  const data = { ...(state.heavenManualData || {}) };
  delete data.note;
  document.querySelectorAll("[data-heaven-manual-field]").forEach((input) => {
    const current = String(input.value || "").trim();
    const original = String(input.dataset.originalValue || "").trim();
    if (!current) return;
    if (current !== original || input.dataset.manual === "true") {
      data[input.dataset.heavenManualField] = input.type === "number" ? Number(current) : current;
    }
  });
  const note = document.querySelector("#heavenCalibrationNote").value.trim();
  if (note) data.note = note;
  if (!Object.keys(data).some((key) => key !== "note")) {
    showToast("请先补充或修改至少一项量化数据");
    return;
  }
  state.heavenManualData = data;
  loadHeavenSetup(true, "", document.querySelector("#heavenStockInput").value.trim());
}

function resetHeavenCalibration() {
  state.heavenManualData = null;
  document.querySelector("#heavenCalibrationNote").value = "";
  loadHeavenSetup(true, "", document.querySelector("#heavenStockInput").value.trim());
}

function selectHeavenPanel(panel, updateUrl = false) {
  state.heavenPanel = panel;
  if (state.heavenSetup) {
    const calendarDate = state.heavenSetup.calendar_date || state.heavenSetup.trade_date;
    const dateLabel = panel === "trend"
      ? (calendarDate === state.heavenSetup.trade_date
        ? `行情 ${displayCompactDate(state.heavenSetup.trade_date)}`
        : `行情 ${displayCompactDate(state.heavenSetup.trade_date)} · 历法 ${displayCompactDate(calendarDate)}`)
      : `历法 ${displayCompactDate(calendarDate)}`;
    setText("heavenDataDate", dateLabel);
  }
  document.querySelectorAll("[data-heaven-panel]").forEach((button) => {
    const active = button.dataset.heavenPanel === panel;
    button.classList.toggle("active", active);
    button.classList.toggle("on", active);
    button.setAttribute("aria-current", active ? "page" : "false");
  });
  document.querySelectorAll(".heaven-panel").forEach((item) => {
    item.classList.toggle("active-heaven-panel", item.id === `heaven${capitalize(panel)}Panel`);
  });
  if (
    panel === "fortune"
    && state.heavenSetup?.field
    && state.heavenPerformancePanels.has("fortune")
  ) {
    requestAnimationFrame(() => renderQiFieldCanvas(state.heavenSetup.field.balance || [], { intro: false }));
  } else {
    stopQiFieldCanvas();
  }
  if (panel === "heart") {
    initializeHeartAtmosphere();
    setHeartLamp(state.heartStage);
  } else {
    stopHeartDust();
  }
  if (panel !== "heart") requestAnimationFrame(() => queueHeavenPerformance(panel));
  if (updateUrl) {
    const url = new URL(window.location.href);
    url.searchParams.set("heaven", panel);
    history.replaceState(null, "", url);
  }
}

function showHeartRitualCurtain() {
  const curtain = document.querySelector("#heartRitualCurtain");
  if (!curtain || curtain.classList.contains("is-visible")) return;
  if (state.heartCurtainTimer) clearTimeout(state.heartCurtainTimer);
  document.querySelectorAll(".heart-stage.active-heart-stage .heart-rise").forEach((item) => item.classList.remove("is-visible"));
  curtain.classList.remove("is-leaving");
  curtain.classList.add("is-visible");
  state.heartCurtainTimer = setTimeout(() => {
    curtain.classList.add("is-leaving");
    activateHeartRises(document.querySelector(".heart-stage.active-heart-stage"));
    state.heartCurtainTimer = setTimeout(() => {
      curtain.classList.remove("is-visible", "is-leaving");
      state.heartCurtainTimer = null;
    }, motionEnabled() ? 1450 : 10);
  }, motionEnabled() ? 3000 : 20);
}

function renderHeavenWorkspace() {
  const setup = state.heavenSetup;
  if (!setup) return;
  initializeWentianV2Atmosphere();
  renderMarketHexagram(setup.chart);
  renderFivePhaseField(setup.field);
  renderPersonalFortune();
  renderHeartStage();
  selectHeavenPanel(state.heavenPanel);
}

function buildWentianStars(id, count) {
  const element = document.getElementById(id);
  if (!element || element.children.length) return;
  element.innerHTML = Array.from({ length: count }, () => {
    const size = (Math.random() * 1.6 + 0.8).toFixed(1);
    return `<i style="left:${(Math.random() * 100).toFixed(1)}%;top:${(Math.random() * 100).toFixed(1)}%;width:${size}px;height:${size}px;animation-delay:${(Math.random() * 4).toFixed(1)}s;animation-duration:${(3 + Math.random() * 4).toFixed(1)}s"></i>`;
  }).join("");
}

function buildWentianBagua(svg) {
  if (!svg || svg.children.length) return;
  const trigrams = ["乾", "兑", "离", "震", "巽", "坎", "艮", "坤"];
  let characters = "";
  let ticks = "";
  for (let index = 0; index < 8; index += 1) {
    const angle = (index * 45 - 90) * Math.PI / 180;
    const x = 150 + 129 * Math.cos(angle);
    const y = 150 + 129 * Math.sin(angle);
    characters += `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" fill="#c9a55c" font-size="17" text-anchor="middle" dominant-baseline="middle" transform="rotate(${index * 45} ${x.toFixed(1)} ${y.toFixed(1)})">${trigrams[index]}</text>`;
  }
  for (let index = 0; index < 24; index += 1) {
    const angle = (index * 15 - 90) * Math.PI / 180;
    ticks += `<line x1="${(150 + 100 * Math.cos(angle)).toFixed(1)}" y1="${(150 + 100 * Math.sin(angle)).toFixed(1)}" x2="${(150 + 108 * Math.cos(angle)).toFixed(1)}" y2="${(150 + 108 * Math.sin(angle)).toFixed(1)}" stroke="#c9a55c" stroke-width="1"/>`;
  }
  svg.innerHTML = `<g class="ring"><circle cx="150" cy="150" r="146" fill="none" stroke="#c9a55c" stroke-width="1" stroke-dasharray="2 6"/><circle cx="150" cy="150" r="112" fill="none" stroke="#c9a55c" stroke-width="1" stroke-dasharray="10 5"/>${characters}</g><g class="ring2">${ticks}</g>`;
}

function buildWentianFortuneOrbit(svg) {
  if (!svg || svg.children.length) return;
  const sixQi = ["厥阴木", "少阴火", "少阳火", "太阴土", "阳明金", "太阳水"];
  const movements = ["木运", "火运", "土运", "金运", "水运"];
  const polarText = (items, radius, fontSize, offset = -90) => items.map((label, index) => {
    const degrees = offset + index * 360 / items.length;
    const angle = degrees * Math.PI / 180;
    const x = 150 + radius * Math.cos(angle);
    const y = 150 + radius * Math.sin(angle);
    return `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" fill="#c9a55c" font-size="${fontSize}" text-anchor="middle" dominant-baseline="middle" transform="rotate(${degrees + 90} ${x.toFixed(1)} ${y.toFixed(1)})">${label}</text>`;
  }).join("");
  const ticks = Array.from({ length: 30 }, (_, index) => {
    const angle = (index * 12 - 90) * Math.PI / 180;
    const inner = index % 5 === 0 ? 96 : 101;
    return `<line x1="${(150 + inner * Math.cos(angle)).toFixed(1)}" y1="${(150 + inner * Math.sin(angle)).toFixed(1)}" x2="${(150 + 108 * Math.cos(angle)).toFixed(1)}" y2="${(150 + 108 * Math.sin(angle)).toFixed(1)}" stroke="#c9a55c" stroke-width="${index % 5 === 0 ? 1.4 : 0.7}"/>`;
  }).join("");
  svg.innerHTML = `<g class="ring"><circle cx="150" cy="150" r="146" fill="none" stroke="#c9a55c" stroke-width="1" stroke-dasharray="2 6"/><circle cx="150" cy="150" r="116" fill="none" stroke="#c9a55c" stroke-width="1" stroke-dasharray="10 5"/>${polarText(sixQi, 132, 8.5)}</g><g class="ring2"><circle cx="150" cy="150" r="88" fill="none" stroke="#c9a55c" stroke-width="1" stroke-dasharray="4 5"/>${ticks}${polarText(movements, 70, 10)}<text x="150" y="150" fill="#c9a55c" font-size="11" text-anchor="middle" dominant-baseline="middle">五运六气</text></g>`;
}

function initializeWentianV2Atmosphere() {
  buildWentianStars("stars", 90);
  buildWentianStars("fortuneStars", 100);
  buildWentianStars("heartStars", 110);
  buildWentianBagua(document.querySelector("#baguaSvg"));
  buildWentianFortuneOrbit(document.querySelector("#fortuneBagua"));
  buildWentianBagua(document.querySelector("#heartBagua"));
}

function renderCompactHexagrams(hexagram) {
  const original = document.querySelector("#heavenOriginalHexLines");
  const changed = document.querySelector("#heavenChangedHexLines");
  if (!original || !changed) return;
  if (!hexagram?.lines?.length) {
    original.innerHTML = "";
    changed.innerHTML = "";
    setText("heavenOriginalHexName", "待定");
    setText("heavenChangedHexName", "待定");
    setText("heavenOriginalHexDetail", "六爻尚未齐备");
    setText("heavenChangedHexDetail", "待动爻化变");
    return;
  }
  const values = hexagram.lines.map((line) => number(line.value));
  const changedValues = values.map((value) => value === 6 ? 7 : value === 9 ? 8 : value);
  const lines = (items, showMoving) => [...items].reverse().map((value) => {
    const moving = showMoving && [6, 9].includes(value);
    return `<div class="compact-hex-line ${value % 2 ? "yang" : "yin"} ${moving ? "moving" : ""}"><i></i>${value % 2 ? "" : "<i></i>"}</div>`;
  }).join("");
  original.innerHTML = lines(values, true);
  changed.innerHTML = lines(changedValues, false);
  setText("heavenOriginalHexName", hexagram.name || "--");
  setText("heavenChangedHexName", hexagram.transformed?.name || "--");
  setText("heavenOriginalHexDetail", `${hexagram.outer_trigram || "--"}上 · ${hexagram.inner_trigram || "--"}下`);
  setText("heavenChangedHexDetail", `${hexagram.transformed?.outer_trigram || "--"}上 · ${hexagram.transformed?.inner_trigram || "--"}下`);
}

function cancelHeavenPerformance() {
  heavenPerformanceToken += 1;
  state.heavenPerformanceActive = "";
  document.querySelectorAll("#heavenTrendPanel, #heavenFortunePanel").forEach((panel) => {
    panel.classList.remove("heaven-performance-pending", "heaven-performance-running");
    panel.classList.add("heaven-performance-complete");
  });
}

function queueHeavenPerformance(panel) {
  if (!state.heavenSetup || !["trend", "fortune"].includes(panel)) return;
  const performanceId = `${state.heavenPerformanceKey}:${panel}`;
  if (
    state.heavenPerformancePanels.has(panel)
    || state.heavenPerformanceActive === performanceId
    || state.heavenPanel !== panel
  ) return;
  const token = ++heavenPerformanceToken;
  state.heavenPerformanceActive = performanceId;
  const runner = panel === "trend"
    ? playTrendPerformance(state.heavenSetup.chart, token)
    : playFortunePerformance(state.heavenSetup.field, token);
  runner.then((completed) => {
    if (!completed || token !== heavenPerformanceToken) return;
    state.heavenPerformancePanels.add(panel);
    state.heavenPerformanceActive = "";
  });
}

function heavenPerformanceDelay(duration, token) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(token === heavenPerformanceToken), motionEnabled() ? duration : 0);
  });
}

async function typeHeavenText(element, text, token, speed = 38) {
  if (!element) return false;
  if (!motionEnabled()) {
    element.textContent = text;
    return token === heavenPerformanceToken;
  }
  element.textContent = "";
  element.classList.add("heaven-typing");
  for (const character of text) {
    if (token !== heavenPerformanceToken) return false;
    element.append(document.createTextNode(character));
    if (!await heavenPerformanceDelay(speed, token)) return false;
  }
  element.classList.remove("heaven-typing");
  return true;
}

function countHeavenNumber(element, target, token, duration = 1300, suffix = "") {
  return new Promise((resolve) => {
    if (!element || !motionEnabled()) {
      if (element) element.textContent = `${target > 0 ? "+" : ""}${target}${suffix}`;
      resolve(token === heavenPerformanceToken);
      return;
    }
    const startedAt = performance.now();
    const step = (now) => {
      if (token !== heavenPerformanceToken) {
        resolve(false);
        return;
      }
      const progress = Math.min(1, (now - startedAt) / duration);
      const eased = 1 - (1 - progress) ** 3;
      const value = Math.round(target * eased);
      element.textContent = `${value > 0 ? "+" : ""}${value}${suffix}`;
      if (progress < 1) requestAnimationFrame(step);
      else resolve(true);
    };
    requestAnimationFrame(step);
  });
}

async function playTrendPerformance(chart, token) {
  const panel = document.querySelector("#heavenTrendPanel");
  if (!panel || state.heavenPanel !== "trend") return false;
  panel.classList.remove(
    "heaven-performance-complete",
    "performance-title-ready",
    "performance-change-ready",
    "performance-score-ready",
    "performance-text-ready",
  );
  panel.classList.add("heaven-performance-pending", "heaven-performance-running");
  panel.querySelectorAll(".talent-line-group, .hexagram-line-row, .talent-reading, .heaven-index-strip > *").forEach((item) => {
    item.classList.remove("is-ready");
  });
  if (!chart?.available) {
    panel.classList.remove("heaven-performance-pending", "heaven-performance-running");
    panel.classList.add("heaven-performance-complete");
    return true;
  }

  const guaci = chart.hexagram.text || "";
  const scoreElement = document.querySelector("#heavenMomentumScore");
  const guaciElement = document.querySelector("#marketHexagramText");
  if (scoreElement) scoreElement.textContent = "0";
  if (guaciElement) guaciElement.textContent = "";
  if (!await heavenPerformanceDelay(220, token)) return false;

  const groups = [...panel.querySelectorAll(".talent-line-group")].reverse();
  const readings = [...panel.querySelectorAll(".talent-reading")];
  for (let index = 0; index < groups.length; index += 1) {
    const group = groups[index];
    group.classList.add("is-ready");
    if (!await heavenPerformanceDelay(280, token)) return false;
    const rows = [...group.querySelectorAll(".hexagram-line-row")].reverse();
    for (const row of rows) {
      row.classList.add("is-ready");
      if (!await heavenPerformanceDelay(560, token)) return false;
    }
    readings[index]?.classList.add("is-ready");
    if (!await heavenPerformanceDelay(220, token)) return false;
  }

  panel.classList.add("performance-title-ready");
  if (!await heavenPerformanceDelay(650, token)) return false;
  panel.classList.add("performance-change-ready");
  if (!await heavenPerformanceDelay(420, token)) return false;
  panel.classList.add("performance-score-ready");
  if (!await countHeavenNumber(scoreElement, number(chart.momentum_score), token)) return false;
  panel.querySelectorAll(".heaven-index-strip > *").forEach((item, index) => {
    setTimeout(() => {
      if (token === heavenPerformanceToken) item.classList.add("is-ready");
    }, motionEnabled() ? index * 90 : 0);
  });
  if (!await heavenPerformanceDelay(620, token)) return false;
  if (!await typeHeavenText(guaciElement, guaci, token, 30)) return false;
  panel.classList.add("performance-text-ready");
  panel.classList.remove("heaven-performance-pending", "heaven-performance-running");
  panel.classList.add("heaven-performance-complete");
  return true;
}

async function playFortunePerformance(field, token) {
  const panel = document.querySelector("#heavenFortunePanel");
  if (!panel || !field || state.heavenPanel !== "fortune") return false;
  panel.classList.remove("heaven-performance-complete", "performance-climate-ready", "performance-use-ready");
  panel.classList.add("heaven-performance-pending", "heaven-performance-running");
  panel.querySelectorAll(".phase-balance-row, .qi-framework-layer, .human-field-grid > div, .personal-fortune-panel").forEach((item) => {
    item.classList.remove("is-ready");
  });
  const climateTone = document.querySelector("#qiClimateTone");
  const climateText = climateTone?.textContent || "";
  if (climateTone) climateTone.textContent = "";
  renderQiFieldCanvas(field.balance || [], { intro: true });
  if (!await heavenPerformanceDelay(900, token)) return false;
  panel.classList.add("performance-climate-ready");
  if (!await heavenPerformanceDelay(720, token)) return false;
  if (!await typeHeavenText(climateTone, climateText, token, 58)) return false;

  const balanceRows = [...panel.querySelectorAll(".phase-balance-row")];
  for (const row of balanceRows) {
    row.classList.add("is-ready");
    const percent = number(row.dataset.phasePercent);
    if (!await countHeavenNumber(row.querySelector(":scope > b"), percent, token, 520, "%")) return false;
    if (!await heavenPerformanceDelay(90, token)) return false;
  }
  const layers = [...panel.querySelectorAll(".qi-framework-layer")];
  for (const layer of layers) {
    layer.classList.add("is-ready");
    if (!await heavenPerformanceDelay(250, token)) return false;
  }
  panel.querySelectorAll(".human-field-grid > div").forEach((item, index) => {
    setTimeout(() => {
      if (token === heavenPerformanceToken) item.classList.add("is-ready");
    }, motionEnabled() ? index * 150 : 0);
  });
  if (!await heavenPerformanceDelay(820, token)) return false;
  panel.querySelector(".personal-fortune-panel")?.classList.add("is-ready");
  panel.classList.add("performance-use-ready");
  drawQiUseConnections(true);
  panel.classList.remove("heaven-performance-pending", "heaven-performance-running");
  panel.classList.add("heaven-performance-complete");
  return true;
}

function heavenSourcePhrase(item = {}) {
  const stateLabel = item.realtime ? "当下之象" : "既成之象";
  const layerLabel = {
    指数: "天象合参",
    行业: "人势同观",
    个股: "地脉验真",
    用户补充: "人工验数",
  }[item.layer] || "三才合参";
  return `${layerLabel} · ${stateLabel}`;
}

function renderHeavenLineChecks(chart) {
  const checks = [...(chart.data_checks || [])].sort((left, right) => number(right.line) - number(left.line));
  const container = document.querySelector("#heavenLineChecks");
  const status = document.querySelector("#heavenCalibrationStatus");
  const passedCount = checks.filter((item) => item.passed).length;
  const manualCount = checks.filter((item) => item.status === "manual").length;
  status.textContent = checks.length ? `${passedCount}/6 通过${manualCount ? ` · ${manualCount} 爻含补录` : ""}` : "等待载入";
  status.className = passedCount === 6 ? (manualCount ? "is-manual" : "is-passed") : "is-failed";
  if (!checks.length) {
    renderEmptyState(container, "载入股票后查看六爻数据状态");
    return;
  }
  const lineValueLabel = { 6: "老阴 · 动", 7: "少阳 · 静", 8: "少阴 · 静", 9: "老阳 · 动" };
  container.innerHTML = checks.map((check) => {
    const stateLabel = check.status === "manual" ? "补录通过" : check.passed ? "自动通过" : "未通过";
    const score = check.score === null || check.score === undefined ? "--" : signedScore(check.score);
    const fields = (check.fields || []).map((field) => {
      const rawValue = field.value === null || field.value === undefined ? "" : String(field.value);
      const source = field.manual ? "用户补录" : rawValue ? "自动行情" : "等待补充";
      const common = `data-heaven-manual-field="${escapeHtml(field.key)}" data-original-value="${escapeHtml(rawValue)}" data-manual="${field.manual ? "true" : "false"}"`;
      const control = field.type === "select"
        ? `<select ${common}>${(field.options || []).map((option) => `<option value="${escapeHtml(option)}" ${option === rawValue ? "selected" : ""}>${escapeHtml(option)}</option>`).join("")}</select>`
        : field.type === "text"
          ? `<input type="text" maxlength="50" value="${escapeHtml(rawValue)}" placeholder="待补充" ${common}>`
          : `<input type="number" inputmode="decimal" value="${escapeHtml(rawValue)}" placeholder="待补充" min="${field.min ?? ""}" max="${field.max ?? ""}" step="${field.integer ? "1" : "any"}" ${common}>`;
      return `<label class="heaven-manual-field ${field.manual ? "is-manual" : ""}">
        <span>${escapeHtml(field.label)}<small>${escapeHtml(source)}</small></span>
        <span class="heaven-field-control">${control}${field.unit ? `<b>${escapeHtml(field.unit)}</b>` : ""}</span>
      </label>`;
    }).join("");
    const reasons = (check.reasons || []).map((reason) => `<li>${escapeHtml(reason)}</li>`).join("");
    return `<details class="heaven-line-check is-${escapeHtml(check.status)}" ${check.passed ? "" : "open"}>
      <summary>
        <span class="heaven-check-state"><i class="status-dot ${escapeHtml(check.status)}"></i><b>${escapeHtml(stateLabel)}</b></span>
        <span class="heaven-check-name"><strong>${escapeHtml(check.position)} · ${escapeHtml(check.layer)}</strong><small>${escapeHtml(check.formula)}</small></span>
        <span class="heaven-check-result"><b>${check.line_value ? escapeHtml(lineValueLabel[check.line_value] || check.line_value) : "待定"}</b><small>得分 ${escapeHtml(score)}</small></span>
        <i data-lucide="chevron-down" aria-hidden="true"></i>
      </summary>
      <div class="heaven-line-check-body">
        ${reasons ? `<ul class="heaven-check-reasons">${reasons}</ul>` : `<p class="heaven-check-evidence">${(check.evidence || []).map(escapeHtml).join("；") || "数据已通过安全门"}</p>`}
        <div class="heaven-manual-fields">${fields}</div>
      </div>
    </details>`;
  }).join("");
  document.querySelector("#heavenCalibrationNote").value = chart.manual_data?.note || "";
  window.lucide?.createIcons();
}

function renderMarketHexagram(chart) {
  const stockInput = document.querySelector("#heavenStockInput");
  if (document.activeElement !== stockInput) stockInput.value = chart.stock.code || "";
  const selectionRequired = Boolean(chart.selection_required);
  const emptyState = document.querySelector("#heavenTrendEmpty");
  const trendLayout = document.querySelector("#heavenTrendPanel .heaven-trend-layout");
  const calibrationPanel = document.querySelector("#heavenCalibrationPanel");
  const stockIdentity = document.querySelector("#heavenStockIdentity");
  if (emptyState) emptyState.hidden = !selectionRequired;
  if (trendLayout) trendLayout.hidden = selectionRequired;
  if (calibrationPanel) calibrationPanel.hidden = selectionRequired;
  if (stockIdentity) stockIdentity.hidden = selectionRequired;
  if (selectionRequired) {
    document.querySelector("#interpretTrendButton").disabled = true;
    setText("heavenStockName", "--");
    setText("heavenStockSector", "--");
    renderHeavenInterpretation("trend", "");
    return;
  }
  setText("heavenStockName", chart.stock.name || "--");
  setText(
    "heavenStockSector",
    chart.sector || "--",
  );
  setText("heavenStockTaxonomy", chart.sector_taxonomy === "sw_l2" ? "申万二级 ·" : "所属行业 ·");
  renderHeavenLineChecks(chart);

  const interpretButton = document.querySelector("#interpretTrendButton");
  const scoreMeter = document.querySelector(".trend-score-meter");
  const scoreNeedle = document.querySelector("#heavenMomentumNeedle");
  const renderTrendEvidence = () => {
    const rows = chart.quality?.sources || [];
    document.querySelector("#heavenTrendEvidence").innerHTML = rows.length
      ? rows.map((item) => `
          <div>
            <strong>${escapeHtml(item.lines)} · ${escapeHtml(item.layer)}</strong>
            <span>${escapeHtml(heavenSourcePhrase(item))}</span>
            <small>${escapeHtml(item.detail || "")}</small>
          </div>
        `).join("")
      : '<p>暂无可核验的数据来源。</p>';
  };
  renderTrendEvidence();
  if (!chart.available) {
    interpretButton.disabled = true;
    setText("marketHexagramName", "暂不成卦");
    setText("marketTransformedName", "--");
    setText("marketHexagramText", chart.quality?.principle || "六爻数据尚未齐备。");
    setText("marketMovementSummary", (chart.quality?.issues || []).join("；") || "等待有效行情数据");
    setText("heavenMomentumScore", "--");
    setText("heavenMomentumLabel", "数据未齐");
    renderCompactHexagrams(null);
    scoreMeter?.setAttribute("aria-valuenow", "0");
    if (scoreNeedle) scoreNeedle.style.setProperty("--momentum-position", "50%");
    document.querySelector("#marketHexagramLines").innerHTML = "";
    const sourceRows = chart.quality?.sources || [];
    document.querySelector("#threeTalentReadings").innerHTML = [
      ...(chart.quality?.issues || []).map((issue) => `
        <div class="talent-reading"><strong>未通过</strong><span>${escapeHtml(issue)}</span></div>
      `),
      ...sourceRows.map((item) => `
        <div class="talent-reading">
          <strong>${escapeHtml(item.lines)} · ${escapeHtml(item.layer)}</strong>
          <span>${escapeHtml(heavenSourcePhrase(item))}</span>
          <small>${escapeHtml(item.detail || "")}</small>
        </div>
      `),
    ].join("");
    document.querySelector("#heavenIndexStrip").innerHTML = "<p>天象尚未应时，待三才数据齐备后再观。</p>";
    renderHeavenInterpretation("trend", "");
    return;
  }
  interpretButton.disabled = false;

  setText("marketHexagramName", `${chart.hexagram.outer_trigram}上${chart.hexagram.inner_trigram}下 · ${chart.hexagram.name}`);
  setText("marketTransformedName", chart.hexagram.transformed.name);
  renderCompactHexagrams(chart.hexagram);
  setText("marketHexagramText", chart.hexagram.text);
  setText("marketMovementSummary", `${chart.movement.label}。${chart.movement.explanation}`);
  setText("heavenMomentumScore", `${chart.momentum_score > 0 ? "+" : ""}${chart.momentum_score}`);
  setText("heavenMomentumLabel", chart.momentum_label);
  const momentumPosition = clamp((number(chart.momentum_score) + 100) / 2, 0, 100);
  scoreMeter?.setAttribute("aria-valuenow", String(number(chart.momentum_score)));
  if (scoreNeedle) scoreNeedle.style.setProperty("--momentum-position", `${momentumPosition}%`);
  renderMarketHexagramLines(chart.hexagram.lines);
  document.querySelector("#threeTalentReadings").innerHTML = chart.pair_readings.map((item) => `
    <div class="talent-reading">
      <strong>${escapeHtml(item.level)}</strong><span>${escapeHtml(item.state)}</span>
      <div class="talent-balance">
        <small>内 ${signedScore(item.inner)}</small><i><b style="--talent-value:${clamp((number(item.inner) + 1) * 50, 0, 100)}%"></b></i>
        <small>外 ${signedScore(item.outer)}</small><i><b style="--talent-value:${clamp((number(item.outer) + 1) * 50, 0, 100)}%"></b></i>
      </div>
    </div>
  `).join("");
  const indexContext = chart.index_context || {};
  document.querySelector("#heavenIndexStrip").innerHTML = (indexContext.indices || []).length
    ? indexContext.indices.map((item) => `
        <div><span>${escapeHtml(item.name)}</span><strong class="${changeClass(item.pct_chg)}">${signed(item.pct_chg)}%</strong><small>5日 ${signed(item.return_5d)}%</small></div>
      `).join("")
    : `<p>${escapeHtml(indexContext.notice || "指数数据暂不可用")}</p>`;
  renderHeavenInterpretation("trend", state.heavenInterpretations.trend);
}

function renderMarketHexagramLines(lines) {
  const groups = [
    { talent: "天", caption: "指数 · 外显为上，内核为下", lines: [lines[5], lines[4]] },
    { talent: "人", caption: "行业 · 外显为上，内核为下", lines: [lines[3], lines[2]] },
    { talent: "地", caption: "个股 · 外显为上，内核为下", lines: [lines[1], lines[0]] },
  ];
  document.querySelector("#marketHexagramLines").innerHTML = groups.map((group, groupIndex) => `
    <section class="talent-line-group" style="--group-delay:${groupIndex * 70}ms">
      <div class="talent-seal" aria-hidden="true">${group.talent}</div>
      <div class="talent-line-content">
        <p>${group.caption}</p>
        ${group.lines.map((line) => `
          <div class="hexagram-line-row ${line.moving ? "moving" : ""}">
            <span class="hexagram-position">${escapeHtml(line.position_name)}</span>
            ${hexagramLineGraphic(line.value)}
            <div class="hexagram-line-detail">
              <strong>${escapeHtml(line.role || line.line_name)} · ${line.value}${line.moving ? " 变" : ""}</strong>
              <small>${(line.evidence || []).map(escapeHtml).join("；")}</small>
            </div>
          </div>
        `).join("")}
      </div>
    </section>
  `).join("");
}

function stopQiFieldCanvas() {
  if (qiFieldAnimationFrame) cancelAnimationFrame(qiFieldAnimationFrame);
  qiFieldAnimationFrame = 0;
}

function renderQiFieldCanvas(balance, options = {}) {
  stopQiFieldCanvas();
  const canvas = document.querySelector("#qiFieldCanvas");
  const shell = canvas?.parentElement;
  if (!canvas || !shell || !shell.clientWidth || !shell.clientHeight) return;
  const context = canvas.getContext("2d");
  const ratio = Math.min(2, window.devicePixelRatio || 1);
  const width = shell.clientWidth;
  const height = shell.clientHeight;
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  const phaseColors = { 木: "#4a7c59", 火: "#b53a30", 土: "#b08a3e", 金: "#9c7c3c", 水: "#31505f" };
  const positions = {
    水: [0.50, 0.23],
    火: [0.50, 0.77],
    金: [0.28, 0.50],
    木: [0.72, 0.50],
    土: [0.50, 0.50],
  };
  const introStartedAt = options.intro && motionEnabled() ? performance.now() : 0;
  const items = balance.map((item, index) => ({
    ...item,
    color: phaseColors[item.element] || "#6d685b",
    x: positions[item.element]?.[0] || 0.5,
    y: positions[item.element]?.[1] || 0.5,
    phase: index * 1.7,
    alpha: introStartedAt ? 0 : 1,
  }));
  const draw = (now = 0) => {
    context.clearRect(0, 0, width, height);
    context.globalCompositeOperation = "multiply";
    items.forEach((item, index) => {
      const strength = Math.max(0.14, number(item.percent) / 100);
      const introProgress = introStartedAt ? clamp((now - introStartedAt) / 2600, 0, 1) : 1;
      const introEase = 1 - (1 - introProgress) ** 3;
      const breath = motionEnabled() ? Math.sin(now * 0.00055 + item.phase) : 0;
      const radius = Math.min(width, height) * (0.13 + Math.sqrt(strength) * 0.12) * (1 + breath * 0.06);
      const targetAlpha = qiFieldSoloElement ? (qiFieldSoloElement === item.element ? 1 : 0.1) : 1;
      item.alpha += (targetAlpha - item.alpha) * 0.06;
      const targetX = width * item.x + (motionEnabled() ? Math.sin(now * (0.00012 + index * 0.000015) + item.phase) * 10 : 0);
      const targetY = height * item.y + (motionEnabled() ? Math.cos(now * (0.0001 + index * 0.000013) + item.phase) * 8 : 0);
      const centerX = width * 0.5;
      const centerY = height * 0.47;
      const x = centerX + (targetX - centerX) * introEase;
      const y = centerY + (targetY - centerY) * introEase;
      const gradient = context.createRadialGradient(x, y, 0, x, y, radius);
      const rgb = item.color.match(/[a-f\d]{2}/gi).map((part) => parseInt(part, 16));
      const alpha = item.alpha * introEase;
      gradient.addColorStop(0, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${(0.28 + strength * 0.22) * alpha})`);
      gradient.addColorStop(0.5, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${(0.13 + strength * 0.12) * alpha})`);
      gradient.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0)`);
      context.fillStyle = gradient;
      context.fillRect(x - radius, y - radius, radius * 2, radius * 2);
    });
    context.globalCompositeOperation = "source-over";
    if (motionEnabled() && state.activeView === "heavenView" && state.heavenPanel === "fortune") {
      qiFieldAnimationFrame = requestAnimationFrame(draw);
    }
  };
  draw(performance.now());
}

function wentianClimateVerdict(field) {
  const balance = field?.balance || [];
  const dominant = balance[0]?.element;
  const secondary = balance[1]?.element;
  const tertiary = balance[2]?.element;
  const pair = [dominant, secondary].filter(Boolean).sort().join("");
  const primary = {
    木火: "风火相煽", 木土: "风湿相搏", 木金: "风燥相激", 木水: "风寒相薄",
    土火: "湿热交蒸", 火金: "燥热相煽", 水火: "寒热相争", 土金: "燥湿相搏",
    土水: "寒湿交织", 水金: "寒燥相参",
  }[pair] || ({ 木: "风木疏展", 火: "热火升明", 土: "湿滞偏重", 金: "燥金肃降", 水: "寒水潜藏" }[dominant] || "气机交会");
  const following = { 木: "风象暗动", 火: "热象内蕴", 土: "湿滞内结", 金: "燥气相参", 水: "寒意潜行" }[tertiary]
    || ({ 木: "风象相随", 火: "热象相随", 土: "湿象相随", 金: "燥象相随", 水: "寒象相随" }[secondary] || "诸气相参");
  return `${primary} · ${following}`;
}

function renderFivePhaseField(field) {
  if (!field) return;
  setText("fortuneLunarDate", `${field.date} · ${field.lunar_date}`);
  setText("fortunePillars", `${field.pillars.year}年 · ${field.pillars.month}月 · ${field.pillars.day}日`);
  const metrics = [
    ["中运", field.movement.label, field.movement.basis],
    ["司天", field.six_qi.sitian, "岁半以前主气候背景"],
    ["在泉", field.six_qi.zaiquan, "岁半以后主气候背景"],
    [field.six_qi.step_name, `主 ${field.six_qi.host_qi}`, `客 ${field.six_qi.guest_qi}`],
    ["当前节气", field.solar_terms.current, field.solar_terms.current_at],
    ["下一节气", field.solar_terms.next, field.solar_terms.next_at],
  ];
  document.querySelector("#fortuneMetrics").innerHTML = metrics.map(([label, value, detail]) => `
    <div class="fortune-metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong><small>${escapeHtml(detail)}</small></div>
  `).join("");
  const framework = field.framework || {};
  setText("qiFrameworkPrinciple", framework.principle || "--");
  const layerLabels = { year: "年运与岁气", current: "客主加临", day: "日辰触发" };
  document.querySelector("#qiFrameworkLayers").innerHTML = (framework.layers || []).map((layer) => `
    <div class="qi-framework-layer" data-qi-layer="${escapeHtml(layer.id)}">
      <span>${escapeHtml(layerLabels[layer.id] || layer.label)}</span>
      <strong>${escapeHtml(layer.dominant)}气</strong>
      <small>${escapeHtml(layer.summary)}</small>
      <div>${(layer.balance || []).map((item) => `<i class="phase-${phaseClass(item.element)}" style="--qi-segment:${number(item.percent)}%" title="${escapeHtml(item.element)}"></i>`).join("")}</div>
    </div>
  `).join("");
  const human = field.human_field || {};
  const dominantPhase = (field.balance || [])[0];
  setText("qiClimateKeyword", wentianClimateVerdict(field));
  setText("qiClimateTone", (human.emotional_tendency || [])[0] || "留意当下身心反应");
  setText("humanFieldSummary", human.summary || "--");
  setText("humanEmotionList", (human.emotional_tendency || []).join("；") || "--");
  setText("humanBiasList", (human.decision_biases || []).join("；") || "--");
  setText("humanOperation", human.operation_tendency || "--");
  setText(
    "humanBalanceActions",
    [...(human.risk_reminders || []), ...(human.balancing_actions || [])].join("；") || "--",
  );
  document.querySelector("#fivePhaseBalance").innerHTML = field.balance.map((item) => `
    <div class="phase-balance-row" data-phase-element="${escapeHtml(item.element)}" data-phase-percent="${number(item.percent)}" tabindex="0">
      <strong class="phase-symbol phase-${phaseClass(item.element)}">${escapeHtml(item.element)}</strong>
      <div><div class="phase-track"><span class="phase-${phaseClass(item.element)}" style="--phase-width:${number(item.percent)}%"></span></div><small>${escapeHtml(item.motion)} · ${escapeHtml(item.mind)}</small></div>
      <b>${number(item.percent)}%</b>
    </div>
  `).join("");
  document.querySelectorAll("#fivePhaseBalance .phase-balance-row").forEach((row) => {
    const focusPhase = () => { qiFieldSoloElement = row.dataset.phaseElement || ""; };
    const clearPhase = () => { qiFieldSoloElement = ""; };
    row.addEventListener("mouseenter", focusPhase);
    row.addEventListener("mouseleave", clearPhase);
    row.addEventListener("focus", focusPhase);
    row.addEventListener("blur", clearPhase);
    row.addEventListener("click", () => {
      qiFieldSoloElement = qiFieldSoloElement === row.dataset.phaseElement ? "" : row.dataset.phaseElement;
    });
  });
  setText("phaseSectorTitle", "五行行业归属");
  setText("phaseSectorContext", "传统取象 · 手动归类优先");
  renderQiUseMap(field);
  renderFortuneSectorCatalog(field);
  renderSectorPhaseOverrides(state.heavenSetup?.sector_phase_overrides || []);
  setText("fortuneNotice", field.notice);
  renderHeavenInterpretation("fortune", state.heavenInterpretations.fortune);
}

function renderFortuneSectorCatalog(field) {
  const container = document.querySelector("#fortuneSectorGroups");
  if (!container) return;
  const phaseOrder = new Map((field.balance || []).map((item, index) => [item.element, index]));
  const canonical = { 木: 0, 火: 1, 土: 2, 金: 3, 水: 4 };
  const catalog = [...(field.sector_catalog || [])].sort((left, right) => (
    (canonical[left.element] ?? phaseOrder.get(left.element) ?? 99)
    - (canonical[right.element] ?? phaseOrder.get(right.element) ?? 99)
  ));
  container.innerHTML = catalog.map((group) => `
    <section class="fortune-sector-group">
      <header><i class="phase-${phaseClass(group.element)}"></i><strong class="phase-text-${phaseClass(group.element)}">${escapeHtml(group.element)}属性</strong><small>${number(group.count || group.industries?.length)} 类</small></header>
      <ul>${(group.industries || []).map((item) => `<li>${escapeHtml(item.name)}</li>`).join("")}</ul>
    </section>
  `).join("") || '<p class="personal-profile-empty">行业五行归类尚未建立</p>';
}

function renderQiUseMap(field) {
  const sourceContainer = document.querySelector("#qiUseSources");
  const sectorContainer = document.querySelector("#phaseSectorList");
  if (!sourceContainer || !sectorContainer) return;
  const balance = field.balance || [];
  const phaseOrder = new Map(balance.map((item, index) => [item.element, index]));
  const catalog = [...(field.sector_catalog || [])].sort(
    (left, right) => (phaseOrder.get(left.element) ?? 99) - (phaseOrder.get(right.element) ?? 99),
  );
  const catalogElements = new Set(catalog.map((item) => item.element));
  sourceContainer.innerHTML = balance.map((item) => `
    <div class="qi-use-source ${catalogElements.has(item.element) ? "has-catalog" : "no-catalog"}" data-qi-source="${escapeHtml(item.element)}">
      <strong class="phase-text-${phaseClass(item.element)}">${escapeHtml(item.element)}</strong>
      <span><b>${escapeHtml(item.motion)}</b><small>${number(item.percent)}%</small></span>
    </div>
  `).join("");
  sectorContainer.innerHTML = catalog.length ? catalog.map((group) => {
    const element = group.element;
    const items = group.industries || [];
    return `
      <details class="qi-sector-group" data-qi-sector="${escapeHtml(element)}">
        <summary>
          <span class="qi-sector-group-title"><i class="phase-${phaseClass(element)}"></i><strong>${escapeHtml(element)}属性</strong></span>
          <span class="qi-sector-group-count">${number(group.count)} 类</span>
          <i class="qi-sector-chevron" data-lucide="chevron-down" aria-hidden="true"></i>
        </summary>
        <div class="qi-sector-fold">
          <ul class="qi-sector-tags" aria-label="${escapeHtml(element)}属性行业">
            ${items.map((item) => `<li class="phase-border-${phaseClass(element)}">${escapeHtml(item.name)}${item.classification_source === "manual" ? '<small>手动</small>' : ""}</li>`).join("")}
          </ul>
        </div>
      </details>
    `;
  }).join("") : '<p class="qi-use-empty">行业五行归类尚未建立。</p>';
  sectorContainer.querySelectorAll(".qi-sector-group").forEach((group) => {
    group.addEventListener("toggle", () => requestAnimationFrame(() => drawQiUseConnections(false)));
  });
  refreshIcons();
  requestAnimationFrame(() => drawQiUseConnections(false));
}

function drawQiUseConnections(animate = false) {
  const map = document.querySelector("#qiUseMap");
  const svg = document.querySelector("#qiUseConnections");
  if (!map || !svg || !map.clientWidth || !map.clientHeight) return;
  const bounds = map.getBoundingClientRect();
  svg.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
  svg.innerHTML = "";
  document.querySelectorAll("#phaseSectorList [data-qi-sector]").forEach((group) => {
    const element = group.dataset.qiSector;
    const source = document.querySelector(`#qiUseSources [data-qi-source="${CSS.escape(element)}"]`);
    const target = group.querySelector("summary");
    if (!source || !target) return;
    const from = source.getBoundingClientRect();
    const to = target.getBoundingClientRect();
    const x1 = from.right - bounds.left - 4;
    const y1 = from.top + from.height / 2 - bounds.top;
    const x2 = to.left - bounds.left + 2;
    const y2 = to.top + to.height / 2 - bounds.top;
    const bend = Math.max(46, (x2 - x1) * 0.42);
    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttribute("d", `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`);
    path.setAttribute("pathLength", "1");
    path.classList.add(`phase-stroke-${phaseClass(element)}`);
    if (animate && motionEnabled()) path.classList.add("is-drawing");
    else path.classList.add("is-flowing");
    svg.appendChild(path);
    if (animate && motionEnabled()) {
      requestAnimationFrame(() => path.classList.add("is-visible"));
      setTimeout(() => {
        if (!path.isConnected) return;
        path.classList.remove("is-drawing", "is-visible");
        path.classList.add("is-flowing");
      }, 1900);
    }
  });
}

function renderSectorPhaseOverrides(items) {
  const container = document.querySelector("#sectorPhaseOverrides");
  const canManage = state.user?.role === "admin";
  container.innerHTML = items.length ? items.map((item) => `
    <div class="sector-phase-override-row">
      <span class="phase-symbol phase-${phaseClass(item.element)}">${escapeHtml(item.element)}</span>
      <strong>${escapeHtml(item.name)}</strong>
      ${canManage ? `<button class="icon-button" type="button" data-sector-phase-delete="${escapeHtml(item.name)}" title="删除手动归类" aria-label="删除 ${escapeHtml(item.name)} 的手动归类"><i data-lucide="trash-2"></i></button>` : ""}
    </div>
  `).join("") : '<p class="sector-phase-empty">暂无手动归类</p>';
  container.querySelectorAll("[data-sector-phase-delete]").forEach((button) => {
    button.addEventListener("click", () => deleteSectorPhaseOverride(button.dataset.sectorPhaseDelete));
  });
  refreshIcons();
}

async function saveSectorPhaseOverride(event) {
  event.preventDefault();
  const name = document.querySelector("#sectorPhaseName").value.trim();
  const element = document.querySelector("#sectorPhaseElement").value;
  if (!name) return;
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    await apiRequest("/api/heaven/sector-phases", "POST", { name, element });
    document.querySelector("#sectorPhaseName").value = "";
    await loadHeavenSetup(true);
    showToast(`已将 ${name} 归为${element}`);
  } catch (error) {
    showToast(error.message || "手动归类保存失败");
  } finally {
    button.disabled = false;
  }
}

async function deleteSectorPhaseOverride(name) {
  try {
    await apiRequest(`/api/heaven/sector-phases/${encodeURIComponent(name)}`, "DELETE");
    await loadHeavenSetup(true);
    showToast(`已删除 ${name} 的手动归类`);
  } catch (error) {
    showToast(error.message || "手动归类删除失败");
  }
}

async function saveAccountBirthProfile(event) {
  event.preventDefault();
  const birthDate = document.querySelector("#accountBirthDate").value;
  const birthTime = document.querySelector("#accountBirthTime").value;
  if (!birthDate || !birthTime) {
    showToast("请填写完整出生日期和时间");
    return;
  }
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  const originalText = button.textContent;
  button.textContent = "正在排盘";
  try {
    await apiRequest("/api/account/birth-profile", "POST", {
      trade_date: document.querySelector("#qiObservationDate").value || elements.tradeDate.value,
      birth_datetime: `${birthDate}T${birthTime}`,
      gender: document.querySelector("#accountBirthGender").value,
    });
    event.currentTarget.reset();
    setText("birthProfileStatus", "已加密保存");
    document.querySelector("#deleteBirthProfileButton").disabled = false;
    state.heavenInterpretations.fortune = "";
    await loadHeavenSetup(true);
    showToast("个人命理资料已保存到当前账号");
  } catch (error) {
    showToast(error.message || "个人命理资料保存失败");
  } finally {
    button.disabled = false;
    button.textContent = originalText;
  }
}

async function deleteAccountBirthProfile() {
  if (!window.confirm("确定删除当前账号保存的个人命理资料吗？")) return;
  try {
    await apiRequest("/api/account/birth-profile", "DELETE");
    state.personalField = null;
    state.heavenInterpretations.fortune = "";
    setText("birthProfileStatus", "尚未设置");
    document.querySelector("#deleteBirthProfileButton").disabled = true;
    renderPersonalFortune();
    showToast("个人命理资料已删除");
  } catch (error) {
    showToast(error.message || "个人命理资料删除失败");
  }
}

function renderPersonalFortune() {
  const container = document.querySelector("#personalFortuneResult");
  const empty = document.querySelector("#personalProfileEmpty");
  const personal = state.personalField;
  if (!personal) {
    empty.hidden = false;
    container.hidden = true;
    container.innerHTML = "";
    return;
  }
  empty.hidden = true;
  container.hidden = false;
  const tenGods = personal.ten_god_tendency || { favorable: [], caution: [] };
  const elementTendency = personal.balance_tendency || { favorable: [], caution: [] };
  const preferenceTags = (items) => (items || []).map((item) => `<em>${escapeHtml(item)}</em>`).join("") || "--";
  container.innerHTML = `
    <div class="personal-primary-grid">
      <div class="personal-day-master">
        <span>日主</span>
        <strong class="personal-day-master-character phase-text-${phaseClass(personal.day_master?.element)}">${escapeHtml(personal.day_master?.stem || "--")}</strong>
        <b class="personal-day-master-element phase-text-${phaseClass(personal.day_master?.element)}">${escapeHtml(personal.day_master?.element || "--")}</b>
        <small>${escapeHtml(personal.day_master?.strength || "")}</small>
      </div>
      <div class="personal-preferences">
        <section><span>十神喜恶</span><div class="personal-preference-line"><strong>偏宜</strong><p>${preferenceTags(tenGods.favorable)}</p></div><div class="personal-preference-line"><strong>偏慎</strong><p>${preferenceTags(tenGods.caution)}</p></div></section>
        <section><span>五行喜忌</span><div class="personal-preference-line"><strong>偏喜</strong><p>${preferenceTags(elementTendency.favorable)}</p></div><div class="personal-preference-line"><strong>偏忌</strong><p>${preferenceTags(elementTendency.caution)}</p></div></section>
      </div>
    </div>`;
}

function renderHexagramLines(containerId, lines, includeEvidence = false) {
  const container = document.querySelector(`#${containerId}`);
  container.innerHTML = [...lines].reverse().map((line) => `
    <div class="hexagram-line-row ${line.moving ? "moving" : ""}">
      <span class="hexagram-position">${escapeHtml(line.position_name)}</span>
      ${hexagramLineGraphic(line.value)}
      <div class="hexagram-line-detail">
        <strong>${escapeHtml(line.role || line.line_name)} · ${line.value}${line.moving ? " 变" : ""}</strong>
        ${includeEvidence ? `<small>${(line.evidence || []).map(escapeHtml).join("；")}</small>` : `<small>${escapeHtml(line.text || "")}</small>`}
      </div>
    </div>
  `).join("");
}

function hexagramLineGraphic(value) {
  const yang = value % 2 === 1;
  return `
    <span class="hex-line ${yang ? "yang-line" : "yin-line"}">
      <i></i>${yang ? "" : "<i></i>"}${[6, 9].includes(value) ? `<b>${value === 9 ? "○" : "×"}</b>` : ""}
    </span>
  `;
}

const HEAVEN_READING_META = {
  trend: { panel: "观势", action: "解势", done: "查看解势", status: "势已成" },
  fortune: { panel: "观气", action: "解运", done: "已解运", status: "气已定" },
  heart: { panel: "观心", action: "我已察念，开始解卦", done: "查看解卦", status: "卦已解" },
};

function heavenReadingMeta(mode = state.heavenReadingMode) {
  return HEAVEN_READING_META[mode] || HEAVEN_READING_META.trend;
}

function heavenReadingAnimationData() {
  const field = state.heavenSetup?.field || {};
  return {
    yearPillar: field.pillars?.year || "",
    movement: field.movement?.label || "",
    sixQi: {
      sitian: field.six_qi?.sitian || "",
      zaiquan: field.six_qi?.zaiquan || "",
      step: number(field.six_qi?.step) || 1,
    },
  };
}

function syncHeavenReadingAnimation() {
  const canvas = document.querySelector("#heavenReadingCanvas");
  const shouldRun = state.heavenReadingLoading && state.heavenReadingTab === "current";
  if (!canvas || !window.HeavenLoadingCanvas) return;
  if (!shouldRun) {
    stopHeavenReadingAnimation();
    return;
  }
  if (!heavenReadingAnimation) heavenReadingAnimation = new window.HeavenLoadingCanvas(canvas);
  const scene = state.heavenReadingMode === "fortune" ? "fortune" : "hexagram";
  heavenReadingAnimation.start(scene, heavenReadingAnimationData());
}

function stopHeavenReadingAnimation() {
  heavenReadingAnimation?.stop();
}

function finishHeavenReadingAnimation() {
  if (!elements.heavenReadingDialog.open || !heavenReadingAnimation?.running) {
    stopHeavenReadingAnimation();
    return Promise.resolve();
  }
  return heavenReadingAnimation.complete();
}

function openHeavenReading(mode, options = {}) {
  state.heavenReadingMode = mode;
  state.heavenReadingTab = "current";
  state.heavenReadingError = options.error || "";
  state.heavenReadingLoading = Object.hasOwn(options, "loading")
    ? Boolean(options.loading)
    : false;
  renderHeavenReadingDialog();
  openModalDialog(elements.heavenReadingDialog);
  requestAnimationFrame(() => {
    syncHeavenReadingAnimation();
    document.querySelector("#closeHeavenReadingDialog").focus();
  });
}

async function openHeavenHistory(mode) {
  state.heavenReadingMode = mode;
  state.heavenReadingTab = "history";
  state.heavenReadingSelectedId = 0;
  renderHeavenReadingDialog();
  openModalDialog(elements.heavenReadingDialog);
  await loadHeavenReadingHistory(mode);
}

function selectHeavenReadingTab(tab) {
  state.heavenReadingTab = tab === "history" ? "history" : "current";
  renderHeavenReadingDialog();
  if (state.heavenReadingTab === "history") loadHeavenReadingHistory(state.heavenReadingMode);
}

async function loadHeavenReadingHistory(mode) {
  const list = document.querySelector("#heavenReadingHistoryList");
  renderEmptyState(list, "正在读取历史记录");
  try {
    const query = new URLSearchParams({ mode, limit: "100" });
    const payload = await apiRequest(`/api/heaven/readings?${query}`);
    state.heavenReadingHistory[mode] = payload.items || [];
    if (!state.heavenReadingHistory[mode].some((item) => number(item.id) === state.heavenReadingSelectedId)) {
      state.heavenReadingSelectedId = number(state.heavenReadingHistory[mode][0]?.id);
    }
    renderHeavenReadingHistory();
  } catch (error) {
    renderEmptyState(list, error.message || "历史记录加载失败");
  }
}

function renderHeavenReadingDialog() {
  const meta = heavenReadingMeta();
  setText("heavenReadingEyebrow", `问天 · ${meta.panel}`);
  setText("heavenReadingDialogTitle", state.heavenReadingTab === "history" ? "历史记录" : meta.status);
  document.querySelectorAll("[data-heaven-reading-tab]").forEach((button) => {
    const active = button.dataset.heavenReadingTab === state.heavenReadingTab;
    button.classList.toggle("active", active);
    button.setAttribute("aria-selected", String(active));
  });
  document.querySelector("#heavenReadingCurrent").hidden = state.heavenReadingTab !== "current";
  document.querySelector("#heavenReadingHistory").hidden = state.heavenReadingTab !== "history";
  if (state.heavenReadingTab === "current") {
    renderHeavenReadingCurrent();
  } else {
    stopHeavenReadingAnimation();
    renderHeavenReadingHistory();
  }
  refreshIcons();
}

function renderHeavenReadingCurrent() {
  const reading = state.heavenInterpretations[state.heavenReadingMode];
  const loading = document.querySelector("#heavenReadingLoading");
  const empty = document.querySelector("#heavenReadingEmpty");
  const result = document.querySelector("#heavenReadingResult");
  const error = document.querySelector("#heavenReadingError");
  loading.hidden = !state.heavenReadingLoading;
  syncHeavenReadingAnimation();
  error.hidden = !state.heavenReadingError;
  error.textContent = state.heavenReadingError;
  result.hidden = state.heavenReadingLoading || !reading;
  empty.hidden = state.heavenReadingLoading || Boolean(reading) || Boolean(state.heavenReadingError);
  if (!reading || state.heavenReadingLoading) return;
  setText("heavenReadingResultStatus", heavenReadingMeta().status);
  setText("heavenReadingSubject", reading.subject || `${heavenReadingMeta().panel}解读`);
  setText("heavenReadingSubjectDetail", reading.subject_detail || displayCompactDate(reading.context_date || ""));
  setText("heavenReadingCreatedAt", reading.created_at ? formatTimestamp(reading.created_at) : "刚刚完成");
  document.querySelector("#heavenReadingAnswer").innerHTML = formatMentorAnswer(reading.answer || "");
}

function renderHeavenReadingHistory() {
  const mode = state.heavenReadingMode;
  const items = state.heavenReadingHistory[mode] || [];
  setText("heavenReadingHistoryTitle", `${heavenReadingMeta(mode).panel}记录`);
  setText("heavenReadingHistoryCount", `${items.length} 条`);
  const list = document.querySelector("#heavenReadingHistoryList");
  list.innerHTML = items.map((item) => `
    <button class="heaven-reading-history-item ${number(item.id) === state.heavenReadingSelectedId ? "active" : ""}" type="button" data-heaven-reading-id="${number(item.id)}">
      <span>${escapeHtml(item.subject)}</span>
      <small>${escapeHtml(item.subject_detail || displayCompactDate(item.context_date))}</small>
      <time>${formatTimestamp(item.created_at)}</time>
    </button>
  `).join("") || emptyStateHtml("暂无历史解读");
  const selected = items.find((item) => number(item.id) === state.heavenReadingSelectedId);
  const detail = document.querySelector("#heavenReadingHistoryDetail");
  detail.innerHTML = selected ? `
    <header><div><span>${escapeHtml(heavenReadingMeta(mode).status)}</span><h3>${escapeHtml(selected.subject)}</h3></div><time>${formatTimestamp(selected.created_at)}</time></header>
    <p>${escapeHtml(selected.subject_detail || displayCompactDate(selected.context_date))}</p>
    <div class="heaven-reading-answer">${formatMentorAnswer(selected.answer || "")}</div>
    <footer><button class="button" type="button" data-delete-heaven-reading="${number(selected.id)}"><i data-lucide="trash-2"></i><span>删除记录</span></button></footer>
  ` : emptyStateHtml("选择一条记录查看完整解读");
  refreshIcons();
}

function handleHeavenHistorySelection(event) {
  const button = event.target.closest("[data-heaven-reading-id]");
  if (!button) return;
  state.heavenReadingSelectedId = number(button.dataset.heavenReadingId);
  renderHeavenReadingHistory();
}

async function handleHeavenHistoryAction(event) {
  const button = event.target.closest("[data-delete-heaven-reading]");
  if (!button || !window.confirm("确定删除这条解读记录吗？")) return;
  const id = number(button.dataset.deleteHeavenReading);
  try {
    await apiRequest(`/api/heaven/readings/${id}`, "DELETE");
    const mode = state.heavenReadingMode;
    state.heavenReadingHistory[mode] = (state.heavenReadingHistory[mode] || []).filter((item) => number(item.id) !== id);
    if (number(state.heavenInterpretations[mode]?.id) === id) {
      state.heavenInterpretations[mode] = "";
      if (mode === "fortune" && state.heavenSetup) state.heavenSetup.daily_fortune_reading = null;
      updateHeavenInterpretationControls();
    }
    state.heavenReadingSelectedId = number(state.heavenReadingHistory[mode][0]?.id);
    renderHeavenReadingHistory();
  } catch (error) {
    showToast(error.message || "解读记录删除失败");
  }
}

async function interpretHeaven(mode) {
  const existing = state.heavenInterpretations[mode];
  if (existing) {
    openHeavenReading(mode, { loading: false });
    return;
  }
  const button = document.querySelector(mode === "trend" ? "#interpretTrendButton" : mode === "fortune" ? "#interpretFortuneButton" : "#interpretHeartButton");
  if (button.disabled) return;
  state.heavenReadingMode = mode;
  state.heavenReadingLoading = true;
  state.heavenReadingError = "";
  openHeavenReading(mode, { loading: true });
  updateHeavenInterpretationControls();
  hideHeavenNotice();
  try {
    const payload = {
      mode,
      trade_date: document.querySelector("#qiObservationDate").value || elements.tradeDate.value,
      sector: state.heavenSetup?.chart?.sector || "",
      stock_code: state.heavenSetup?.chart?.stock?.code || "",
    };
    if (mode === "trend" && state.heavenManualData) payload.manual_data = state.heavenManualData;
    if (mode === "heart") payload.lines = state.heartLines;
    const result = await apiRequest("/api/heaven/interpret", "POST", payload);
    state.heavenInterpretations[mode] = result.reading || {
      answer: result.answer,
      subject: `${heavenReadingMeta(mode).panel}解读`,
      context_date: payload.trade_date,
      created_at: new Date().toISOString(),
    };
    state.heavenReadingHistory[mode] = [];
    await finishHeavenReadingAnimation();
    state.heavenReadingLoading = false;
    renderHeavenReadingDialog();
    if (result.notice) showHeavenNotice(result.notice);
    if (mode === "heart") {
      if (await transitionHeartStage("interpretation")) await playHeartReadSequence();
    } else {
      renderHeavenInterpretation(mode, state.heavenInterpretations[mode]);
    }
  } catch (error) {
    stopHeavenReadingAnimation();
    state.heavenReadingLoading = false;
    state.heavenReadingError = error.message || "问天解读失败";
    renderHeavenReadingDialog();
    showHeavenNotice(state.heavenReadingError);
    showToast(state.heavenReadingError);
  } finally {
    state.heavenReadingLoading = false;
    updateHeavenInterpretationControls();
  }
}

function updateHeavenInterpretationControls() {
  const loading = state.heavenReadingLoading;
  const trendButton = document.querySelector("#interpretTrendButton");
  const fortuneButton = document.querySelector("#interpretFortuneButton");
  const heartButton = document.querySelector("#interpretHeartButton");
  trendButton.disabled = loading || !state.heavenSetup?.chart?.available;
  fortuneButton.disabled = loading || !state.heavenSetup?.field;
  heartButton.disabled = loading || state.heartLines.length !== 6;
  trendButton.textContent = loading && state.heavenReadingMode === "trend" ? "正在观势" : state.heavenInterpretations.trend ? HEAVEN_READING_META.trend.done : HEAVEN_READING_META.trend.action;
  fortuneButton.textContent = loading && state.heavenReadingMode === "fortune" ? "正在察运" : state.heavenInterpretations.fortune ? HEAVEN_READING_META.fortune.done : HEAVEN_READING_META.fortune.action;
  heartButton.textContent = loading && state.heavenReadingMode === "heart" ? "正在解卦" : state.heavenInterpretations.heart ? HEAVEN_READING_META.heart.done : HEAVEN_READING_META.heart.action;
  document.querySelector("#viewHeartReadingButton").disabled = !state.heavenInterpretations.heart;
}

function renderHeavenInterpretation() {
  updateHeavenInterpretationControls();
}

function initializeHeartAtmosphere() {
  const whisperContainer = document.querySelector("#heartWhispers");
  if (whisperContainer && !whisperContainer.children.length) {
    whisperContainer.innerHTML = HEART_WHISPERS.map(([text, x, y, index]) => `
      <span style="--whisper-x:${x}%;--whisper-y:${y}%;--whisper-duration:${16 + index * 1.4}s;--whisper-delay:${index * -2.1}s">${escapeHtml(text)}</span>
    `).join("");
  }
  activateHeartRises(document.querySelector(".heart-stage.active-heart-stage"));
}

function toggleHeartSound() {
  heartSound.enabled = !heartSound.enabled;
  const button = document.querySelector("#heartSoundToggle");
  button.setAttribute("aria-pressed", String(heartSound.enabled));
  button.setAttribute("aria-label", heartSound.enabled ? "关闭观心声音" : "开启观心声音");
  button.innerHTML = `<i data-lucide="${heartSound.enabled ? "volume-2" : "volume-x"}"></i><span>${heartSound.enabled ? "有声" : "静音"}</span>`;
  if (heartSound.enabled) {
    heartSound.ensure();
    heartSound.chime(520);
  }
  refreshIcons();
}

function setHeartLamp(stage) {
  const lamp = document.querySelector("#heartLamp");
  if (lamp) lamp.dataset.heartStage = stage;
}

function startHeartDust() {
  stopHeartDust();
  const canvas = document.querySelector("#heartDustCanvas");
  const panel = document.querySelector("#heavenHeartPanel");
  if (!canvas || !panel || !panel.clientWidth || !panel.clientHeight) return;
  const context = canvas.getContext("2d");
  const ratio = Math.min(2, window.devicePixelRatio || 1);
  const width = panel.clientWidth;
  const height = panel.clientHeight;
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  canvas.style.height = `${height}px`;
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  if (!heartDustParticles.length) {
    heartDustParticles = Array.from({ length: 60 }, (_, index) => ({
      x: Math.random(),
      y: Math.random(),
      radius: 0.6 + Math.random() * 1.5,
      alpha: 0.03 + Math.random() * 0.09,
      vx: (Math.random() - 0.5) * 0.00006,
      vy: -(0.00002 + Math.random() * 0.00008),
      phase: Math.random() * Math.PI * 2,
      gold: index % 2 === 0,
    }));
  }
  const draw = (now) => {
    context.clearRect(0, 0, width, height);
    heartDustParticles.forEach((particle) => {
      if (motionEnabled()) {
        particle.x += particle.vx;
        particle.y += particle.vy;
        particle.phase += 0.006;
      }
      if (particle.y < -0.02) {
        particle.y = 1.02;
        particle.x = Math.random();
      }
      if (particle.x < -0.02) particle.x = 1.02;
      if (particle.x > 1.02) particle.x = -0.02;
      const alpha = particle.alpha * (0.65 + 0.35 * Math.sin(particle.phase));
      context.beginPath();
      context.arc(particle.x * width, particle.y * height, particle.radius, 0, Math.PI * 2);
      context.fillStyle = particle.gold ? `rgba(220,195,140,${alpha})` : `rgba(190,200,225,${alpha * 0.8})`;
      context.fill();
    });
    if (motionEnabled() && state.activeView === "heavenView" && state.heavenPanel === "heart") {
      heartDustAnimationFrame = requestAnimationFrame(draw);
    } else {
      heartDustAnimationFrame = 0;
    }
  };
  heartDustAnimationFrame = requestAnimationFrame(draw);
}

function stopHeartDust() {
  if (heartDustAnimationFrame) cancelAnimationFrame(heartDustAnimationFrame);
  heartDustAnimationFrame = 0;
}

function activateHeartRises(stage) {
  if (!stage) return;
  stage.querySelectorAll(".heart-rise").forEach((item) => {
    item.classList.remove("is-visible");
    const delay = motionEnabled() ? number(item.dataset.heartDelay) : 0;
    setTimeout(() => {
      if (stage.classList.contains("active-heart-stage")) item.classList.add("is-visible");
    }, delay);
  });
}

async function transitionHeartStage(nextStage) {
  const token = ++state.heartStageToken;
  state.heartRevealToken += 1;
  const current = document.querySelector(".heart-stage.active-heart-stage");
  current?.classList.add("is-leaving");
  if (current && !await waitForHeartMotion(1050, token)) return false;
  state.heartStage = nextStage;
  renderHeartStage();
  return token === state.heartStageToken;
}

function waitForHeartMotion(duration, token = state.heartStageToken) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(token === state.heartStageToken), motionEnabled() ? duration : 0);
  });
}

async function startHeartBreathing() {
  if (state.heartTimer) clearInterval(state.heartTimer);
  state.heartSeconds = HEART_BREATH_TOTAL_MS / 1000;
  state.heartBreathingEndsAt = 0;
  document.querySelector("#beginCastingButton")?.classList.remove("is-ready");
  if (!await transitionHeartStage("breathing")) return;
  state.heartBreathingEndsAt = Date.now() + HEART_BREATH_TOTAL_MS;
  const ember = document.querySelector("#heartIncenseEmber");
  heartIncenseAnimation?.cancel();
  ember?.classList.remove("is-burning");
  if (ember) void ember.offsetWidth;
  ember?.classList.add("is-burning");
  heartIncenseAnimation = ember?.animate(
    [{ top: "0%" }, { top: "100%" }],
    {
      duration: HEART_BREATH_ACTIVE_MS,
      delay: HEART_BREATH_PREPARE_MS,
      easing: "linear",
      fill: "forwards",
    },
  ) || null;
  updateBreathingDisplay();
  state.heartTimer = setInterval(() => {
    state.heartSeconds = Math.max(0, Math.ceil((state.heartBreathingEndsAt - Date.now()) / 1000));
    updateBreathingDisplay();
    if (state.heartSeconds <= 0) finishHeartBreathing();
  }, 200);
}

function finishHeartBreathing() {
  if (state.heartTimer) clearInterval(state.heartTimer);
  state.heartTimer = null;
  state.heartBreathingEndsAt = 0;
  state.heartSeconds = 0;
  updateBreathingDisplay();
  const button = document.querySelector("#beginCastingButton");
  button.disabled = false;
  button.classList.add("is-ready");
  heartSound.chime(520);
}

function updateBreathingDisplay() {
  const remainingMs = state.heartBreathingEndsAt
    ? Math.max(0, state.heartBreathingEndsAt - Date.now())
    : Math.max(0, state.heartSeconds * 1000);
  const elapsedMs = HEART_BREATH_TOTAL_MS - remainingMs;
  const activeElapsedMs = Math.max(0, elapsedMs - HEART_BREATH_PREPARE_MS);
  const cycleElapsedMs = activeElapsedMs % HEART_BREATH_CYCLE_MS;
  const breathPhase = elapsedMs < HEART_BREATH_PREPARE_MS
    ? "prepare"
    : cycleElapsedMs < HEART_BREATH_INHALE_MS
      ? "inhale"
      : cycleElapsedMs < HEART_BREATH_INHALE_MS + HEART_BREATH_HOLD_MS
        ? "hold"
        : "exhale";
  const phase = state.heartSeconds <= 0 ? "settled" : breathPhase;
  const scene = document.querySelector("#breathingScene");
  scene.dataset.phase = phase;
  setText("breathingPhase", phase === "settled" ? "静" : phase === "prepare" ? "静" : phase === "inhale" ? "吸" : phase === "hold" ? "顿" : "呼");
  const prompt = state.heartSeconds <= 0
    ? "静心已成，可以起卦"
    : phase === "prepare"
      ? "放松片刻，准备呼吸"
      : phase === "hold"
      ? "停驻片刻，让念头自然沉下"
      : activeElapsedMs < 18_000
        ? phase === "inhale" ? "缓慢吸气，放下对答案的预设" : "缓慢呼气，让预设随之松开"
        : activeElapsedMs < 36_000
          ? phase === "inhale" ? "吸气，只留下真正想问的事" : "呼气，不急着寻找答案"
          : phase === "inhale" ? "吸气，让心停在此刻" : "呼气，不追逐经过的念头";
  setText("breathingPrompt", prompt);
}

async function beginHeartCasting() {
  if (state.heartSeconds > 0) return;
  state.heartLines = [];
  state.heartThrows = [];
  state.heartHexagram = null;
  state.heavenInterpretations.heart = "";
  heartCastingBusy = false;
  resetHeartCoins();
  await transitionHeartStage("casting");
}

function initializeHeartCoinHold() {
  const button = document.querySelector("#tossCoinsButton");
  const coins = [...document.querySelectorAll(".heart-coin")];
  const cancelHold = (cancelled = true) => {
    if (heartHoldTimer) clearTimeout(heartHoldTimer);
    heartHoldTimer = null;
    cancelAnimationFrame(heartHoldAnimationFrame);
    heartHoldAnimationFrame = 0;
    button.classList.remove("is-holding");
    button.style.setProperty("--hold-progress", "0turn");
    coins.forEach((coin) => coin.classList.remove("is-shaking"));
    if (cancelled) heartHoldStartedAt = 0;
  };
  button.addEventListener("pointerdown", (event) => {
    if (button.disabled || heartCastingBusy || (event.button !== 0 && event.pointerType !== "touch")) return;
    event.preventDefault();
    heartSound.ensure();
    heartHoldTriggered = false;
    heartHoldStartedAt = performance.now();
    button.setPointerCapture?.(event.pointerId);
    button.classList.add("is-holding");
    coins.forEach((coin) => coin.classList.add("is-shaking"));
    const charge = () => {
      if (!heartHoldStartedAt) return;
      const progress = Math.min(1, (performance.now() - heartHoldStartedAt) / 1400);
      button.style.setProperty("--hold-progress", `${progress}turn`);
      if (progress < 1) heartHoldAnimationFrame = requestAnimationFrame(charge);
    };
    heartHoldAnimationFrame = requestAnimationFrame(charge);
  });
  button.addEventListener("pointerup", async () => {
    if (!heartHoldStartedAt) return;
    const heldFor = performance.now() - heartHoldStartedAt;
    heartHoldStartedAt = 0;
    cancelHold(false);
    heartHoldTriggered = true;
    if (heldFor < 550) await waitForMotion(550 - heldFor);
    await tossHeartCoins();
  });
  button.addEventListener("pointercancel", () => cancelHold(true));
  button.addEventListener("click", (event) => {
    if (heartHoldTriggered) {
      heartHoldTriggered = false;
      event.preventDefault();
      return;
    }
    if (event.detail === 0 && !heartCastingBusy) tossHeartCoins();
  });
}

async function tossHeartCoins() {
  if (heartCastingBusy) return;
  if (state.heartLines.length >= 6) {
    heartCastingBusy = true;
    await finalizeHeartHexagram();
    return;
  }
  const stageToken = state.heartStageToken;
  const button = document.querySelector("#tossCoinsButton");
  heartCastingBusy = true;
  button.disabled = true;
  const random = new Uint32Array(3);
  crypto.getRandomValues(random);
  const coins = [...random].map((value) => value % 2 === 1);
  await animateHeartCoins(coins);
  if (stageToken !== state.heartStageToken || state.heartStage !== "casting") {
    heartCastingBusy = false;
    return;
  }
  const heads = coins.filter(Boolean).length;
  const lineValue = 6 + heads;
  state.heartLines.push(lineValue);
  state.heartThrows.push(coins.map((head) => head ? "正" : "背"));
  renderHeartCasting();
  if (state.heartLines.length === 6) {
    await finalizeHeartHexagram();
  } else {
    await waitForMotion(720);
    heartCastingBusy = false;
    button.disabled = false;
  }
}

async function animateHeartCoins(results) {
  const coinElements = [...document.querySelectorAll(".heart-coin")];
  setText("castingPrompt", "铜钱离手");
  const animations = coinElements.map((coin, index) => {
    coin.getAnimations().forEach((animation) => animation.cancel());
    const inner = coin.querySelector(".heart-coin-inner");
    inner.getAnimations().forEach((animation) => animation.cancel());
    const current = heartCoinRotations[index];
    const faceRotation = results[index] ? 0 : 180;
    const delta = ((faceRotation - (current % 360)) + 360) % 360;
    const target = current + 1440 + index * 360 + delta;
    heartCoinRotations[index] = target;
    const duration = motionEnabled() ? 1500 + index * 160 : 10;
    const delay = motionEnabled() ? index * 150 : 0;
    coin.dataset.face = results[index] ? "front" : "back";
    const spin = inner.animate(
      [{ transform: `rotateY(${current}deg)` }, { transform: `rotateY(${target}deg)` }],
      { duration, delay, easing: "cubic-bezier(.25,.55,.3,1)", fill: "forwards" },
    );
    const tilt = Math.random() * 10 - 5;
    const flight = coin.animate([
      { transform: "translateY(0) rotateZ(0deg)" },
      { transform: `translateY(-30vh) rotateZ(${tilt}deg)`, offset: 0.42 },
      { transform: `translateY(0) rotateZ(${tilt}deg)`, offset: 0.78 },
      { transform: "translateY(-13px) rotateZ(0deg)", offset: 0.9 },
      { transform: "translateY(0) rotateZ(0deg)" },
    ], { duration, delay, easing: "cubic-bezier(.3,.6,.35,1)", fill: "forwards" });
    setTimeout(() => {
      const ring = coin.querySelector(".heart-coin-ring");
      ring.classList.remove("is-bursting");
      void ring.offsetWidth;
      ring.classList.add("is-bursting");
      heartSound.coin();
    }, delay + duration * 0.79);
    return Promise.allSettled([spin.finished, flight.finished]);
  });
  await Promise.all(animations);
  setText("castingPrompt", "听其落定");
  await waitForMotion(420);
}

async function finalizeHeartHexagram() {
  const stageToken = state.heartStageToken;
  const button = document.querySelector("#tossCoinsButton");
  button.disabled = true;
  button.textContent = "正在成卦";
  try {
    const payload = await apiRequest("/api/heaven/hexagram", "POST", { lines: state.heartLines });
    if (stageToken !== state.heartStageToken || state.heartStage !== "casting") return;
    state.heartHexagram = payload.hexagram;
    updateHeavenInterpretationControls();
    document.querySelector(".heart-hexagram-shell")?.classList.add("is-complete");
    setText("castingPrompt", "卦成了");
    heartSound.chime(660);
    await waitForMotion(2200);
    if (!await transitionHeartStage("reveal")) return;
    await playHeartRevealSequence();
  } catch (error) {
    showHeavenNotice(error.message || "成卦失败");
    button.disabled = false;
    button.innerHTML = '<i class="heart-hold-charge" aria-hidden="true"></i><span>按住<br>重新成卦</span>';
    heartCastingBusy = false;
  }
}

function renderHeartStage() {
  document.querySelectorAll(".heart-stage").forEach((stage) => stage.classList.remove("active-heart-stage"));
  const stageMap = {
    intro: "heartIntro",
    breathing: "heartBreathing",
    casting: "heartCasting",
    reveal: "heartReveal",
    interpretation: "heartInterpretationStage",
  };
  document.querySelectorAll(".heart-stage").forEach((stage) => stage.classList.remove("is-leaving"));
  const activeStage = document.querySelector(`#${stageMap[state.heartStage]}`);
  activeStage.classList.add("active-heart-stage");
  document.querySelectorAll("[data-heart-step]").forEach((step) => {
    step.classList.toggle("active", step.dataset.heartStep === state.heartStage);
  });
  setHeartLamp(state.heartStage);
  activateHeartRises(activeStage);
  if (state.heartStage === "breathing") {
    document.querySelector("#beginCastingButton").disabled = state.heartSeconds > 0;
    updateBreathingDisplay();
  }
  if (state.heartStage === "casting") renderHeartCasting();
  if (state.heartStage === "reveal" && state.heartHexagram) renderHeartReveal();
  if (state.heartStage === "interpretation" && state.heartHexagram) renderHeartRead();
}

function renderHeartCasting() {
  setText("castingProgress", `${state.heartLines.length} / 6`);
  const latestThrow = state.heartThrows[state.heartThrows.length - 1] || ["静", "静", "静"];
  document.querySelectorAll(".heart-coin").forEach((coin, index) => {
    coin.setAttribute("aria-label", latestThrow[index] === "静" ? `第 ${index + 1} 枚铜钱待掷` : `第 ${index + 1} 枚铜钱${latestThrow[index]}`);
  });
  const nextPosition = LINE_POSITIONS_CLIENT[state.heartLines.length] || "成卦";
  setText(
    "castingPrompt",
    state.heartLines.length < 6
      ? `心中默念所问之事，然后掷出${nextPosition}`
      : "六爻已具，正在成卦",
  );
  const button = document.querySelector("#tossCoinsButton");
  button.innerHTML = state.heartLines.length < 6
    ? `<i aria-hidden="true"></i><span>按住<br>摇${nextPosition}</span>`
    : '<i aria-hidden="true"></i><span>正在<br>成卦</span>';
  button.disabled = heartCastingBusy || state.heartLines.length >= 6;
  const rows = [];
  for (let index = 5; index >= 0; index -= 1) {
    const value = state.heartLines[index];
    rows.push(`
      <div class="hexagram-line-row ${[6, 9].includes(value) ? "moving" : ""} ${value ? "" : "empty-line"} ${value && index === state.heartLines.length - 1 ? "new-line" : ""}">
        <span class="hexagram-position">${LINE_POSITIONS_CLIENT[index]}</span>
        ${value ? hexagramLineGraphic(value) : '<span class="hex-line heart-yao-empty" aria-hidden="true"></span>'}
        <div class="hexagram-line-detail"><strong>${value ? `${lineValueName(value)} · ${value}` : "未得"}</strong></div>
      </div>
    `);
  }
  document.querySelector("#heartCastingLines").innerHTML = rows.join("");
}

function renderHeartReveal() {
  const hexagram = state.heartHexagram;
  setText("heartHexagramName", `${hexagram.outer_trigram}上${hexagram.inner_trigram}下 · ${hexagram.name}`);
  setText("heartTransformedName", hexagram.transformed.name);
  setText("heartHexagramText", hexagram.text);
  renderHexagramLines("heartHexagramLines", hexagram.lines, false);
  document.querySelector("#heartHexagramLines").querySelectorAll(".hexagram-line-row").forEach((row) => row.classList.add("heart-reveal-line"));
  document.querySelector("#heartReveal").classList.remove("is-sequence-ready", "is-title-ready", "is-thought-typing", "is-thought-ready");
  const prompt = document.querySelector("#heartFirstThoughtPrompt");
  prompt.dataset.fullText = "看见卦象与爻辞后，心里升起的第一念是什么？";
  prompt.textContent = "";
  const button = document.querySelector("#interpretHeartButton");
  button.disabled = true;
  button.classList.remove("is-ready");
}

async function playHeartRevealSequence() {
  const token = ++state.heartRevealToken;
  const stageToken = state.heartStageToken;
  const stage = document.querySelector("#heartReveal");
  const lines = [...stage.querySelectorAll(".heart-reveal-line")].reverse();
  lines.forEach((line) => line.classList.remove("is-revealed"));
  if (!await waitForHeartMotion(280, stageToken)) return;
  for (const line of lines) {
    if (token !== state.heartRevealToken || state.heartStage !== "reveal") return;
    line.classList.add("is-revealed");
    if (!await waitForHeartMotion(520, stageToken)) return;
  }
  stage.classList.add("is-title-ready", "is-sequence-ready");
  heartSound.chime(520);
  if (!await waitForHeartMotion(1200, stageToken)) return;
  const prompt = document.querySelector("#heartFirstThoughtPrompt");
  stage.classList.add("is-thought-typing");
  if (!await typeHeartText(prompt, prompt.dataset.fullText, token, 72)) return;
  stage.classList.add("is-thought-ready");
  if (!await waitForHeartMotion(2400, stageToken)) return;
  const button = document.querySelector("#interpretHeartButton");
  button.disabled = false;
  button.classList.add("is-ready");
}

function initializeHeartLineInspection() {
  const container = document.querySelector("#heartLineTexts");
  container.addEventListener("click", (event) => {
    const item = event.target.closest(".heart-line-text");
    if (!item) return;
    const inspected = item.classList.toggle("is-inspected");
    item.setAttribute("aria-expanded", String(inspected));
  });
}

async function typeHeartText(element, text, token, speed = 72) {
  if (!element) return false;
  if (!motionEnabled()) {
    element.textContent = text;
    return true;
  }
  element.textContent = "";
  element.classList.add("heart-typing");
  for (const character of text) {
    if (token !== state.heartRevealToken || state.heartStage !== "reveal") return false;
    element.append(document.createTextNode(character));
    await new Promise((resolve) => setTimeout(resolve, speed));
  }
  element.classList.remove("heart-typing");
  return true;
}

function renderHeartRead() {
  const hexagram = state.heartHexagram;
  setText("heartReadTitle", hexagram.name);
  setText("heartReadChange", hexagram.transformed.name === hexagram.name ? "六爻安静，无之卦" : `之卦 · ${hexagram.transformed.name}`);
  setText("heartReadGuaci", hexagram.text);
  document.querySelector("#heartReadLines").innerHTML = [...hexagram.lines].reverse().map((line) => `
    <div class="heart-read-line ${line.moving ? "moving" : ""}">
      <span>${escapeHtml(line.position_name)}</span>${hexagramLineGraphic(line.value)}
    </div>
  `).join("");
  document.querySelector("#heartReadTexts").innerHTML = hexagram.lines.map((line) => `
    <article class="heart-read-text ${line.moving ? "moving" : ""}">
      <strong>${escapeHtml(line.line_name)}${line.moving ? " · 动" : ""}</strong><p>${escapeHtml(line.text)}</p>
    </article>
  `).join("");
  renderHeavenInterpretation("heart", state.heavenInterpretations.heart);
  const stage = document.querySelector("#heartInterpretationStage");
  stage.classList.remove("is-read-heading-ready", "is-read-complete");
}

async function playHeartReadSequence() {
  const token = state.heartStageToken;
  const stage = document.querySelector("#heartInterpretationStage");
  if (!await waitForHeartMotion(420, token)) return;
  stage.classList.add("is-read-heading-ready");
  const lines = [...stage.querySelectorAll(".heart-read-line")].reverse();
  const texts = [...stage.querySelectorAll(".heart-read-text")];
  for (let index = 0; index < 6; index += 1) {
    lines[index]?.classList.add("is-visible");
    texts[index]?.classList.add("is-visible");
    if (!await waitForHeartMotion(680, token)) return;
  }
  stage.classList.add("is-read-complete");
}

function resetHeartCoins() {
  heartCoinRotations.fill(0);
  document.querySelectorAll(".heart-coin").forEach((coin) => {
    coin.getAnimations().forEach((animation) => animation.cancel());
    const inner = coin.querySelector(".heart-coin-inner");
    inner.getAnimations().forEach((animation) => animation.cancel());
    inner.style.transform = "";
    coin.style.transform = "";
    coin.dataset.face = "";
    coin.querySelector(".heart-coin-ring").classList.remove("is-bursting");
  });
  const shell = document.querySelector(".heart-hexagram-shell");
  shell?.classList.remove("is-complete");
}

async function resetHeartRitual() {
  if (state.heartTimer) clearInterval(state.heartTimer);
  state.heartTimer = null;
  state.heartSeconds = HEART_BREATH_TOTAL_MS / 1000;
  state.heartBreathingEndsAt = 0;
  state.heartLines = [];
  state.heartThrows = [];
  state.heartHexagram = null;
  state.heavenInterpretations.heart = "";
  heartIncenseAnimation?.cancel();
  heartIncenseAnimation = null;
  document.querySelector("#heartIncenseEmber")?.classList.remove("is-burning");
  updateHeavenInterpretationControls();
  state.heartRevealToken += 1;
  heartCastingBusy = false;
  resetHeartCoins();
  hideHeavenNotice();
  await transitionHeartStage("intro");
}

function showHeavenNotice(message) {
  const notice = document.querySelector("#heavenNotice");
  notice.textContent = message;
  notice.hidden = false;
}

function hideHeavenNotice() {
  document.querySelector("#heavenNotice").hidden = true;
}

function phaseClass(element) {
  return { 木: "wood", 火: "fire", 土: "earth", 金: "metal", 水: "water" }[element] || "earth";
}

function signedScore(value) {
  const parsed = number(value);
  return `${parsed > 0 ? "+" : ""}${formatNumber(parsed, 2)}`;
}

function lineValueName(value) {
  return { 6: "老阴", 7: "少阳", 8: "少阴", 9: "老阳" }[value] || "";
}

function capitalize(value) {
  return value.charAt(0).toUpperCase() + value.slice(1);
}

const LINE_POSITIONS_CLIENT = ["初爻", "二爻", "三爻", "四爻", "五爻", "上爻"];

function renderScreenerResult() {
  const mode = state.screenerMode || "smart";
  const result = activeScreenerResult(mode);
  const context = activeScreenerResultContext(mode);
  const source = document.querySelector("#screenerResultSource");
  const emptyMessages = {
    smart: "当日盘后候选尚未生成",
    curated: "所选策略的当日候选尚未生成",
    quant: "尚未执行自定义选股",
  };
  if (!result) {
    setText("screenerResultCount", "0 只");
    source.hidden = true;
    source.textContent = "";
    setText("screenerDisclaimer", "历史统计不代表未来收益");
    document.querySelector("#screenerTableBody").innerHTML = "";
    document.querySelector("#screenerEmpty").textContent = emptyMessages[mode];
    document.querySelector("#screenerEmpty").hidden = false;
    renderBacktest(null);
    if (mode === "smart") setText("screenerRunStatus", "等待执行");
    updateBacktestTaskStatus();
    renderScreenerProgress();
    return;
  }
  const candidates = result.candidates || [];
  setText("screenerResultCount", `${candidates.length} 只`);
  const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "自定义选股" };
  const sourceParts = [modeLabels[mode]];
  if (mode === "smart" && context?.regime) sourceParts.push(regimeLabel(context.regime));
  sourceParts.push(mode === "quant" ? "自定义因子权重" : context?.strategyName || result.meta?.strategy_name || "未命名策略");
  source.textContent = sourceParts.join(" · ");
  source.hidden = false;
  const meta = result.meta || {};
  setText(
    "screenerDisclaimer",
    meta.realtime
      ? `盘中行情 · 历史样本截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}`
      : `盘后数据 ${displayCompactDate(meta.trade_date)} · ${result.disclaimer}`,
  );
  const empty = document.querySelector("#screenerEmpty");
  empty.textContent = mode === "curated" ? "暂无符合条件个股" : emptyMessages[mode];
  empty.hidden = candidates.length > 0;
  const body = document.querySelector("#screenerTableBody");
  const runId = number(meta.run_id);
  body.innerHTML = candidates.map((row, index) => `
    <tr data-code="${escapeHtml(row.code)}"><td class="row-number num muted">${index + 1}</td>
      <td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td><td>${escapeHtml(row.sector)}</td>
      <td class="number num streak-value">${formatNumber(row.score_display, 1)}</td>
      <td class="number num probability-value"><strong>${row.historical_probability === null ? "" : formatNumber(row.historical_probability, 1)}</strong><small>${number(row.probability_samples)} 个样本</small></td>
      <td class="number num ${changeClass(row.pct_chg)}">${signed(row.pct_chg)}</td>
      <td class="number num ${changeClass(row.return_5d)}">${signed(row.return_5d)}</td>
      <td class="number num">${formatNumber(row.volume_ratio_5d, 2)}</td><td class="number num">${formatNumber(row.sector_strength, 1)}</td>
      <td class="reason-column" title="${escapeHtml(row.reason)}">${escapeHtml(row.reason)}</td>
      <td class="risk-cell" title="${escapeHtml(row.risk_flags.join("；"))}">${escapeHtml(row.risk_flags.join("；"))}</td>
      <td><span class="screener-row-actions"><button class="table-action" type="button" data-screen-detail="${escapeHtml(row.code)}">详情</button><button class="table-action tracking-action ${isCandidateTracked(runId, row.code) ? "tracked" : ""}" type="button" data-add-tracking="${escapeHtml(row.code)}" ${!runId || isCandidateTracked(runId, row.code) ? "disabled" : ""}>${isCandidateTracked(runId, row.code) ? "已跟踪" : "加入跟踪"}</button></span></td></tr>
  `).join("");
  body.querySelectorAll("[data-screen-detail]").forEach((button) => {
    button.addEventListener("click", () => {
      const row = candidates.find((item) => item.code === button.dataset.screenDetail);
      openStock(row.code, row);
    });
  });
  body.querySelectorAll("[data-add-tracking]").forEach((button) => {
    button.addEventListener("click", () => addCandidateToTracking(button.dataset.addTracking, button));
  });
  bindStockRows(body);
  renderBacktest(result.backtest);
  if (mode === "smart") setText("screenerRunStatus", `完成 · ${candidates.length} 只`);
  updateBacktestTaskStatus();
  renderScreenerProgress();
}

async function loadScreenerTracking(force = false) {
  if (state.screenerTracking && !force) {
    renderScreenerTracking();
    return;
  }
  try {
    state.screenerTracking = await apiRequest("/api/screener/tracking?limit=12");
    renderScreenerTracking();
    if (activeScreenerResult()) renderScreenerResult();
  } catch (error) {
    showToast(error.message || "策略跟踪加载失败");
  }
}

function isCandidateTracked(runId, code) {
  if (!runId) return false;
  return (state.screenerTracking?.batches || []).some((batch) =>
    number(batch.run_id) === number(runId)
    && (batch.items || []).some((item) => item.code === code));
}

async function addCandidateToTracking(code, button) {
  const runId = number(activeScreenerResult()?.meta?.run_id);
  if (!runId) {
    showToast("本次结果缺少选股批次，请重新执行后再加入跟踪");
    return;
  }
  button.disabled = true;
  try {
    const payload = await apiRequest("/api/screener/tracking", "POST", { run_id: runId, code });
    state.screenerTracking = payload.tracking;
    renderScreenerTracking();
    renderScreenerResult();
    showToast(`${code} 已加入策略跟踪`);
  } catch (error) {
    button.disabled = false;
    showToast(error.message || "加入跟踪失败");
  }
}

async function refreshScreenerTracking() {
  const button = document.querySelector("#refreshTrackingButton");
  button.disabled = true;
  setStatus("正在更新策略跟踪");
  try {
    const payload = await apiRequest("/api/screener/tracking/refresh", "POST", {
      trade_date: elements.tradeDate.value,
    });
    state.screenerTracking = payload.tracking;
    renderScreenerTracking();
    if (payload.notice) showToast(payload.notice);
    setStatus("策略跟踪已更新");
  } catch (error) {
    showToast(error.message || "策略跟踪刷新失败");
    setStatus("策略跟踪刷新失败");
  } finally {
    button.disabled = false;
  }
}

function renderScreenerTracking() {
  const payload = state.screenerTracking || { batches: [], summary: {} };
  const batches = payload.batches || [];
  const rows = batches.flatMap((batch) => (batch.items || []).map((item) => ({
    ...item,
    run_id: batch.run_id,
    selection_date: batch.selection_date,
    strategy_name: batch.strategy_name,
  })));
  setText("trackingBatchCount", `${batches.length} 批`);
  const summary = payload.summary || {};
  document.querySelector("#trackingSummary").innerHTML = [
    ["跟踪标的", `${number(summary.total)} 只`],
    ["已有 T+1", `${number(summary.observed)} 只`],
    ["T+1 胜率", trackingPercent(summary.t1_win_rate)],
    ["T+5 胜率", trackingPercent(summary.t5_win_rate)],
    ["T+5 平均", trackingReturn(summary.average_t5)],
  ].map(([label, value]) => `<div><span>${label}</span><strong>${value}</strong></div>`).join("");
  document.querySelector("#trackingEmpty").hidden = rows.length > 0;
  document.querySelector("#trackingTableBody").innerHTML = rows.map((row) => `
    <tr data-code="${escapeHtml(row.code)}">
      <td>${displayCompactDate(row.selection_date)}</td>
      <td class="tracking-strategy" title="${escapeHtml(row.strategy_name)}">${escapeHtml(row.strategy_name)}</td>
      <td><span class="stock-cell"><strong class="sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
      <td class="number num">${row.entry_price == null ? "" : formatNumber(row.entry_price, 2)}</td>
      ${["t1_open", "t1_close", "t3_close", "t5_close", "max_gain", "max_drawdown"].map((key) => `<td class="number num ${row[key] == null ? "" : changeClass(row[key])}">${trackingReturn(row[key], false)}</td>`).join("")}
      <td><span class="tracking-status ${row.status === "已完成" ? "complete" : row.observed_days ? "active" : "pending"}">${escapeHtml(row.status)}</span></td>
      <td><button class="table-action danger" type="button" data-remove-tracking="${row.id}">移除</button></td>
    </tr>
  `).join("");
  bindStockRows(document.querySelector("#trackingTableBody"));
}

async function handleTrackingTableAction(event) {
  const button = event.target.closest("[data-remove-tracking]");
  if (!button) return;
  if (!window.confirm("确定停止跟踪这只股票吗？")) return;
  button.disabled = true;
  try {
    const payload = await apiRequest(`/api/screener/tracking/${button.dataset.removeTracking}`, "DELETE");
    state.screenerTracking = payload.tracking;
    renderScreenerTracking();
    if (activeScreenerResult()) renderScreenerResult();
    showToast("已移出策略跟踪");
  } catch (error) {
    button.disabled = false;
    showToast(error.message || "移除跟踪失败");
  }
}

function trackingReturn(value, includeUnit = true) {
  return value == null ? (includeUnit ? "--" : "") : `${signed(value)}${includeUnit ? "%" : ""}`;
}

function trackingPercent(value) {
  return value == null ? "--" : `${formatNumber(value, 1)}%`;
}

function renderBacktest(backtest) {
  const panel = document.querySelector("#backtestPanel");
  panel.hidden = !backtest;
  if (!backtest) return;
  setText("backtestDefinition", backtest.definition);
  document.querySelector("#backtestMetrics").innerHTML = [
    ["历史样本", `${number(backtest.samples)} 个`],
    ["条件胜率", `${formatNumber(backtest.win_rate, 1)}%`],
    ["平均3日收益", `${signed(backtest.average_3d_return)}%`],
    ["平均最大回撤", `${signed(backtest.average_drawdown)}%`],
  ].map(([label, value]) => `<div class="dragon-metric"><span>${label}</span><strong>${value}</strong></div>`).join("");
}

function parseFormulaEditor() {
  try {
    return JSON.parse(document.querySelector("#formulaEditor").value);
  } catch {
    throw new Error("受控公式不是有效的 JSON");
  }
}

function exportScreenerResults() {
  const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "量化选股" };
  exportRows(modeLabels[state.screenerMode] || "智能选股", activeScreenerResult()?.candidates || [], [
    ["股票代码", "code"], ["股票名称", "name"], ["板块", "sector"], ["综合分", "score_display"],
    ["历史条件估计%", "historical_probability"], ["当日涨幅%", "pct_chg"], ["5日涨幅%", "return_5d"],
    ["10日涨幅%", "return_10d"], ["量比", "volume_ratio_5d"], ["板块强度", "sector_strength"],
    ["主要贡献", "reason"], ["风险标记", "risk_flags"],
  ]);
}

function regimeLabel(regime) {
  return state.screenerSetup?.regimes?.find((item) => item.id === regime)?.label || regime;
}

function currentChartPalette() {
  const style = getComputedStyle(document.documentElement);
  const color = (token, fallback) => style.getPropertyValue(token).trim() || fallback;
  return {
    background: color("--chart-background", "#fbfcfd"),
    grid: color("--chart-grid", "#e2e8ec"),
    axis: color("--chart-axis", "#6c7983"),
    zero: color("--chart-zero", "#aeb7c1"),
    line: color("--chart-line", "#1d65c1"),
    average: color("--chart-average", "#b7791f"),
    up: color("--chart-up", "#c93f45"),
    down: color("--chart-down", "#087a55"),
    upVolume: color("--chart-up-volume", "rgba(201, 63, 69, .58)"),
    downVolume: color("--chart-down-volume", "rgba(8, 122, 85, .58)"),
    area: color("--chart-area", "rgba(37, 99, 235, .07)"),
    alertArea: color("--chart-alert-area", "rgba(224, 69, 54, .05)"),
    movingAverage: color("--chart-moving-average", "#d1d5db"),
    repair: color("--chart-repair", "#f59e0b"),
    ma10: color("--chart-ma-10", "#a76500"),
    ma20: color("--chart-ma-20", "#626c78"),
  };
}

function drawCandlestick(context, x, item, priceY, candleWidth, palette = currentChartPalette()) {
  const rising = number(item.close) >= number(item.open);
  const color = rising ? palette.up : palette.down;
  const highY = priceY(item.high);
  const lowY = priceY(item.low);
  const openY = priceY(item.open);
  const closeY = priceY(item.close);
  const bodyTop = Math.min(openY, closeY);
  const bodyBottom = Math.max(openY, closeY);
  const bodyHeight = Math.max(1, bodyBottom - bodyTop);

  context.strokeStyle = color;
  context.fillStyle = color;
  context.lineWidth = 1;
  context.beginPath();
  context.moveTo(x, highY);
  context.lineTo(x, bodyTop);
  context.moveTo(x, bodyBottom);
  context.lineTo(x, lowY);
  context.stroke();

  const bodyLeft = x - candleWidth / 2;
  if (rising) {
    context.fillStyle = palette.background;
    context.fillRect(bodyLeft, bodyTop, candleWidth, bodyHeight);
    context.strokeStyle = color;
    context.strokeRect(bodyLeft, bodyTop, candleWidth, bodyHeight);
  } else {
    context.fillStyle = color;
    context.fillRect(bodyLeft, bodyTop, candleWidth, bodyHeight);
  }
  return color;
}

function drawPriceChart(prices) {
  const canvas = elements.priceChart;
  if (!prices?.length) {
    clearPriceChart("暂无日 K 数据");
    return;
  }
  const rect = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  const width = Math.max(320, rect.width);
  const height = Math.max(220, rect.height);
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);

  const left = 48;
  const right = 12;
  const top = 14;
  const bottom = 22;
  const volumeHeight = 54;
  const gap = 12;
  const priceBottom = height - bottom - volumeHeight - gap;
  const plotWidth = width - left - right;
  const highs = prices.map((item) => number(item.high));
  const lows = prices.map((item) => number(item.low));
  const maximum = Math.max(...highs);
  const minimum = Math.min(...lows);
  const range = Math.max(maximum - minimum, maximum * 0.01, 0.01);
  const volumes = prices.map((item) => number(item.volume));
  const maxVolume = Math.max(...volumes, 1);
  const priceY = (value) => top + (maximum - value) / range * (priceBottom - top);
  const step = plotWidth / prices.length;
  const candleWidth = clamp(step * 0.62, 2, 8);

  context.strokeStyle = palette.grid;
  context.fillStyle = palette.axis;
  context.font = "11px Microsoft YaHei";
  context.textAlign = "right";
  for (let line = 0; line <= 4; line += 1) {
    const y = top + (priceBottom - top) * line / 4;
    context.beginPath();
    context.moveTo(left, y);
    context.lineTo(width - right, y);
    context.stroke();
    context.fillText((maximum - range * line / 4).toFixed(2), left - 5, y + 4);
  }

  prices.forEach((item, index) => {
    const x = left + step * index + step / 2;
    const color = drawCandlestick(context, x, item, priceY, candleWidth, palette);
    const volumeBarHeight = number(item.volume) / maxVolume * volumeHeight;
    context.fillStyle = color;
    context.globalAlpha = 0.75;
    context.fillRect(x - candleWidth / 2, height - bottom - volumeBarHeight, candleWidth, volumeBarHeight);
    context.globalAlpha = 1;
  });

  context.textAlign = "center";
  context.fillStyle = palette.axis;
  const labelIndexes = [0, Math.floor((prices.length - 1) / 2), prices.length - 1];
  labelIndexes.forEach((index) => {
    const x = left + step * index + step / 2;
    context.fillText(String(prices[index].trade_date).slice(5), x, height - 5);
  });
}

function clearPriceChart(message) {
  const canvas = elements.priceChart;
  const context = canvas.getContext("2d");
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.max(320, Math.round(rect.width));
  canvas.height = Math.max(220, Math.round(rect.height));
  const palette = currentChartPalette();
  context.fillStyle = palette.background;
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = palette.axis;
  context.font = "13px Microsoft YaHei";
  context.textAlign = "center";
  context.fillText(message, canvas.width / 2, canvas.height / 2);
}

function prepareStockPreviewCanvas() {
  const canvas = elements.stockPreviewChart;
  const rect = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  const width = Math.max(300, rect.width || 488);
  const height = Math.max(210, rect.height || 232);
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  context.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei UI", sans-serif';
  return { canvas, context, width, height, palette };
}

function drawPreviewGrid(context, width, top, bottom, left, right, maximum, range) {
  const palette = currentChartPalette();
  context.strokeStyle = palette.grid;
  context.fillStyle = palette.axis;
  context.textAlign = "right";
  context.lineWidth = 1;
  for (let line = 0; line <= 3; line += 1) {
    const y = top + (bottom - top) * line / 3;
    context.beginPath();
    context.moveTo(left, y);
    context.lineTo(width - right, y);
    context.stroke();
    context.fillText((maximum - range * line / 3).toFixed(2), left - 5, y + 4);
  }
}

function intradayMinuteOffset(value) {
  const [hour, minute] = String(value || "").split(":").map((part) => number(part));
  const clockMinute = hour * 60 + minute;
  const morningStart = 9 * 60 + 30;
  const morningEnd = 11 * 60 + 30;
  const afternoonStart = 13 * 60;
  const afternoonEnd = 15 * 60;
  if (clockMinute <= morningEnd) return clamp(clockMinute - morningStart, 0, 120);
  if (clockMinute < afternoonStart) return 120;
  return 120 + clamp(clockMinute - afternoonStart, 0, afternoonEnd - afternoonStart);
}

function drawIntradayCanvas(canvas, points, dailyPrices = [], referenceClose = 0) {
  const rect = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  const width = Math.max(300, rect.width || 488);
  const height = Math.max(210, rect.height || 232);
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  context.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei UI", sans-serif';
  const left = 45;
  const right = 10;
  const top = 12;
  const volumeHeight = 38;
  const bottom = 18;
  const gap = 9;
  const priceBottom = height - bottom - volumeHeight - gap;
  const closes = points.map((point) => number(point.close));
  const previousClose = number(referenceClose || dailyPrices.at(-2)?.close || points[0]?.open || closes[0]);
  const maximum = Math.max(...points.map((point) => number(point.high || point.close)), previousClose);
  const minimum = Math.min(...points.map((point) => number(point.low || point.close)), previousClose);
  const deviation = Math.max(
    Math.abs(maximum - previousClose),
    Math.abs(previousClose - minimum),
    previousClose * 0.003,
    0.01,
  ) * 1.08;
  const chartMaximum = previousClose + deviation;
  const chartMinimum = previousClose - deviation;
  const range = Math.max(chartMaximum - chartMinimum, 0.01);
  const plotWidth = width - left - right;
  const priceY = (value) => top + (chartMaximum - value) / range * (priceBottom - top);
  const pointX = (index) => left + plotWidth * intradayMinuteOffset(points[index]?.time) / 240;
  drawPreviewGrid(context, width, top, priceBottom, left, right, chartMaximum, range);

  context.save();
  context.setLineDash([4, 4]);
  context.strokeStyle = palette.zero;
  context.beginPath();
  context.moveTo(left, priceY(previousClose));
  context.lineTo(width - right, priceY(previousClose));
  context.stroke();
  context.restore();
  context.fillStyle = palette.axis;
  context.textAlign = "right";
  context.fillText("0.00%", width - right, priceY(previousClose) - 4);

  context.strokeStyle = palette.line;
  context.lineWidth = 1.7;
  context.beginPath();
  points.forEach((point, index) => {
    const x = pointX(index);
    const y = priceY(point.close);
    if (index === 0) context.moveTo(x, y);
    else context.lineTo(x, y);
  });
  context.stroke();

  const averages = points.map((point) => number(point.average)).filter((value) => value > 0);
  if (averages.length) {
    context.strokeStyle = palette.average;
    context.lineWidth = 1.25;
    context.beginPath();
    let averageStarted = false;
    points.forEach((point, index) => {
      const average = number(point.average);
      if (average <= 0) return;
      const x = pointX(index);
      const y = priceY(average);
      if (!averageStarted) {
        context.moveTo(x, y);
        averageStarted = true;
      } else context.lineTo(x, y);
    });
    context.stroke();
  }

  const maxVolume = Math.max(...points.map((point) => number(point.volume)), 1);
  const barWidth = clamp(plotWidth / Math.max(points.length, 1) * 0.72, 1, 3);
  points.forEach((point, index) => {
    const x = pointX(index);
    const barHeight = number(point.volume) / maxVolume * volumeHeight;
    context.fillStyle = number(point.close) >= number(point.open) ? palette.upVolume : palette.downVolume;
    context.fillRect(x - barWidth / 2, height - bottom - barHeight, barWidth, barHeight);
  });

  context.fillStyle = palette.axis;
  context.textAlign = "center";
  [
    { offset: 0, label: "09:30" },
    { offset: 120, label: "11:30 / 13:00" },
    { offset: 240, label: "15:00" },
  ].forEach((marker) => {
    context.fillText(marker.label, left + plotWidth * marker.offset / 240, height - 4);
  });
  return {
    latest: closes.at(-1),
    maximum,
    minimum,
  };
}

function drawIntradayPreviewChart(points, dailyPrices, referenceClose = 0) {
  const summary = drawIntradayCanvas(elements.stockPreviewChart, points, dailyPrices, referenceClose);
  setText(
    "stockPreviewSummary",
    `分时 ${points.length} 点，最新 ${formatNumber(summary.latest, 2)}，最高 ${formatNumber(summary.maximum, 2)}，最低 ${formatNumber(summary.minimum, 2)}。`,
  );
}

function drawDailyPreviewChart(prices) {
  const { context, width, height, palette } = prepareStockPreviewCanvas();
  const visible = prices.slice(-45);
  const visibleStart = prices.length - visible.length;
  const left = 45;
  const right = 10;
  const top = 24;
  const volumeHeight = 34;
  const bottom = 18;
  const gap = 8;
  const priceBottom = height - bottom - volumeHeight - gap;
  const maximum = Math.max(...visible.map((item) => number(item.high)));
  const minimum = Math.min(...visible.map((item) => number(item.low)));
  const padding = Math.max((maximum - minimum) * 0.05, maximum * 0.002, 0.01);
  const chartMaximum = maximum + padding;
  const chartMinimum = minimum - padding;
  const range = Math.max(chartMaximum - chartMinimum, 0.01);
  const plotWidth = width - left - right;
  const step = plotWidth / Math.max(visible.length, 1);
  const candleWidth = clamp(step * 0.58, 2, 7);
  const priceY = (value) => top + (chartMaximum - value) / range * (priceBottom - top);
  drawPreviewGrid(context, width, top, priceBottom, left, right, chartMaximum, range);

  const maxVolume = Math.max(...visible.map((item) => number(item.volume)), 1);
  visible.forEach((item, index) => {
    const x = left + step * index + step / 2;
    const color = drawCandlestick(context, x, item, priceY, candleWidth, palette);
    const volumeBarHeight = number(item.volume) / maxVolume * volumeHeight;
    context.fillStyle = color;
    context.globalAlpha = 0.62;
    context.fillRect(x - candleWidth / 2, height - bottom - volumeBarHeight, candleWidth, volumeBarHeight);
    context.globalAlpha = 1;
  });

  const movingAverages = [
    { days: 5, color: palette.line },
    { days: 10, color: palette.ma10 },
    { days: 20, color: palette.ma20 },
  ];
  movingAverages.forEach(({ days, color }) => {
    context.strokeStyle = color;
    context.lineWidth = 1.25;
    context.beginPath();
    let started = false;
    visible.forEach((_item, index) => {
      const absoluteIndex = visibleStart + index;
      if (absoluteIndex < days - 1) return;
      const values = prices.slice(absoluteIndex - days + 1, absoluteIndex + 1);
      const average = values.reduce((sum, item) => sum + number(item.close), 0) / days;
      const x = left + step * index + step / 2;
      const y = priceY(average);
      if (!started) {
        context.moveTo(x, y);
        started = true;
      } else context.lineTo(x, y);
    });
    context.stroke();
  });

  context.textAlign = "left";
  movingAverages.forEach(({ days, color }, index) => {
    context.fillStyle = color;
    context.fillText(`MA${days}`, left + index * 42, 12);
  });
  context.fillStyle = palette.axis;
  context.textAlign = "center";
  [0, Math.floor((visible.length - 1) / 2), visible.length - 1].forEach((index) => {
    const x = left + step * index + step / 2;
    context.fillText(String(visible[index]?.trade_date || "").slice(5), x, height - 4);
  });
  const firstClose = number(visible[0]?.close);
  const latestClose = number(visible.at(-1)?.close);
  const periodChange = firstClose ? (latestClose / firstClose - 1) * 100 : 0;
  setText(
    "stockPreviewSummary",
    `近 ${visible.length} 日涨跌 ${signed(periodChange)}%，区间最高 ${formatNumber(maximum, 2)}，最低 ${formatNumber(minimum, 2)}。`,
  );
}

function clearStockPreviewChart(message) {
  const { context, width, height } = prepareStockPreviewCanvas();
  if (!message) return;
  context.fillStyle = "#74808d";
  context.textAlign = "center";
  context.fillText(message, width / 2, height / 2);
}

function bindStockRows(container) {
  animateRows(container);
  decorateStockPreviewTargets(container);
  container.querySelectorAll("[data-code]").forEach((rowElement) => {
    rowElement.addEventListener("click", (event) => {
      const interactive = event.target.closest("button, a, input, select, textarea, summary");
      if (interactive && interactive !== rowElement) return;
      openStock(rowElement.dataset.code, findStockFallback(rowElement.dataset.code));
    });
  });
}

function decorateStockPreviewTargets(container) {
  container.querySelectorAll(".stock-code").forEach((trigger) => {
    const code = stockCodeFromTrigger(trigger);
    if (!code) return;
    trigger.classList.add("stock-preview-trigger");
    trigger.tabIndex = 0;
    trigger.setAttribute("role", "button");
    trigger.setAttribute("aria-label", `预览 ${code} 行情`);
    trigger.title = "悬停预览行情，点击查看完整详情";
  });
}

function stockCodeFromTrigger(trigger) {
  const candidate = trigger?.dataset?.stockPreviewCode
    || trigger?.closest?.("[data-code]")?.dataset?.code
    || trigger?.textContent?.trim();
  const matched = String(candidate || "").match(/\b(\d{6})\b/);
  return matched ? matched[1] : "";
}

function marketPreviewTargetFromTrigger(trigger) {
  if (trigger?.classList?.contains("market-preview-trigger")) {
    const type = String(trigger.dataset.marketPreviewType || "").trim().toLowerCase();
    const id = String(trigger.dataset.marketPreviewId || "").trim().toUpperCase();
    if (type === "theme" && id) {
      const item = (state.themeLibrary?.items || []).find((row) => String(row.code) === id) || {};
      return {
        type,
        id,
        code: id,
        name: item.name || trigger.textContent?.trim() || "--",
        type_label: "题材",
        change: item.change,
        value: item.close,
      };
    }
  }
  const code = stockCodeFromTrigger(trigger);
  return code ? { type: "stock", id: code, code } : null;
}

function previewTriggerFromEvent(event) {
  return event.target.closest?.(".stock-preview-trigger, .market-preview-trigger");
}

function showMarketPreview(target, trigger) {
  if (!target) return;
  if (target.type === "stock") showStockPreview(target.id, trigger);
  else showEntityPreview(target, trigger);
}

function findStockFallback(code) {
  const dashboardRows = [
    ...(state.dashboard?.limits || []),
    ...(state.dashboard?.broken || []),
    ...(state.dashboard?.down_limits || []),
    ...(state.dashboard?.yesterday_limits || []),
  ];
  const screenerRows = Object.values(state.screenerResultStore)
    .flatMap((entry) => entry?.result?.candidates || []);
  const dragonRows = (state.dragonTiger?.traders || []).flatMap((trader) => trader.operations || []);
  const auctionRows = state.auctionData?.rows || [];
  const themeRows = state.themeDetail?.members || [];
  const popularityRows = state.popularityData?.combined || [];
  const row = [...dashboardRows, ...screenerRows, ...dragonRows, ...auctionRows, ...themeRows, ...popularityRows, ...(state.watchlist || [])]
    .find((item) => String(item.code) === String(code));
  if (!row) return { code, name: "--", sector: "其他" };
  return {
    ...row,
    code,
    change: row.change ?? row.current_change ?? row.pct_chg ?? 0,
    sector: row.sector || row.industry || "其他",
  };
}

function supportsStockPreviewHover() {
  return window.matchMedia("(hover: hover) and (pointer: fine)").matches
    && window.innerWidth > 720;
}

function handleStockPreviewPointerOver(event) {
  if (!supportsStockPreviewHover()) return;
  const trigger = previewTriggerFromEvent(event);
  if (!trigger || trigger === event.relatedTarget?.closest?.(".stock-preview-trigger, .market-preview-trigger")) return;
  const target = marketPreviewTargetFromTrigger(trigger);
  if (!target) return;
  cancelStockPreviewClose();
  clearTimeout(stockPreviewOpenTimer);
  stockPreviewOpenTimer = setTimeout(() => showMarketPreview(target, trigger), STOCK_PREVIEW_DELAY);
}

function handleStockPreviewPointerOut(event) {
  if (!supportsStockPreviewHover()) return;
  const trigger = previewTriggerFromEvent(event);
  if (!trigger || trigger === event.relatedTarget?.closest?.(".stock-preview-trigger, .market-preview-trigger")) return;
  clearTimeout(stockPreviewOpenTimer);
  if (event.relatedTarget instanceof Node && elements.stockPreview.contains(event.relatedTarget)) return;
  scheduleStockPreviewClose();
}

function handleStockPreviewFocus(event) {
  if (!supportsStockPreviewHover()) return;
  const trigger = event.target.closest?.(".stock-preview-trigger");
  if (!trigger) return;
  const code = stockCodeFromTrigger(trigger);
  if (!code) return;
  clearTimeout(stockPreviewOpenTimer);
  stockPreviewOpenTimer = setTimeout(() => showStockPreview(code, trigger), 120);
}

function handleStockPreviewFocusOut(event) {
  const trigger = event.target.closest?.(".stock-preview-trigger");
  if (!trigger) return;
  if (event.relatedTarget instanceof Node && elements.stockPreview.contains(event.relatedTarget)) return;
  clearTimeout(stockPreviewOpenTimer);
  scheduleStockPreviewClose();
}

function handleMobileStockPreviewClick(event) {
  if (window.innerWidth > 720) return;
  const trigger = event.target.closest?.(".stock-preview-trigger");
  if (!trigger) return;
  const code = stockCodeFromTrigger(trigger);
  if (!code) return;
  event.preventDefault();
  event.stopPropagation();
  showStockPreview(code, trigger);
}

function handleStockPreviewKeydown(event) {
  if (event.key === "Escape" && !elements.stockPreview.hidden) {
    closeStockPreview();
    stockPreviewAnchor?.focus?.();
    return;
  }
  if (event.key !== "Enter") return;
  const trigger = event.target.closest?.(".stock-preview-trigger");
  if (!trigger) return;
  const code = stockCodeFromTrigger(trigger);
  if (!code) return;
  event.preventDefault();
  if (window.innerWidth <= 720) showStockPreview(code, trigger);
  else openStock(code, findStockFallback(code));
}

function cancelStockPreviewClose() {
  clearTimeout(stockPreviewCloseTimer);
}

function scheduleStockPreviewClose() {
  clearTimeout(stockPreviewCloseTimer);
  stockPreviewCloseTimer = setTimeout(closeStockPreview, 160);
}

async function showStockPreview(code, trigger) {
  clearTimeout(stockPreviewOpenTimer);
  cancelStockPreviewClose();
  if (!/^\d{6}$/.test(String(code))) return;
  stockPreviewAnchor = trigger;
  state.stockPreviewCode = String(code);
  state.stockPreviewType = "stock";
  state.stockPreviewItem = null;
  state.stockPreviewFallback = findStockFallback(code);
  state.stockPreviewPayload = null;
  state.stockPreviewChart = "daily";
  renderStockPreviewLoading();
  elements.stockPreview.hidden = false;
  const mobile = window.innerWidth <= 720;
  elements.stockPreviewBackdrop.hidden = !mobile;
  document.body.classList.toggle("stock-preview-open", mobile);
  requestAnimationFrame(repositionStockPreview);

  const cacheKey = `${code}:latest`;
  const cached = stockPreviewCache.get(cacheKey);
  if (cached && cached.expiresAt > Date.now()) {
    renderStockPreview(cached.payload);
    return;
  }
  if (cached) stockPreviewCache.delete(cacheKey);
  stockPreviewAbortController?.abort();
  stockPreviewAbortController = new AbortController();
  try {
    const payload = await apiRequest(
      `/api/stock/${encodeURIComponent(code)}/preview`,
      "GET",
      null,
      { signal: stockPreviewAbortController.signal },
    );
    if (state.stockPreviewCode !== String(code) || elements.stockPreview.hidden) return;
    const cacheMs = payload.meta?.realtime ? LIVE_REFRESH_DEFAULT_MS : STOCK_PREVIEW_CACHE_MS;
    stockPreviewCache.set(cacheKey, { payload, expiresAt: Date.now() + cacheMs });
    while (stockPreviewCache.size > 48) stockPreviewCache.delete(stockPreviewCache.keys().next().value);
    renderStockPreview(payload);
  } catch (error) {
    if (error.name === "AbortError" || state.stockPreviewCode !== String(code)) return;
    renderStockPreviewError(error.message || "行情预览加载失败");
  }
}

async function showEntityPreview(item, trigger) {
  const type = String(item?.type || "").trim().toLowerCase();
  const id = String(item?.id || item?.code || "").trim().toUpperCase();
  if (type !== "theme" || !id) return;
  clearTimeout(stockPreviewOpenTimer);
  cancelStockPreviewClose();
  stockPreviewAnchor = trigger;
  state.stockPreviewCode = id;
  state.stockPreviewType = type;
  state.stockPreviewItem = { ...item, id, code: item.code || id, type, type_label: item.type_label || "题材" };
  state.stockPreviewFallback = {
    code: item.code || id,
    name: item.name || "--",
    sector: item.type_label || "题材",
    price: item.value,
    change: item.change,
  };
  state.stockPreviewPayload = null;
  state.stockPreviewChart = "daily";
  renderStockPreviewLoading();
  elements.stockPreview.hidden = false;
  const mobile = window.innerWidth <= 720;
  elements.stockPreviewBackdrop.hidden = !mobile;
  document.body.classList.toggle("stock-preview-open", mobile);
  requestAnimationFrame(repositionStockPreview);

  const cacheKey = `${type}:${id}:latest`;
  const cached = stockPreviewCache.get(cacheKey);
  if (cached && cached.expiresAt > Date.now()) {
    renderStockPreview(cached.payload);
    return;
  }
  if (cached) stockPreviewCache.delete(cacheKey);
  stockPreviewAbortController?.abort();
  stockPreviewAbortController = new AbortController();
  try {
    const params = new URLSearchParams({ type, id, trade_date: todayString() });
    const detail = await apiRequest(
      `/api/search/detail?${params}`,
      "GET",
      null,
      { signal: stockPreviewAbortController.signal },
    );
    if (state.stockPreviewType !== type || state.stockPreviewCode !== id || elements.stockPreview.hidden) return;
    const entity = detail.entity || {};
    const payload = {
      stock: {
        code: entity.code || id,
        name: entity.name || item.name || "--",
        industry: entity.type_label || item.type_label || "题材",
        price: entity.value,
        change: entity.change,
      },
      prices: detail.series || [],
      intraday: [],
      meta: {
        trade_date: detail.meta?.trade_date || "",
        realtime: Boolean(detail.meta?.realtime),
        intraday_status: "idle",
        intraday_notice: "",
      },
    };
    stockPreviewCache.set(cacheKey, { payload, expiresAt: Date.now() + STOCK_PREVIEW_CACHE_MS });
    while (stockPreviewCache.size > 48) stockPreviewCache.delete(stockPreviewCache.keys().next().value);
    renderStockPreview(payload);
  } catch (error) {
    if (error.name === "AbortError" || state.stockPreviewType !== type || state.stockPreviewCode !== id) return;
    renderStockPreviewError(error.message || "题材行情预览加载失败");
  }
}

function renderStockPreviewLoading() {
  const fallback = state.stockPreviewFallback || {};
  selectStockPreviewChart("daily");
  setText("stockPreviewCode", state.stockPreviewCode || "--");
  setText("stockPreviewName", fallback.name || "正在加载");
  setText("stockPreviewSector", fallback.sector || "--");
  setText("stockPreviewPrice", "--");
  setText("stockPreviewChange", "--");
  document.querySelector("#stockPreviewChange").className = "";
  setText("stockPreviewDate", "最新行情");
  setText("stockPreviewSource", "正在读取行情");
  setText("stockPreviewSummary", "等待行情数据");
  document.querySelector("#stockPreviewLoading").hidden = false;
  clearStockPreviewChart("");
}

function renderStockPreview(payload) {
  state.stockPreviewPayload = payload;
  const fallback = state.stockPreviewFallback || {};
  const stock = payload.stock || {};
  const price = stock.price;
  const change = stock.change;
  setText("stockPreviewCode", stock.code || state.stockPreviewCode);
  setText("stockPreviewName", stock.name && stock.name !== "--" ? stock.name : fallback.name || "--");
  setText("stockPreviewSector", stock.industry && stock.industry !== "其他" ? stock.industry : fallback.sector || "其他");
  setText("stockPreviewPrice", meaningfulNumber(price) ? formatNumber(price, 2) : "--");
  setText("stockPreviewChange", meaningfulNumber(change) ? `${signed(change)}%` : "--");
  document.querySelector("#stockPreviewChange").className = changeClass(change);
  document.querySelector("#stockPreviewLoading").hidden = true;
  selectStockPreviewChart("daily");
  requestAnimationFrame(repositionStockPreview);
}

function renderStockPreviewError(message) {
  document.querySelector("#stockPreviewLoading").hidden = true;
  setText("stockPreviewSource", "行情加载失败");
  setText("stockPreviewSummary", message);
  clearStockPreviewChart("加载失败");
}

function selectStockPreviewChart(chart) {
  state.stockPreviewChart = chart === "daily" ? "daily" : "intraday";
  document.querySelectorAll("[data-preview-chart]").forEach((button) => {
    const active = button.dataset.previewChart === state.stockPreviewChart;
    button.classList.toggle("active", active);
    button.setAttribute("aria-selected", String(active));
  });
  const payload = state.stockPreviewPayload;
  if (!payload) return;
  if (state.stockPreviewChart === "intraday") {
    if (state.stockPreviewType !== "stock" && payload.meta?.intraday_status === "idle") {
      payload.meta.intraday_status = "loading";
      setText("stockPreviewDate", "正在加载分时");
      setText("stockPreviewSource", "正在读取最新分时");
      setText("stockPreviewSummary", "等待分时行情数据");
      clearStockPreviewChart("");
      loadEntityPreviewIntraday();
      return;
    }
    if (state.stockPreviewType !== "stock" && payload.meta?.intraday_status === "loading") return;
    setText("stockPreviewDate", payload.meta?.intraday_trade_date || payload.meta?.trade_date || "最新行情");
    setText(
      "stockPreviewSource",
      (payload.intraday || []).length ? "最新分时 · 1分钟" : "分时暂不可用",
    );
    if ((payload.intraday || []).length) {
      drawIntradayPreviewChart(
        payload.intraday,
        payload.prices || [],
        payload.meta?.intraday_previous_close,
      );
    }
    else {
      clearStockPreviewChart("分时数据不可用");
      setText("stockPreviewSummary", payload.meta?.intraday_notice || "该交易日暂无分时数据。");
    }
  } else if ((payload.prices || []).length) {
    setText("stockPreviewDate", payload.meta?.trade_date || "最新行情");
    setText("stockPreviewSource", `日 K 行情 · ${payload.prices.length} 个交易日`);
    drawDailyPreviewChart(payload.prices);
  } else {
    setText("stockPreviewDate", payload.meta?.trade_date || "最新行情");
    setText("stockPreviewSource", "日 K 行情暂不可用");
    clearStockPreviewChart("暂无日K数据");
    setText("stockPreviewSummary", "该股票暂无可用的日K数据。");
  }
}

async function loadEntityPreviewIntraday() {
  const type = state.stockPreviewType;
  const id = state.stockPreviewCode;
  const payload = state.stockPreviewPayload;
  if (type === "stock" || !id || !payload) return;
  stockPreviewAbortController?.abort();
  stockPreviewAbortController = new AbortController();
  try {
    const params = new URLSearchParams({ type, id });
    const intraday = await apiRequest(
      `/api/chart/intraday?${params}`,
      "GET",
      null,
      { signal: stockPreviewAbortController.signal },
    );
    if (state.stockPreviewType !== type || state.stockPreviewCode !== id || elements.stockPreview.hidden) return;
    payload.intraday = intraday.points || [];
    payload.meta.intraday_status = payload.intraday.length ? "available" : "empty";
    payload.meta.intraday_trade_date = intraday.meta?.trade_date || "";
    payload.meta.intraday_previous_close = intraday.meta?.previous_close || 0;
    payload.meta.intraday_notice = payload.intraday.length ? "" : "该题材暂无可用分时数据。";
    if (state.stockPreviewChart === "intraday") selectStockPreviewChart("intraday");
  } catch (error) {
    if (error.name === "AbortError" || state.stockPreviewType !== type || state.stockPreviewCode !== id) return;
    payload.meta.intraday_status = "unavailable";
    payload.meta.intraday_notice = error.message || "题材分时行情暂不可用。";
    if (state.stockPreviewChart === "intraday") selectStockPreviewChart("intraday");
  }
}

function closeStockPreview() {
  clearTimeout(stockPreviewOpenTimer);
  clearTimeout(stockPreviewCloseTimer);
  stockPreviewAbortController?.abort();
  stockPreviewAbortController = null;
  elements.stockPreview.hidden = true;
  elements.stockPreviewBackdrop.hidden = true;
  document.body.classList.remove("stock-preview-open");
  state.stockPreviewPayload = null;
  state.stockPreviewCode = "";
  state.stockPreviewType = "stock";
  state.stockPreviewItem = null;
}

function openStockDetailFromPreview() {
  const code = state.stockPreviewCode;
  const fallback = state.stockPreviewFallback;
  const type = state.stockPreviewType;
  const item = state.stockPreviewItem;
  if (!code) return;
  closeStockPreview();
  if (type === "stock") openStock(code, fallback);
  else if (item) openEntityDetail(item);
}

function repositionStockPreview() {
  if (elements.stockPreview.hidden || window.innerWidth <= 720 || !stockPreviewAnchor?.isConnected) return;
  const anchor = stockPreviewAnchor.getBoundingClientRect();
  const preview = elements.stockPreview.getBoundingClientRect();
  const gap = 12;
  let left = anchor.right + gap;
  if (left + preview.width > window.innerWidth - 8) left = anchor.left - preview.width - gap;
  left = clamp(left, 8, Math.max(8, window.innerWidth - preview.width - 8));
  const top = clamp(anchor.top - 48, 64, Math.max(64, window.innerHeight - preview.height - 8));
  elements.stockPreview.style.left = `${Math.round(left)}px`;
  elements.stockPreview.style.top = `${Math.round(top)}px`;
}

async function loadAlerts(openDialog = false) {
  try {
    const query = new URLSearchParams({ status: state.alertFilter, as_of: todayString() });
    const payload = await apiRequest(`/api/alerts?${query}`);
    state.alerts = payload.items || [];
    state.alertUnreadCount = number(payload.unread_count);
    renderAlerts();
    if (openDialog) openModalDialog(elements.alertsDialog);
  } catch (error) {
    if (openDialog) showToast(error.message || "提醒加载失败");
  }
}

function openAlerts() {
  toggleHeaderCommandMenu(false);
  toggleAccountDropdown(false);
  document.querySelector("#alertDate").value ||= todayString();
  openModalDialog(elements.alertsDialog);
  loadAlerts();
}

function openStockReminder() {
  const stock = state.activeStock || {};
  document.querySelector("#alertTitle").value = `${stock.name || stock.code || "个股"}观察提醒`;
  document.querySelector("#alertCode").value = stock.code || "";
  document.querySelector("#alertDate").value = todayString();
  if (elements.stockDialog.open) elements.stockDialog.close();
  openAlerts();
  document.querySelector("#alertContent").focus();
}

function selectAlertFilter(filter) {
  state.alertFilter = filter === "unread" ? "unread" : "all";
  document.querySelectorAll("[data-alert-filter]").forEach((button) => {
    button.classList.toggle("active", button.dataset.alertFilter === state.alertFilter);
  });
  loadAlerts();
}

async function saveAlert(event) {
  event.preventDefault();
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    const payload = await apiRequest("/api/alerts", "POST", {
      title: document.querySelector("#alertTitle").value.trim(),
      remind_date: document.querySelector("#alertDate").value,
      code: document.querySelector("#alertCode").value.trim(),
      content: document.querySelector("#alertContent").value.trim(),
    });
    event.currentTarget.reset();
    document.querySelector("#alertDate").value = todayString();
    state.alertFilter = "all";
    state.alerts = payload.items || [];
    state.alertUnreadCount = number(payload.unread_count);
    renderAlerts();
    showToast("提醒已保存");
  } catch (error) {
    showToast(error.message || "提醒保存失败");
  } finally {
    button.disabled = false;
  }
}

async function markAllAlertsRead() {
  try {
    await apiRequest("/api/alerts/read-all", "POST", { as_of: todayString() });
    await loadAlerts();
  } catch (error) {
    showToast(error.message || "提醒状态更新失败");
  }
}

async function handleAlertAction(event) {
  const button = event.target.closest("[data-alert-action]");
  if (!button) return;
  const id = number(button.dataset.alertId);
  if (!id) return;
  try {
    if (button.dataset.alertAction === "delete") {
      await apiRequest(`/api/alerts/${id}`, "DELETE");
    } else {
      await apiRequest(`/api/alerts/${id}/read`, "POST", {});
    }
    await loadAlerts();
  } catch (error) {
    showToast(error.message || "提醒操作失败");
  }
}

function renderAlerts() {
  const badge = document.querySelector("#alertBadge");
  badge.hidden = state.alertUnreadCount <= 0;
  badge.textContent = state.alertUnreadCount > 99 ? "99+" : String(state.alertUnreadCount);
  document.querySelector("#alertButton").classList.toggle("has-alerts", state.alertUnreadCount > 0);
  setText("alertListCount", `${state.alerts.length} 条`);
  document.querySelectorAll("[data-alert-filter]").forEach((button) => {
    button.classList.toggle("active", button.dataset.alertFilter === state.alertFilter);
  });
  document.querySelector("#markAllAlertsRead").disabled = state.alertUnreadCount <= 0;
  const container = document.querySelector("#alertList");
  container.innerHTML = state.alerts.map((item) => {
    const upcoming = !item.due;
    const kindLabel = item.kind === "manual" ? "自定提醒" : item.kind === "strategy_t5" ? "跟踪完成" : "策略反馈";
    return `<article class="alert-item ${item.is_read ? "is-read" : "is-unread"} ${upcoming ? "is-upcoming" : ""}">
      <div class="alert-item-icon"><i data-lucide="${upcoming ? "calendar-clock" : item.kind === "manual" ? "bell" : "chart-no-axes-combined"}"></i></div>
      <div class="alert-item-copy">
        <div><span>${escapeHtml(kindLabel)}</span><time>${displayCompactDate(item.available_date)}</time></div>
        <strong>${escapeHtml(item.title)}</strong>
        ${item.content ? `<p>${escapeHtml(item.content)}</p>` : ""}
        ${item.code ? `<button class="stock-preview-trigger alert-stock-link" type="button" data-code="${escapeHtml(item.code)}">${escapeHtml(item.code)}</button>` : ""}
      </div>
      <div class="alert-item-actions">
        ${!item.is_read && !upcoming ? `<button class="icon-button" type="button" data-alert-action="read" data-alert-id="${number(item.id)}" title="标为已读" aria-label="标为已读"><i data-lucide="check"></i></button>` : ""}
        <button class="icon-button" type="button" data-alert-action="delete" data-alert-id="${number(item.id)}" title="删除提醒" aria-label="删除提醒"><i data-lucide="trash-2"></i></button>
      </div>
    </article>`;
  }).join("") || emptyStateHtml("暂无提醒");
  bindStockRows(container);
  refreshIcons();
}

async function openReviewAssistant() {
  toggleHeaderCommandMenu(false);
  toggleAccountDropdown(false);
  openModalDialog(elements.assistantDialog);
  updateAssistantControls();
  if (!hasMemberAccess()) {
    document.querySelector("#closeAssistantDialog").focus();
    return;
  }
  try {
    const payload = await apiRequest("/api/assistant/messages");
    state.assistantMessages = payload.items || [];
    renderAssistantMessages();
  } catch (error) {
    showToast(error.message || "对话记录加载失败");
  }
  document.querySelector("#assistantQuestion").focus();
}

function useAssistantPrompt(prompt) {
  const input = document.querySelector("#assistantQuestion");
  input.value = prompt;
  input.focus();
}

async function sendAssistantQuestion(event) {
  event.preventDefault();
  if (state.assistantLoading) return;
  const input = document.querySelector("#assistantQuestion");
  const question = input.value.trim();
  if (!question) return;
  input.value = "";
  state.assistantMessages.push({ role: "user", content: question, context_date: elements.tradeDate.value.replaceAll("-", "") });
  state.assistantMessages.push({ role: "assistant", content: "", streaming: true, context_date: elements.tradeDate.value.replaceAll("-", "") });
  state.assistantLoading = true;
  state.assistantController = new AbortController();
  updateAssistantControls();
  renderAssistantMessages();
  try {
    await streamAssistantRequest(question, state.assistantController.signal, (chunk) => {
      const message = state.assistantMessages.at(-1);
      if (message?.role === "assistant") message.content += chunk;
      scheduleAssistantRender();
    });
    const message = state.assistantMessages.at(-1);
    if (message) message.streaming = false;
    setStatus("复盘助手回答完成");
  } catch (error) {
    const message = state.assistantMessages.at(-1);
    if (message?.role === "assistant") {
      message.streaming = false;
      message.error = true;
      if (!message.content) message.content = error.name === "AbortError" ? "已停止生成。" : error.message || "回答失败，请稍后重试。";
    }
    if (error.name !== "AbortError") showToast(error.message || "复盘助手回答失败");
  } finally {
    state.assistantLoading = false;
    state.assistantController = null;
    updateAssistantControls();
    renderAssistantMessages();
    input.focus();
  }
}

async function streamAssistantRequest(question, signal, onDelta) {
  await window.XiaobaiAPI.streamNdjson("/api/assistant/chat", {
    method: "POST",
    body: { question, trade_date: elements.tradeDate.value },
    signal,
    errorMessage: "复盘助手暂不可用",
    onEvent: (event) => {
      if (event.type === "delta") onDelta(String(event.content || ""));
    },
  });
}

function stopAssistantResponse() {
  state.assistantController?.abort();
}

async function clearAssistantConversation() {
  if (state.assistantLoading || !state.assistantMessages.length) return;
  if (!window.confirm("确定清空复盘助手的对话记录吗？")) return;
  try {
    await apiRequest("/api/assistant/messages", "DELETE");
    state.assistantMessages = [];
    renderAssistantMessages();
  } catch (error) {
    showToast(error.message || "对话记录清空失败");
  }
}

function scheduleAssistantRender() {
  if (assistantRenderFrame) return;
  assistantRenderFrame = requestAnimationFrame(() => {
    assistantRenderFrame = 0;
    renderAssistantMessages();
  });
}

function renderAssistantMessages() {
  const container = document.querySelector("#assistantMessages");
  container.innerHTML = state.assistantMessages.map((message) => `
    <article class="assistant-message ${message.role} ${message.error ? "is-error" : ""}">
      <div class="assistant-message-label">${message.role === "user" ? "我" : "复盘助手"}${message.context_date ? `<time>${displayCompactDate(message.context_date)}</time>` : ""}</div>
      <div class="assistant-message-content">${message.role === "assistant" ? (message.content ? formatMentorAnswer(message.content) : '<span class="assistant-thinking">正在整理复盘数据</span>') : escapeHtml(message.content)}</div>
      ${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
    </article>
  `).join("") || emptyStateHtml("可以从市场、策略或自己的交易记录开始复盘");
  updateAssistantControls();
  requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
}

function updateAssistantControls() {
  const unlocked = hasMemberAccess();
  elements.assistantDialog.classList.toggle("member-locked", !unlocked);
  document.querySelector("#assistantMemberGate").hidden = unlocked;
  document.querySelector("#assistantMemberContent").setAttribute("aria-disabled", String(!unlocked));
  document.querySelector("#assistantQuestion").disabled = !unlocked || state.assistantLoading;
  document.querySelector("#sendAssistant").disabled = !unlocked || state.assistantLoading;
  document.querySelector("#stopAssistant").hidden = !unlocked || !state.assistantLoading;
  document.querySelector("#clearAssistantMessages").disabled = !unlocked || state.assistantLoading || !state.assistantMessages.length;
  document.querySelectorAll("[data-assistant-prompt]").forEach((button) => {
    button.disabled = !unlocked || state.assistantLoading;
  });
}

function openGlobalSearch() {
  if (!state.user) return;
  toggleHeaderCommandMenu(false);
  openModalDialog(elements.globalSearchDialog);
  requestAnimationFrame(() => {
    elements.globalSearchInput.focus();
    elements.globalSearchInput.select();
  });
}

function handleGlobalSearchShortcut(event) {
  if (!event.ctrlKey || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
  if (!state.user) return;
  if (event.defaultPrevented) {
    showToast("Ctrl+K 已被其他功能占用，请点击顶部搜索按钮");
    return;
  }
  event.preventDefault();
  openGlobalSearch();
}

function closeGlobalSearch() {
  clearTimeout(globalSearchTimer);
  if (elements.globalSearchDialog.open) elements.globalSearchDialog.close();
}

function scheduleGlobalSearch() {
  clearTimeout(globalSearchTimer);
  const query = elements.globalSearchInput.value.trim();
  state.globalSearchActiveIndex = -1;
  if (!query) {
    state.globalSearchResults = [];
    renderGlobalSearchEmpty("输入名称或代码开始搜索", "使用方向键选择，回车打开详情", "corner-down-left");
    return;
  }
  elements.globalSearchResults.innerHTML = '<div class="global-search-loading"><span class="spinner" aria-hidden="true"></span><span>正在搜索</span></div>';
  globalSearchTimer = setTimeout(() => runGlobalSearch(query), 160);
}

async function runGlobalSearch(query) {
  const requestSequence = ++state.globalSearchRequestSequence;
  const params = new URLSearchParams({ q: query, trade_date: elements.tradeDate.value });
  try {
    const payload = await apiRequest(`/api/search?${params}`);
    if (requestSequence !== state.globalSearchRequestSequence || elements.globalSearchInput.value.trim() !== query) return;
    renderGlobalSearchResults(payload.groups || {});
  } catch (error) {
    if (requestSequence !== state.globalSearchRequestSequence) return;
    state.globalSearchResults = [];
    renderGlobalSearchEmpty(error.message || "搜索失败", "请稍后重试", "circle-alert");
  }
}

function renderGlobalSearchResults(groups) {
  const definitions = [
    ["stocks", "股票"],
    ["sectors", "板块"],
    ["themes", "题材"],
    ["indices", "指数"],
  ];
  const iconNames = { stock: "chart-candlestick", sector: "layout-grid", theme: "lightbulb", index: "chart-line" };
  const flattened = [];
  const sections = [];
  definitions.forEach(([key, label]) => {
    const items = Array.isArray(groups[key]) ? groups[key] : [];
    if (!items.length) return;
    const rows = items.map((item) => {
      const index = flattened.length;
      flattened.push(item);
      return `<button class="global-search-result" type="button" role="option" aria-selected="false" data-search-result-index="${index}">
        <span class="global-search-result-icon"><i data-lucide="${iconNames[item.type] || "search"}"></i></span>
        <span class="global-search-result-copy"><strong>${escapeHtml(item.name || "--")}</strong><span>${escapeHtml(item.subtitle || item.type_label || label)}</span></span>
        <span class="global-search-result-code">${escapeHtml(item.code || "")}</span>
      </button>`;
    }).join("");
    sections.push(`<section class="global-search-group" aria-label="${label}"><h3 class="global-search-group-title">${label}</h3>${rows}</section>`);
  });
  state.globalSearchResults = flattened;
  state.globalSearchActiveIndex = flattened.length ? 0 : -1;
  if (!flattened.length) {
    renderGlobalSearchEmpty("没有找到相关结果", "可尝试输入完整名称或六位股票代码", "search-x");
    return;
  }
  elements.globalSearchResults.innerHTML = sections.join("");
  updateGlobalSearchSelection(false);
  refreshIcons();
}

function renderGlobalSearchEmpty(title, hint, iconName) {
  elements.globalSearchResults.innerHTML = `<div class="global-search-empty"><i data-lucide="${iconName}"></i><p>${escapeHtml(title)}</p><span>${escapeHtml(hint)}</span></div>`;
  refreshIcons();
}

function handleGlobalSearchInputKeydown(event) {
  if (event.key === "Escape") {
    event.preventDefault();
    closeGlobalSearch();
    return;
  }
  if (!["ArrowDown", "ArrowUp", "Enter"].includes(event.key)) return;
  if (!state.globalSearchResults.length) return;
  event.preventDefault();
  if (event.key === "Enter") {
    openGlobalSearchResult(state.globalSearchActiveIndex);
    return;
  }
  const direction = event.key === "ArrowDown" ? 1 : -1;
  state.globalSearchActiveIndex = (state.globalSearchActiveIndex + direction + state.globalSearchResults.length) % state.globalSearchResults.length;
  updateGlobalSearchSelection(true);
}

function updateGlobalSearchSelection(scrollIntoView) {
  elements.globalSearchResults.querySelectorAll("[data-search-result-index]").forEach((item) => {
    const selected = number(item.dataset.searchResultIndex) === state.globalSearchActiveIndex;
    item.classList.toggle("is-active", selected);
    item.setAttribute("aria-selected", String(selected));
    if (selected && scrollIntoView) item.scrollIntoView({ block: "nearest" });
  });
}

function openGlobalSearchResult(index) {
  const item = state.globalSearchResults[index];
  if (!item) return;
  closeGlobalSearch();
  if (item.type === "stock") {
    openStock(item.id, { code: item.code, name: item.name, sector: item.industry || "其他" });
    return;
  }
  openEntityDetail(item);
}

async function openEntityDetail(item) {
  state.entityDetailItem = item;
  state.entityDetailPayload = null;
  state.entityDetailIntraday = null;
  state.entityDetailChartMode = "daily";
  const requestSequence = ++state.entityDetailRequestSequence;
  syncDetailChartButtons("entity", "daily");
  setText("entityDetailCode", item.code || item.id || "--");
  setText("entityDetailName", item.name || "--");
  setText("entityDetailValue", "--");
  setText("entityDetailChange", "--");
  setText("entityDetailType", item.type_label || "--");
  setText("entityDetailDate", "正在加载行情");
  document.querySelector("#entityDetailChange").className = "";
  renderEmptyState("entityDetailMetrics", "正在加载交易数据");
  openModalDialog(elements.entityDetailDialog);
  clearEntityDetailChart("正在加载日 K 数据");
  try {
    const params = new URLSearchParams({ type: item.type, id: item.id, trade_date: elements.tradeDate.value });
    const payload = await apiRequest(`/api/search/detail?${params}`);
    if (requestSequence !== state.entityDetailRequestSequence) return;
    state.entityDetailPayload = payload;
    const entity = payload.entity || {};
    setText("entityDetailCode", entity.code || item.code || "--");
    setText("entityDetailName", entity.name || item.name || "--");
    setText("entityDetailValue", meaningfulNumber(entity.value) && number(entity.value) !== 0 ? formatNumber(entity.value, 2) : "--");
    setText("entityDetailChange", `${signed(entity.change)}%`);
    setText("entityDetailType", entity.type_label || item.type_label || "--");
    document.querySelector("#entityDetailChange").className = changeClass(entity.change);
    renderEntityDetailMetrics(payload.metrics || []);
    if (state.entityDetailChartMode === "daily") {
      setText("entityDetailDate", `${payload.meta?.realtime ? "实时" : "收盘"} · ${payload.meta?.trade_date || "--"}`);
      requestAnimationFrame(() => drawEntityDetailChart(payload.series || []));
    }
  } catch (error) {
    if (requestSequence !== state.entityDetailRequestSequence) return;
    setText("entityDetailDate", "行情加载失败");
    renderEmptyState("entityDetailMetrics", error.message || "交易数据加载失败");
    if (state.entityDetailChartMode === "daily") clearEntityDetailChart(error.message || "行情加载失败");
    showToast(error.message || "详情加载失败");
  }
}

async function selectEntityDetailChart(mode) {
  const selected = mode === "intraday" ? "intraday" : "daily";
  state.entityDetailChartMode = selected;
  syncDetailChartButtons("entity", selected);
  if (selected === "daily") {
    const payload = state.entityDetailPayload;
    if (payload) {
      setText("entityDetailDate", `${payload.meta?.realtime ? "实时" : "收盘"} · ${payload.meta?.trade_date || "--"}`);
      requestAnimationFrame(() => drawEntityDetailChart(payload.series || []));
    } else clearEntityDetailChart("正在加载日 K 数据");
    return;
  }

  if (state.entityDetailIntraday) {
    renderEntityIntraday(state.entityDetailIntraday);
    return;
  }
  const item = state.entityDetailItem;
  if (!item) return;
  const requestSequence = state.entityDetailRequestSequence;
  setText("entityDetailDate", "正在加载分时");
  clearEntityDetailChart("正在加载分时数据");
  try {
    const params = new URLSearchParams({ type: item.type, id: item.id });
    const payload = await apiRequest(`/api/chart/intraday?${params}`);
    if (requestSequence !== state.entityDetailRequestSequence) return;
    state.entityDetailIntraday = payload;
    if (state.entityDetailChartMode === "intraday") renderEntityIntraday(payload);
  } catch (error) {
    if (requestSequence !== state.entityDetailRequestSequence || state.entityDetailChartMode !== "intraday") return;
    setText("entityDetailDate", "分时暂不可用");
    clearEntityDetailChart(error.message || "分时行情暂不可用");
  }
}

function renderEntityIntraday(payload) {
  const points = payload.points || [];
  if (!points.length) {
    setText("entityDetailDate", "分时暂不可用");
    clearEntityDetailChart("分时行情暂不可用");
    return;
  }
  setText("entityDetailDate", `分时 · ${payload.meta?.trade_date || "--"}`);
  requestAnimationFrame(() => {
    if (state.entityDetailChartMode !== "intraday") return;
    drawIntradayCanvas(elements.entityDetailChart, points, [], payload.meta?.previous_close);
  });
}

function syncDetailChartButtons(scope, mode) {
  const selector = scope === "stock" ? "[data-stock-detail-chart]" : "[data-entity-detail-chart]";
  const datasetKey = scope === "stock" ? "stockDetailChart" : "entityDetailChart";
  document.querySelectorAll(selector).forEach((button) => {
    const active = button.dataset[datasetKey] === mode;
    button.classList.toggle("active", active);
    button.setAttribute("aria-pressed", String(active));
  });
}

function renderEntityDetailMetrics(metrics) {
  const container = document.querySelector("#entityDetailMetrics");
  if (!metrics.length) {
    renderEmptyState(container, "暂无交易数据");
    return;
  }
  container.innerHTML = metrics.map((metric) => {
    const value = typeof metric.value === "number" ? formatNumber(metric.value, Number.isInteger(metric.value) ? 0 : 2) : String(metric.value ?? "--");
    const tone = metric.tone === "change" ? changeClass(metric.value) : "";
    return `<div><dt>${escapeHtml(metric.label)}</dt><dd class="${tone}">${escapeHtml(value)}${escapeHtml(metric.unit || "")}</dd></div>`;
  }).join("");
}

function drawEntityDetailChart(series, canvas = elements.entityDetailChart) {
  const candles = (series || []).filter((item) => number(item.close) > 0).map((item) => {
    const close = number(item.close);
    const open = number(item.open) || close;
    const high = Math.max(number(item.high) || close, open, close);
    const low = Math.min(number(item.low) || close, open, close);
    return { ...item, open, high, low, close };
  });
  if (!candles.length) {
    clearEntityDetailChart("暂无日 K 数据", canvas);
    return;
  }
  const rect = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  const width = Math.max(320, rect.width);
  const height = Math.max(220, rect.height);
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  const left = 48;
  const right = 12;
  const top = 14;
  const bottom = 22;
  const volumeHeight = 54;
  const gap = 12;
  const priceBottom = height - bottom - volumeHeight - gap;
  const plotWidth = width - left - right;
  const maximum = Math.max(...candles.map((item) => item.high));
  const minimum = Math.min(...candles.map((item) => item.low));
  const range = Math.max(maximum - minimum, maximum * 0.01, 0.01);
  const maxVolume = Math.max(...candles.map((item) => number(item.volume)), 1);
  const priceY = (value) => top + (maximum - value) / range * (priceBottom - top);
  const step = plotWidth / candles.length;
  const candleWidth = clamp(step * 0.62, 2, 8);
  context.strokeStyle = palette.grid;
  context.fillStyle = palette.axis;
  context.font = "11px Microsoft YaHei";
  context.textAlign = "right";
  for (let line = 0; line <= 4; line += 1) {
    const lineY = top + (priceBottom - top) * line / 4;
    context.beginPath();
    context.moveTo(left, lineY);
    context.lineTo(width - right, lineY);
    context.stroke();
    context.fillText((maximum - range * line / 4).toFixed(2), left - 6, lineY + 4);
  }

  candles.forEach((item, index) => {
    const x = left + step * index + step / 2;
    const color = drawCandlestick(context, x, item, priceY, candleWidth, palette);
    const volumeBarHeight = number(item.volume) / maxVolume * volumeHeight;
    context.fillStyle = color;
    context.globalAlpha = 0.72;
    context.fillRect(x - candleWidth / 2, height - bottom - volumeBarHeight, candleWidth, volumeBarHeight);
    context.globalAlpha = 1;
  });

  context.textAlign = "center";
  context.fillStyle = palette.axis;
  [0, Math.floor((candles.length - 1) / 2), candles.length - 1].forEach((index) => {
    const x = left + step * index + step / 2;
    context.fillText(String(candles[index].trade_date || "").slice(5), x, height - 5);
  });
}

function clearEntityDetailChart(message, canvas = elements.entityDetailChart) {
  const rect = canvas.getBoundingClientRect();
  const width = Math.max(320, Math.round(rect.width || 680));
  const height = Math.max(220, Math.round(rect.height || 300));
  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  context.fillStyle = palette.axis;
  context.font = "13px Microsoft YaHei";
  context.textAlign = "center";
  context.fillText(message, width / 2, height / 2);
}

function meaningfulNumber(value) {
  return value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value));
}

async function openStock(code, fallback = null) {
  closeStockPreview();
  const pools = [state.dashboard?.limits || [], state.dashboard?.broken || [], state.dashboard?.down_limits || []];
  const row = pools.flat().find((item) => String(item.code) === String(code)) || fallback || { code, name: "--", sector: "其他" };
  state.activeStock = row;
  state.stockDetail = null;
  state.stockDetailIntraday = null;
  state.stockDetailChartMode = "daily";
  const requestSequence = ++state.stockDetailRequestSequence;
  syncDetailChartButtons("stock", "daily");
  setText("detailCode", row.code);
  setText("detailName", row.name);
  setText("detailPrice", formatNumber(row.price, 2));
  setText("detailChange", `${signed(row.change)}%`);
  const changeElement = document.querySelector("#detailChange");
  changeElement.className = changeClass(row.change);
  setText("detailStreak", row.status === "涨停" ? streakLabel(row.streak) : row.status || "--");
  setText("detailReason", row.reason || "--");
  setText("detailSector", row.sector || "其他");
  setText("detailFirst", row.first_time || "--");
  setText("detailLast", row.last_time || "--");
  setText("detailOpen", `${number(row.open_times)} 次`);
  setText("detailTurnover", `${formatNumber(row.turnover_rate, 2)}%`);
  setText("detailAmount", `${formatNumber(row.amount_billion, 2)} 亿`);
  setText("detailSeal", `${formatNumber(row.seal_amount_million, 0)} 万`);
  setText("chartSource", "正在加载行情");
  setText("flowNet", "--");
  setText("flowLarge", "--");
  setText("flowMedium", "--");
  setText("flowSmall", "--");
  document.querySelector("#reasonInput").value = row.reason || "";
  document.querySelector("#stockNoteContent").value = "";
  document.querySelector("#stockNotePlan").value = "";
  renderEmptyState("stockNotes", "正在加载笔记");
  updateWatchButton();
  openModalDialog(elements.stockDialog);
  clearPriceChart("正在加载日 K 数据");
  try {
    const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
    const payload = await apiRequest(`/api/stock/${encodeURIComponent(code)}?${query}`);
    if (requestSequence !== state.stockDetailRequestSequence) return;
    state.stockDetail = payload;
    const stock = payload.stock || {};
    state.activeStock = { ...row, name: stock.name || row.name, sector: stock.industry || row.sector };
    setText("detailName", stock.name || row.name);
    setText("detailPrice", formatNumber(stock.price || row.price, 2));
    setText("detailChange", `${signed(stock.change ?? row.change)}%`);
    renderMoneyflow(payload.moneyflow || {});
    renderStockNotes(payload.notes || []);
    updateWatchButton();
    if (state.stockDetailChartMode === "daily") {
      setText("chartSource", `日 K 行情 · ${payload.prices.length} 个交易日`);
      requestAnimationFrame(() => drawPriceChart(payload.prices || []));
    }
  } catch (error) {
    if (requestSequence !== state.stockDetailRequestSequence) return;
    setText("chartSource", "行情加载失败");
    if (state.stockDetailChartMode === "daily") clearPriceChart(error.message || "行情加载失败");
    showToast(error.message || "个股详情加载失败");
  }
}

async function selectStockDetailChart(mode) {
  const selected = mode === "intraday" ? "intraday" : "daily";
  state.stockDetailChartMode = selected;
  syncDetailChartButtons("stock", selected);
  if (selected === "daily") {
    const prices = state.stockDetail?.prices || [];
    setText("chartSource", prices.length ? `日 K 行情 · ${prices.length} 个交易日` : "正在加载行情");
    if (prices.length) requestAnimationFrame(() => drawPriceChart(prices));
    else clearPriceChart("正在加载日 K 数据");
    return;
  }

  if (state.stockDetailIntraday) {
    renderStockDetailIntraday(state.stockDetailIntraday);
    return;
  }
  const code = String(state.activeStock?.code || "");
  if (!/^\d{6}$/.test(code)) return;
  const requestSequence = state.stockDetailRequestSequence;
  setText("chartSource", "正在加载分时");
  clearPriceChart("正在加载分时数据");
  try {
    const params = new URLSearchParams({ type: "stock", id: code });
    const payload = await apiRequest(`/api/chart/intraday?${params}`);
    if (requestSequence !== state.stockDetailRequestSequence) return;
    state.stockDetailIntraday = payload;
    if (state.stockDetailChartMode === "intraday") renderStockDetailIntraday(payload);
  } catch (error) {
    if (requestSequence !== state.stockDetailRequestSequence || state.stockDetailChartMode !== "intraday") return;
    setText("chartSource", "分时暂不可用");
    clearPriceChart(error.message || "分时行情暂不可用");
  }
}

function renderStockDetailIntraday(payload) {
  const points = payload.points || [];
  if (!points.length) {
    setText("chartSource", "分时暂不可用");
    clearPriceChart("分时行情暂不可用");
    return;
  }
  setText("chartSource", `分时 · ${payload.meta?.trade_date || "--"}`);
  requestAnimationFrame(() => {
    if (state.stockDetailChartMode !== "intraday") return;
    drawIntradayCanvas(elements.priceChart, points, [], payload.meta?.previous_close);
  });
}

function openActiveStockInHeaven() {
  const code = state.activeStock?.code;
  if (!/^\d{6}$/.test(String(code || ""))) return;
  elements.stockDialog.close();
  state.heavenPanel = "trend";
  state.heavenManualData = null;
  const input = document.querySelector("#heavenStockInput");
  input.value = code;
  openView("heavenView");
  selectHeavenPanel("trend", true);
}

function hasMemberAccess() {
  return state.user?.role === "admin" || Boolean(state.user?.membership?.active);
}

function updateAccountIdentityBadges(membership = {}) {
  const isAdmin = state.user?.role === "admin" || Boolean(membership.is_admin);
  const subscribed = Boolean(membership.subscribed);
  document.querySelector("#accountAdminBadge").hidden = !isAdmin;
  const vipBadge = document.querySelector("#accountVipBadge");
  vipBadge.hidden = false;
  vipBadge.classList.toggle("is-nonmember", !subscribed);
  setText("accountVipLabel", subscribed ? "会员" : "非会员");
  vipBadge.title = subscribed ? "查看会员状态" : "查看会员权益";
}

function applyMembershipAccess() {
  const unlocked = hasMemberAccess();
  document.querySelectorAll(".member-feature-view").forEach((view) => {
    view.classList.toggle("member-locked", !unlocked);
    const gate = view.querySelector(".member-gate");
    if (gate) gate.hidden = unlocked;
    view.querySelectorAll("button, input, textarea, select").forEach((control) => {
      if (control.closest(".member-gate") || control.hasAttribute("data-member-navigation")) return;
      control.disabled = !unlocked;
    });
  });
  const assistantButton = document.querySelector("#assistantButton");
  assistantButton.classList.toggle("member-locked-control", !unlocked);
  assistantButton.title = unlocked ? "复盘助手" : "复盘助手（会员可用）";
  updateAssistantControls();
}

function openView(viewId, updateHash = true) {
  if (!applicationShell.page(viewId) || !pageModules.has(viewId)) return;
  const previousView = state.activeView;
  pageModules.beforeMount(viewId, previousView);
  if (!applicationShell.mount(viewId, { updateUrl: updateHash })) return;
  pageModules.afterMount(viewId, previousView);
}

function initializeAutoTableSorting() {
  markAutoSortableHeaders(document);
  document.addEventListener("click", (event) => {
    const header = event.target.closest?.("th[data-auto-sort]");
    if (!header || header.closest("#limitTable")) return;
    const table = header.closest("table");
    const body = table?.tBodies?.[0];
    if (!body || body.rows.length < 2) return;
    const direction = header.classList.contains("sort-asc") ? "desc" : "asc";
    table.querySelectorAll("th.sort-asc, th.sort-desc").forEach((item) => {
      item.classList.remove("sort-asc", "sort-desc", "sorted");
      item.removeAttribute("aria-sort");
      const arrow = item.querySelector(".arr");
      if (arrow) arrow.textContent = "↕";
    });
    header.classList.add(`sort-${direction}`, "sorted");
    header.setAttribute("aria-sort", direction === "asc" ? "ascending" : "descending");
    const activeArrow = header.querySelector(".arr");
    if (activeArrow) activeArrow.textContent = direction === "asc" ? "▲" : "▼";
    const columnIndex = header.cellIndex;
    const rows = [...body.rows].map((row, index) => ({ row, index }));
    rows.sort((left, right) => {
      const leftValue = autoSortValue(left.row.cells[columnIndex]);
      const rightValue = autoSortValue(right.row.cells[columnIndex]);
      let result;
      if (leftValue.kind === "number" && rightValue.kind === "number") result = leftValue.value - rightValue.value;
      else result = String(leftValue.value).localeCompare(String(rightValue.value), "zh-CN", { numeric: true, sensitivity: "base" });
      if (result === 0) result = left.index - right.index;
      return direction === "asc" ? result : -result;
    });
    rows.forEach(({ row }) => body.appendChild(row));
    const firstHeader = [...header.parentElement.cells][0]?.textContent.trim();
    if (["#", "排名"].includes(firstHeader)) {
      [...body.rows].forEach((row, index) => {
        if (row.cells[0]) row.cells[0].textContent = String(index + 1);
      });
    }
  });
}

function markAutoSortableHeaders(root) {
  root.querySelectorAll?.(".data-table:not(#limitTable) thead th").forEach((header) => {
    if (header.closest("#brokenTable, #downTable, #yesterdayTable, #rotationTable")) return;
    if (number(header.colSpan) > 1) return;
    const label = header.textContent.trim();
    if (!label || ["#", "操作"].includes(label)) return;
    header.dataset.autoSort = "true";
    header.classList.add("sortable");
    if (!header.querySelector(".arr")) header.insertAdjacentHTML("beforeend", '<span class="arr">↕</span>');
    header.title = `${label}：点击排序`;
  });
}

function autoSortValue(cell) {
  const text = String(cell?.dataset?.sortValue || cell?.textContent || "").trim();
  if (!text || text === "--" || text.includes("样本不足")) return { kind: "text", value: "\uffff" };
  const boardMatch = text.match(/(\d+)\s*板/);
  if (boardMatch) return { kind: "number", value: Number(boardMatch[1]) };
  const normalized = text.replaceAll(",", "").replace(/[+%]/g, "");
  const numericMatch = normalized.match(/^-?\d+(?:\.\d+)?/);
  if (numericMatch) {
    let value = Number(numericMatch[0]);
    if (text.includes("亿")) value *= 10000;
    return { kind: "number", value };
  }
  return { kind: "text", value: text };
}

function changeSort(key) {
  if (state.sortKey === key) state.sortDirection = state.sortDirection === "asc" ? "desc" : "asc";
  else {
    state.sortKey = key;
    state.sortDirection = ["name", "code", "sector", "first_time", "last_time"].includes(key) ? "asc" : "desc";
  }
  renderLimitTable();
}

function compareRows(left, right) {
  const leftValue = left[state.sortKey] ?? "";
  const rightValue = right[state.sortKey] ?? "";
  let result = typeof leftValue === "number" || typeof rightValue === "number"
    ? number(leftValue) - number(rightValue)
    : String(leftValue).localeCompare(String(rightValue), "zh-CN", { numeric: true });
  if (result === 0 && state.sortKey !== "first_time") result = String(left.first_time || "").localeCompare(String(right.first_time || ""));
  return state.sortDirection === "asc" ? result : -result;
}

function updateSortHeaders() {
  document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
    header.classList.remove("sort-asc", "sort-desc", "sorted");
    const active = header.dataset.sort === state.sortKey;
    if (active) header.classList.add(state.sortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
    const arrow = header.querySelector(".arr");
    if (arrow) arrow.textContent = active ? (state.sortDirection === "asc" ? "▲" : "▼") : "↕";
  });
}

function shiftDate(delta) {
  const current = parseLocalDate(elements.tradeDate.value);
  current.setDate(current.getDate() + delta);
  const next = localDateString(current);
  if (next > todayString()) return;
  elements.tradeDate.value = next;
  state.heavenManualData = null;
  document.querySelector("#qiObservationDate").value = next;
  loadDashboard();
}

function updateDateButtons() {
  document.querySelector("#nextDate").disabled = elements.tradeDate.value >= todayString();
}

function selectAccountPanel(panel) {
  const selected = ["profile", "membership", "password"].includes(panel) ? panel : "profile";
  const titles = { profile: "个人资料", membership: "会员状态", password: "修改密码" };
  setText("accountDialogTitle", titles[selected]);
  document.querySelectorAll("[data-account-panel-content]").forEach((section) => {
    section.hidden = section.dataset.accountPanelContent !== selected;
  });
  document.querySelector("#connectionStatus").hidden = selected !== "membership";
  return selected;
}

async function openSettings(panel = "profile") {
  selectAccountPanel(panel);
  toggleAccountDropdown(false);
  toggleHeaderCommandMenu(false);
  const status = document.querySelector("#connectionStatus");
  status.className = "connection-status";
  status.textContent = "正在读取账号状态";
  openModalDialog(elements.settingsDialog);
  try {
    const payload = await apiRequest("/api/account/status");
    const access = payload.llm_access || {};
    const membership = access.membership || {};
    if (state.user) {
      state.user.membership = membership;
      updateAccountIdentityBadges(membership);
      applyMembershipAccess();
    }
    status.textContent = membership.active ? "账户权益已同步" : "账户信息已同步";
    status.classList.toggle("connected", true);
    setText("membershipBadge", membership.subscribed ? "会员有效" : membership.is_admin ? "管理员权限" : "普通用户");
    setText("membershipStateValue", membership.subscribed ? "已开通" : membership.is_admin ? "管理员可用" : "未开通");
    setText("membershipRemainingValue", membership.subscribed && membership.expires_at
      ? `${number(membership.remaining_days)} 天`
      : membership.is_admin || membership.subscribed ? "长期有效" : "--");
    setText("membershipDetail", membership.subscribed
      ? `${membership.plan || "会员"}${membership.expires_at ? ` · 有效至 ${membershipDateDisplay(membership.expires_at, true)}` : " · 长期有效"}`
      : membership.is_admin
        ? "管理员拥有智能功能管理权限，但不会因此显示为已开通会员。"
      : "开通会员后可使用智能选股、问师、问天、复盘助手等智能功能。");
    setText("membershipQuotaHint", `会员默认每日智能分析额度 ${number(access.daily_limit)} 次，由管理员统一设置。`);
    setText("membershipUsage", membership.active
      ? `今日已用 ${number(access.used_today)} 次`
      : "今日智能分析：--");
    setText("membershipUsageSummary", membership.active ? `${number(access.used_today)} / ${number(access.daily_limit)}` : "--");
    setText("membershipRemainingUsage", membership.is_admin ? "不限" : membership.active ? `${number(access.remaining_calls)} 次` : "--");
    const birth = payload.birth_profile || {};
    if (birth.birth_datetime) {
      const [birthDate, birthTime] = String(birth.birth_datetime).split("T");
      document.querySelector("#accountBirthDate").value = birthDate || "";
      document.querySelector("#accountBirthTime").value = (birthTime || "").slice(0, 5);
      document.querySelector("#accountBirthGender").value = birth.gender || "unspecified";
    }
    setText("birthProfileStatus", payload.birth_profile_configured ? "已加密保存" : "尚未设置");
    document.querySelector("#deleteBirthProfileButton").disabled = !payload.birth_profile_configured;
  } catch (error) {
    status.hidden = false;
    status.textContent = "账户状态暂时无法同步";
    showToast(error.message || "账号信息加载失败");
  }
}

async function changeAccountPassword(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const button = form.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    await apiRequest("/api/account/password", "POST", {
      current_password: document.querySelector("#currentPassword").value,
      new_password: document.querySelector("#newPassword").value,
      confirm_password: document.querySelector("#confirmPassword").value,
    });
    form.reset();
    showToast("密码已更新");
  } catch (error) {
    showToast(error.message || "密码更新失败");
  } finally {
    button.disabled = false;
  }
}

async function switchAccount() {
  const button = document.querySelector("#switchAccountMenuButton");
  button.disabled = true;
  toggleAccountDropdown(false);
  try {
    await apiRequest("/api/auth/logout", "POST", {});
    window.location.reload();
  } catch (error) {
    showToast(error.message || "切换账号失败");
    button.disabled = false;
  }
}

async function openAdminSettings(refreshOnly = false) {
  if (state.user?.role !== "admin") return;
  if (!refreshOnly) openModalDialog(elements.adminDialog);
  const status = document.querySelector("#adminConnectionStatus");
  status.textContent = "正在读取系统状态";
  try {
    const payload = await apiRequest("/api/admin/settings");
    const data = payload.data || {};
    const ifind = data.ifind || {};
    const llm = payload.llm || {};
    const membership = payload.membership || {};
    status.textContent = `Tushare ${data.configured ? "已配置" : "未配置"} · iFinD ${ifind.configured ? "已配置" : "未配置"} · ${number(data.snapshot_dates)} 个交易日`;
    status.classList.toggle("connected", Boolean(data.configured));
    setText("systemDataStatus", data.background_refresh_enabled ? "后台刷新已启用" : "后台刷新已暂停");
    document.querySelector("#systemTokenInput").value = "";
    document.querySelector("#systemIfindTokenInput").value = "";
    document.querySelector("#systemBackgroundRefresh").checked = Boolean(data.background_refresh_enabled);
    document.querySelector("#memberDailyLimit").value = number(membership.member_daily_limit) || 50;
    renderModelPool(llm.models || [], llm.primary_model_id || "", llm.fallback_model_id || "");
    renderAdminUsers(payload.users || []);
  } catch (error) {
    status.textContent = error.message || "系统配置读取失败";
  }
}

function selectAdminPanel(panel) {
  const selected = ["market", "models", "members"].includes(panel) ? panel : "market";
  document.querySelector("#adminSectionSelect").value = selected;
  document.querySelectorAll("[data-admin-panel]").forEach((item) => {
    item.hidden = item.dataset.adminPanel !== selected;
  });
}

function renderModelPool(models, primaryId = "", fallbackId = "") {
  state.adminModels = models.map((item) => ({ ...item, api_key: item.api_key || "" }));
  const container = document.querySelector("#modelPoolList");
  container.innerHTML = state.adminModels.map((item, index) => `
    <article class="model-pool-row" data-model-id="${escapeHtml(item.id)}">
      <div class="model-pool-heading"><strong>${escapeHtml(item.name || `模型 ${index + 1}`)}</strong><span>${item.configured ? "已保存密钥" : "待配置"}</span></div>
      <div class="model-pool-fields">
        <label class="form-field"><span>显示名称 *</span><input data-model-field="name" maxlength="50" value="${escapeHtml(item.name || "")}" required></label>
        <label class="form-field"><span>API Base URL *</span><input data-model-field="base_url" type="url" value="${escapeHtml(item.base_url || "https://api.openai.com/v1")}" required></label>
        <label class="form-field"><span>模型标识 *</span><input data-model-field="model" maxlength="100" value="${escapeHtml(item.model || "")}" required></label>
        <label class="form-field"><span>API Key${item.configured ? "" : " *"}</span><input data-model-field="api_key" type="password" autocomplete="off" maxlength="300" placeholder="${item.configured ? "留空保留已保存的 Key" : "输入 API Key"}" ${item.configured ? "" : "required"}></label>
      </div>
      <div class="model-test-row"><button class="button" type="button" data-test-model>测试连接</button><span class="model-test-status" aria-live="polite">未测试</span><button class="icon-button model-delete-button" type="button" data-delete-model aria-label="删除模型" title="删除模型"><i data-lucide="trash-2"></i></button></div>
    </article>
  `).join("") || emptyStateHtml("模型池为空，请先添加模型");
  updateModelRoleOptions(primaryId, fallbackId);
  container.querySelectorAll("[data-test-model]").forEach((button) => button.addEventListener("click", () => testPlatformModel(button.closest("[data-model-id]"))));
  container.querySelectorAll("[data-delete-model]").forEach((button) => button.addEventListener("click", () => deletePlatformModel(button.closest("[data-model-id]"))));
  container.querySelectorAll("[data-model-field='name']").forEach((input) => input.addEventListener("input", updateModelRoleLabels));
  refreshIcons();
}

function collectModelPool() {
  const saved = new Map(state.adminModels.map((item) => [item.id, item]));
  return [...document.querySelectorAll("#modelPoolList [data-model-id]")].map((row) => ({
    id: row.dataset.modelId,
    name: row.querySelector("[data-model-field='name']").value.trim(),
    base_url: row.querySelector("[data-model-field='base_url']").value.trim(),
    model: row.querySelector("[data-model-field='model']").value.trim(),
    api_key: row.querySelector("[data-model-field='api_key']").value.trim(),
    configured: Boolean(saved.get(row.dataset.modelId)?.configured),
  }));
}

function updateModelRoleOptions(primaryId = document.querySelector("#platformPrimaryModelSelect").value, fallbackId = document.querySelector("#platformFallbackModelSelect").value) {
  const models = collectModelPool();
  const options = models.map((item) => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.name || item.model || "未命名模型")}</option>`).join("");
  const primary = document.querySelector("#platformPrimaryModelSelect");
  const fallback = document.querySelector("#platformFallbackModelSelect");
  primary.innerHTML = models.length ? options : '<option value="">暂无模型</option>';
  fallback.innerHTML = `<option value="">不启用辅助模型</option>${options}`;
  primary.value = models.some((item) => item.id === primaryId) ? primaryId : models[0]?.id || "";
  fallback.value = models.some((item) => item.id === fallbackId) && fallbackId !== primary.value ? fallbackId : "";
}

function updateModelRoleLabels() {
  updateModelRoleOptions();
}

function addPlatformModel() {
  const models = collectModelPool();
  const id = `model-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
  models.push({ id, name: `模型 ${models.length + 1}`, base_url: "https://api.openai.com/v1", model: "", api_key: "", configured: false });
  renderModelPool(models, document.querySelector("#platformPrimaryModelSelect").value || id, document.querySelector("#platformFallbackModelSelect").value);
  document.querySelector(`[data-model-id="${CSS.escape(id)}"] [data-model-field="name"]`)?.focus();
}

function deletePlatformModel(row) {
  if (!row) return;
  const id = row.dataset.modelId;
  const primary = document.querySelector("#platformPrimaryModelSelect").value;
  const fallback = document.querySelector("#platformFallbackModelSelect").value;
  if (id === primary || id === fallback) {
    showToast("请先为主模型或辅助模型选择其他模型，再删除当前模型");
    return;
  }
  const models = collectModelPool().filter((item) => item.id !== id);
  renderModelPool(models, primary, fallback);
}

function renderAdminUsers(users) {
  const container = document.querySelector("#adminUsersList");
  container.innerHTML = users.map((user) => {
    const admin = user.role === "admin";
    const member = Boolean(user.membership_subscribed);
    const identityLabels = [admin ? "管理员" : "", member ? "会员有效" : "普通用户"].filter(Boolean).join(" · ");
    const expiry = member
      ? (user.membership_expires_at ? `有效至 ${membershipDateDisplay(user.membership_expires_at)}` : "永久有效")
      : user.membership_status === "suspended"
        ? "会员已停用"
        : user.membership_status === "active" && user.membership_expires_at
          ? `已于 ${membershipDateDisplay(user.membership_expires_at)} 到期`
          : "尚未开通";
    return `<article class="admin-user-row" data-admin-user="${number(user.id)}">
      <div class="admin-user-identity"><strong>${escapeHtml(user.username)}</strong><span>${escapeHtml(identityLabels)}</span><small>${escapeHtml(expiry)}</small></div>
      <div class="admin-user-usage">今日调用 <b>${number(user.used_today)}</b></div>
      <form class="membership-form">
        <input type="hidden" name="user_id" value="${number(user.id)}">
        <label><span>状态</span><select name="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></label>
        <label><span>开通 / 续期时长</span><select name="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></label>
        <div class="membership-expiry"><span>当前到期</span><strong>${escapeHtml(expiry)}</strong></div>
        <button class="button" type="submit">应用</button>
      </form>
    </article>`;
  }).join("") || emptyStateHtml("暂无注册用户");
  container.querySelectorAll(".membership-form").forEach((form) => form.addEventListener("submit", saveMembership));
}

async function saveMembership(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  const button = form.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    const payload = await apiRequest("/api/admin/membership", "POST", data);
    renderAdminUsers(payload.users || []);
    showToast("会员状态已更新");
  } catch (error) {
    showToast(error.message || "会员状态保存失败");
  } finally {
    button.disabled = false;
  }
}

async function saveMarketSettings(event) {
  event.preventDefault();
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    await apiRequest("/api/admin/settings", "POST", {
      tushare_token: document.querySelector("#systemTokenInput").value.trim(),
      ifind_refresh_token: document.querySelector("#systemIfindTokenInput").value.trim(),
      background_refresh_enabled: document.querySelector("#systemBackgroundRefresh").checked,
    });
    document.querySelector("#systemTokenInput").value = "";
    document.querySelector("#systemIfindTokenInput").value = "";
    showToast("行情配置已保存");
    await openAdminSettings(true);
  } catch (error) {
    showToast(error.message || "系统配置保存失败");
  } finally {
    button.disabled = false;
  }
}

async function saveModelPool(event) {
  event.preventDefault();
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    await apiRequest("/api/admin/settings", "POST", {
      models: collectModelPool(),
      primary_model_id: document.querySelector("#platformPrimaryModelSelect").value,
      fallback_model_id: document.querySelector("#platformFallbackModelSelect").value,
    });
    showToast("模型池已保存");
    await openAdminSettings(true);
  } catch (error) {
    showToast(error.message || "模型池保存失败");
  } finally {
    button.disabled = false;
  }
}

async function saveMembershipSettings(event) {
  event.preventDefault();
  const button = event.currentTarget.querySelector("button[type='submit']");
  button.disabled = true;
  try {
    await apiRequest("/api/admin/settings", "POST", {
      member_daily_limit: number(document.querySelector("#memberDailyLimit").value),
    });
    showToast("会员调用额度已保存");
    await openAdminSettings(true);
  } catch (error) {
    showToast(error.message || "会员调用额度保存失败");
  } finally {
    button.disabled = false;
  }
}

async function testPlatformModel(row) {
  if (!row) return;
  const button = row.querySelector("[data-test-model]");
  const status = row.querySelector(".model-test-status");
  const profile = collectModelPool().find((item) => item.id === row.dataset.modelId) || {};
  button.disabled = true;
  status.textContent = "连接中";
  try {
    const payload = await apiRequest("/api/admin/settings/test", "POST", { model_id: row.dataset.modelId, profile });
    status.textContent = `已连通 · ${number(payload.result.latency_ms)} ms`;
    status.className = "model-test-status success";
  } catch (error) {
    status.textContent = error.message;
    status.className = "model-test-status failure";
  } finally {
    button.disabled = false;
  }
}

function membershipDateDisplay(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 exportStocks() {
  exportRows("涨停池", getVisibleStocks(), [
    ["股票代码", "code"], ["股票名称", "name"], ["连板", "streak"], ["涨幅%", "change"],
    ["价格", "price"], ["所属板块", "sector"], ["涨停原因", "reason"], ["首封", "first_time"],
    ["最后封板", "last_time"], ["开板次数", "open_times"], ["换手率%", "turnover_rate"],
    ["成交额亿", "amount_billion"], ["封单额万", "seal_amount_million"],
  ]);
}

function exportBroken() {
  exportRows("炸板池", getVisibleBrokenRows(), [
    ["股票代码", "code"], ["股票名称", "name"], ["现价涨幅%", "change"], ["距涨停%", "limitGap"],
    ["价格", "price"], ["所属板块", "sector"], ["首次触板", "first_time"], ["开板次数", "open_times"],
    ["换手率%", "turnover_rate"], ["成交额亿", "amount_billion"],
  ]);
}

function exportDown() {
  exportRows("跌停板", getVisibleDownRows(), [
    ["股票代码", "code"], ["股票名称", "name"], ["跌幅%", "change"], ["价格", "price"],
    ["所属板块", "sector"], ["换手率%", "turnover_rate"], ["成交额亿", "amount_billion"],
  ]);
}

function exportYesterday() {
  exportRows("昨日涨停", getVisibleYesterdayRows(), [
    ["股票代码", "code"], ["股票名称", "name"], ["昨日高度", "prior_streak"],
    ["今日涨幅%", "current_change"], ["今日结果", "outcome"], ["当前高度", "current_streak"],
    ["所属板块", "sector"],
  ]);
}

function exportLadder() {
  const rows = (state.dashboard?.ladders || []).flatMap((group) => (group.stocks || []).map((stock) => ({
    level: group.label || group.level,
    ...stock,
  })));
  exportRows("市场天梯", rows, [
    ["梯队", "level"], ["股票代码", "code"], ["股票名称", "name"], ["所属板块", "sector"],
    ["封板时间", "first_time"], ["开板次数", "open_times"], ["封单额万", "seal_amount_million"], ["成交额亿", "amount_billion"],
  ]);
}

function exportRotation() {
  const sectorMap = new Map((state.dashboard?.sectors || []).map((sector) => [sector.name, sector]));
  const rows = (state.dashboard?.sector_rotation || []).map((row) => ({
    ...row,
    average_change: sectorMap.get(row.name)?.change ?? 0,
  }));
  exportRows("板块轮动", rows, [
    ["排名", "rank"], ["板块", "name"], ["趋势", "trend"], ["今日涨停", "count"],
    ["昨日涨停", "previous_count"], ["变化", "delta"], ["强度", "strength"],
    ["最高板", "max_streak"], ["平均涨幅%", "average_change"],
    ["领涨股", "leader"], ["涨停股成交额亿", "amount_billion"],
  ]);
}

function exportSentimentHistory() {
  const rows = state.sentimentHistory?.rows || [];
  if (!rows.length) {
    showToast("暂无可导出的情绪周期数据");
    return;
  }
  const exportRowsData = rows.map((row) => ({
    ...row,
    breadth_score: row.components?.breadth?.score,
    limit_ecology_score: row.components?.limit_ecology?.score,
    profit_effect_score: row.components?.profit_effect?.score,
    ladder_structure_score: row.components?.ladder_structure?.score,
    liquidity_score: row.components?.liquidity?.score,
  }));
  exportRows("情绪周期", exportRowsData, [
    ["交易日", "trade_date"], ["情绪温度", "score"], ["周期阶段", "phase"], ["方向", "direction"],
    ["涨停", "limit_up_count"], ["首板", "first_board_count"], ["二板", "second_board_count"],
    ["三板以上", "three_plus_count"], ["连板高度", "max_height"], ["炸板", "broken_count"],
    ["跌停", "limit_down_count"], ["昨日涨停", "previous_limit_count"],
    ["昨日涨停红盘", "previous_positive_count"], ["昨日涨停红盘率%", "previous_positive_rate"],
    ["市场宽度", "breadth_score"], ["涨停生态", "limit_ecology_score"],
    ["赚钱效应", "profit_effect_score"], ["连板结构", "ladder_structure_score"],
    ["成交活跃度", "liquidity_score"],
  ]);
}

function exportDragonTiger() {
  const rows = (state.dragonTiger?.traders || []).flatMap((trader) => (
    (trader.operations || []).map((operation) => ({
      trader_name: trader.name,
      identity_type: dragonIdentityLabel(trader.identity_type),
      ...operation,
    }))
  ));
  exportRows("游资龙虎榜", rows, [
    ["游资或席位", "trader_name"], ["身份", "identity_type"], ["股票代码", "code"],
    ["股票名称", "name"], ["方向", "direction"], ["涨幅%", "change"],
    ["买入百万元", "buy_million"], ["卖出百万元", "sell_million"], ["净额百万元", "net_buy_million"],
    ["关联席位", "seat_name"], ["上榜原因", "reason"],
  ]);
}

function exportHotMoneyProfiles() {
  const rows = state.hotMoneyProfiles?.profiles || [];
  if (!rows.length) {
    showToast("暂无可导出的游资档案");
    return;
  }
  downloadCsv(
    `游资档案-${todayString()}.csv`,
    ["游资名称", "简介", "关联营业部", "席位数量"],
    rows.map((profile) => [
      profile.name,
      profile.description,
      (profile.organizations || []).join("；"),
      number(profile.organization_count),
    ]),
  );
}

function commonReviewColumns() {
  return [["股票代码", "code"], ["股票名称", "name"], ["状态", "status"], ["涨跌幅%", "change"],
    ["价格", "price"], ["所属板块", "sector"], ["原因", "reason"], ["首次触板", "first_time"],
    ["最后触板", "last_time"], ["开板次数", "open_times"], ["换手率%", "turnover_rate"], ["成交额亿", "amount_billion"]];
}

function exportRows(label, rows, columns) {
  const headers = columns.map(([header]) => header);
  const data = rows.map((row) => columns.map(([, key]) => row[key] ?? ""));
  downloadCsv(`${label}-${state.dashboard.meta.trade_date}.csv`, headers, data);
}

function downloadCsv(filename, headers, rows) {
  const lines = [headers, ...rows].map((row) => row.map(csvCell).join(","));
  const blob = new Blob(["\ufeff", lines.join("\r\n")], { type: "text/csv;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(url);
  showToast(`已导出 ${rows.length} 条数据`);
}

function csvCell(value) {
  let text = String(value ?? "");
  if (/^[=+\-@]/.test(text)) text = `'${text}`;
  return `"${text.replaceAll('"', '""')}"`;
}

function outcomeClass(outcome) {
  return { "晋级": "outcome-advance", "炸板": "outcome-broken", "跌停": "outcome-down", "断板": "outcome-open" }[outcome] || "outcome-open";
}

function trendClass(trend) {
  return { "升温": "trend-hot", "降温": "trend-cool", "新进": "trend-new", "持平": "trend-flat" }[trend] || "trend-flat";
}

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

function sentimentLabel(score) {
  const value = number(score);
  if (value >= 80) return "情绪高涨";
  if (value >= 60) return "情绪偏强";
  if (value >= 40) return "情绪中性";
  if (value >= 20) return "情绪偏弱";
  return "情绪冰点";
}

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

function signed(value) {
  const parsed = number(value);
  return `${parsed > 0 ? "+" : ""}${formatNumber(parsed, 2)}`;
}

function dashboardDataTimestamp(meta = {}) {
  const tradeDate = displayCompactDate(meta.trade_date);
  if (tradeDate === "--") return "--";
  const intraday = tradeDate === todayString() && Boolean(meta.realtime) && !["closed", "after_hours"].includes(String(meta.market_status || ""));
  if (intraday) {
    const updated = new Date(meta.updated_at);
    if (!Number.isNaN(updated.getTime())) {
      const dateText = `${updated.getFullYear()}-${String(updated.getMonth() + 1).padStart(2, "0")}-${String(updated.getDate()).padStart(2, "0")}`;
      const timeText = updated.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
      return `${dateText} ${timeText}`;
    }
  }
  return `${tradeDate} 15:00`;
}

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)} 万`;
}

async function apiRequest(url, method = "GET", body = null, requestOptions = {}) {
  return window.XiaobaiAPI.request(url, method, body, requestOptions);
}

function setLoading(loading, text = "正在加载复盘数据", context = "default") {
  elements.loading.hidden = !loading;
  elements.loading.dataset.context = loading ? context : "default";
  setText("loadingTitle", text);
  setText(
    "loadingHint",
    context === "screener"
      ? "正在完成因子筛选、候选排序与历史样本回测，这通常需要一点时间"
      : "请稍候",
  );
}

function setStatus(text) {
  applicationShell.setStatus(text);
}

let toastTimer;
function showToast(message) {
  clearTimeout(toastTimer);
  elements.toast.textContent = message;
  elements.toast.hidden = false;
  toastTimer = setTimeout(() => { elements.toast.hidden = true; }, 3600);
}

function setText(id, value) {
  const element = document.getElementById(id);
  if (element) element.textContent = value;
}

function motionEnabled() {
  return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

function refreshIcons() {
  if (!window.lucide?.createIcons) return;
  window.lucide.createIcons({ attrs: { "aria-hidden": "true" } });
}

function toggleHeaderCommandMenu(force) {
  applicationShell.toggleHeaderCommandMenu(force);
}

function toggleAccountDropdown(force, returnFocus = false) {
  const menu = document.querySelector("#accountDropdown");
  const button = document.querySelector("#accountButton");
  if (!menu || !button) return;
  const open = typeof force === "boolean" ? force : menu.hidden;
  menu.hidden = !open;
  button.setAttribute("aria-expanded", String(open));
  document.querySelector(".account-menu-shell")?.classList.toggle("is-open", open);
  if (open) {
    setText("accountMenuName", state.user?.username || "当前账号");
    const membership = state.user?.membership || {};
    setText("accountMenuRole", state.user?.role === "admin" ? (membership.subscribed ? "管理员 · 会员" : "管理员") : membership.subscribed ? "会员用户" : "普通用户");
  } else if (returnFocus) {
    button.focus();
  }
}

function handleAccountMenuKeydown(event) {
  const menu = document.querySelector("#accountDropdown");
  if (!menu) return;
  if (menu.hidden) {
    if (document.activeElement?.id === "accountButton" && event.key === "ArrowDown") {
      event.preventDefault();
      toggleAccountDropdown(true);
      menu.querySelector('[role="menuitem"]')?.focus();
    }
    return;
  }
  const items = [...menu.querySelectorAll('[role="menuitem"]:not(:disabled)')];
  if (!items.length) return;
  const current = items.indexOf(document.activeElement);
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
    event.preventDefault();
    const offset = event.key === "ArrowDown" ? 1 : -1;
    items[(current + offset + items.length) % items.length].focus();
  } else if (event.key === "Home" || event.key === "End") {
    event.preventDefault();
    items[event.key === "Home" ? 0 : items.length - 1].focus();
  }
}

function updateSentimentGauge(rawScore) {
  const gauge = document.querySelector("#sentimentGauge");
  if (!gauge) return;
  const score = clamp(rawScore, 0, 100);
  const previous = Number(gauge.dataset.score);
  gauge.dataset.score = String(score);
  gauge.style.setProperty("--score", score);
  if (!motionEnabled() || !Number.isFinite(previous) || Math.abs(previous - score) < 15) return;
  gauge.classList.remove("sentiment-pulse");
  void gauge.offsetWidth;
  gauge.classList.add("sentiment-pulse");
  gauge.addEventListener("animationend", () => gauge.classList.remove("sentiment-pulse"), { once: true });
}

function animateMetric(id, rawValue, formatter = (value) => value) {
  const element = document.getElementById(id);
  const target = Number(rawValue);
  if (!element || !Number.isFinite(target)) {
    setText(id, formatter(rawValue));
    return;
  }
  const storedValue = Number(element.dataset.metricValue);
  const previous = Number.isFinite(storedValue) ? storedValue : 0;
  element.dataset.metricValue = String(target);
  const existingFrame = metricAnimationFrames.get(element);
  if (existingFrame) cancelAnimationFrame(existingFrame);
  if (!motionEnabled() || previous === target) {
    element.textContent = formatter(target);
    return;
  }
  element.classList.remove("metric-changed");
  void element.offsetWidth;
  element.classList.add("metric-changed");
  const startedAt = performance.now();
  const duration = 560;
  const update = (now) => {
    const progress = Math.min(1, (now - startedAt) / duration);
    const eased = 1 - (1 - progress) ** 3;
    element.textContent = formatter(previous + (target - previous) * eased);
    if (progress < 1) {
      metricAnimationFrames.set(element, requestAnimationFrame(update));
    } else {
      element.textContent = formatter(target);
      metricAnimationFrames.delete(element);
      setTimeout(() => element.classList.remove("metric-changed"), 80);
    }
  };
  metricAnimationFrames.set(element, requestAnimationFrame(update));
}

function animateRows(container) {
  if (!container) return;
  const rows = [...container.children].filter((item) => item.matches("tr, [data-code]"));
  if (!motionEnabled()) {
    rows.forEach((row) => row.classList.remove("row-pending", "row-enter"));
    return;
  }
  const unseenRows = rows.filter((row) => row.dataset.motionSeen !== "1");
  unseenRows.slice(0, 12).forEach((row, index) => {
    row.dataset.motionSeen = "1";
    row.classList.remove("row-pending", "row-enter");
    row.style.setProperty("--row-delay", `${index * 24}ms`);
    requestAnimationFrame(() => row.classList.add("row-enter"));
    row.addEventListener("animationend", () => row.classList.remove("row-enter"), { once: true });
  });
  if (!("IntersectionObserver" in window)) {
    unseenRows.slice(12).forEach((row) => { row.dataset.motionSeen = "1"; });
    return;
  }
  if (!rowAnimationObserver) {
    rowAnimationObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return;
        const row = entry.target;
        rowAnimationObserver.unobserve(row);
        row.dataset.motionSeen = "1";
        row.classList.remove("row-pending");
        row.style.setProperty("--row-delay", "0ms");
        requestAnimationFrame(() => row.classList.add("row-enter"));
        row.addEventListener("animationend", () => row.classList.remove("row-enter"), { once: true });
      });
    }, { threshold: 0.08, rootMargin: "0px 0px 40px 0px" });
  }
  unseenRows.slice(12).forEach((row) => {
    row.classList.add("row-pending");
    rowAnimationObserver.observe(row);
  });
}

function waitForMotion(duration) {
  return new Promise((resolve) => setTimeout(resolve, motionEnabled() ? duration : 0));
}
