<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import {
  Check,
  Clock,
  Close,
  DocumentChecked,
  Plus,
  Refresh,
  Search,
  Switch,
} from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'

interface CourseOption {
  teachingTaskId: string
  courseId: string
  taskNumber: string
  courseCode: string
  courseName: string
  credits: number
  academicTermName: string
  teacherNames: string[]
  hasPublishedSchedule: boolean
  schedulingMode: 'Standard' | 'Flexible'
}

interface GradeOption {
  courseId: string
  courseCode: string
  courseName: string
  credits: number
  totalScore: number | null
  gradePoint: number | null
  examStatus: string
  academicTermName: string
  taskNumber: string
  publishedAt: string | null
}

interface SubstitutionTarget {
  courseId: string
  courseCode: string
  courseName: string
  credits: number
  source: 'assigned' | 'failed'
  detail: string
}

const auth = useAuthStore()
const isManager = computed(() => auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)) ?? false)
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
const isStudent = computed(() => auth.user?.roles.includes('Student') && !isManager.value)

const pending = ref<any[]>([])
const pendingPage = ref(1)
const pendingTotal = ref(0)
const pendingPageSize = 20
const loading = ref(false)
const submitting = ref(false)
const tab = ref(isManager.value ? 'pending' : 'mine')
const dialog = ref(false)
const dialogType = ref('')
const courseKeyword = ref('')
const originalKeyword = ref('')
const substituteKeyword = ref('')
const rejectDialog = ref(false)
const rejectItem = ref<any>(null)
const rejectComment = ref('')

const enrolledCourses = ref<CourseOption[]>([])
const gradeRecords = ref<any[]>([])
const myGrades = ref<GradeOption[]>([])
const myExemptions = ref<any[]>([])
const myDeferred = ref<any[]>([])
const mySubstitutions = ref<any[]>([])

const form = reactive({
  teachingTaskId: '',
  reason: '',
  originalCourseId: '',
  substituteCourseId: '',
  gradeRecordId: '',
  requestedScore: 0,
})

const dialogTitle = computed(() => ({
  exemption: '发起免修申请',
  deferred: '发起缓考申请',
  gradeMod: '成绩修改申请',
  substitution: '发起课程替代申请',
}[dialogType.value] ?? '发起申请'))

const selectedCourse = computed(() =>
  enrolledCourses.value.find(course => course.teachingTaskId === form.teachingTaskId))

const filteredCourses = computed(() => {
  const keyword = courseKeyword.value.trim().toLocaleLowerCase()
  if (!keyword) return enrolledCourses.value
  return enrolledCourses.value.filter(course =>
    [course.courseCode, course.courseName, course.taskNumber, ...course.teacherNames]
      .some(value => value?.toLocaleLowerCase().includes(keyword)))
})

function uniqueGrades(predicate: (grade: GradeOption) => boolean) {
  const result = new Map<string, GradeOption>()
  for (const grade of myGrades.value) {
    if (predicate(grade) && !result.has(grade.courseId)) result.set(grade.courseId, grade)
  }
  return [...result.values()]
}

const failedGrades = computed(() =>
  uniqueGrades(grade => grade.totalScore != null && Number(grade.totalScore) < 60))
const passedGrades = computed(() =>
  uniqueGrades(grade => grade.totalScore != null && Number(grade.totalScore) >= 60))

const substitutionTargets = computed<SubstitutionTarget[]>(() => {
  const result = new Map<string, SubstitutionTarget>()
  for (const course of enrolledCourses.value) {
    result.set(course.courseId, {
      courseId: course.courseId,
      courseCode: course.courseCode,
      courseName: course.courseName,
      credits: course.credits,
      source: 'assigned',
      detail: `${course.academicTermName} · ${course.taskNumber}`,
    })
  }
  for (const grade of failedGrades.value) {
    if (!result.has(grade.courseId)) {
      result.set(grade.courseId, {
        courseId: grade.courseId,
        courseCode: grade.courseCode,
        courseName: grade.courseName,
        credits: grade.credits,
        source: 'failed',
        detail: `${grade.academicTermName} · ${grade.totalScore} 分`,
      })
    }
  }
  return [...result.values()]
})

