<script setup lang="ts">
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import WordCard from '../components/WordCard.vue'

const MemoryCurveChart = defineAsyncComponent(
  () => import('../components/MemoryCurveChart.vue')
)
const WordGraphCanvas = defineAsyncComponent(
  () => import('../components/WordGraphCanvas.vue')
)
import {
  api,
  type MemoryCurvePoint,
  type MemoryVisualization,
  type MemoryWordSummary,
  type Word,
  type WordMemoryDetail,
  type MemoryTransformerPredict,
} from '../api/request'
import { formatTrainSeconds } from '../composables/useQuizTimer'
import {
  buildFocusNodes,
  LINK_KIND_META,
  useGraphFilter,
  type LinkKind,
} from '../composables/useGraphFilter'
import {
  getGraphPracticeWordIds,
  isPracticeSessionComplete,
  loadPracticeResults,
  startGraphPractice,
  type PracticeResult,
} from '../utils/practiceGraphSession'

const router = useRouter()
const route = useRoute()
const libraryBookId = computed(() => {
  const raw = route.query.book_id
  if (raw) {
    const id = Number(raw)
    return Number.isFinite(id) && id > 0 ? id : 0
  }
  return 0
})
const libraryTitle = computed(() =>
  libraryBookId.value > 0 ? '词书记忆曲线' : '积累记忆曲线'
)
const graphPracticeResults = ref<Record<string, PracticeResult>>(loadPracticeResults())

const graphSessionComplete = computed(() => {
  const ids = getGraphPracticeWordIds()
  if (!ids.length) return false
  return isPracticeSessionComplete(ids, graphPracticeResults.value)
})

function refreshGraphPracticeResults() {
  graphPracticeResults.value = loadPracticeResults()
}

const viewTabs = [
  { key: 'memory', label: '记忆曲线' },
  { key: 'list', label: '单词列表' },
]

const tabs = [
  { key: '', label: '全部' },
  { key: 'new', label: '新词' },
  { key: 'learning', label: '学习中' },
  { key: 'mastered', label: '已掌握' },
  { key: 'weak', label: '易错词' },
]

const PAGE_SIZE = 20

const activeView = ref('memory')
const activeTab = ref('')
const words = ref<Word[]>([])
const currentPage = ref(1)
const loading = ref(true)
const vizLoading = ref(false)
const viz = ref<MemoryVisualization | null>(null)
const selectedWord = ref<MemoryWordSummary | null>(null)
const detailExpanded = ref(true)
const wordMemory = ref<WordMemoryDetail | null>(null)
const wordMemoryLoading = ref(false)
const memoryModel = ref<MemoryTransformerPredict | null>(null)
const memoryModelLoading = ref(false)
const listEditMode = ref(false)
const selectedIds = ref<Set<number>>(new Set())
const batchDeleting = ref(false)
const pageSelectRef = ref<HTMLInputElement | null>(null)

const {
  graphStatusFilter,
  graphSearch,
  graphViewMode,
  focusCenterId,
  linkFilters,
  filteredGraphNodes,
  filteredGraphLinks,
  graphStatsText,
  isNodeVisible,
  setFocusCenter,
  exitFocusMode,
} = useGraphFilter(viz)

const wordCurvePoints = computed((): MemoryCurvePoint[] => {
  if (!wordMemory.value?.curve_points.length) return []
  return wordMemory.value.curve_points.map((p, i) => ({
    day_index: i,
    date: p.date,
    forgetting: p.forgetting,
    mastery: p.mastery,
    risk: p.risk,
  }))
})

const totalPages = computed(() =>
  Math.max(1, Math.ceil(words.value.length / PAGE_SIZE))
)

const paginatedWords = computed(() => {
  const start = (currentPage.value - 1) * PAGE_SIZE
  return words.value.slice(start, start + PAGE_SIZE)
})

const pageSummary = computed(() => {
  if (!words.value.length) return ''
  const start = (currentPage.value - 1) * PAGE_SIZE + 1
  const end = Math.min(currentPage.value * PAGE_SIZE, words.value.length)
  return `第 ${currentPage.value} / ${totalPages.value} 页，显示 ${start}–${end}，共 ${words.value.length} 个`
})

