window.XiaobaiPageModules.register("heaven", ["heavenView"], {
  enter: ["loadHeaven"],
  leave: ["stopHeaven"],
});

/* PRESERVATION-SOURCE-BEGIN app.js:4828-6667 */
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 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 = ["初爻", "二爻", "三爻", "四爻", "五爻", "上爻"];

/* PRESERVATION-SOURCE-END app.js:4828-6667 */
