<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { Download, Refresh, Search, Setting } from '@element-plus/icons-vue'
import * as echarts from 'echarts/core'
import { BarChart, LineChart } from 'echarts/charts'
import { AriaComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
import { academicTermLabel, defaultAcademicTermId } from '../utils/academicTerms'
import { useAuthStore } from '../stores/auth'

echarts.use([
  AriaComponent,
  BarChart,
  CanvasRenderer,
  GridComponent,
  LegendComponent,
  LineChart,
  TooltipComponent,
])

interface TeachingClassItem {
  gradeSheetId: string
  teachingTaskId: string
  taskNumber: string
  taskName: string
  courseCode: string
  courseName: string
  termName: string
  academicTermId: string
  teacherNames: string[]
  classNames: string[]
  studentCount: number
  averageScore: number
  passRate: number
  excellentRate: number
  calculatedAt: string
}

const terms = ref<any[]>([])
const colleges = ref<any[]>([])
const auth = useAuthStore()
const canManageSchedule = computed(() => auth.user?.roles.some(role => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
const classes = ref<TeachingClassItem[]>([])
const selected = ref<TeachingClassItem>()
const report = ref<any>()
const termId = ref<string>()
const keyword = ref('')
const collegeId = ref<string>()
const teacherKeyword = ref('')
const riskOnly = ref(false)
const page = ref(1)
const total = ref(0)
const pageSize = 12
const loading = ref(false)
const reportLoading = ref(false)
const exporting = ref(false)
const historyMetric = ref<'average' | 'passRate' | 'excellentRate'>('average')
const scheduleDialogVisible = ref(false)
const scheduleLoading = ref(false)
const scheduleSaving = ref(false)
const schedule = reactive({
  isEnabled: true,
  intervalMinutes: 5,
  batchSize: 100,
  lastRunAt: null as string | null,
  nextRunAt: null as string | null,
})

const distributionElement = ref<HTMLElement>()
const peerAverageElement = ref<HTMLElement>()
const peerPassElement = ref<HTMLElement>()
const benchmarkElement = ref<HTMLElement>()
const historyElement = ref<HTMLElement>()
const charts: echarts.ECharts[] = []

const summary = computed(() => report.value?.summary)
const delta = computed(() => report.value?.universityDelta)
const historyMetricMeta = computed(() => ({
  average: { label: '平均分', course: '同课程全校平均', instructor: '当前任课教师历年', suffix: ' 分' },
  passRate: { label: '合格率', course: '同课程全校合格率', instructor: '当前任课教师历年', suffix: '%' },
  excellentRate: { label: '优秀率', course: '同课程全校优秀率', instructor: '当前任课教师历年', suffix: '%' },
}[historyMetric.value]))

function number(value: unknown, digits = 1) {
  const parsed = Number(value)
  return Number.isFinite(parsed) ? parsed.toFixed(digits) : '—'
}

function signed(value: unknown, suffix = '') {
  const parsed = Number(value)
  if (!Number.isFinite(parsed)) return '—'
  return `${parsed > 0 ? '+' : ''}${parsed.toFixed(1)}${suffix}`
}

function disposeCharts() {
  charts.splice(0).forEach(chart => chart.dispose())
}

function createChart(element: HTMLElement | undefined) {
  if (!element) return undefined
  const chart = echarts.init(element)
  charts.push(chart)
  return chart
}

async function loadClasses(reset = false) {
  if (reset) page.value = 1
  loading.value = true
  try {
    const data = (await http.get('/grade-analytics/teaching-classes', {
      params: {
        academicTermId: termId.value,
        keyword: keyword.value.trim() || undefined,
        collegeId: collegeId.value,
        teacherKeyword: teacherKeyword.value.trim() || undefined,
        riskOnly: riskOnly.value || undefined,
        page: page.value,
        pageSize,
      },
    })).data
    classes.value = data.items
    total.value = data.total
    const retained = classes.value.find(item => item.gradeSheetId === selected.value?.gradeSheetId)
    selected.value = retained ?? classes.value[0]
    if (selected.value) await loadReport(selected.value)
    else {
      report.value = undefined
      disposeCharts()
    }
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    loading.value = false
  }
}

async function loadReport(item = selected.value) {
  if (!item) return
  selected.value = item
  reportLoading.value = true
  try {
    report.value = (await http.get(`/grade-analytics/teaching-classes/${item.gradeSheetId}`)).data
    await nextTick()
    drawCharts()
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    reportLoading.value = false
  }
}

async function refreshStatistics() {
  if (!selected.value) {
    await loadClasses()
    return
  }
  reportLoading.value = true
  try {
    await http.post(`/grade-analytics/teaching-classes/${selected.value.gradeSheetId}/refresh`)
    ElMessage.success('已提交统计刷新任务。')
    window.setTimeout(() => loadClasses(), 900)
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
    reportLoading.value = false
  }
}

async function openScheduleSettings() {
  scheduleDialogVisible.value = true
  scheduleLoading.value = true
  try {
    const data = (await http.get('/grade-analytics/refresh-schedule')).data
    schedule.isEnabled = data.isEnabled
    schedule.intervalMinutes = Math.max(1, Math.round(data.intervalSeconds / 60))
    schedule.batchSize = data.batchSize
    schedule.lastRunAt = data.lastRunAt
    schedule.nextRunAt = data.nextRunAt
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    scheduleLoading.value = false
  }
}

async function saveScheduleSettings() {
  scheduleSaving.value = true
  try {
    await http.put('/grade-analytics/refresh-schedule', {
      isEnabled: schedule.isEnabled,
      intervalSeconds: schedule.intervalMinutes * 60,
      batchSize: schedule.batchSize,
    })
    ElMessage.success('成绩统计定时刷新设置已保存')
    scheduleDialogVisible.value = false
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    scheduleSaving.value = false
  }
}

async function exportWordReport() {
  if (!selected.value) return
  exporting.value = true
  try {
    await downloadApiFile(
      `/grade-analytics/teaching-classes/${selected.value.gradeSheetId}/report.docx`,
      `${selected.value.courseCode}-${selected.value.taskNumber}-成绩分析报告.docx`,
    )
    ElMessage.success('成绩分析报告已导出。')
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    exporting.value = false
  }
}

function baseGrid(left = 48, right = 24) {
  return { left, right, top: 42, bottom: 44, containLabel: true }
}

function drawCharts() {
  disposeCharts()
  if (!summary.value) return

  const distribution = createChart(distributionElement.value)
  distribution?.setOption({
    aria: { enabled: true, decal: { show: true } },
    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
    grid: baseGrid(),
    xAxis: { type: 'category', data: summary.value.scoreBands.map((x: any) => x.label), axisTick: { show: false } },
    yAxis: { type: 'value', minInterval: 1, name: '人数', splitLine: { lineStyle: { color: '#edf1f6' } } },
    series: [{
      name: '人数', type: 'bar', barMaxWidth: 54,
      data: summary.value.scoreBands.map((x: any, index: number) => ({
        value: x.studentCount,
        itemStyle: { color: ['#b9c6d8', '#95a9c5', '#6f8db5', '#496f9f', '#204f87'][index] },
      })),
      label: { show: true, position: 'top', formatter: (p: any) => `${p.value} 人` },
      itemStyle: { borderRadius: [4, 4, 0, 0] },
    }],
  })

  const peers = report.value.peerTeachingClasses ?? []
  const peerNames = peers.map((x: any) => `${x.taskNumber} · ${x.teacherNames || '未标注教师'}`)
  const peerAverage = createChart(peerAverageElement.value)
  peerAverage?.setOption({
    aria: { enabled: true, decal: { show: true } },
    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (v: any) => `${number(v)} 分` },
    grid: baseGrid(20, 36),
    xAxis: { type: 'value', min: 0, max: 100, name: '平均分', splitLine: { lineStyle: { color: '#edf1f6' } } },
    yAxis: { type: 'category', inverse: true, data: peerNames, axisTick: { show: false }, axisLabel: { width: 150, overflow: 'truncate' } },
    series: [{
      type: 'bar', barMaxWidth: 24,
      data: peers.map((x: any) => ({ value: x.averageScore, itemStyle: { color: x.isSelected ? '#204f87' : '#aebdd1' } })),
      label: { show: true, position: 'right', formatter: (p: any) => number(p.value) },
      itemStyle: { borderRadius: [0, 4, 4, 0] },
    }],
  })

  const peerPass = createChart(peerPassElement.value)
  peerPass?.setOption({
    aria: { enabled: true, decal: { show: true } },
    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (v: any) => `${number(v)}%` },
    grid: baseGrid(20, 42),
    xAxis: { type: 'value', min: 0, max: 100, name: '合格率', axisLabel: { formatter: '{value}%' }, splitLine: { lineStyle: { color: '#edf1f6' } } },
    yAxis: { type: 'category', inverse: true, data: peerNames, axisTick: { show: false }, axisLabel: { width: 150, overflow: 'truncate' } },
    series: [{
      type: 'bar', barMaxWidth: 18,
      data: peers.map((x: any) => ({ value: x.passRate, itemStyle: { color: x.isSelected ? '#b8732d' : '#dcc3a5' } })),
      label: { show: true, position: 'right', formatter: (p: any) => `${number(p.value)}%` },
      itemStyle: { borderRadius: [0, 4, 4, 0] },
    }],
  })

  const benchmarks = report.value.scopeBenchmarks ?? []
  const benchmark = createChart(benchmarkElement.value)
  benchmark?.setOption({
    aria: { enabled: true, decal: { show: true } },
    tooltip: {
      trigger: 'axis',
      formatter: (params: any) => {
        const item = benchmarks[params[0]?.dataIndex ?? 0]
        return `<b>${item.scope} · ${item.name}</b><br/>样本　${item.studentCount} 人<br/>最高分　${number(item.highestScore)}<br/>平均分　${number(item.averageScore)}<br/>最低分　${number(item.lowestScore)}<br/>合格率　${number(item.passRate)}%`
      },
    },
    legend: { top: 0, data: ['分数区间', '平均分', '合格率'] },
    grid: { left: 30, right: 54, top: 58, bottom: 52, containLabel: true },
    xAxis: { type: 'category', data: benchmarks.map((x: any) => `${x.scope}\n${x.name}`), axisTick: { show: false } },
    yAxis: [
      { type: 'value', min: 0, max: 100, name: '分数', splitLine: { lineStyle: { color: '#edf1f6' } } },
      { type: 'value', min: 0, max: 100, name: '合格率', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } },
    ],
    series: [
      { name: '区间起点', type: 'bar', stack: 'range', silent: true, itemStyle: { color: 'transparent' }, data: benchmarks.map((x: any) => x.lowestScore) },
      { name: '分数区间', type: 'bar', stack: 'range', barWidth: 20, itemStyle: { color: '#a7b8cf', borderRadius: 6 }, data: benchmarks.map((x: any) => x.highestScore - x.lowestScore) },
      { name: '平均分', type: 'line', symbolSize: 9, lineStyle: { color: '#204f87', width: 3 }, itemStyle: { color: '#204f87' }, data: benchmarks.map((x: any) => x.averageScore) },
      { name: '合格率', type: 'line', yAxisIndex: 1, symbol: 'diamond', symbolSize: 9, lineStyle: { color: '#b8732d', type: 'dashed', width: 2 }, itemStyle: { color: '#b8732d' }, data: benchmarks.map((x: any) => x.passRate) },
    ],
  })

  drawHistoryChart()
}