const selectedCount = computed(() => selectedIds.value.size)

const allPageSelected = computed(() => {
  if (!paginatedWords.value.length) return false
  return paginatedWords.value.every((w) => selectedIds.value.has(w.id))
})

const pageSelectIndeterminate = computed(() => {
  const pageIds = paginatedWords.value.map((w) => w.id)
  const selectedOnPage = pageIds.filter((id) => selectedIds.value.has(id)).length
  return selectedOnPage > 0 && selectedOnPage < pageIds.length
})

function clearSelection() {
  selectedIds.value = new Set()
}

function enterListEdit() {
  listEditMode.value = true
}

function exitListEdit() {
  listEditMode.value = false
  clearSelection()
}

function toggleWordSelect(id: number) {
  const next = new Set(selectedIds.value)
  if (next.has(id)) next.delete(id)
  else next.add(id)
  selectedIds.value = next
}

function togglePageSelect() {
  const next = new Set(selectedIds.value)
  if (allPageSelected.value) {
    paginatedWords.value.forEach((w) => next.delete(w.id))
  } else {
    paginatedWords.value.forEach((w) => next.add(w.id))
  }
  selectedIds.value = next
}

function selectAllWords() {
  selectedIds.value = new Set(words.value.map((w) => w.id))
}

function clampPage() {
  if (currentPage.value > totalPages.value) {
    currentPage.value = totalPages.value
  }
  if (currentPage.value < 1) {
    currentPage.value = 1
  }
}

function goToPage(page: number) {
  currentPage.value = Math.min(Math.max(1, page), totalPages.value)
}

function goToPageAndScroll(page: number) {
  goToPage(page)
  window.scrollTo({ top: 0, behavior: 'smooth' })
}

async function loadWords() {
  loading.value = true
  try {
    const { data } = await api.listWords(
      activeTab.value || undefined,
      libraryBookId.value > 0 ? libraryBookId.value : 0
    )
    words.value = data
    clampPage()
  } finally {
    loading.value = false
  }
}

async function loadViz() {
  vizLoading.value = true
  try {
    const { data } = await api.memoryViz(libraryBookId.value)
    viz.value = data
    selectedWord.value = data.words[0] ?? null
    if (data.words[0]) {
      loadWordMemory(data.words[0].id)
    }
  } finally {
    vizLoading.value = false
  }
}

async function handleDelete(id: number) {
  if (!confirm('确定删除这个单词？')) return
  await api.deleteWord(id)
  if (selectedIds.value.has(id)) {
    const next = new Set(selectedIds.value)
    next.delete(id)
    selectedIds.value = next
  }
  await loadWords()
  if (activeView.value === 'memory') await loadViz()
}

async function handleBatchDelete() {
  const ids = [...selectedIds.value]
  if (!ids.length) return
  if (!confirm(`确定删除选中的 ${ids.length} 个单词？`)) return
  batchDeleting.value = true
  try {
    const results = await Promise.allSettled(ids.map((id) => api.deleteWord(id)))
    const failed = results.filter((r) => r.status === 'rejected').length
    clearSelection()
    await loadWords()
    if (failed > 0) {
      alert(`${failed} 个单词删除失败，请重试`)
    }
  } finally {
    batchDeleting.value = false
  }
}

async function loadWordMemory(wordId: number) {
  wordMemoryLoading.value = true
  memoryModelLoading.value = true
  try {
    const [memRes, modelRes] = await Promise.all([
      api.wordMemory(wordId),
      api.wordMemoryModel(wordId),
    ])
    wordMemory.value = memRes.data
    memoryModel.value = modelRes.data
  } catch {
    wordMemory.value = null
    memoryModel.value = null
  } finally {
    wordMemoryLoading.value = false
    memoryModelLoading.value = false
  }
}

function onGraphSelect(id: string) {
  const w = viz.value?.words.find((x) => String(x.id) === id)
  if (w) {
    selectedWord.value = w
    loadWordMemory(w.id)
  }
}

function goGraphPractice(wordIds?: string[]) {
  const ids = wordIds ?? filteredGraphNodes.value.map((n) => n.id)
  if (!ids.length) return
  const total = viz.value?.words.length ?? words.value.length
  if (total < 4) {
    alert('词库至少 4 个单词才能进行选择题练习')
    return
  }
  startGraphPractice(ids, ids)
  router.push('/graph-practice')
}