function matchesCourse(item: { courseCode: string; courseName: string }, keyword: string) {
  const normalized = keyword.trim().toLocaleLowerCase()
  return !normalized ||
    item.courseCode.toLocaleLowerCase().includes(normalized) ||
    item.courseName.toLocaleLowerCase().includes(normalized)
}

function selectOriginal(courseId: string) {
  form.originalCourseId = courseId
  if (form.substituteCourseId === courseId) form.substituteCourseId = ''
}

const filteredOriginalCourses = computed(() =>
  substitutionTargets.value.filter(item => matchesCourse(item, originalKeyword.value)))
const filteredSubstituteCourses = computed(() =>
  passedGrades.value.filter(item => matchesCourse(item, substituteKeyword.value)))

const selectedOriginal = computed(() =>
  substitutionTargets.value.find(item => item.courseId === form.originalCourseId))
const selectedSubstitute = computed(() =>
  passedGrades.value.find(item => item.courseId === form.substituteCourseId))

const recordCount = computed(() =>
  myExemptions.value.length + myDeferred.value.length + mySubstitutions.value.length)

async function load() {
  loading.value = true
  try {
    if (isManager.value) {
      const { data } = await http.get('/approvals/pending', {
        params: { page: pendingPage.value, pageSize: pendingPageSize },
      })
      pending.value = data.items
      pendingTotal.value = data.total
      if (!pending.value.length && pendingPage.value > 1) {
        pendingPage.value--
        return await load()
      }
    }
    if (isStudent.value) {
      const [courses, grades, ex, df, substitutions] = await Promise.all([
        http.get('/approvals/my-courses'),
        http.get('/approvals/my-grades'),
        http.get('/approvals/exemptions/mine'),
        http.get('/approvals/deferred/mine'),
        http.get('/approvals/substitutions/mine'),
      ])
      enrolledCourses.value = courses.data
      myGrades.value = grades.data
      myExemptions.value = ex.data
      myDeferred.value = df.data
      mySubstitutions.value = substitutions.data
    }
    if (isTeacher.value) gradeRecords.value = (await http.get('/approvals/grade-records')).data
  } catch (e) {
    ElMessage.error(apiErrorMessage(e))
  } finally {
    loading.value = false
  }
}

function openDialog(type: string) {
  dialogType.value = type
  courseKeyword.value = ''
  originalKeyword.value = ''
  substituteKeyword.value = ''
  Object.assign(form, {
    teachingTaskId: '',
    reason: '',
    originalCourseId: '',
    substituteCourseId: '',
    gradeRecordId: '',
    requestedScore: 0,
  })
  dialog.value = true
}

async function submitForm() {
  if (!form.reason.trim()) {
    ElMessage.warning('请填写申请原因')
    return
  }
  submitting.value = true
  try {
    if (dialogType.value === 'exemption') {
      if (!form.teachingTaskId) {
        ElMessage.warning('请选择申请免修的课程')
        return
      }
      await http.post('/approvals/exemptions', {
        teachingTaskId: form.teachingTaskId,
        reason: form.reason,
      })
    } else if (dialogType.value === 'deferred') {
      if (!form.teachingTaskId) {
        ElMessage.warning('请选择申请缓考的课程')
        return
      }
      await http.post('/approvals/deferred', {
        teachingTaskId: form.teachingTaskId,
        reason: form.reason,
      })
    } else if (dialogType.value === 'gradeMod') {
      if (!form.gradeRecordId) {
        ElMessage.warning('请选择成绩记录')
        return
      }
      await http.post('/approvals/grade-modifications', {
        gradeRecordId: form.gradeRecordId,
        requestedScore: form.requestedScore,
        reason: form.reason,
      })
    } else if (dialogType.value === 'substitution') {
      if (!form.originalCourseId || !form.substituteCourseId) {
        ElMessage.warning('请完整选择被替代课程和替代课程')
        return
      }
      await http.post('/approvals/substitutions', {
        originalCourseId: form.originalCourseId,
        substituteCourseId: form.substituteCourseId,
        reason: form.reason,
      })
    }
    dialog.value = false
    ElMessage.success('申请已提交')
    await load()
  } catch (e) {
    ElMessage.error(apiErrorMessage(e))
  } finally {
    submitting.value = false
  }
}