function drawHistoryChart() {
  const old = charts.find(chart => chart.getDom() === historyElement.value)
  if (old) {
    old.dispose()
    charts.splice(charts.indexOf(old), 1)
  }
  const history = report.value?.history ?? []
  const chart = createChart(historyElement.value)
  if (!chart) return
  const field = historyMetric.value === 'average' ? 'AverageScore' : historyMetric.value === 'passRate' ? 'PassRate' : 'ExcellentRate'
  const courseField = `course${field}`
  const instructorField = field[0].toLowerCase() + field.slice(1)
  const seriesType = history.length >= 4 ? 'line' : 'bar'
  chart.setOption({
    aria: { enabled: true, decal: { show: true } },
    tooltip: { trigger: 'axis', valueFormatter: (v: any) => `${number(v)}${historyMetricMeta.value.suffix}` },
    legend: { top: 0, data: [historyMetricMeta.value.course, historyMetricMeta.value.instructor] },
    grid: { left: 46, right: 28, top: 58, bottom: 54, containLabel: true },
    xAxis: { type: 'category', data: history.map((x: any) => x.termName), axisLabel: { rotate: history.length > 5 ? 24 : 0 } },
    yAxis: { type: 'value', min: 0, max: 100, name: historyMetricMeta.value.label, axisLabel: { formatter: historyMetric.value === 'average' ? '{value}' : '{value}%' }, splitLine: { lineStyle: { color: '#edf1f6' } } },
    series: [
      {
        name: historyMetricMeta.value.course, type: seriesType, smooth: seriesType === 'line', symbol: 'circle', symbolSize: 8,
        barMaxWidth: 34, itemStyle: { color: '#9eafc5' }, lineStyle: { color: '#9eafc5', width: 2 },
        data: history.map((x: any) => x[courseField]),
      },
      {
        name: historyMetricMeta.value.instructor, type: seriesType, smooth: seriesType === 'line', symbol: 'diamond', symbolSize: 9,
        barMaxWidth: 34, itemStyle: { color: '#204f87' }, lineStyle: { color: '#204f87', width: 3 },
        data: history.map((x: any) => x.instructor?.[instructorField] ?? null),
      },
    ],
  })
}