function graphIdsForPracticeContext(centerId: string): string[] {
  if (!viz.value) return [centerId]
  if (graphViewMode.value === 'focus' && focusCenterId.value) {
    return filteredGraphNodes.value.map((n) => n.id)
  }
  return buildFocusNodes(
    viz.value,
    centerId,
    graphStatusFilter.value,
    graphSearch.value,
    linkFilters.value
  ).map((n) => n.id)
}

function goPracticeSelectedWord() {
  if (!selectedWord.value) return
  const centerId = String(selectedWord.value.id)
  const graphIds = graphIdsForPracticeContext(centerId)
  const total = viz.value?.words.length ?? words.value.length
  if (total < 4) {
    alert('词库至少 4 个单词才能进行选择题练习')
    return
  }
  startGraphPractice([centerId], graphIds)
  router.push('/graph-practice')
}

watch([graphStatusFilter, graphSearch, linkFilters], () => {
  if (selectedWord.value && !isNodeVisible(String(selectedWord.value.id))) {
    const first = filteredGraphNodes.value[0]
    if (first) {
      const w = viz.value?.words.find((x) => String(x.id) === first.id)
      if (w) {
        selectedWord.value = w
        loadWordMemory(w.id)
      }
    }
  }
}, { deep: true })

watch(selectedWord, (w) => {
  if (w && activeView.value === 'memory') loadWordMemory(w.id)
})

watch(activeTab, () => {
  currentPage.value = 1
  exitListEdit()
  if (activeView.value === 'list') loadWords()
})

watch([allPageSelected, pageSelectIndeterminate], () => {
  if (pageSelectRef.value) {
    pageSelectRef.value.indeterminate = pageSelectIndeterminate.value
  }
})

watch(activeView, (v) => {
  if (v === 'list') {
    loadWords()
  } else {
    exitListEdit()
    refreshGraphPracticeResults()
    loadViz()
  }
})

watch(
  () => route.query.view,
  (v) => {
    if (v === 'memory') {
      activeView.value = 'memory'
      refreshGraphPracticeResults()
      if (!viz.value && !vizLoading.value) loadViz()
    }
  },
  { immediate: true }
)

watch(libraryBookId, () => {
  if (activeView.value === 'memory') {
    viz.value = null
    loadViz()
  } else {
    loadWords()
  }
})

onMounted(() => {
  if (activeView.value === 'memory') {
    refreshGraphPracticeResults()
    loadViz()
  } else {
    loadWords()
  }
})
</script>