async function approveItem(item: any) {
  try {
    const ep = `/approvals/${item.type === 'CourseExemption' ? 'exemptions' : item.type === 'DeferredExam' ? 'deferred' : item.type === 'CourseSubstitution' ? 'substitutions' : ''}`
    if (ep) {
      await http.post(`${ep}/${item.id}/approve`)
      ElMessage.success('已通过')
      await load()
    }
  } catch (e) {
    ElMessage.error(apiErrorMessage(e))
  }
}

function openReject(item: any) {
  rejectItem.value = item
  rejectComment.value = ''
  rejectDialog.value = true
}

async function rejectSubmit() {
  if (!rejectItem.value) return
  try {
    const ep = `/approvals/${rejectItem.value.type === 'CourseExemption' ? 'exemptions' : rejectItem.value.type === 'DeferredExam' ? 'deferred' : rejectItem.value.type === 'CourseSubstitution' ? 'substitutions' : ''}`
    if (ep) {
      await http.post(`${ep}/${rejectItem.value.id}/reject`, { comment: rejectComment.value })
      rejectDialog.value = false
      ElMessage.success('已驳回')
      await load()
    }
  } catch (e) {
    ElMessage.error(apiErrorMessage(e))
  }
}

function statusText(status: string) {
  return status === 'Submitted' ? '待审核' : status === 'Approved' ? '已通过' : '已驳回'
}

function statusType(status: string) {
  return status === 'Approved' ? 'success' : status === 'Rejected' ? 'danger' : 'warning'
}

function courseScheduleText(course: CourseOption) {
  if (course.schedulingMode === 'Flexible') return '非排时课程'
  return course.hasPublishedSchedule ? '课表已发布' : '等待课表发布'
}

onMounted(load)
</script>

