<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Delete, MagicStick, Refresh, Search } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'

interface PlanningTerm {
  semester: number
  label: string
  isBeyondStandard: boolean
  courseIds: string[]
}

const loading = ref(false)
const simulationLoading = ref(false)
const initialized = ref(false)
const payload = ref<any>(null)
const simulation = ref<any>(null)
const terms = ref<PlanningTerm[]>([])
const keyword = ref('')
const moduleFilter = ref('')
let simulationTimer: ReturnType<typeof setTimeout> | undefined

const courses = computed<any[]>(() => payload.value?.courses ?? [])
const baseline = computed(() => payload.value?.baseline ?? {})
const projected = computed(() => simulation.value?.projected ?? {
  earnedCredits: baseline.value.earnedCredits ?? 0,
  planCompletedCredits: baseline.value.planCompletedCredits ?? 0,
  creditGap: baseline.value.creditGap ?? 0,
  completionRate: baseline.value.completionRate ?? 0,
  requirementCount: baseline.value.requirementCount ?? 0,
  passedRequirementCount: baseline.value.passedRequirementCount ?? 0,
  missingRequirements: baseline.value.missingRequirements ?? [],
  graduationConclusion: 'Ineligible',
  estimatedGraduationTerm: '等待模拟',
  isBeyondStandard: false,
})
const modules = computed(() =>
  [...new Map(courses.value.map((course) =>
    [course.moduleCode, { code: course.moduleCode, name: course.moduleName }],
  )).values()],
)
const assigned = computed(() => {
  const result = new Map<string, number>()
  for (const term of terms.value) {
    for (const courseId of term.courseIds) result.set(courseId, term.semester)
  }
  return result
})
const remainingCourses = computed(() => {
  const normalized = keyword.value.trim().toLowerCase()
  return courses.value.filter((course) => {
    if (['Completed', 'InProgress', 'Retaking'].includes(course.status)) return false
    if (moduleFilter.value && course.moduleCode !== moduleFilter.value) return false
    return !normalized ||
      `${course.courseCode} ${course.courseName} ${course.moduleName}`
        .toLowerCase()
        .includes(normalized)
  })
})
const plannedCourseCount = computed(() =>
  terms.value.reduce((count, term) => count + term.courseIds.length, 0),
)
const plannedCredits = computed(() =>
  courses.value
    .filter((course) => assigned.value.has(course.courseId))
    .reduce((sum, course) => sum + Number(course.credits), 0),
)
const conflictCourseIds = computed(() =>
  new Set<string>((simulation.value?.conflicts ?? []).map((item: any) => item.courseId)),
)
const termSummaryMap = computed(() =>
  new Map<number, any>((simulation.value?.termSummaries ?? [])
    .map((item: any) => [item.semester, item])),
)

function courseById(courseId: string) {
  return courses.value.find((course) => course.courseId === courseId)
}

function typeLabel(type: string) {
  return type === 'Required' ? '指定必修' : '组内选修'
}

function setCourseSemester(courseId: string, semester?: number) {
  for (const term of terms.value) {
    term.courseIds = term.courseIds.filter((id) => id !== courseId)
  }
  if (semester) {
    const target = terms.value.find((term) => term.semester === semester)
    if (target) target.courseIds.push(courseId)
  }
}

function removeCourse(courseId: string) {
  setCourseSemester(courseId)
}

function applySuggestion() {
  const target = terms.value.find(
    (term) => term.semester === payload.value?.plan?.nextSemester,
  )
  if (!target) return
  const ids = (payload.value?.nextSemesterSuggestion ?? [])
    .map((course: any) => course.courseId)
    .filter((courseId: string) => !assigned.value.has(courseId))
  target.courseIds.push(...ids)
  ElMessage.success(`已把 ${ids.length} 门建议课程安排到下一学期`)
}

function clearPlan() {
  for (const term of terms.value) term.courseIds = []
}