<template>
  <div class="page">
    <h1 class="page-title">{{ libraryTitle }}</h1>

    <div class="view-tabs">
      <button
        v-for="v in viewTabs"
        :key="v.key"
        :class="['view-tab', { active: activeView === v.key }]"
        @click="activeView = v.key"
      >
        {{ v.label }}
      </button>
    </div>

    <template v-if="activeView === 'list'">
      <div class="tabs">
        <button
          v-for="t in tabs"
          :key="t.key"
          :class="['tab', { active: activeTab === t.key }]"
          @click="activeTab = t.key"
        >
          {{ t.label }}
        </button>
      </div>
      <p v-if="loading" style="color: var(--muted)">加载中...</p>
      <p v-else-if="words.length === 0" style="color: var(--muted); text-align: center">
        暂无单词，去翻译页添加吧
      </p>
      <template v-else>
        <Teleport to="body">
          <button
            v-if="totalPages > 1"
            type="button"
            class="float-page-arrow prev"
            :disabled="currentPage <= 1"
            aria-label="上一页"
            @click="goToPageAndScroll(currentPage - 1)"
          >
            ‹
          </button>
          <button
            v-if="totalPages > 1"
            type="button"
            class="float-page-arrow next"
            :disabled="currentPage >= totalPages"
            aria-label="下一页"
            @click="goToPageAndScroll(currentPage + 1)"
          >
            ›
          </button>
        </Teleport>
        <div class="bulk-bar" :class="{ editing: listEditMode }">
          <button
            v-if="!listEditMode"
            type="button"
            class="btn btn-outline bulk-edit"
            @click="enterListEdit"
          >
            编辑
          </button>
          <template v-else>
            <label class="bulk-select">
              <input
                ref="pageSelectRef"
                type="checkbox"
                :checked="allPageSelected"
                @change="togglePageSelect"
              />
              全选本页
            </label>
            <button
              v-if="words.length > paginatedWords.length"
              type="button"
              class="bulk-link"
              @click="selectAllWords"
            >
              全选全部 {{ words.length }} 个
            </button>
            <span v-if="selectedCount" class="bulk-count">已选 {{ selectedCount }} 个</span>
            <button
              type="button"
              class="btn btn-danger bulk-delete"
              :disabled="!selectedCount || batchDeleting"
              @click="handleBatchDelete"
            >
              {{ batchDeleting ? '删除中...' : '批量删除' }}
            </button>
            <button type="button" class="bulk-done" @click="exitListEdit">完成</button>
          </template>
        </div>
        <p class="page-summary">{{ pageSummary }}</p>
        <WordCard
          v-for="w in paginatedWords"
          :key="w.id"
          :word="w"
          :selectable="listEditMode"
          :selected="selectedIds.has(w.id)"
          @delete="handleDelete"
          @toggle-select="toggleWordSelect"
        />
        <div v-if="totalPages > 1" class="pagination">
          <button
            type="button"
            class="page-btn"
            :disabled="currentPage <= 1"
            @click="goToPageAndScroll(currentPage - 1)"
          >
            上一页
          </button>
          <span class="page-indicator">{{ currentPage }} / {{ totalPages }}</span>
          <button
            type="button"
            class="page-btn"
            :disabled="currentPage >= totalPages"
            @click="goToPageAndScroll(currentPage + 1)"
          >
            下一页
          </button>
        </div>
      </template>
    </template>

    <template v-else>
      <p v-if="vizLoading" style="color: var(--muted)">加载记忆数据...</p>
      <template v-else-if="viz && viz.graph.nodes.length">
        <div class="card chart-card">
          <h3 class="section-title">记忆曲线</h3>
          <p class="section-desc">
            红线：遗忘曲线 · 绿线：熟练曲线 · 橙虚线：可能遗忘(历史) · 紫点线：未来预测
          </p>
          <MemoryCurveChart :curve-points="viz.curve_points" :future-risk="viz.future_risk" />
        </div>

        <div class="card graph-card">
          <h3 class="section-title">单词关系图</h3>
          <p class="section-desc">筛选后展示子图，减轻卡顿并突出记忆关联</p>

          <div class="graph-toolbar">
            <input
              v-model="graphSearch"
              class="graph-search input"
              type="search"
              placeholder="搜索英文或中文"
            />
            <select v-model="graphStatusFilter" class="graph-select">
              <option value="">全部状态</option>
              <option value="new">新词</option>
              <option value="learning">学习中</option>
              <option value="mastered">已掌握</option>
              <option value="weak">易错词</option>
            </select>
          </div>

          <div class="link-filters">
            <label
              v-for="(meta, kind) in LINK_KIND_META"
              :key="kind"
              class="link-filter-item"
            >
              <input v-model="linkFilters[kind as LinkKind]" type="checkbox" />
              <span class="link-swatch" :style="{ background: meta.color }" />
              {{ meta.label }}
            </label>
          </div>

          <div class="graph-mode-row">
            <p class="graph-stats">{{ graphStatsText }}</p>
            <div class="graph-mode-actions">
              <button
                v-if="filteredGraphNodes.length"
                type="button"
                class="btn-mini primary"
                @click="goGraphPractice()"
              >
                开始练习
              </button>
              <button
                v-if="graphViewMode === 'focus'"
                type="button"
                class="btn-mini"
                @click="exitFocusMode"
              >
                退出聚焦
              </button>
              <button
                v-else-if="selectedWord"
                type="button"
                class="btn-mini"
                @click="setFocusCenter(String(selectedWord.id))"
              >
                聚焦此词
              </button>
            </div>
          </div>

          <p v-if="Object.keys(graphPracticeResults).length" class="practice-color-hint">
            {{ graphSessionComplete ? '上次练习已全部完成 · 节点与连线按对错着色' : '图中保留上次练习进度 · 答对绿、答错红' }}
          </p>

          <WordGraphCanvas
            v-if="filteredGraphNodes.length"
            :nodes="filteredGraphNodes"
            :links="filteredGraphLinks"
            :practice-results="graphPracticeResults"
            :session-complete="graphSessionComplete"
            @select="onGraphSelect"
          />
          <p v-else class="curve-loading">当前筛选无匹配单词，请调整条件</p>

          <div class="graph-legend">
            <span v-for="(meta, kind) in LINK_KIND_META" :key="kind" class="legend-item">
              <span class="link-swatch" :style="{ background: meta.color }" />
              {{ meta.label }}：{{ meta.desc }}
            </span>
          </div>
        </div>

        <div v-if="selectedWord" class="card word-detail">
          <div class="detail-header">
            <h3 class="detail-word-title">{{ selectedWord.en }} — {{ selectedWord.zh }}</h3>
            <div class="detail-header-actions">
              <button type="button" class="btn-mini primary" @click="goPracticeSelectedWord">
                练这个词
              </button>
              <button type="button" class="detail-toggle" @click="detailExpanded = !detailExpanded">
                {{ detailExpanded ? '收起' : '展开' }}
              </button>
            </div>
          </div>

          <div v-show="detailExpanded" class="detail-expanded">
            <div class="detail-meta">
              <span>进入词库：{{ selectedWord.entered_at.replace('T', ' ').slice(0, 16) }}</span>
              <span>训练 {{ selectedWord.train_count }} 次 · 累计 {{ formatTrainSeconds(selectedWord.total_train_seconds) }}</span>
              <span>答对 {{ selectedWord.correct_count }} · 答错 {{ selectedWord.wrong_count }}</span>
              <span>掌握率 {{ selectedWord.mastery_score }}%</span>
              <span>当前记忆保留 {{ selectedWord.retention_now }}%</span>
              <span>7 日后预测 {{ selectedWord.risk_7d }}%</span>
            </div>
            <div v-if="memoryModel && !memoryModelLoading" class="model-panel">
              <h4 class="word-curve-title">Transformer 记忆预测</h4>
              <p class="section-desc">
                模型回忆率 {{ memoryModel.model_recall_percent }}% · 公式
                {{ memoryModel.formula_recall_percent }}% · 融合
                {{ memoryModel.recall_now_percent }}% · 建议
                {{ memoryModel.recommended_review_days }} 天后复习
              </p>
              <div class="attn-chips">
                <span
                  v-for="a in memoryModel.attention_hint"
                  :key="a.index"
                  class="attn-chip"
                >
                  {{ a.label }} {{ (a.weight * 100).toFixed(0) }}%
                </span>
              </div>
            </div>
            <p v-if="wordMemoryLoading" class="curve-loading">加载该词记忆曲线...</p>
            <template v-else-if="wordCurvePoints.length">
              <h4 class="word-curve-title">该单词记忆曲线</h4>
              <p class="section-desc">每次训练后更新 · 点与答题记录对应</p>
              <MemoryCurveChart
                :curve-points="wordCurvePoints"
                :future-risk="wordMemory?.future_risk ?? []"
              />
            </template>
            <p v-else class="curve-loading">暂无训练记录，完成每日训练或拼写练习后生成曲线</p>
          </div>
        </div>
      </template>
      <p v-else style="color: var(--muted); text-align: center">暂无单词，无法生成记忆曲线</p>
    </template>
  </div>