<template>
  <div class="page-stack appr-page">
    <section class="page-intro">
      <div>
        <span class="section-kicker">APPROVAL CENTER</span>
        <h2>审批中心</h2>
        <p>{{ isStudent ? '从实际修读课程发起申请，查看审核进度与处理结果。' : '集中处理教务申请与审核任务。' }}</p>
      </div>
      <el-button :icon="Refresh" @click="load">刷新</el-button>
    </section>

    <section v-if="isStudent" v-loading="loading" class="application-launcher">
      <button class="launch-card exemption" type="button" @click="openDialog('exemption')">
        <span class="launch-icon"><DocumentChecked /></span>
        <span class="launch-copy">
          <small>COURSE EXEMPTION</small>
          <b>申请免修</b>
          <em>从当前或已发布课表的修读课程中选择</em>
        </span>
        <span class="launch-meta">{{ enrolledCourses.length }} 门可选 <Plus /></span>
      </button>
      <button class="launch-card deferred" type="button" @click="openDialog('deferred')">
        <span class="launch-icon"><Clock /></span>
        <span class="launch-copy">
          <small>DEFERRED EXAM</small>
          <b>申请缓考</b>
          <em>课程与教学班信息一并提交</em>
        </span>
        <span class="launch-meta">{{ enrolledCourses.length }} 门可选 <Plus /></span>
      </button>
      <button class="launch-card substitution" type="button" @click="openDialog('substitution')">
        <span class="launch-icon"><Switch /></span>
        <span class="launch-copy">
          <small>COURSE SUBSTITUTION</small>
          <b>申请课程替代</b>
          <em>修读中或未通过课程 → 已通过课程</em>
        </span>
        <span class="launch-meta">{{ passedGrades.length }} 门可替代 <Plus /></span>
      </button>
    </section>

    <div class="approval-tabs">
      <el-segmented
        v-model="tab"
        :options="[
          ...(isManager ? [{ label: `待审批 (${pendingTotal})`, value: 'pending' }] : []),
          { label: `我的记录${isStudent ? ` (${recordCount})` : ''}`, value: 'mine' },
        ]"
      />
      <el-button v-if="isTeacher" :icon="Plus" type="primary" @click="openDialog('gradeMod')">成绩修改</el-button>
    </div>

    <section v-if="tab === 'pending'" v-loading="loading" class="appr-list">
      <article v-for="item in pending" :key="`${item.type}-${item.id}`" class="appr-card">
        <el-tag size="small" :type="item.label === '考勤申诉' ? 'warning' : 'primary'">{{ item.label }}</el-tag>
        <div class="appr-body">
          <b>{{ item.title }}</b>
          <p>{{ item.desc }}</p>
          <small>{{ item.college }} · {{ new Date(item.time).toLocaleString('zh-CN') }}</small>
        </div>
        <div v-if="['CourseExemption','DeferredExam','CourseSubstitution'].includes(item.type)" class="appr-actions">
          <el-button size="small" type="danger" plain :icon="Close" @click="openReject(item)">驳回</el-button>
          <el-button size="small" type="success" :icon="Check" @click="approveItem(item)">通过</el-button>
        </div>
        <div v-else class="appr-actions">
          <el-button
            size="small"
            text
            type="primary"
            @click="$router.push(item.type==='StudentStatusChange'?'/student-status-changes':item.type==='CourseAdjustment'?'/course-adjustments':item.type==='GradeSheet'?'/grades':'/teacher-attendance')"
          >
            查看 →
          </el-button>
        </div>
      </article>
      <el-empty v-if="!pending.length" description="暂无待审批" />
      <el-pagination
        v-if="pendingTotal > pendingPageSize"
        v-model:current-page="pendingPage"
        class="approval-pagination"
        layout="prev, pager, next"
        :page-size="pendingPageSize"
        :total="pendingTotal"
        @current-change="() => load()"
      />
    </section>

    <section v-if="tab === 'mine'" v-loading="loading">
      <template v-if="isStudent">
        <div v-if="recordCount" class="record-columns">
          <section class="record-group">
            <header><span class="record-mark exemption"></span><b>免修申请</b><em>{{ myExemptions.length }}</em></header>
            <div class="appr-list">
              <article v-for="item in myExemptions" :key="item.id" class="appr-card compact">
                <el-tag size="small" :type="statusType(item.status)">{{ statusText(item.status) }}</el-tag>
                <div class="appr-body">
                  <b>{{ item.courseName }}</b>
                  <p>{{ item.reason }}</p>
                  <small v-if="item.reviewComment">审核意见：{{ item.reviewComment }}</small>
                </div>
              </article>
              <p v-if="!myExemptions.length" class="record-empty">尚未提交免修申请</p>
            </div>
          </section>
          <section class="record-group">
            <header><span class="record-mark deferred"></span><b>缓考申请</b><em>{{ myDeferred.length }}</em></header>
            <div class="appr-list">
              <article v-for="item in myDeferred" :key="item.id" class="appr-card compact">
                <el-tag size="small" :type="statusType(item.status)">{{ statusText(item.status) }}</el-tag>
                <div class="appr-body">
                  <b>{{ item.courseName }}</b>
                  <p>{{ item.reason }}</p>
                  <small v-if="item.reviewComment">审核意见：{{ item.reviewComment }}</small>
                </div>
              </article>
              <p v-if="!myDeferred.length" class="record-empty">尚未提交缓考申请</p>
            </div>
          </section>
          <section class="record-group substitution-records">
            <header><span class="record-mark substitution"></span><b>课程替代申请</b><em>{{ mySubstitutions.length }}</em></header>
            <div class="appr-list">
              <article v-for="item in mySubstitutions" :key="item.id" class="appr-card compact">
                <el-tag size="small" :type="statusType(item.status)">{{ statusText(item.status) }}</el-tag>
                <div class="appr-body">
                  <b>{{ item.substituteCourseCode }} {{ item.substituteCourseName }} → {{ item.originalCourseCode }} {{ item.originalCourseName }}</b>
                  <p>{{ item.reason }}</p>
                  <small v-if="item.reviewComment">审核意见：{{ item.reviewComment }}</small>
                </div>
              </article>
              <p v-if="!mySubstitutions.length" class="record-empty">尚未提交课程替代申请</p>
            </div>
          </section>
        </div>
        <el-empty v-else description="还没有申请记录">
          <el-button type="primary" @click="openDialog('exemption')">发起第一项申请</el-button>
        </el-empty>
      </template>
    </section>

    <el-dialog v-model="dialog" :title="dialogTitle" width="min(780px, calc(100vw - 24px))" class="application-dialog" destroy-on-close>
      <el-form label-position="top">
        <template v-if="dialogType === 'exemption' || dialogType === 'deferred'">
          <div class="dialog-lead">
            <span :class="['dialog-lead-icon', dialogType]">
              <DocumentChecked v-if="dialogType === 'exemption'" />
              <Clock v-else />
            </span>
            <div>
              <b>{{ dialogType === 'exemption' ? '选择申请免修的教学班' : '选择申请缓考的教学班' }}</b>
              <p>列表包含当前学期课程，以及仍未归档且课表已发布的行政班或已选课程。</p>
            </div>
          </div>
          <el-form-item label="课程" required>
            <el-input v-model="courseKeyword" :prefix-icon="Search" clearable placeholder="搜索课程名称、代码、教学班号或教师" />
            <div v-if="filteredCourses.length" class="course-picker">
              <button
                v-for="course in filteredCourses"
                :key="course.teachingTaskId"
                type="button"
                :class="['course-choice', { selected: form.teachingTaskId === course.teachingTaskId }]"
                @click="form.teachingTaskId = course.teachingTaskId"
              >
                <span class="course-code">{{ course.courseCode }}</span>
                <span class="course-main">
                  <b>{{ course.courseName }}</b>
                  <small>{{ course.taskNumber }} · {{ course.academicTermName }}</small>
                  <em>{{ course.teacherNames.length ? course.teacherNames.join('、') : '教师待定' }} · {{ course.credits }} 学分</em>
                </span>
                <span :class="['schedule-state', { ready: course.hasPublishedSchedule || course.schedulingMode === 'Flexible' }]">
                  {{ courseScheduleText(course) }}
                </span>
                <span class="choice-check"><Check /></span>
              </button>
            </div>
            <el-empty
              v-else
              :description="enrolledCourses.length ? '没有匹配的课程' : '暂无可申请课程'"
              :image-size="76"
            >
              <p v-if="!enrolledCourses.length" class="empty-explanation">
                课程需为已发布教学任务，并分配到您的行政班或已完成选课；当前学期课程或已发布课表的课程均会显示。
              </p>
            </el-empty>
          </el-form-item>
        </template>

        <template v-if="dialogType === 'substitution'">
          <el-alert
            title="替代关系须同时满足两个条件"
            description="被替代课程应为当前或已发布课表的修读课程，或已有未通过成绩的课程；替代课程必须已有正式发布且不低于 60 分的成绩。"
            type="info"
            :closable="false"
            show-icon
          />
          <div class="substitution-flow">
            <section class="substitution-column">
              <header><span>1</span><div><b>被替代课程</b><small>修读课程 / 历史未通过</small></div></header>
              <el-input v-model="originalKeyword" :prefix-icon="Search" clearable placeholder="搜索课程" />
              <div v-if="filteredOriginalCourses.length" class="mini-course-list">
                <button
                  v-for="course in filteredOriginalCourses"
                  :key="course.courseId"
                  type="button"
                  :class="{ selected: form.originalCourseId === course.courseId }"
                  @click="selectOriginal(course.courseId)"
                >
                  <span><b>{{ course.courseCode }} · {{ course.courseName }}</b><small>{{ course.detail }} · {{ course.credits }} 学分</small></span>
                  <el-tag size="small" :type="course.source === 'assigned' ? 'primary' : 'danger'">
                    {{ course.source === 'assigned' ? '修读' : '未通过' }}
                  </el-tag>
                </button>
              </div>
              <el-empty v-else :image-size="62" description="暂无可被替代课程" />
            </section>
            <div class="flow-arrow">→</div>
            <section class="substitution-column">
              <header><span>2</span><div><b>替代课程</b><small>已发布且成绩及格</small></div></header>
              <el-input v-model="substituteKeyword" :prefix-icon="Search" clearable placeholder="搜索课程" />
              <div v-if="filteredSubstituteCourses.length" class="mini-course-list">
                <button
                  v-for="course in filteredSubstituteCourses"
                  :key="course.courseId"
                  type="button"
                  :disabled="course.courseId === form.originalCourseId"
                  :class="{ selected: form.substituteCourseId === course.courseId }"
                  @click="form.substituteCourseId = course.courseId"
                >
                  <span><b>{{ course.courseCode }} · {{ course.courseName }}</b><small>{{ course.academicTermName }} · {{ course.credits }} 学分</small></span>
                  <strong>{{ course.totalScore }}<small>分</small></strong>
                </button>
              </div>
              <el-empty v-else :image-size="62" description="暂无已通过课程">
                <p class="empty-explanation">已排课但尚未发布成绩的课程不属于可替代课程。</p>
              </el-empty>
            </section>
          </div>
        </template>

        <template v-if="dialogType === 'gradeMod'">
          <el-form-item label="成绩记录" required>
            <el-select v-model="form.gradeRecordId" filterable placeholder="选择已发布成绩">
              <el-option
                v-for="record in gradeRecords"
                :key="record.id"
                :label="`${record.studentName}(${record.studentNumber}) — ${record.courseName} — ${record.totalScore}分`"
                :value="record.id"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="修改为" required>
            <el-input-number v-model="form.requestedScore" :min="0" :max="100" />
          </el-form-item>
        </template>

        <el-form-item class="reason-field" label="申请原因" required>
          <el-input
            v-model="form.reason"
            type="textarea"
            :rows="4"
            maxlength="500"
            show-word-limit
            :placeholder="dialogType === 'deferred' ? '请说明无法按时参加考试的原因及相关情况' : dialogType === 'substitution' ? '请说明两门课程在内容、学分或培养要求上的对应关系' : '请说明申请依据及相关情况'"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <span v-if="selectedCourse">{{ selectedCourse.courseCode }} · {{ selectedCourse.courseName }}</span>
          <span v-else-if="selectedOriginal || selectedSubstitute">
            {{ selectedSubstitute?.courseName ?? '请选择替代课程' }} → {{ selectedOriginal?.courseName ?? '请选择被替代课程' }}
          </span>
          <el-button @click="dialog = false">取消</el-button>
          <el-button type="primary" :loading="submitting" @click="submitForm">提交申请</el-button>
        </div>
      </template>
    </el-dialog>

    <el-dialog v-model="rejectDialog" title="驳回申请" width="min(460px, calc(100vw - 24px))">
      <el-input v-model="rejectComment" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写驳回原因" />
      <template #footer>
        <el-button @click="rejectDialog = false">取消</el-button>
        <el-button type="danger" @click="rejectSubmit">确认驳回</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped>