async function simulate() {
  simulationLoading.value = true
  try {
    simulation.value = (await http.post('/student/academic-planning/simulate', {
      terms: terms.value.map((term) => ({
        semester: term.semester,
        courseIds: term.courseIds,
      })),
    })).data
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    simulationLoading.value = false
  }
}

function scheduleSimulation() {
  if (!initialized.value) return
  if (simulationTimer) clearTimeout(simulationTimer)
  simulationTimer = setTimeout(simulate, 180)
}

async function load() {
  loading.value = true
  initialized.value = false
  try {
    payload.value = (await http.get('/student/academic-planning')).data
    terms.value = payload.value.terms.map((term: any) => ({
      ...term,
      courseIds: [],
    }))
    initialized.value = true
    await simulate()
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    loading.value = false
  }
}

watch(terms, scheduleSimulation, { deep: true })
onMounted(load)
onBeforeUnmount(() => {
  if (simulationTimer) clearTimeout(simulationTimer)
})
</script>

<template>
  <div class="page-stack academic-planning-page" v-loading="loading">
    <section class="page-intro">
      <div>
        <span class="section-kicker">DEGREE FLIGHT PLAN</span>
        <h2>学业规划与毕业模拟</h2>
        <p>把未来课程排进学期航线，实时检查培养方案完成度、先修顺序与毕业时间。</p>
      </div>
      <div class="page-actions">
        <el-button :icon="Refresh" @click="load">重置数据</el-button>
        <el-button
          type="primary"
          :icon="MagicStick"
          :disabled="!payload?.nextSemesterSuggestion?.length"
          @click="applySuggestion"
        >
          采用下学期建议
        </el-button>
      </div>
    </section>

    <template v-if="payload">
      <section class="planning-cockpit">
        <div class="student-call-sign">
          <span>{{ payload.student.enrollmentYear }} 级 · {{ payload.student.collegeName }}</span>
          <h3>{{ payload.student.name }}的毕业航线</h3>
          <p>{{ payload.student.studentNumber }} · {{ payload.student.majorName }} · {{ payload.plan.name }} {{ payload.plan.version }}</p>
        </div>
        <div class="completion-dial" aria-label="模拟培养方案完成度">
          <span>方案完成度</span>
          <b>{{ projected.completionRate }}<small>%</small></b>
          <i><em :style="{ width: `${projected.completionRate}%` }" /></i>
        </div>
        <div class="arrival-board">
          <span>预计毕业</span>
          <b>{{ projected.estimatedGraduationTerm }}</b>
          <small :class="{ delayed: projected.isBeyondStandard }">
            {{ projected.isBeyondStandard ? '超过标准学制' : '标准学制内' }}
          </small>
        </div>
      </section>

      <section class="planning-metrics" aria-label="毕业模拟摘要">
        <div>
          <span>已获 / 预计学分</span>
          <b>{{ baseline.earnedCredits }} <small>→</small> {{ projected.earnedCredits }}</b>
        </div>
        <div :class="{ alert: projected.creditGap > 0 }">
          <span>毕业学分缺口</span>
          <b>{{ projected.creditGap }}<small> 学分</small></b>
        </div>
        <div>
          <span>培养要求</span>
          <b>{{ projected.passedRequirementCount }}<small> / {{ projected.requirementCount }} 项</small></b>
        </div>
        <div>
          <span>本次模拟</span>
          <b>{{ plannedCourseCount }}<small> 门 · {{ plannedCredits }} 学分</small></b>
        </div>
      </section>

      <section class="next-term-brief">
        <header>
          <span>NEXT TERM ADVICE</span>
          <h3>下一学期建议</h3>
          <p>{{ payload.terms[0]?.label }}</p>
        </header>
        <div v-if="payload.nextSemesterSuggestion.length" class="suggestion-list">
          <article
            v-for="course in payload.nextSemesterSuggestion"
            :key="course.courseId"
          >
            <div>
              <span>{{ course.courseCode }} · {{ typeLabel(course.type) }}</span>
              <b>{{ course.courseName }}</b>
            </div>
            <small>{{ course.credits }} 学分</small>
            <p>{{ course.reason }}</p>
          </article>
        </div>
        <p v-else class="no-suggestion">
          结合当前在读课程、先修条件和培养方案建议学期，下一学期暂无需要额外安排的课程；仍可在下方课程清单模拟其他路径。
        </p>
      </section>

      <section
        v-if="payload.latestGraduationAudit"
        class="official-audit-strip"
      >
        <div>
          <span>最近正式毕业审核</span>
          <b>{{ payload.latestGraduationAudit.batchName }}</b>
          <small>{{ payload.latestGraduationAudit.publishedAt?.slice(0, 10) }}</small>
        </div>
        <p>
          正式结果：{{ payload.latestGraduationAudit.conclusion === 'Eligible' ? '符合毕业条件' : '暂不符合毕业条件' }}
          · 已认定 {{ payload.latestGraduationAudit.earnedCredits }} 学分
        </p>
        <em>模拟不会更改正式审核结果</em>
      </section>

      <section
        v-if="simulation?.conflicts?.length || simulation?.warnings?.length"
        class="planning-alerts"
      >
        <article v-for="item in simulation.conflicts" :key="`${item.type}-${item.courseId}-${item.prerequisiteCourseId}`">
          <span>先修冲突</span>
          <p>{{ item.message }}</p>
        </article>
        <article v-for="(item, index) in simulation.warnings" :key="`${item.type}-${index}`" class="warning">
          <span>安排提示</span>
          <p>{{ item.message }}</p>
        </article>
      </section>

      <section class="planner-workspace">
        <aside class="course-pool">
          <header>
            <div>
              <span>COURSE MANIFEST</span>
              <h3>待规划课程</h3>
            </div>
            <b>{{ remainingCourses.length }}</b>
          </header>
          <div class="pool-filters">
            <el-input
              v-model="keyword"
              :prefix-icon="Search"
              clearable
              placeholder="搜索课程"
            />
            <el-select v-model="moduleFilter" clearable placeholder="全部培养模块">
              <el-option
                v-for="module in modules"
                :key="module.code"
                :label="module.name"
                :value="module.code"
              />
            </el-select>
          </div>
          <div class="pool-list">
            <article
              v-for="course in remainingCourses"
              :key="course.courseId"
              :class="{
                assigned: assigned.has(course.courseId),
                conflicted: conflictCourseIds.has(course.courseId),
              }"
            >
              <div class="course-manifest-line">
                <span>{{ course.courseCode }} · {{ course.moduleName }}</span>
                <i>{{ course.credits }} 学分</i>
              </div>
              <h4>{{ course.courseName }}</h4>
              <div class="course-flags">
                <span>{{ typeLabel(course.type) }}</span>
                <span>建议第 {{ course.recommendedSemester }} 学期</span>
                <span v-if="course.status === 'Failed'" class="failed">有未通过记录</span>
              </div>
              <p v-if="course.prerequisites.length">
                先修：
                <template v-for="(item, index) in course.prerequisites" :key="item.courseId">
                  <b :class="{ ready: item.isCompleted || item.isInProgress }">{{ item.courseName }}</b>{{ Number(index) < course.prerequisites.length - 1 ? '、' : '' }}
                </template>
              </p>
              <el-select
                :model-value="assigned.get(course.courseId)"
                clearable
                placeholder="安排到学期"
                @change="setCourseSemester(course.courseId, $event)"
              >
                <el-option
                  v-for="term in terms"
                  :key="term.semester"
                  :label="`第 ${term.semester} 学期 · ${term.label}`"
                  :value="term.semester"
                />
              </el-select>
            </article>
            <el-empty v-if="!remainingCourses.length" description="没有符合条件的待规划课程" />
          </div>
        </aside>

        <main class="semester-runway" v-loading="simulationLoading">
          <header>
            <div>
              <span>SEMESTER RUNWAY</span>
              <h3>未来学期航线</h3>
              <p>课程必须在其先修课程之后；单学期超过 30 学分会标记负荷风险。</p>
            </div>
            <el-button text :icon="Delete" @click="clearPlan">清空安排</el-button>
          </header>

          <div class="runway-line">
            <article
              v-for="term in terms"
              :key="term.semester"
              class="semester-stop"
              :class="{ beyond: term.isBeyondStandard }"
            >
              <div class="semester-marker">
                <i>{{ term.semester }}</i>
                <span>{{ term.isBeyondStandard ? '延长学期' : `第 ${term.semester} 学期` }}</span>
              </div>
              <div class="semester-sheet">
                <header>
                  <div>
                    <span>{{ term.label }}</span>
                    <h4>{{ term.semester === payload.plan.nextSemester ? '下一学期' : `未来第 ${term.semester - payload.plan.currentSemester} 学期` }}</h4>
                  </div>
                  <b>
                    {{ termSummaryMap.get(term.semester)?.credits ?? 0 }}
                    <small>学分</small>
                  </b>
                </header>
                <div v-if="term.courseIds.length" class="scheduled-courses">
                  <article
                    v-for="courseId in term.courseIds"
                    :key="courseId"
                    :class="{ conflicted: conflictCourseIds.has(courseId) }"
                  >
                    <div>
                      <span>{{ courseById(courseId)?.courseCode }} · {{ typeLabel(courseById(courseId)?.type) }}</span>
                      <b>{{ courseById(courseId)?.courseName }}</b>
                    </div>
                    <small>{{ courseById(courseId)?.credits }} 学分</small>
                    <el-button
                      text
                      type="danger"
                      aria-label="移除课程"
                      @click="removeCourse(courseId)"
                    >×</el-button>
                  </article>
                </div>
                <p v-else class="empty-semester">尚未安排课程，可从左侧课程清单选择学期。</p>
              </div>
            </article>
            <div class="graduation-gate" :class="{ ready: projected.graduationConclusion === 'Eligible' }">
              <span>GRADUATION GATE</span>
              <b>{{ projected.graduationConclusion === 'Eligible' ? '模拟达到毕业条件' : '仍有培养要求未完成' }}</b>
              <p>{{ projected.estimatedGraduationTerm }}</p>
            </div>
          </div>
        </main>
      </section>

      <section class="planning-closeout">
        <div>
          <span>剩余培养要求</span>
          <h3>{{ projected.missingRequirements?.length ? `还需完成 ${projected.missingRequirements.length} 项` : '培养要求已覆盖' }}</h3>
        </div>
        <div class="missing-requirements">
          <span v-for="item in projected.missingRequirements" :key="item">{{ item }}</span>
          <em v-if="!projected.missingRequirements?.length">本次模拟已覆盖全部指定课程和课程组要求</em>
        </div>
        <ul>
          <li v-for="item in payload.assumptions" :key="item">{{ item }}</li>
        </ul>
      </section>
    </template>
  </div>