</template>

<style scoped>
.view-tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}
.view-tab {
  flex: 1;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  font-size: 14px;
  cursor: pointer;
}
.view-tab.active {
  border-color: var(--primary);
  background: rgba(79, 110, 247, 0.1);
  color: var(--primary);
  font-weight: 600;
}
.model-panel {
  margin-bottom: 14px;
  padding: 12px;
  background: rgba(79, 110, 247, 0.06);
  border-radius: var(--radius);
  border: 1px solid rgba(79, 110, 247, 0.15);
}
.attn-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.attn-chip {
  font-size: 11px;
  padding: 4px 8px;
  background: #fff;
  border-radius: 6px;
  border: 1px solid var(--border);
}
.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 12px;
  margin-bottom: 12px;
}
.bulk-bar.editing {
  padding: 10px 12px;
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.bulk-edit {
  width: auto;
  padding: 8px 16px;
  font-size: 14px;
  margin-left: auto;
}
.bulk-done {
  margin-left: auto;
  border: none;
  background: transparent;
  color: var(--primary);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  padding: 8px 4px;
}
.bulk-select {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
}
.bulk-select input {
  width: 18px;
  height: 18px;
  accent-color: var(--primary);
  cursor: pointer;
}
.bulk-link {
  border: none;
  background: transparent;
  color: var(--primary);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}
.bulk-count {
  font-size: 13px;
  color: var(--muted);
}
.bulk-delete {
  margin-left: auto;
  width: auto;
  padding: 8px 14px;
  font-size: 13px;
}
.page-summary {
  font-size: 13px;
  color: var(--muted);
  margin: 0 0 12px;
}
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 16px;
  padding-top: 12px;
}
.page-btn {
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  font-size: 14px;
  color: var(--primary);
  cursor: pointer;
}
.page-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  color: var(--muted);
}
.page-indicator {
  font-size: 14px;
  color: var(--muted);
  min-width: 72px;
  text-align: center;
}
.section-title {
  font-size: 16px;
  margin: 0 0 6px;
}
.section-desc {
  font-size: 12px;
  color: var(--muted);
  margin: 0 0 12px;
}
.chart-card,
.graph-card {
  margin-bottom: 16px;
}
.graph-toolbar {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.graph-search {
  flex: 1;
  min-width: 0;
}
.graph-select {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 14px;
  background: #fff;
}
.link-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-bottom: 8px;
}
.link-filter-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
}
.link-swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex-shrink: 0;
}
.graph-mode-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.graph-stats {
  font-size: 12px;
  color: var(--muted);
  margin: 0;
  flex: 1;
}
.practice-color-hint {
  font-size: 12px;
  color: var(--muted);
  margin: 0 0 8px;
}
.graph-mode-actions {
  flex-shrink: 0;
}
.btn-mini {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  font-size: 12px;
  color: var(--primary);
  cursor: pointer;
}
.btn-mini.primary {
  border-color: var(--primary);
  background: rgba(79, 110, 247, 0.1);
}
.detail-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
.word-detail {
  margin-top: 12px;
}
.detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.detail-word-title {
  font-size: 17px;
  font-weight: 600;
  margin: 0;
  flex: 1;
  min-width: 0;
}
.detail-toggle {
  flex-shrink: 0;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  font-size: 13px;
  color: var(--primary);
  cursor: pointer;
}
.detail-toggle:hover {
  background: rgba(79, 110, 247, 0.08);
}
.detail-expanded {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.detail-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  color: var(--muted);
}
.word-curve-title {
  font-size: 14px;
  margin: 14px 0 4px;
  color: #1e293b;
}
.graph-legend {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.legend-item {
  font-size: 11px;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 6px;
}
.curve-loading {
  font-size: 13px;
  color: var(--muted);
  margin: 12px 0;
}
</style>

<style>
/* 悬浮翻页箭头挂到 body，避免被页面容器裁剪 */
.float-page-arrow {
  position: fixed;
  top: calc(50% - 28px);
  z-index: 100;
  width: 44px;
  height: 52px;
  border: 1px solid var(--border, #e2e8f0);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 4px 16px rgba(15, 23, 42, 0.12);
  font-size: 28px;
  line-height: 1;
  color: var(--primary, #4f6ef7);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  backdrop-filter: blur(6px);
  -webkit-tap-highlight-color: transparent;
}
.float-page-arrow.prev {
  left: max(6px, env(safe-area-inset-left));
}
.float-page-arrow.next {
  right: max(6px, env(safe-area-inset-right));
}
.float-page-arrow:hover:not(:disabled) {
  background: #fff;
  border-color: var(--primary, #4f6ef7);
}
.float-page-arrow:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  box-shadow: none;
}
@media (min-width: 480px) {
  .float-page-arrow.prev {
    left: 12px;
  }
  .float-page-arrow.next {
    right: 12px;
  }
}
</style>