.application-launcher {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.launch-card {
  position: relative;
  min-height: 132px;
  padding: 20px;
  display: flex;
  align-items: flex-start;
  gap: 14px;
  overflow: hidden;
  text-align: left;
  color: #1e293b;
  background: linear-gradient(145deg, #fff 48%, #f6f8fb);
  border: 1px solid #dfe4eb;
  border-radius: 14px;
  cursor: pointer;
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}

.launch-card::after {
  content: "";
  position: absolute;
  right: -26px;
  bottom: -50px;
  width: 120px;
  height: 120px;
  border: 18px solid rgba(67, 97, 238, .05);
  border-radius: 50%;
}

.launch-card:hover {
  transform: translateY(-2px);
  border-color: #aebbd0;
  box-shadow: 0 12px 28px rgba(30, 41, 59, .08);
}

.launch-icon, .dialog-lead-icon {
  width: 42px;
  height: 42px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  color: #3556b5;
  background: #edf2ff;
  border-radius: 12px;
}

.launch-icon svg, .dialog-lead-icon svg { width: 21px; }
.deferred .launch-icon, .dialog-lead-icon.deferred { color: #a85d00; background: #fff3dc; }
.substitution .launch-icon { color: #087f5b; background: #e8f8f1; }
.launch-copy { display: grid; gap: 3px; min-width: 0; }
.launch-copy small { color: #8791a3; font: 600 9px/1.2 ui-monospace, monospace; letter-spacing: .1em; }
.launch-copy b { margin-top: 3px; font-size: 17px; }
.launch-copy em { color: #667085; font-size: 11px; font-style: normal; line-height: 1.5; }
.launch-meta { position: absolute; left: 76px; bottom: 17px; display: flex; align-items: center; gap: 5px; color: #506079; font-size: 10px; }
.launch-meta svg { width: 12px; }

.approval-tabs { display: flex; justify-content: space-between; gap: 12px; }
.appr-list { display: grid; gap: 8px; }
.appr-card { display: flex; align-items: center; gap: 14px; padding: 14px 18px; background: #fff; border: 1px solid #e4e7ed; border-radius: 10px; }
.appr-card.compact { align-items: flex-start; padding: 13px; }
.appr-body { flex: 1; min-width: 0; }
.appr-body b { display: block; margin-bottom: 4px; font-size: 13px; }
.appr-body p { margin: 0 0 4px; color: #606266; font-size: 12px; line-height: 1.55; }
.appr-body small { color: var(--muted); font-size: 10px; }
.appr-actions { display: flex; flex-shrink: 0; gap: 6px; }

.record-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
.record-group { padding: 16px; background: #f8fafc; border: 1px solid #e5e9f0; border-radius: 12px; }
.record-group.substitution-records { grid-column: 1 / -1; }
.record-group > header { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.record-group > header b { font-size: 13px; }
.record-group > header em { margin-left: auto; min-width: 22px; padding: 3px 7px; color: #667085; background: #fff; border-radius: 10px; font-size: 10px; font-style: normal; text-align: center; }
.record-mark { width: 8px; height: 8px; background: #4b6fd8; border-radius: 50%; box-shadow: 0 0 0 4px #e8edfb; }
.record-mark.deferred { background: #d5841c; box-shadow: 0 0 0 4px #fff0d8; }
.record-mark.substitution { background: #13916a; box-shadow: 0 0 0 4px #dff5ed; }
.record-empty { margin: 20px 0; color: #98a2b3; font-size: 11px; text-align: center; }

.dialog-lead { margin-bottom: 18px; padding: 14px; display: flex; align-items: center; gap: 12px; background: #f7f9fc; border: 1px solid #e6eaf1; border-radius: 10px; }
.dialog-lead b { display: block; margin-bottom: 3px; font-size: 13px; }
.dialog-lead p { margin: 0; color: #667085; font-size: 11px; }
.course-picker { width: 100%; max-height: 330px; margin-top: 10px; display: grid; gap: 8px; overflow: auto; }
.course-choice { width: 100%; padding: 13px; display: grid; grid-template-columns: 68px minmax(0, 1fr) auto 20px; align-items: center; gap: 12px; text-align: left; background: #fff; border: 1px solid #e1e6ee; border-radius: 10px; cursor: pointer; transition: border-color .15s, background .15s; }
.course-choice:hover { border-color: #9aabd4; }
.course-choice.selected { background: #f4f7ff; border-color: #4b6fd8; box-shadow: inset 3px 0 #4b6fd8; }
.course-code { color: #3651a3; font: 700 11px/1.3 ui-monospace, monospace; }
.course-main { display: grid; min-width: 0; gap: 3px; }
.course-main b { color: #1e293b; font-size: 13px; }
.course-main small, .course-main em { overflow: hidden; color: #7b8495; font-size: 10px; font-style: normal; text-overflow: ellipsis; white-space: nowrap; }
.schedule-state { padding: 4px 7px; color: #8a6116; background: #fff4d9; border-radius: 6px; font-size: 9px; }
.schedule-state.ready { color: #087f5b; background: #e5f7ef; }
.choice-check { display: grid; place-items: center; width: 18px; height: 18px; color: transparent; border: 1px solid #cfd5df; border-radius: 50%; }
.choice-check svg { width: 11px; }
.course-choice.selected .choice-check { color: #fff; background: #4b6fd8; border-color: #4b6fd8; }
.empty-explanation { max-width: 450px; margin: -10px auto 0; color: #8992a3; font-size: 10px; line-height: 1.6; }

.substitution-flow { margin-top: 16px; display: grid; grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr); align-items: stretch; gap: 10px; }
.substitution-column { min-width: 0; padding: 14px; background: #f8fafc; border: 1px solid #e5e9f0; border-radius: 12px; }
.substitution-column > header { margin-bottom: 12px; display: flex; align-items: center; gap: 9px; }
.substitution-column > header > span { width: 25px; height: 25px; display: grid; place-items: center; color: #fff; background: #405fb7; border-radius: 8px; font: 700 11px/1 ui-monospace, monospace; }
.substitution-column > header div { display: grid; gap: 2px; }
.substitution-column > header b { font-size: 12px; }
.substitution-column > header small { color: #8a94a5; font-size: 9px; }
.flow-arrow { display: grid; place-items: center; color: #7d8fbf; font-size: 18px; }
.mini-course-list { max-height: 255px; margin-top: 9px; display: grid; align-content: start; gap: 6px; overflow: auto; }
.mini-course-list button { padding: 10px; display: flex; align-items: center; gap: 8px; text-align: left; background: #fff; border: 1px solid #e2e7ee; border-radius: 8px; cursor: pointer; }
.mini-course-list button:hover { border-color: #a9b5d3; }
.mini-course-list button.selected { background: #f0f4ff; border-color: #4b6fd8; box-shadow: inset 3px 0 #4b6fd8; }
.mini-course-list button:disabled { opacity: .45; cursor: not-allowed; }
.mini-course-list button > span { min-width: 0; display: grid; flex: 1; gap: 3px; }
.mini-course-list button b { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.mini-course-list button span small { color: #8a94a5; font-size: 9px; }
.mini-course-list button > strong { color: #087f5b; font-size: 16px; }
.mini-course-list button > strong small { margin-left: 1px; font-size: 8px; font-weight: 500; }
.reason-field { margin-top: 18px; }
.dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.dialog-footer > span { margin-right: auto; overflow: hidden; color: #667085; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 860px) {
  .application-launcher { grid-template-columns: 1fr; }
  .launch-card { min-height: 112px; }
  .record-columns { grid-template-columns: 1fr; }
  .record-group.substitution-records { grid-column: auto; }
  .substitution-flow { grid-template-columns: 1fr; }
  .flow-arrow { transform: rotate(90deg); }
}

@media (max-width: 600px) {
  .page-intro { align-items: flex-start; }
  .approval-tabs { align-items: flex-start; }
  .course-choice { grid-template-columns: 58px minmax(0, 1fr) 18px; }
  .schedule-state { grid-column: 2; justify-self: start; }
  .appr-card { align-items: flex-start; flex-wrap: wrap; }
  .appr-actions { width: 100%; justify-content: flex-end; }
}
</style>