</template>

<style scoped>
.planning-cockpit {
  min-height: 154px;
  display: grid;
  grid-template-columns: minmax(300px, 1.2fr) minmax(230px, .7fr) minmax(280px, .9fr);
  color: white;
  border: 1px solid #15275b;
  background:
    linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px),
    linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px),
    linear-gradient(112deg, #142557, #253e78 63%, #0c6f70);
  background-size: 26px 26px, 26px 26px, auto;
}
.student-call-sign,
.completion-dial,
.arrival-board { padding: 26px 28px; }
.student-call-sign > span,
.completion-dial > span,
.arrival-board > span,
.course-pool header span,
.semester-runway > header span {
  color: #69d8c7;
  font: 700 9px/1.2 Consolas, monospace;
  letter-spacing: .12em;
}
.student-call-sign h3 {
  margin: 10px 0 7px;
  font-family: "STZhongsong", "Songti SC", serif;
  font-size: 25px;
  letter-spacing: .04em;
}
.student-call-sign p { margin: 0; color: #c8d1e6; font-size: 10px; }
.completion-dial,
.arrival-board {
  border-left: 1px solid rgba(255,255,255,.16);
}
.completion-dial b {
  display: block;
  margin: 12px 0 11px;
  font: 700 34px/1 Consolas, monospace;
}
.completion-dial b small { font-size: 13px; }
.completion-dial i {
  display: block;
  height: 6px;
  background: rgba(255,255,255,.17);
}
.completion-dial em {
  display: block;
  height: 100%;
  background: #5fe0c8;
  transition: width .2s ease;
}
.arrival-board b {
  display: block;
  margin: 14px 0 10px;
  font-family: "STZhongsong", "Songti SC", serif;
  font-size: 16px;
  line-height: 1.45;
}
.arrival-board small {
  padding-left: 8px;
  color: #76e1cf;
  border-left: 3px solid #5fe0c8;
  font-size: 10px;
}
.arrival-board small.delayed { color: #ffd48a; border-color: #e8a840; }
.planning-metrics {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border: 1px solid var(--line);
  background: white;
}
.planning-metrics > div {
  min-height: 80px;
  padding: 15px 19px;
  display: grid;
  align-content: center;
  border-right: 1px solid var(--line);
  box-shadow: inset 0 3px #23827a;
}
.planning-metrics > div:last-child { border-right: 0; }
.planning-metrics > div.alert { box-shadow: inset 0 3px #bd5a4f; }
.planning-metrics span { color: var(--muted); font-size: 9px; }
.planning-metrics b {
  margin-top: 8px;
  color: var(--ink);
  font: 700 21px/1 Consolas, monospace;
}
.planning-metrics small { color: var(--muted); font-size: 9px; }
.next-term-brief {
  min-height: 74px;
  padding: 13px 16px;
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 18px;
  align-items: center;
  border: 1px solid #cfd8e5;
  background: #f7f9fc;
}
.next-term-brief > header span {
  color: var(--teal);
  font: 700 9px/1.2 Consolas, monospace;
  letter-spacing: .1em;
}
.next-term-brief > header h3 { margin: 5px 0 3px; font-size: 14px; }
.next-term-brief > header p,
.no-suggestion { margin: 0; color: var(--muted); font-size: 9px; line-height: 1.6; }
.suggestion-list { display: flex; flex-wrap: wrap; gap: 7px; }
.suggestion-list article {
  min-width: 220px;
  padding: 9px 11px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  border-left: 3px solid #168377;
  background: white;
}
.suggestion-list span { color: var(--teal); font-size: 8px; }
.suggestion-list b { display: block; margin-top: 3px; font-size: 10px; }
.suggestion-list small { color: var(--indigo); font: 700 10px/1.2 Consolas, monospace; }
.suggestion-list p {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--muted);
  font-size: 8px;
}
.official-audit-strip {
  min-height: 58px;
  padding: 10px 16px;
  display: grid;
  grid-template-columns: minmax(220px, .8fr) 1fr auto;
  gap: 22px;
  align-items: center;
  border: 1px solid #cdd6e5;
  background: #f5f8fc;
}
.official-audit-strip div { display: grid; gap: 3px; }
.official-audit-strip span { color: var(--teal); font-size: 9px; font-weight: 700; }
.official-audit-strip b { font-size: 11px; }
.official-audit-strip small,
.official-audit-strip p { margin: 0; color: var(--muted); font-size: 9px; }
.official-audit-strip em {
  padding: 5px 8px;
  color: #52617a;
  border: 1px solid #cdd5e1;
  font-size: 9px;
  font-style: normal;
}
.planning-alerts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.planning-alerts article {
  min-height: 55px;
  padding: 10px 13px;
  display: grid;
  grid-template-columns: 74px 1fr;
  gap: 10px;
  align-items: center;
  border: 1px solid #e2b9b5;
  background: #fff7f6;
}
.planning-alerts article.warning { border-color: #e4cca0; background: #fffaf0; }
.planning-alerts span { color: #a4423b; font-size: 9px; font-weight: 700; }
.planning-alerts .warning span { color: #9b6417; }
.planning-alerts p { margin: 0; color: #5d4b4b; font-size: 10px; line-height: 1.5; }
.planner-workspace {
  display: grid;
  grid-template-columns: minmax(310px, 360px) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}
.course-pool,
.semester-runway {
  border: 1px solid var(--line);
  background: white;
}
.course-pool {
  position: sticky;
  top: 14px;
}
.course-pool > header,
.semester-runway > header {
  min-height: 72px;
  padding: 15px 17px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--line);
  background: #f7f9fc;
}
.course-pool h3,
.semester-runway h3 { margin: 5px 0 0; font-size: 15px; }
.course-pool > header b {
  color: var(--indigo);
  font: 700 25px/1 Consolas, monospace;
}
.pool-filters {
  padding: 11px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  border-bottom: 1px solid var(--line);
}
.pool-list {
  max-height: 680px;
  overflow: auto;
}
.pool-list > article {
  padding: 14px;
  border-bottom: 1px solid #e8ebf0;
  box-shadow: inset 3px 0 #8d97aa;
  transition: background .15s ease, box-shadow .15s ease;
}
.pool-list > article.assigned { background: #f1f8f7; box-shadow: inset 3px 0 #0d8175; }
.pool-list > article.conflicted { background: #fff6f5; box-shadow: inset 3px 0 #b64b43; }
.course-manifest-line {
  display: flex;
  justify-content: space-between;
  gap: 8px;
}
.course-manifest-line span { color: var(--teal); font: 700 9px/1.2 Consolas, monospace; }
.course-manifest-line i { color: var(--muted); font-size: 9px; font-style: normal; }
.pool-list h4 { margin: 7px 0; font-size: 13px; }
.course-flags { display: flex; flex-wrap: wrap; gap: 5px; }
.course-flags span {
  padding: 3px 5px;
  color: #58657a;
  background: #edf0f4;
  font-size: 8px;
}
.course-flags span.failed { color: #a43f3b; background: #faeae8; }
.pool-list article > p { margin: 8px 0; color: var(--muted); font-size: 9px; line-height: 1.5; }
.pool-list article > p b { color: #a4423b; font-weight: 600; }
.pool-list article > p b.ready { color: #0b796f; }
.pool-list .el-select { width: 100%; margin-top: 9px; }
.semester-runway > header p { margin: 5px 0 0; color: var(--muted); font-size: 9px; }
.runway-line {
  position: relative;
  padding: 18px 18px 22px 82px;
}
.runway-line::before {
  position: absolute;
  top: 31px;
  bottom: 64px;
  left: 43px;
  width: 2px;
  content: "";
  background: linear-gradient(#233b77, #0d8275 76%, #d0d6df);
}
.semester-stop {
  position: relative;
  min-height: 116px;
  margin-bottom: 13px;
}
.semester-marker {
  position: absolute;
  top: 15px;
  left: -67px;
  width: 52px;
  display: grid;
  justify-items: center;
  gap: 5px;
}
.semester-marker i {
  width: 31px;
  height: 31px;
  display: grid;
  place-items: center;
  color: white;
  border: 4px solid white;
  outline: 1px solid #263c78;
  background: #263c78;
  font: 700 11px/1 Consolas, monospace;
  font-style: normal;
}
.semester-marker span { color: var(--muted); font-size: 8px; text-align: center; }
.semester-stop.beyond .semester-marker i { outline-color: #a7752a; background: #a7752a; }
.semester-sheet { border: 1px solid #dce1e9; }
.semester-sheet > header {
  min-height: 61px;
  padding: 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #f8f9fb;
  border-bottom: 1px solid #e1e5eb;
}
.semester-sheet header span { color: var(--teal); font-size: 9px; }
.semester-sheet header h4 { margin: 4px 0 0; font-size: 13px; }
.semester-sheet header > b { color: var(--indigo); font: 700 18px/1 Consolas, monospace; }
.semester-sheet header > b small { color: var(--muted); font-size: 8px; }
.scheduled-courses {
  padding: 9px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}
.scheduled-courses article {
  min-height: 58px;
  padding: 9px 8px 9px 11px;
  display: grid;
  grid-template-columns: 1fr auto 25px;
  gap: 8px;
  align-items: center;
  border-left: 3px solid #0c8275;
  background: #f1f8f7;
}
.scheduled-courses article.conflicted { border-color: #b64b43; background: #fff1ef; }
.scheduled-courses span { color: var(--muted); font-size: 8px; }
.scheduled-courses b { display: block; margin-top: 4px; font-size: 10px; }
.scheduled-courses small { color: var(--muted); font-size: 8px; }
.empty-semester { margin: 0; padding: 18px 14px; color: #9199a8; font-size: 9px; }
.graduation-gate {
  min-height: 76px;
  padding: 14px 18px;
  display: grid;
  grid-template-columns: 145px 1fr auto;
  gap: 16px;
  align-items: center;
  color: #5d6676;
  border: 1px dashed #aeb6c3;
  background: #f7f8fa;
}
.graduation-gate.ready {
  color: #075f59;
  border-color: #2b9187;
  background: #edf8f6;
}
.graduation-gate span { font: 700 9px/1.2 Consolas, monospace; letter-spacing: .1em; }
.graduation-gate b { font-size: 13px; }
.graduation-gate p { margin: 0; font-size: 9px; }
.planning-closeout {
  padding: 19px;
  display: grid;
  grid-template-columns: 190px 1fr minmax(260px, .7fr);
  gap: 20px;
  align-items: start;
  border: 1px solid var(--line);
  background: white;
}
.planning-closeout > div:first-child span { color: var(--teal); font-size: 9px; font-weight: 700; }
.planning-closeout h3 { margin: 7px 0 0; font-size: 15px; }
.missing-requirements { display: flex; flex-wrap: wrap; gap: 6px; }
.missing-requirements span {
  padding: 5px 7px;
  color: #705829;
  border: 1px solid #ead6aa;
  background: #fff9ed;
  font-size: 9px;
}
.missing-requirements em { color: #0d766d; font-size: 10px; font-style: normal; }
.planning-closeout ul { margin: 0; padding-left: 16px; color: var(--muted); font-size: 9px; line-height: 1.7; }

@media (max-width: 1050px) {
  .planning-cockpit { grid-template-columns: 1.2fr .8fr; }
  .arrival-board { grid-column: 1 / -1; border-top: 1px solid rgba(255,255,255,.16); border-left: 0; }
  .planner-workspace { grid-template-columns: 300px minmax(0, 1fr); }
  .scheduled-courses { grid-template-columns: 1fr; }
  .planning-closeout { grid-template-columns: 170px 1fr; }
  .planning-closeout ul { grid-column: 1 / -1; }
}

@media (max-width: 760px) {
  .planning-cockpit { grid-template-columns: 1fr; }
  .completion-dial,
  .arrival-board { border-top: 1px solid rgba(255,255,255,.16); border-left: 0; }
  .planning-metrics { grid-template-columns: 1fr 1fr; }
  .planning-metrics > div:nth-child(2) { border-right: 0; }
  .planning-metrics > div:nth-child(-n + 2) { border-bottom: 1px solid var(--line); }
  .official-audit-strip { grid-template-columns: 1fr; gap: 7px; }
  .official-audit-strip em { width: max-content; }
  .planning-alerts { grid-template-columns: 1fr; }
  .next-term-brief { grid-template-columns: 1fr; gap: 8px; }
  .planner-workspace { grid-template-columns: 1fr; }
  .course-pool { position: static; }
  .pool-list { max-height: 460px; }
  .runway-line { padding-left: 64px; }
  .runway-line::before { left: 31px; }
  .semester-marker { left: -55px; }
  .graduation-gate { grid-template-columns: 1fr; gap: 6px; }
  .planning-closeout { grid-template-columns: 1fr; }
  .planning-closeout ul { grid-column: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .completion-dial em,
  .pool-list > article { transition: none; }
}
</style>