function resizeCharts() {
  charts.forEach(chart => chart.resize())
}

watch(historyMetric, async () => {
  await nextTick()
  drawHistoryChart()
})

onMounted(async () => {
  try {
  const [termResponse, collegeResponse] = await Promise.all([
    http.get('/base-data/terms'),
    http.get('/base-data/colleges'),
  ])
  terms.value = termResponse.data
  colleges.value = collegeResponse.data
    termId.value = defaultAcademicTermId(terms.value)
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
  await loadClasses(true)
  window.addEventListener('resize', resizeCharts)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', resizeCharts)
  disposeCharts()
})
</script>

<template>
  <div class="page-stack grade-analytics">
    <section class="page-intro analytics-intro">
      <div>
        <span class="section-kicker">TEACHING CLASS ANALYTICS</span>
        <h2>教学班成绩分析</h2>
        <p>对比当前教学班、同课程教学班、学生来源范围与历年成绩；统计仅使用已正式发布成绩。</p>
      </div>
      <div class="intro-actions">
        <el-button v-if="canManageSchedule" :icon="Setting" @click="openScheduleSettings">定时刷新设置</el-button>
        <el-button
          type="primary"
          :icon="Download"
          :loading="exporting"
          :disabled="!selected || !summary"
          @click="exportWordReport"
        >导出 Word 报告</el-button>
        <el-button :icon="Refresh" @click="refreshStatistics">重新计算当前教学班</el-button>
      </div>
    </section>

    <el-dialog v-model="scheduleDialogVisible" title="成绩统计定时刷新" width="520px">
      <el-form v-loading="scheduleLoading" label-width="130px">
        <el-form-item label="启用定时刷新">
          <el-switch v-model="schedule.isEnabled" />
        </el-form-item>
        <el-form-item label="刷新间隔">
          <el-input-number v-model="schedule.intervalMinutes" :min="1" :max="1440" :disabled="!schedule.isEnabled" />
          <span class="schedule-unit">分钟</span>
        </el-form-item>
        <el-form-item label="单次处理上限">
          <el-input-number v-model="schedule.batchSize" :min="1" :max="5000" :step="50" :disabled="!schedule.isEnabled" />
          <span class="schedule-unit">个课程学期</span>
        </el-form-item>
        <el-form-item label="运行状态">
          <div class="schedule-status">
            <span>上次扫描：{{ schedule.lastRunAt ? new Date(schedule.lastRunAt).toLocaleString('zh-CN') : '尚未运行' }}</span>
            <span v-if="schedule.isEnabled">下次扫描：{{ schedule.nextRunAt ? new Date(schedule.nextRunAt).toLocaleString('zh-CN') : '启用后将尽快运行' }}</span>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="scheduleDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="scheduleSaving" @click="saveScheduleSettings">保存设置</el-button>
      </template>
    </el-dialog>

    <section class="filter-bar">
      <el-select v-model="termId" clearable placeholder="全部学期" style="width: 240px" @change="loadClasses(true)">
        <el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" />
      </el-select>
      <el-select v-model="collegeId" clearable placeholder="全部开课学院" style="width: 190px" @change="loadClasses(true)">
        <el-option v-for="college in colleges" :key="college.id" :label="college.name" :value="college.id" />
      </el-select>
      <el-input v-model="keyword" clearable placeholder="课程、教学班名称或编号" :prefix-icon="Search" @keyup.enter="loadClasses(true)" />
      <el-input v-model="teacherKeyword" clearable placeholder="任课教师姓名或工号" :prefix-icon="Search" @keyup.enter="loadClasses(true)" @clear="loadClasses(true)" />
      <el-checkbox v-model="riskOnly" @change="loadClasses(true)">仅看预警班</el-checkbox>
      <el-button type="primary" @click="loadClasses(true)">查询</el-button>
    </section>

    <section class="analytics-shell">
      <aside class="class-rail" v-loading="loading">
        <header><strong>可查看教学班</strong><small>{{ total }} 个已生成统计</small></header>
        <button
          v-for="item in classes"
          :key="item.gradeSheetId"
          type="button"
          :class="['class-option', { active: selected?.gradeSheetId === item.gradeSheetId }]"
          @click="loadReport(item)"
        >
          <span class="course-code">{{ item.courseCode }} · {{ item.taskNumber }}</span>
          <strong>{{ item.courseName }}</strong>
          <small>{{ item.teacherNames.join('、') || '未标注教师' }} · {{ item.studentCount }} 人</small>
          <span class="option-metrics"><b>{{ number(item.averageScore) }}</b> 平均分 · <b>{{ number(item.passRate) }}%</b> 合格</span>
        </button>
        <el-empty v-if="!loading && !classes.length" :image-size="72" description="当前范围暂无已发布成绩统计" />
        <el-pagination
          v-if="total > pageSize"
          small layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page"
          @current-change="loadClasses()"
        />
      </aside>

      <main class="analysis-canvas" v-loading="reportLoading">
        <el-empty v-if="!selected" description="请选择一个教学班查看分析" />
        <el-alert v-else-if="report?.isRefreshing" type="info" :closable="false" title="该教学班统计正在生成，请稍后刷新。" />
        <template v-else-if="summary">
          <header class="report-heading">
            <div>
              <span>{{ report.courseCode }} · {{ report.taskNumber }} · {{ report.termName }}</span>
              <h3>{{ report.courseName }}</h3>
              <p>{{ report.taskName }} · 数据更新时间 {{ new Date(summary.calculatedAt).toLocaleString() }}</p>
            </div>
            <div class="sample-stamp"><b>{{ summary.studentCount }}</b><span>有效成绩</span></div>
          </header>

          <section class="kpi-strip">
            <article><span>平均分</span><strong>{{ number(summary.averageScore) }}</strong><small>全体有效成绩</small></article>
            <article><span>中位数</span><strong>{{ number(summary.medianScore) }}</strong><small>降低极端值影响</small></article>
            <article><span>标准差</span><strong>{{ number(summary.standardDeviation, 2) }}</strong><small>越大表示分数越分散</small></article>
            <article><span>最高 / 最低</span><strong>{{ number(summary.highestScore) }} / {{ number(summary.lowestScore) }}</strong><small>成绩跨度 {{ number(summary.highestScore - summary.lowestScore) }}</small></article>
            <article><span>合格率</span><strong>{{ number(summary.passRate) }}%</strong><small>{{ summary.passedCount }} / {{ summary.studentCount }} 人</small></article>
            <article><span>优秀率</span><strong>{{ number(summary.excellentRate) }}%</strong><small>{{ summary.excellentCount }} 人达到 90 分</small></article>
          </section>

          <section v-if="delta" class="benchmark-note">
            <span>相对本学期全校同课程</span>
            <strong :class="{ negative: delta.averageScoreDifference < 0 }">平均分 {{ signed(delta.averageScoreDifference, ' 分') }}</strong>
            <strong :class="{ negative: delta.passRateDifference < 0 }">合格率 {{ signed(delta.passRateDifference, '%') }}</strong>
            <small>全校基准：平均 {{ number(delta.universityAverageScore) }}，合格率 {{ number(delta.universityPassRate) }}%</small>
          </section>

          <section class="chart-grid">
            <article class="chart-card">
              <header><div><span>DISTRIBUTION</span><h4>本教学班分数段分布</h4></div><p>{{ summary.studentCount }} 人，按五个固定区间统计</p></header>
              <div ref="distributionElement" class="chart" />
            </article>
            <article class="chart-card">
              <header><div><span>PEER AVERAGE</span><h4>同课程教学班平均分</h4></div><p>当前教学班使用深色标识</p></header>
              <div ref="peerAverageElement" class="chart" />
            </article>
            <article class="chart-card">
              <header><div><span>PEER PASS RATE</span><h4>同课程教学班合格率</h4></div><p>同一课程、同一学期横向比较</p></header>
              <div ref="peerPassElement" class="chart" />
            </article>
            <article class="chart-card chart-card-wide">
              <header><div><span>SCOPE BENCHMARK</span><h4>行政班、专业、学院与全校基准</h4></div><p>对照本教学班学生来源范围，显示分数区间、平均分和合格率</p></header>
              <div ref="benchmarkElement" class="chart chart-large" />
            </article>
            <article class="chart-card chart-card-wide">
              <header class="history-heading">
                <div><span>HISTORICAL COMPARISON</span><h4>当前任课教师与全校同课程历年对比</h4></div>
                <el-radio-group v-model="historyMetric" size="small">
                  <el-radio-button value="average">平均分</el-radio-button>
                  <el-radio-button value="passRate">合格率</el-radio-button>
                  <el-radio-button value="excellentRate">优秀率</el-radio-button>
                </el-radio-group>
              </header>
              <p class="chart-caption">教师序列按当前教学班任课教师在各学期所带同课程教学班加权汇总；全校序列为该课程当期全部已发布成绩。</p>
              <div ref="historyElement" class="chart chart-large" />
            </article>
          </section>
        </template>
      </main>
    </section>
  </div>
</template>

<style scoped>
.grade-analytics { --ink: #18324f; --blue: #204f87; --line: #dce3ec; }
.analytics-intro { align-items: flex-end; }
.filter-bar { display: grid; grid-template-columns: 240px minmax(260px, 560px) auto; gap: 12px; align-items: center; padding: 16px 18px; background: #f7f9fc; border: 1px solid var(--line); }
.analytics-shell { display: grid; grid-template-columns: 292px minmax(0, 1fr); gap: 18px; align-items: start; }
.class-rail { position: sticky; top: 18px; max-height: calc(100vh - 36px); overflow: auto; background: #f6f8fb; border: 1px solid var(--line); }
.class-rail > header { position: sticky; top: 0; z-index: 2; display: flex; justify-content: space-between; gap: 8px; padding: 15px 16px; background: #eef2f7; border-bottom: 1px solid var(--line); }
.class-rail > header small { color: #667085; }
.class-option { width: 100%; padding: 15px 16px; border: 0; border-bottom: 1px solid #e1e6ed; background: transparent; color: #475467; text-align: left; cursor: pointer; transition: background .18s ease, box-shadow .18s ease; }
.class-option:hover { background: #fff; }
.class-option.active { background: #fff; box-shadow: inset 4px 0 var(--blue); }
.class-option:focus-visible { outline: 2px solid var(--blue); outline-offset: -3px; }
.class-option > * { display: block; }
.class-option strong { margin: 5px 0; color: var(--ink); font-size: 14px; }
.class-option small { overflow: hidden; color: #667085; text-overflow: ellipsis; white-space: nowrap; }
.course-code { color: #7b8797; font: 11px Consolas, monospace; }
.option-metrics { margin-top: 9px; color: #667085; font-size: 11px; }
.option-metrics b { color: var(--blue); font: 700 13px Consolas, monospace; }
.class-rail :deep(.el-pagination) { justify-content: center; padding: 14px 4px; }
.analysis-canvas { min-width: 0; }
.report-heading { display: flex; justify-content: space-between; gap: 20px; padding: 22px 24px; color: #fff; background: var(--ink); }
.report-heading span { color: #b9c9da; font: 11px Consolas, monospace; }
.report-heading h3 { margin: 7px 0 5px; font-size: 24px; }
.report-heading p { margin: 0; color: #cbd5e1; font-size: 12px; }
.sample-stamp { display: flex; flex-direction: column; min-width: 94px; justify-content: center; padding-left: 22px; border-left: 1px solid rgb(255 255 255 / 20%); }
.sample-stamp b { font: 700 34px/1 Consolas, monospace; }
.sample-stamp span { margin-top: 5px; color: #b9c9da; font-size: 11px; }
.kpi-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); background: #fff; border: 1px solid var(--line); border-top: 0; }
.kpi-strip article { min-height: 112px; padding: 17px 20px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.kpi-strip article:nth-child(3n) { border-right: 0; }
.kpi-strip span, .kpi-strip small { display: block; color: #667085; }
.kpi-strip span { font-size: 11px; font-weight: 700; }
.kpi-strip strong { display: block; margin: 10px 0 7px; overflow-wrap: anywhere; color: var(--ink); font: 700 24px/1 Consolas, monospace; }
.kpi-strip small { font-size: 10px; }
.benchmark-note { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 20px; padding: 14px 18px; background: #edf3fa; border: 1px solid #ccd9e9; border-top: 0; }
.benchmark-note > span { color: var(--ink); font-weight: 700; }
.benchmark-note strong { color: var(--blue); font: 700 13px Consolas, monospace; }
.benchmark-note strong.negative { color: #9a5b28; }
.benchmark-note small { margin-left: auto; color: #667085; }
.chart-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 18px; }
.chart-card { min-width: 0; padding: 20px 22px; background: #fff; border: 1px solid var(--line); box-shadow: 0 10px 28px rgb(24 50 79 / 5%); }
.chart-card-wide { grid-column: 1 / -1; }
.chart-card header { display: flex; min-width: 0; align-items: end; justify-content: space-between; gap: 18px; }
.chart-card header span { color: #7b8797; font: 10px Consolas, monospace; letter-spacing: .08em; }
.chart-card h4 { margin: 5px 0 0; color: var(--ink); font-size: 16px; }
.chart-card header p, .chart-caption { min-width: 0; margin: 0; color: #667085; font-size: 11px; text-align: right; }
.chart-caption { margin-top: 9px; text-align: left; }
.schedule-unit { margin-left: 10px; color: #667085; font-size: 12px; }
.schedule-status { display: grid; gap: 3px; color: #667085; font-size: 12px; }
.chart { height: 310px; margin-top: 10px; }
.chart-large { height: 360px; }
@media (max-width: 1180px) {
  .analytics-shell { grid-template-columns: 240px minmax(0, 1fr); }
  .chart-grid { grid-template-columns: 1fr; }
  .chart-card-wide { grid-column: auto; }
}
@media (max-width: 820px) {
  .filter-bar { grid-template-columns: 1fr; }
  .filter-bar :deep(.el-select) { width: 100% !important; }
  .analytics-shell { grid-template-columns: 1fr; }
  .class-rail { position: static; max-height: 390px; }
  .kpi-strip { grid-template-columns: repeat(2, 1fr); }
  .kpi-strip article:nth-child(3n) { border-right: 1px solid var(--line); }
  .kpi-strip article:nth-child(2n) { border-right: 0; }
  .history-heading, .chart-card header { align-items: flex-start; flex-direction: column; }
  .chart-card header p { text-align: left; }
}
@media (max-width: 520px) {
  .report-heading { flex-direction: column; }
  .sample-stamp { padding: 14px 0 0; border: 0; border-top: 1px solid rgb(255 255 255 / 20%); }
  .kpi-strip { grid-template-columns: 1fr; }
  .kpi-strip article { border-right: 0 !important; }
  .chart-card { padding: 17px 14px; }
}
</style>
