Revisione/app/pages/course/[id].vue

830 lines
28 KiB
Vue

<script setup lang="ts">
const route = useRoute();
const courseId = route.params.id as string;
const { data: course, error, refresh } = await useAsyncData(
`course-${courseId}`,
() => $fetch<any>(`/api/courses/${courseId}`)
);
let pollTimer: ReturnType<typeof setInterval> | null = null;
function hasUnreadyTopics(): boolean {
return course.value?.topics?.some((t: any) => !t.hasLesson) ?? false;
}
onMounted(() => {
if (course.value?.status === "processing" || hasUnreadyTopics()) startPolling();
});
function startPolling() {
if (pollTimer) return;
pollTimer = setInterval(async () => {
await refresh();
if (course.value?.status !== "processing" && !hasUnreadyTopics()) stopPolling();
}, 3000);
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
onUnmounted(stopPolling);
type StageKey = "parsing_pdfs" | "analysing_sources" | "building_curriculum" | "finalising";
const STEPS: { key: StageKey; title: string; subtitle: string }[] = [
{ key: "parsing_pdfs", title: "Reading your documents", subtitle: "Extracting content from uploaded PDFs" },
{ key: "analysing_sources", title: "Identifying key topics", subtitle: "Scanning past papers & lab worksheets" },
{ key: "building_curriculum", title: "Building your curriculum", subtitle: "Ordering topics from fundamentals up" },
{ key: "finalising", title: "Putting it all together", subtitle: "Saving your personalised course" },
];
const STAGE_ORDER: Record<string, number> = {
parsing_pdfs: 0,
analysing_sources: 1,
building_curriculum: 2,
finalising: 3,
ready: 4,
error: 4,
};
function stageIndex(stage: string | null | undefined): number {
return STAGE_ORDER[stage ?? "parsing_pdfs"] ?? 0;
}
function stepState(i: number, s: string | null | undefined): "done" | "active" | "pending" {
const cur = stageIndex(s);
if (i < cur) return "done";
if (i === cur) return "active";
return "pending";
}
const TIPS = [
"Topics from past papers are weighted more heavily",
"We build from the ground up — starting with the fundamentals",
"Lab worksheets inform the practical skills you'll be tested on",
"Your course is tailored to your exact exam style",
"The more past papers you upload, the better the course",
];
const tipIndex = ref(0);
const tipVisible = ref(true);
let tipTimer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
tipTimer = setInterval(() => {
tipVisible.value = false;
setTimeout(() => { tipIndex.value = (tipIndex.value + 1) % TIPS.length; tipVisible.value = true; }, 350);
}, 4000);
});
onUnmounted(() => { if (tipTimer) clearInterval(tipTimer); });
function completedCount(): number {
return course.value?.topics?.filter((t: any) => t.progress?.lessonComplete).length ?? 0;
}
const auditReport = computed(() => {
if (!course.value?.auditReport) return null;
try { return JSON.parse(course.value.auditReport); } catch { return null; }
});
const auditHidden = ref(false);
const lessonQualityExpanded = ref(false);
if (import.meta.client) {
auditHidden.value = localStorage.getItem("revisione-audit-hidden") === "true";
}
function toggleAudit() {
auditHidden.value = !auditHidden.value;
if (import.meta.client) localStorage.setItem("revisione-audit-hidden", String(auditHidden.value));
}
function scoreColor(score: number): string {
if (score >= 80) return "var(--green)";
if (score >= 50) return "oklch(65% 0.14 65)";
return "oklch(52% 0.14 15)";
}
const editingTitle = ref(false);
const titleDraft = ref("");
function startEditTitle() {
titleDraft.value = course.value?.title ?? "";
editingTitle.value = true;
nextTick(() => (document.querySelector(".title-input") as HTMLInputElement)?.focus());
}
async function saveTitle() {
editingTitle.value = false;
const val = titleDraft.value.trim();
if (!val || val === course.value?.title) return;
await $fetch(`/api/courses/${courseId}`, {
method: "PATCH",
body: { title: val },
});
if (course.value) course.value.title = val;
}
</script>
<template>
<main class="course-page">
<!-- header -->
<header class="course-header">
<div class="header-inner">
<NuxtLink to="/" class="back-link">
<svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 19l-7-7 7-7"/>
</svg>
Home
</NuxtLink>
<span class="header-logo">Revisi<span style="color: var(--accent);">.one</span></span>
</div>
</header>
<div class="page-body">
<!-- dev audit panel -->
<div v-if="auditReport && !auditHidden" class="audit-panel">
<div class="audit-header">
<span class="audit-badge">DEV AUDIT</span>
<div class="audit-score" :style="{ color: scoreColor(auditReport.overallScore) }">
{{ auditReport.overallScore }}<span class="audit-score-denom"> / 100</span>
</div>
<button class="audit-hide-btn" @click="toggleAudit">Hide audit</button>
</div>
<div class="audit-standard-banner" :class="auditReport.passesStandard ? 'audit-standard-banner--pass' : 'audit-standard-banner--fail'">
{{ auditReport.passesStandard ? 'EXAM READY ✓' : 'NOT EXAM READY ✗' }}
</div>
<p class="audit-readiness">{{ auditReport.examReadiness }}</p>
<div class="audit-coverage">
Coverage: <strong>{{ auditReport.coverageAnalysis?.coveredTopics }}</strong> of
<strong>{{ auditReport.coverageAnalysis?.totalExaminedTopics }}</strong> examined topics
({{ auditReport.coverageAnalysis?.coveragePercent }}%)
</div>
<div v-if="auditReport.unansweredPaperQuestions?.length" class="audit-section">
<p class="audit-section-title">Unanswered Past Paper Questions</p>
<table class="audit-table">
<thead><tr><th>Source</th><th>Question</th><th>What's missing</th></tr></thead>
<tbody>
<tr v-for="(q, i) in auditReport.unansweredPaperQuestions" :key="i">
<td class="unanswered-source">{{ q.source }}</td>
<td>{{ q.question }}</td>
<td class="unanswered-gap">{{ q.gap }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="auditReport.gaps?.length" class="audit-section">
<p class="audit-section-title">Gaps</p>
<table class="audit-table">
<thead><tr><th>Topic</th><th>Severity</th><th>Appears in</th><th>Course coverage</th></tr></thead>
<tbody>
<tr v-for="(gap, i) in auditReport.gaps" :key="i">
<td>{{ gap.topic }}</td>
<td><span class="severity-badge" :class="`severity-badge--${gap.severity}`">{{ gap.severity }}</span></td>
<td>{{ gap.appearsInSources }}</td>
<td>{{ gap.courseCoverage }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="auditReport.recommendations?.length" class="audit-section">
<p class="audit-section-title">Recommendations</p>
<ol class="audit-recs">
<li v-for="(rec, i) in auditReport.recommendations" :key="i">{{ rec }}</li>
</ol>
</div>
<div v-if="auditReport.lessonQuality?.length" class="audit-section">
<button class="audit-collapse-btn" @click="lessonQualityExpanded = !lessonQualityExpanded">
Lesson Quality
<span class="collapse-arrow" :class="{ 'collapse-arrow--open': lessonQualityExpanded }"></span>
</button>
<div v-if="lessonQualityExpanded" class="lesson-quality-list">
<div v-for="(lq, i) in auditReport.lessonQuality" :key="i" class="lq-row">
<div class="lq-title">{{ lq.topicTitle }}</div>
<div class="lq-score" :style="{ color: scoreColor(lq.score) }">{{ lq.score }}/100</div>
<div class="lq-notes">{{ lq.notes }}</div>
</div>
</div>
</div>
</div>
<div v-if="auditReport && auditHidden" class="audit-show-row">
<button class="audit-show-btn" @click="toggleAudit">Show audit report</button>
</div>
<!-- error -->
<div v-if="error || course?.status === 'error'" class="error-view">
<div class="error-card">
<p class="error-label">Error</p>
<p class="error-sub">We couldn't generate your course. Please try again.</p>
<NuxtLink to="/" class="error-back">← Start over</NuxtLink>
</div>
</div>
<template v-else-if="course">
<!-- processing stepper (shown while generating) -->
<div v-if="course.status === 'processing'" class="processing-view">
<div class="processing-header">
<p class="processing-label">Generating</p>
<h2 class="processing-title">{{ course.title }}</h2>
</div>
<div class="stepper">
<div
v-for="(step, i) in STEPS"
:key="step.key"
class="stepper-item"
:class="`stepper-item--${stepState(i, course.stage)}`"
>
<div class="stepper-node">
<div v-if="stepState(i, course.stage) === 'done'" class="node node--done">
<svg width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div v-else-if="stepState(i, course.stage) === 'active'" class="node node--active">
<div class="node-pulse" />
<div class="node-dot" />
</div>
<div v-else class="node node--pending">
<div class="node-dot-sm" />
</div>
</div>
<div v-if="i < STEPS.length - 1" class="stepper-line"
:class="stepState(i, course.stage) === 'done' ? 'stepper-line--done' : 'stepper-line--pending'" />
<div class="stepper-text">
<p class="stepper-title">{{ step.title }}</p>
<p class="stepper-sub">{{ step.subtitle }}</p>
</div>
</div>
</div>
<div class="tip-area">
<Transition name="tip">
<p v-if="tipVisible" :key="tipIndex" class="tip-text">"{{ TIPS[tipIndex] }}"</p>
</Transition>
</div>
</div>
<!-- course view (always shown once we have topics, or when ready) -->
<div v-if="course.status === 'ready' || course.topics?.length" class="course-view">
<div class="course-meta">
<div class="course-subject">{{ course.subject }}</div>
<div class="title-edit-wrap" @click="!editingTitle && startEditTitle()">
<input
v-if="editingTitle"
v-model="titleDraft"
class="title-input course-title"
@blur="saveTitle"
@keydown.enter="saveTitle"
@keydown.escape="editingTitle = false"
/>
<h1 v-else class="course-title">
{{ course.title }}
<svg class="title-pencil" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
</svg>
</h1>
</div>
<!-- org chip -->
<div v-if="course.organisation" class="org-chips" style="margin-bottom: 16px;">
<span class="org-chip">{{ course.organisation }}</span>
</div>
<div class="course-stats">
<span class="stat">
<span class="stat-num">{{ course.topics?.length ?? 0 }}</span>
topics
</span>
<span class="stat-divider">·</span>
<span class="stat">
<span class="stat-num stat-num--green">{{ completedCount() }}</span>
completed
</span>
</div>
<div class="progress-track" v-if="course.topics?.length">
<div
class="progress-fill"
:style="{ width: `${(completedCount() / course.topics.length) * 100}%` }"
/>
</div>
</div>
<!-- topic list -->
<div class="topic-list">
<TransitionGroup name="topic-appear" tag="div" class="topic-list-inner">
<template v-for="(topic, idx) in course.topics" :key="topic.id">
<NuxtLink
v-if="topic.hasLesson"
:to="`/learn/${topic.id}`"
class="topic-row topic-row--unlocked"
:class="topic.progress?.lessonComplete ? 'topic-row--complete' : 'topic-row--available'"
>
<span class="topic-index">{{ String(idx + 1).padStart(2, "0") }}</span>
<div class="topic-info">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span class="topic-name">{{ topic.title }}</span>
<span v-if="topic.progress?.lessonComplete" class="done-badge">done</span>
<span
v-if="topic.progress?.lessonComplete && topic.progress?.tookBranches"
class="branch-dot"
title="You needed a little extra help here — that's completely normal."
/>
</div>
<div class="topic-meta">
<div class="diff-dots">
<span
v-for="d in 5"
:key="d"
class="diff-dot"
:class="d <= topic.difficulty ? 'diff-dot--filled' : 'diff-dot--empty'"
/>
</div>
<span class="diff-label">
{{ ['', 'intro', 'basic', 'intermediate', 'advanced', 'expert'][topic.difficulty] }}
</span>
<span v-if="topic.progress?.quizScore != null" class="diff-label">
· quiz {{ topic.progress.quizScore }}/4
</span>
</div>
</div>
<svg class="topic-arrow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"/>
</svg>
</NuxtLink>
<div v-else class="topic-row topic-row--generating">
<span class="topic-index" style="opacity: 0.4;">{{ String(idx + 1).padStart(2, "0") }}</span>
<div class="topic-info">
<span class="topic-name" style="opacity: 0.5;">{{ topic.title }}</span>
</div>
<span class="generating-label">
<span class="generating-dot" />
Generating...
</span>
</div>
</template>
</TransitionGroup>
</div>
</div>
</template>
</div>
</main>
</template>
<style scoped>
.course-page {
min-height: 100dvh;
background: var(--bg);
}
/* header */
.course-header {
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 20;
background: var(--surface);
}
.header-inner {
max-width: 720px;
margin: 0 auto;
padding: 0 1.5rem;
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
}
.back-link {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-3);
font-size: 13px;
text-decoration: none;
transition: color 0.15s;
}
.back-link:hover { color: var(--text); }
.header-logo {
font-family: "Lora", serif;
font-size: 15px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.3px;
}
/* page body */
.page-body {
max-width: 720px;
margin: 0 auto;
padding: 3.5rem 1.5rem 5rem;
}
/* processing */
.processing-view { max-width: 400px; margin: 0 auto; margin-bottom: 3rem; }
.processing-header { margin-bottom: 3rem; }
.processing-label {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent);
font-weight: 500;
margin-bottom: 6px;
}
.processing-title {
font-family: "Lora", serif;
font-size: 1.5rem;
color: var(--text);
font-weight: 600;
}
/* stepper */
.stepper { display: flex; flex-direction: column; }
.stepper-item {
display: grid;
grid-template-columns: 32px 1px 1fr;
grid-template-rows: auto auto;
gap: 0 1.25rem;
}
.stepper-node { grid-column: 1; grid-row: 1; display: flex; justify-content: center; }
.stepper-line {
grid-column: 2; grid-row: 2;
width: 1px; height: 2.5rem;
margin: 0 auto; align-self: stretch;
transition: background 0.4s;
}
.stepper-line--done { background: var(--green); }
.stepper-line--pending { background: var(--border); }
.stepper-text {
grid-column: 3; grid-row: 1 / 3;
padding-top: 5px; padding-bottom: 2.5rem;
}
.stepper-item:last-child .stepper-text { padding-bottom: 0; }
.node {
width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
position: relative;
}
.node--done { background: var(--green); color: white; }
.node--active { background: transparent; border: 1px solid var(--accent); }
.node--pending { background: transparent; border: 1px solid var(--border); }
.node-pulse {
position: absolute; inset: -4px; border-radius: 50%;
border: 1px solid var(--accent);
animation: pulse-ring 2s ease-out infinite; opacity: 0;
}
.node-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
animation: dot-breathe 1.5s ease-in-out infinite;
}
.node-dot-sm { width: 6px; height: 6px; border-radius: 50%; background: var(--border); }
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.6; }
100% { transform: scale(1.6); opacity: 0; }
}
@keyframes dot-breathe {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
.stepper-title {
font-size: 14px; font-weight: 500; color: var(--text); line-height: 1.3; transition: color 0.3s;
}
.stepper-item--pending .stepper-title { color: var(--border-2); }
.stepper-item--active .stepper-title { color: var(--accent); }
.stepper-sub {
font-size: 12px; color: var(--text-3); margin-top: 3px; line-height: 1.4;
}
.stepper-item--pending .stepper-sub { color: var(--border); }
.tip-area { margin-top: 3.5rem; min-height: 36px; text-align: center; }
.tip-text { font-size: 13px; font-style: italic; color: var(--text-3); line-height: 1.6; padding: 0 1rem; }
/* error */
.error-view { text-align: center; padding: 6rem 0; }
.error-card {
display: inline-block;
border: 1px solid oklch(80% 0.06 15);
background: oklch(95% 0.03 15);
border-radius: var(--r-surface);
padding: 2rem 2.5rem;
}
.error-label {
font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
color: oklch(42% 0.12 15); font-weight: 600; margin-bottom: 8px;
}
.error-sub { font-size: 14px; color: var(--text-2); margin-bottom: 16px; }
.error-back { font-size: 13px; color: var(--accent); text-decoration: none; }
.error-back:hover { text-decoration: underline; }
/* course ready */
.course-meta { margin-bottom: 2.5rem; }
.course-subject {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
margin-bottom: 6px;
}
.title-edit-wrap {
cursor: text;
display: inline-block;
margin-bottom: 10px;
}
.title-edit-wrap:hover .title-pencil { opacity: 1; }
.course-title {
font-family: "Lora", serif;
font-size: 2.25rem;
line-height: 1.15;
color: var(--text);
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
}
.title-pencil {
width: 16px; height: 16px; color: var(--text-3);
opacity: 0; transition: opacity 0.15s; flex-shrink: 0; margin-top: 4px;
}
.title-input {
background: transparent;
border: none;
border-bottom: 2px solid var(--accent-dim);
outline: none;
width: 100%;
padding: 0; margin: 0; display: block;
}
.org-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.org-chip {
font-size: 11px; font-weight: 500; color: var(--text-2);
border: 1px solid var(--border-2); border-radius: 20px;
padding: 2px 9px; background: var(--surface-2);
white-space: nowrap; letter-spacing: 0.01em;
}
.course-stats {
display: flex; align-items: center; gap: 8px; margin-bottom: 16px;
}
.stat { font-size: 13px; color: var(--text-2); display: flex; gap: 5px; align-items: center; }
.stat-num { font-family: "Lora", serif; font-size: 17px; font-weight: 600; color: var(--text); }
.stat-num--green { color: var(--green); }
.stat-divider { color: var(--border-2); }
.progress-track {
height: 6px; background: var(--border); border-radius: 99px;
overflow: hidden; max-width: 320px;
}
.progress-fill {
height: 100%; background: var(--accent); border-radius: 99px; transition: width 0.6s ease;
}
/* topic list */
.topic-list {}
.topic-row {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1.25rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-item);
border-left-width: 4px;
text-decoration: none;
transition: box-shadow 0.15s, border-color 0.15s;
}
.topic-row--complete { border-left-color: var(--green); }
.topic-row--available { border-left-color: var(--accent); }
.topic-row--generating { border-left-color: var(--border); cursor: default; animation: gen-pulse 2.2s ease-in-out infinite; }
.topic-row--unlocked:hover {
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.07);
}
.topic-row--unlocked:hover .topic-name { color: var(--accent); }
.topic-row--unlocked:hover .topic-arrow { color: var(--accent); }
.topic-index {
font-size: 11px; color: var(--text-3); min-width: 1.75rem;
letter-spacing: 0.05em; flex-shrink: 0;
}
.topic-info { flex: 1; min-width: 0; }
.topic-name {
font-size: 15px; font-weight: 500; color: var(--text);
transition: color 0.2s; display: block;
}
.topic-meta {
display: flex; align-items: center; gap: 8px; margin-top: 5px;
}
.diff-dots { display: flex; gap: 3px; align-items: center; }
.diff-dot { width: 6px; height: 6px; border-radius: 50%; }
.diff-dot--filled { background: var(--accent); }
.diff-dot--empty { background: var(--border); }
.diff-label { font-size: 11px; color: var(--text-3); letter-spacing: 0.04em; }
.done-badge {
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--green); background: var(--green-light);
padding: 1px 7px; border-radius: 20px; font-weight: 500;
}
.branch-dot {
width: 7px; height: 7px; border-radius: 50%;
background: oklch(65% 0.14 65); flex-shrink: 0; cursor: help;
}
.topic-arrow {
width: 14px; height: 14px; color: var(--border-2);
flex-shrink: 0; transition: color 0.2s;
}
/* generating label */
.generating-label {
display: flex; align-items: center; gap: 5px;
font-size: 11px; letter-spacing: 0.06em; color: var(--text-3); flex-shrink: 0;
}
.generating-dot {
width: 5px; height: 5px; border-radius: 50%;
background: var(--accent); opacity: 0.6;
animation: gen-dot-blink 1.4s ease-in-out infinite;
}
@keyframes gen-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes gen-dot-blink {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
/* topic appear transition */
.topic-list-inner { display: flex; flex-direction: column; gap: 8px; }
.topic-appear-enter-active { transition: opacity 0.4s ease, transform 0.4s ease; }
.topic-appear-enter-from { opacity: 0; transform: scale(0.97); }
/* tip transition */
.tip-enter-active, .tip-leave-active { transition: opacity 0.35s ease, transform 0.35s ease; }
.tip-enter-from { opacity: 0; transform: translateY(5px); }
.tip-leave-to { opacity: 0; transform: translateY(-5px); }
/* audit panel */
.audit-panel {
background: var(--text);
border-radius: var(--r-surface);
padding: 1.5rem;
margin-bottom: 2.5rem;
color: var(--bg);
}
.audit-header {
display: flex; align-items: center; gap: 1rem; margin-bottom: 1.25rem; flex-wrap: wrap;
}
.audit-badge {
font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
background: oklch(65% 0.14 65); color: var(--text);
padding: 2px 8px; border-radius: 4px; font-weight: 600; flex-shrink: 0;
}
.audit-score {
font-family: "Lora", serif; font-size: 2rem; line-height: 1; flex-shrink: 0;
}
.audit-score-denom { font-size: 14px; color: var(--text-3); }
.audit-hide-btn {
margin-left: auto; background: none; border: 1px solid oklch(35% 0.02 55);
color: var(--text-3); font-size: 11px; letter-spacing: 0.08em;
padding: 4px 10px; border-radius: 4px; cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.audit-hide-btn:hover { border-color: var(--text-3); color: var(--bg); }
.audit-standard-banner {
font-size: 14px; font-weight: 700; letter-spacing: 0.1em;
text-align: center; padding: 12px; border-radius: var(--r-sm); margin-bottom: 1.25rem;
}
.audit-standard-banner--pass {
background: rgba(34, 197, 94, 0.15); color: oklch(75% 0.12 155);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.audit-standard-banner--fail {
background: rgba(239, 68, 68, 0.15); color: oklch(75% 0.12 15);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.audit-readiness { font-size: 15px; line-height: 1.65; color: oklch(75% 0.01 75); margin-bottom: 1rem; }
.unanswered-source { font-size: 12px; color: var(--text-3); white-space: nowrap; }
.unanswered-gap { color: oklch(75% 0.12 15); }
.audit-coverage { font-size: 12px; color: var(--text-3); margin-bottom: 1.5rem; }
.audit-coverage strong { color: oklch(85% 0.01 75); }
.audit-section { border-top: 1px solid oklch(35% 0.02 55); padding-top: 1.25rem; margin-top: 1.25rem; }
.audit-section-title {
font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase;
color: var(--text-3); margin-bottom: 14px;
}
.audit-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.audit-table th {
font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--text-3); padding: 0 8px 8px 0; text-align: left; font-weight: 400; white-space: nowrap;
}
.audit-table td {
padding: 6px 8px 6px 0; color: oklch(80% 0.01 75);
border-top: 1px solid oklch(35% 0.02 55); vertical-align: top; line-height: 1.5;
}
.audit-table tr:first-child td { border-top: none; }
.severity-badge { font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px; border-radius: 3px; }
.severity-badge--high { background: rgba(239,68,68,0.15); color: oklch(75% 0.12 15); }
.severity-badge--medium { background: rgba(245,158,11,0.15); color: oklch(75% 0.14 65); }
.severity-badge--low { background: rgba(156,163,175,0.12); color: var(--text-3); }
.audit-recs { padding-left: 1.25rem; display: flex; flex-direction: column; gap: 8px; }
.audit-recs li { font-size: 14px; line-height: 1.6; color: oklch(80% 0.01 75); }
.audit-collapse-btn {
display: flex; align-items: center; gap: 8px; background: none; border: none;
cursor: pointer; font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase;
color: var(--text-3); padding: 0; transition: color 0.15s;
}
.audit-collapse-btn:hover { color: oklch(85% 0.01 75); }
.collapse-arrow { font-size: 12px; transition: transform 0.2s; display: inline-block; }
.collapse-arrow--open { transform: rotate(90deg); }
.lesson-quality-list { display: flex; flex-direction: column; gap: 0; margin-top: 14px; }
.lq-row {
display: grid; grid-template-columns: 1fr 64px 2fr;
gap: 12px; padding: 10px 0; border-top: 1px solid oklch(35% 0.02 55); font-size: 13px; align-items: start;
}
.lq-row:first-child { border-top: none; }
.lq-title { color: oklch(85% 0.01 75); font-weight: 500; }
.lq-score { font-size: 12px; font-weight: 500; text-align: right; }
.lq-notes { color: var(--text-3); line-height: 1.5; }
.audit-show-row { margin-bottom: 2rem; }
.audit-show-btn {
background: none; border: 1px solid var(--border);
color: var(--text-3); font-size: 11px; letter-spacing: 0.1em;
text-transform: uppercase; padding: 5px 12px; border-radius: var(--r-sm);
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.audit-show-btn:hover { border-color: var(--border-2); color: var(--text); }
</style>