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