Files

5965 lines
235 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='%23fafaf5'/><rect x='2' y='2' width='28' height='28' fill='none' stroke='%233553ff' stroke-width='1.2'/><text x='6' y='22' font-size='14' font-family='monospace' fill='%233553ff'>AI</text></svg>">
<!-- AIFS:LESSON-SEO:START -->
<title>Lesson - AI Engineering from Scratch</title>
<meta name="description" content="A lesson from the AI Engineering from Scratch curriculum. 523 lessons across 20 phases, four languages, every algorithm built from raw math.">
<link rel="canonical" href="https://aiengineeringfromscratch.com/lesson">
<meta property="og:title" content="AI Engineering from Scratch · Lesson">
<meta property="og:description" content="523 lessons. 20 phases. Write the backprop, the tokenizer, the attention mechanism, and the agent loop by hand.">
<meta property="og:image" content="https://aiengineeringfromscratch.com/og-image.png?v=4">
<meta property="og:url" content="https://aiengineeringfromscratch.com/lesson">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="AI Engineering from Scratch">
<meta name="twitter:description" content="523 lessons. 20 phases. Build it from raw math, by hand.">
<meta name="twitter:image" content="https://aiengineeringfromscratch.com/og-image.png?v=4">
<!-- AIFS:LESSON-SEO:END -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=VT323&family=Source+Serif+4:ital,opsz,wght@0,8..60,400..700;1,8..60,400..700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=20260824a">
<link rel="stylesheet" href="certifications.css?v=20260809a">
<style>
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 2px;
background: var(--blueprint);
z-index: 9999;
width: 100%;
transform: scaleX(0);
transform-origin: left center;
will-change: transform;
}
.lesson-layout {
display: flex;
padding-top: 64px;
min-height: 100vh;
}
.lesson-sidebar {
position: fixed;
top: 64px;
left: 0;
bottom: 0;
width: 240px;
background: var(--bg);
border-right: 1px solid var(--rule-soft);
overflow-y: auto;
z-index: 50;
padding: 24px 0;
transition: transform var(--motion-drawer) var(--ease-drawer);
}
.lesson-sidebar-toggle {
display: none;
position: fixed;
top: 74px;
left: 10px;
z-index: 60;
background: var(--bg-surface);
border: 1px solid var(--rule-soft);
width: 36px;
height: 36px;
cursor: pointer;
font-family: var(--font-mono);
font-size: 1rem;
color: var(--ink);
align-items: center;
justify-content: center;
transition:
transform var(--motion-press) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out),
color var(--motion-feedback) var(--ease-out);
}
.lesson-sidebar-toggle:hover {
border-color: var(--blueprint);
color: var(--blueprint);
}
.lesson-sidebar-toggle:active {
transform: scale(0.97);
transition: transform var(--motion-press) var(--ease-out);
}
.lesson-sidebar::-webkit-scrollbar { width: 4px; }
.lesson-sidebar::-webkit-scrollbar-thumb { background: var(--rule-soft); }
.sidebar-phase-header {
padding: 18px 20px 8px;
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 500;
color: var(--ink-mute);
text-transform: uppercase;
letter-spacing: 0.14em;
}
.sidebar-lesson-link {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 20px;
font-family: var(--font-body);
font-size: 0.92rem;
color: var(--ink-soft);
text-decoration: none;
border-left: 2px solid transparent;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.sidebar-lesson-link:hover {
background: var(--blueprint-tint);
color: var(--ink);
}
.sidebar-lesson-link.active {
color: var(--blueprint);
background: var(--blueprint-tint);
border-left-color: var(--blueprint);
}
.sidebar-lesson-link.disabled {
opacity: 0.5;
}
.sidebar-lesson-dot {
width: 8px;
height: 8px;
flex-shrink: 0;
border: 1px solid var(--ink-mute);
background: transparent;
}
.sidebar-lesson-dot.complete {
background: var(--blueprint);
border-color: var(--blueprint);
}
.sidebar-lesson-dot.in-progress {
background: linear-gradient(135deg, var(--blueprint) 0%, var(--blueprint) 50%, transparent 50%, transparent 100%);
border-color: var(--blueprint);
}
.sidebar-lesson-dot.planned {
border-style: dashed;
}
.sidebar-phase-nav {
padding: 18px 20px;
border-top: 1px solid var(--rule-soft);
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.sidebar-phase-nav a {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
text-decoration: none;
border-bottom: none;
}
.sidebar-phase-nav a:hover {
color: var(--blueprint);
}
.lesson-main {
flex: 1;
min-width: 0;
margin-left: 240px;
display: flex;
justify-content: center;
padding: 56px 32px 96px;
}
.lesson-content {
max-width: 720px;
width: 100%;
min-width: 0;
}
.lesson-article {
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
}
.lesson-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
gap: 16px;
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--rule-soft);
border-top-color: var(--blueprint);
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.lesson-loading-text {
font-family: var(--font-mono);
font-size: 0.85rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-mute);
}
.lesson-error {
text-align: left;
padding: 64px 0;
}
.lesson-error h2 {
font-family: var(--font-display);
font-size: 1.8rem;
text-transform: uppercase;
color: var(--blueprint);
margin-bottom: 12px;
}
.lesson-error p {
color: var(--ink-soft);
margin-bottom: 24px;
}
.lesson-article h1 {
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.02em;
color: var(--blueprint);
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid var(--blueprint);
line-height: 1;
}
.lesson-article h2 {
font-family: var(--font-display);
font-size: clamp(1.5rem, 3vw, 1.9rem);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.02em;
color: var(--ink);
margin-top: 56px;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: none;
line-height: 1.05;
}
.lesson-article h2.section-build,
.lesson-article h2.section-use,
.lesson-article h2.section-ship {
border-left: 3px solid var(--blueprint);
padding-left: 16px;
}
.lesson-article h3 {
font-family: var(--font-mono);
font-size: 0.95rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink);
margin-top: 36px;
margin-bottom: 12px;
}
.lesson-article h1,
.lesson-article h2,
.lesson-article h3 {
scroll-margin-top: var(--header-offset, 92px);
}
.lesson-article p {
font-family: var(--font-body);
font-size: 1.06rem;
line-height: 1.7;
color: var(--ink);
margin-bottom: 18px;
text-align: left;
hyphens: none;
-webkit-hyphens: none;
}
.lesson-article > p:first-of-type::first-letter,
.lesson-article .drop-cap::first-letter {
float: left;
font-family: var(--font-display);
font-size: 4rem;
line-height: 0.85;
padding: 0.06em 0.16em 0 0;
color: var(--blueprint);
}
.lesson-article .motto {
font-family: var(--font-body);
font-style: italic;
font-size: 1.15rem;
line-height: 1.55;
color: var(--ink);
border-left: 2px solid var(--blueprint);
background: var(--blueprint-tint);
padding: 18px 22px;
margin: 24px 0 32px;
text-align: left;
}
.lesson-article blockquote {
border-left: 2px solid var(--blueprint);
background: var(--blueprint-tint);
padding: 14px 22px;
margin: 20px 0;
color: var(--ink);
font-family: var(--font-body);
font-style: italic;
text-align: left;
}
.lesson-article strong {
color: var(--ink);
font-weight: 600;
}
.lesson-article em {
font-style: italic;
}
.lesson-article a {
color: var(--blueprint);
text-decoration: none;
border-bottom: 1px solid var(--blueprint);
transition: background 0.15s;
}
.lesson-article a:hover {
background: var(--blueprint-tint);
}
.lesson-article hr {
border: none;
height: 6px;
margin: 40px 0;
background-image:
repeating-linear-gradient(to right, var(--blueprint) 0, var(--blueprint) 4px, transparent 4px, transparent 8px),
repeating-linear-gradient(to right, transparent 0, transparent 8px, var(--blueprint-tint-strong) 8px, var(--blueprint-tint-strong) 14px);
background-size: 100% 3px, 100% 3px;
background-position: 0 0, 0 3px;
background-repeat: no-repeat;
}
.lesson-article ul {
list-style: none;
padding-left: 22px;
margin-bottom: 18px;
}
.lesson-article ul li {
font-family: var(--font-body);
font-size: 1.06rem;
line-height: 1.7;
color: var(--ink);
position: relative;
padding-left: 6px;
margin-bottom: 4px;
text-align: left;
}
.lesson-article ul li::before {
content: "";
position: absolute;
left: -16px;
top: 0.65em;
width: 6px;
height: 6px;
background: var(--blueprint);
}
.lesson-article ol {
list-style: none;
padding-left: 28px;
margin-bottom: 18px;
counter-reset: ol-counter;
}
.lesson-article ol li {
font-family: var(--font-body);
font-size: 1.06rem;
line-height: 1.7;
color: var(--ink);
position: relative;
padding-left: 6px;
margin-bottom: 4px;
counter-increment: ol-counter;
text-align: left;
}
.lesson-article ol li::before {
content: counter(ol-counter, decimal-leading-zero);
position: absolute;
left: -28px;
top: 0.1em;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--blueprint);
}
.lesson-article code {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--code-bg);
padding: 1px 6px;
border: 1px solid var(--rule-soft);
color: var(--blueprint);
}
.lesson-article pre {
position: relative;
margin: 20px 0;
padding-top: 44px;
overflow: hidden;
border: 1px solid var(--rule-soft);
background: var(--code-bg);
}
.lesson-article pre code {
display: block;
background: var(--code-bg);
color: var(--ink);
padding: 22px 24px;
font-size: 0.86rem;
line-height: 1.6;
overflow-x: auto;
border: none;
}
.code-lang {
position: absolute;
top: 0;
left: 0;
max-width: calc(100% - 88px);
height: 44px;
display: inline-flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.16em;
padding: 0 12px;
background: var(--blueprint);
color: var(--bg);
z-index: 2;
}
.code-copy {
position: absolute;
top: 0;
right: 0;
min-width: 88px;
min-height: 44px;
background: transparent;
border: 1px solid var(--rule-soft);
padding: 8px 12px;
font-family: var(--font-mono);
font-size: 0.66rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-soft);
cursor: pointer;
z-index: 2;
transition:
transform var(--motion-press) var(--ease-out),
color var(--motion-feedback) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out);
}
.code-copy:hover {
color: var(--blueprint);
border-color: var(--blueprint);
}
.syn-keyword { color: var(--blueprint); font-weight: 500; }
.syn-string { color: var(--ink); }
.syn-number { color: var(--blueprint); }
.syn-comment { color: var(--ink-mute); font-style: italic; }
.syn-function { color: var(--ink); font-weight: 500; }
.syn-operator { color: var(--ink-soft); }
[data-theme="dark"] .syn-keyword { color: var(--blueprint); }
[data-theme="dark"] .syn-string { color: var(--ink); }
[data-theme="dark"] .syn-number { color: var(--blueprint); }
[data-theme="dark"] .syn-comment { color: var(--ink-mute); }
[data-theme="dark"] .syn-function { color: var(--ink); }
[data-theme="dark"] .syn-operator { color: var(--ink-soft); }
.lesson-article .table-wrap {
overflow-x: auto;
margin: 24px 0;
border: 1px solid var(--rule-soft);
}
.lesson-article table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-body);
font-size: 0.96rem;
}
.lesson-article thead th {
background: var(--bg-surface);
color: var(--ink);
font-family: var(--font-mono);
font-size: 0.74rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--ink);
}
.lesson-article tbody td {
padding: 11px 16px;
border-bottom: 1px solid var(--rule-soft);
color: var(--ink);
vertical-align: top;
}
.lesson-article tbody tr:last-child td { border-bottom: none; }
.lesson-article tbody tr:hover {
background: var(--blueprint-tint);
}
.lesson-article .key-terms-table .col-says {
font-family: var(--font-body);
font-style: italic;
color: var(--ink-soft);
}
.learning-objectives {
background: transparent;
border: 1px solid var(--blueprint);
padding: 22px 26px;
margin: 24px 0 32px;
}
.learning-objectives-title {
font-family: var(--font-mono);
font-size: 0.74rem;
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--blueprint);
margin: 0 0 14px;
display: flex;
align-items: center;
gap: 8px;
}
.learning-objectives ul { margin: 0; padding-left: 20px; }
.learning-objectives li {
margin-bottom: 6px;
color: var(--ink);
line-height: 1.55;
font-size: 0.98rem;
text-align: left;
}
.lesson-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 28px;
padding: 14px 0;
border-top: 1px solid var(--rule-soft);
border-bottom: 1px solid var(--rule-soft);
}
.lesson-meta-tag {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 3px 10px;
border: 1px solid var(--rule-soft);
color: var(--ink-soft);
background: transparent;
}
.lesson-meta-tag.time-tag {
border-color: var(--blueprint);
color: var(--blueprint);
background: var(--blueprint-tint);
}
.lesson-action-panel {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 24px 0 34px;
border: 1px solid var(--blueprint);
background: var(--blueprint-tint);
overflow: hidden;
}
.lesson-action-head {
min-width: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: 18px 20px;
border-bottom: 1px solid var(--blueprint);
}
.lesson-action-head > *,
.lesson-action-body > *,
.lesson-action-command-row > * {
min-width: 0;
}
.lesson-action-title {
margin: 0;
font-family: var(--font-display);
font-size: 1.45rem;
line-height: 1;
text-transform: uppercase;
color: var(--blueprint);
}
.lesson-action-intro {
max-width: 450px;
margin: 7px 0 0;
font-family: var(--font-body);
font-size: 0.94rem;
line-height: 1.45;
color: var(--ink-soft);
text-align: left;
}
.lesson-action-status {
flex-shrink: 0;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-soft);
}
.lesson-action-loop {
min-width: 0;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
border-bottom: 1px solid var(--rule-soft);
}
.lesson-action-step {
min-width: 0;
min-height: 112px;
padding: 14px 12px;
border: 0;
border-right: 1px solid var(--rule-soft);
background: transparent;
color: var(--ink);
text-align: left;
cursor: pointer;
transition:
color var(--motion-feedback) var(--ease-out),
background-color var(--motion-feedback) var(--ease-out),
opacity var(--motion-feedback) var(--ease-out);
}
.lesson-action-step:last-child { border-right: 0; }
.lesson-action-step:hover,
.lesson-action-step:focus-visible {
color: var(--blueprint);
background: var(--bg-surface);
}
.lesson-action-step.is-done {
background: var(--blueprint);
color: var(--bg);
}
.lesson-action-step-num,
.lesson-action-step-title,
.lesson-action-step-copy {
display: block;
}
.lesson-action-step-num {
font-family: var(--font-mono);
font-size: 0.64rem;
letter-spacing: 0.12em;
opacity: 0.72;
}
.lesson-action-step-title {
margin-top: 7px;
font-family: var(--font-mono);
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.lesson-action-step-copy {
margin-top: 6px;
font-family: var(--font-body);
font-size: 0.78rem;
line-height: 1.35;
color: inherit;
opacity: 0.82;
}
.lesson-action-body {
min-width: 0;
display: grid;
gap: 14px;
padding: 18px 20px 20px;
}
.lesson-action-tags {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.lesson-action-tag {
padding: 3px 8px;
border: 1px solid var(--rule-soft);
background: var(--bg);
font-family: var(--font-mono);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.lesson-action-run,
.lesson-action-path {
width: 100%;
max-width: 100%;
min-width: 0;
border: 1px solid var(--rule-soft);
background: var(--bg);
padding: 14px 16px;
}
.lesson-action-label {
font-family: var(--font-mono);
font-size: 0.66rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--blueprint);
}
.lesson-action-command-row {
width: 100%;
max-width: 100%;
min-width: 0;
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 9px;
}
.lesson-action-command {
min-width: 0;
max-width: 100%;
flex: 1;
overflow-wrap: anywhere;
word-break: break-word;
padding: 9px 10px;
background: var(--code-bg);
border: 1px solid var(--rule-soft);
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.45;
color: var(--blueprint);
white-space: pre-wrap;
}
.lesson-action-copy {
min-height: 44px;
padding: 7px 10px;
border: 1px solid var(--rule-soft);
background: transparent;
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
.lesson-action-copy:hover,
.lesson-action-copy:focus-visible {
border-color: var(--blueprint);
color: var(--blueprint);
}
.lesson-action-note,
.lesson-action-path p,
.lesson-action-path li {
margin: 7px 0 0;
font-family: var(--font-body);
font-size: 0.88rem;
line-height: 1.5;
color: var(--ink-soft);
text-align: left;
}
.lesson-action-path ul {
margin: 7px 0 0;
padding-left: 18px;
}
.lesson-action-path code {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--blueprint);
overflow-wrap: anywhere;
word-break: break-word;
}
.quiz-section,
.quiz-container {
border: 1px solid var(--blueprint);
padding: 22px 26px;
margin: 32px 0;
background: transparent;
}
.quiz-title {
font-family: var(--font-mono);
font-size: 0.74rem;
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--blueprint);
margin: 0 0 18px;
display: flex;
align-items: center;
gap: 8px;
}
.quiz-question {
margin-bottom: 24px;
border: none;
padding: 0;
background: transparent;
}
.quiz-question:last-child { margin-bottom: 0; }
.quiz-question-num {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--blueprint);
margin-bottom: 6px;
}
.quiz-question-text {
font-family: var(--font-body);
font-size: 1rem;
font-weight: 500;
color: var(--ink);
margin-bottom: 12px;
line-height: 1.5;
text-align: left;
}
.quiz-options {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.quiz-option {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border: 1px solid var(--rule-soft);
font-family: var(--font-body);
font-size: 0.96rem;
color: var(--ink);
cursor: pointer;
transition:
transform var(--motion-press) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out),
background-color var(--motion-feedback) var(--ease-out),
color var(--motion-feedback) var(--ease-out);
background: transparent;
text-align: left;
width: 100%;
appearance: none;
}
.quiz-option:hover:not(.selected):not(.disabled) {
border-color: var(--blueprint);
background: var(--blueprint-tint);
}
.quiz-option:active:not(.disabled) {
transform: scale(0.97);
}
.quiz-option .opt-letter,
.quiz-option .quiz-marker {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid var(--ink-soft);
color: var(--ink-soft);
}
.quiz-option.selected.correct,
.quiz-option.correct,
.quiz-option.disabled.is-answer {
border-color: var(--blueprint);
background: var(--blueprint-tint);
color: var(--blueprint);
}
.quiz-option.selected.correct .opt-letter,
.quiz-option.selected.correct .quiz-marker,
.quiz-option.correct .opt-letter,
.quiz-option.correct .quiz-marker {
border-color: var(--blueprint);
background: var(--blueprint);
color: var(--bg);
}
.quiz-option.selected.wrong,
.quiz-option.incorrect {
border-color: var(--ink);
color: var(--ink);
}
.quiz-option.selected.wrong .opt-letter,
.quiz-option.selected.wrong .quiz-marker,
.quiz-option.incorrect .opt-letter,
.quiz-option.incorrect .quiz-marker {
border-color: var(--ink);
background: var(--ink);
color: var(--bg);
}
.quiz-option.disabled,
.quiz-option:disabled { cursor: default; opacity: 0.7; }
.quiz-explanation {
font-family: var(--font-body);
font-size: 0.92rem;
color: var(--ink-soft);
line-height: 1.55;
padding: 12px 16px;
border-left: 2px solid var(--blueprint);
background: var(--blueprint-tint);
display: none;
transform-origin: top center;
}
.quiz-explanation.visible {
display: block;
animation: lesson-feedback-enter var(--motion-feedback) var(--ease-out) both;
}
.quiz-score {
text-align: center;
padding: 24px;
border: 1px solid var(--blueprint);
background: var(--blueprint-tint);
display: none;
margin-top: 16px;
transform-origin: top center;
}
.quiz-score.visible {
display: block;
animation: lesson-feedback-enter var(--motion-enter) var(--ease-out) both;
}
@keyframes lesson-feedback-enter {
from {
opacity: 0;
transform: scale(0.97);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes lesson-feedback-fade {
from { opacity: 0; }
to { opacity: 1; }
}
.quiz-score-number {
font-family: var(--font-display);
font-size: 2.6rem;
color: var(--blueprint);
line-height: 1;
}
.quiz-score-label {
font-family: var(--font-mono);
font-size: 0.74rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-soft);
margin-top: 6px;
}
.quiz-retry {
margin-top: 16px;
padding: 8px 14px;
border: 1px solid var(--blueprint);
background: transparent;
color: var(--blueprint);
cursor: pointer;
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
transition:
transform var(--motion-press) var(--ease-out),
background-color var(--motion-feedback) var(--ease-out),
color var(--motion-feedback) var(--ease-out);
}
.quiz-retry:hover {
background: var(--blueprint);
color: var(--bg);
}
.quiz-retry:active {
transform: scale(0.97);
}
.quiz-reset-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.quiz-deeper {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--ink-soft);
margin-top: 16px;
padding: 12px;
background: var(--code-bg);
border: 1px solid var(--rule-soft);
}
.quiz-deeper code { color: var(--blueprint); }
.lab-challenge {
background: transparent;
border: 1px dashed var(--blueprint);
padding: 20px 26px;
margin: 28px 0;
}
.lab-challenge h2 {
font-family: var(--font-display);
color: var(--blueprint) !important;
border: none !important;
padding: 0 !important;
margin: 0 0 12px !important;
font-size: 1.4rem;
text-transform: uppercase;
line-height: 1;
}
.lesson-article .mermaid-container {
margin: 24px 0;
display: flex;
justify-content: center;
}
.mermaid-block { width: 100%; max-width: 100%; }
.mermaid-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.mermaid-btn {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 4px 10px;
border: 1px solid var(--rule-soft);
color: var(--ink-soft);
background: transparent;
cursor: pointer;
transition:
transform var(--motion-press) var(--ease-out),
color var(--motion-feedback) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out);
}
.mermaid-btn:hover {
border-color: var(--blueprint);
color: var(--blueprint);
}
.mermaid-render {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid var(--rule-soft);
padding: 16px;
background: var(--bg-surface);
}
.mermaid-source { display: none; }
.mermaid-modal-overlay {
position: fixed;
inset: 0;
background: var(--overlay-bg);
z-index: 99999;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
}
.mermaid-modal-overlay.open { display: flex; }
.mermaid-modal {
width: min(1100px, 96vw);
height: min(760px, 88vh);
background: var(--modal-bg);
border: 1px solid var(--ink);
display: flex;
flex-direction: column;
overflow: hidden;
}
.mermaid-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--rule-soft);
background: var(--bg-surface);
}
.mermaid-modal-title {
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mermaid-modal-actions {
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.mermaid-modal-body {
flex: 1;
overflow: auto;
padding: 18px;
background: var(--bg);
}
.mermaid-modal-center {
min-height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.mermaid-modal-body svg { max-width: none; height: auto; }
.lesson-article .mermaid-container pre {
overflow: visible;
background: none;
border: none;
margin: 0;
padding: 0;
}
.lesson-article .mermaid-container svg {
max-width: 100%;
height: auto;
}
:where(
.lesson-article .mermaid-render foreignObject p,
.lesson-article .mermaid-render foreignObject span,
.mermaid-modal-body foreignObject p,
.mermaid-modal-body foreignObject span
) {
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.4;
letter-spacing: 0.02em;
text-align: center;
text-transform: none;
margin: 0;
hyphens: none;
-webkit-hyphens: none;
color: var(--ink);
}
.lesson-nav-bottom {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
margin-top: 32px;
border: 1px solid var(--rule-soft);
background: var(--bg);
}
.lesson-nav-btn {
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
min-width: 0;
min-height: 88px;
padding: 16px 18px;
background: transparent;
border: 0;
text-decoration: none;
transition:
transform var(--motion-press) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out),
background-color var(--motion-feedback) var(--ease-out);
max-width: none;
}
.lesson-nav-btn + .lesson-nav-btn { border-left: 1px solid var(--rule-soft); }
.lesson-nav-btn:only-child { grid-column: 1 / -1; }
.lesson-nav-btn:hover {
background: var(--blueprint-tint);
}
.lesson-nav-btn:focus-visible,
.timeline-item:focus-visible,
.output-btn:focus-visible,
.code-card-btn:focus-visible {
outline: 2px solid var(--blueprint);
outline-offset: -2px;
}
.lesson-nav-btn:active,
.output-btn:active,
.code-card-btn:active,
.code-copy:active,
.mermaid-btn:active {
transform: scale(0.97);
}
.lesson-nav-btn .nav-label {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--blueprint);
}
.lesson-nav-btn .nav-title {
font-family: var(--font-body);
font-size: 0.98rem;
line-height: 1.35;
color: var(--ink);
font-weight: 500;
}
.lesson-nav-btn.next {
text-align: right;
margin-left: 0;
}
.lesson-nav-btn.next:only-child { text-align: left; }
.ai-panels {
margin-top: 48px;
display: flex;
flex-direction: column;
gap: 28px;
}
.ai-panel {
border: 1px solid var(--ink);
padding: 28px 26px;
background: transparent;
}
.ai-panel-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid var(--rule-soft);
}
.ai-panel-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 0.85rem;
flex-shrink: 0;
border: 1px solid var(--blueprint);
color: var(--blueprint);
background: var(--blueprint-tint);
}
.ai-panel-icon.coral,
.ai-panel-icon.blue,
.ai-panel-icon.green {
background: var(--blueprint-tint);
color: var(--blueprint);
border-color: var(--blueprint);
}
.ai-panel-title {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 400;
text-transform: uppercase;
color: var(--ink);
letter-spacing: 0.02em;
line-height: 1;
}
.ai-panel-subtitle {
font-family: var(--font-body);
font-size: 0.96rem;
color: var(--ink-soft);
margin-top: -8px;
margin-bottom: 18px;
}
.ai-panel--compact,
.ai-panel--path,
.ai-panel--continue {
padding: 20px;
}
.ai-panel--compact .ai-panel-header,
.ai-panel--path .ai-panel-header,
.ai-panel--continue .ai-panel-header {
margin-bottom: 12px;
padding-bottom: 10px;
}
.ai-panel--compact .ai-panel-subtitle,
.ai-panel--continue .ai-panel-subtitle {
margin: 0 0 14px;
}
.output-cards,
.code-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: stretch;
gap: 14px;
}
.output-card {
display: flex;
flex-direction: column;
min-width: 0;
height: 100%;
border: 1px solid var(--rule-soft);
padding: 18px;
background: var(--bg-surface);
}
.output-card-name {
font-family: var(--font-mono);
font-size: 0.84rem;
color: var(--ink);
overflow-wrap: anywhere;
word-break: normal;
text-transform: none;
}
.output-badge {
display: inline-flex;
align-items: center;
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 2px 8px;
border: 1px solid var(--blueprint);
color: var(--blueprint);
background: var(--blueprint-tint);
white-space: nowrap;
}
.output-badge.prompt,
.output-badge.skill,
.output-badge.other {
border-color: var(--blueprint);
color: var(--blueprint);
background: var(--blueprint-tint);
}
.output-desc {
font-family: var(--font-body);
font-size: 0.96rem;
color: var(--ink-soft);
line-height: 1.5;
margin-top: 10px;
text-transform: none;
letter-spacing: 0;
font-variant: normal;
text-align: left;
}
.output-card-main {
min-width: 0;
flex: 1;
}
.output-card-heading {
min-width: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 10px;
}
.output-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: stretch;
margin-top: 14px;
}
.output-btn,
.code-card-btn {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 4px 10px;
text-decoration: none;
border: 1px solid var(--rule-soft);
color: var(--ink-soft);
background: transparent;
cursor: pointer;
min-height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
transition:
transform var(--motion-press) var(--ease-out),
color var(--motion-feedback) var(--ease-out),
border-color var(--motion-feedback) var(--ease-out);
}
.output-btn:hover,
.code-card-btn:hover {
border-color: var(--blueprint);
color: var(--blueprint);
}
.output-install-hint {
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--ink-soft);
background: var(--code-bg);
padding: 8px 12px;
border: 1px solid var(--rule-soft);
margin-top: 10px;
word-break: break-all;
display: none;
}
.output-install-hint.visible { display: block; }
.output-cards.single,
.code-cards.single {
grid-template-columns: minmax(0, 1fr);
}
.output-cards.single .output-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px 16px;
padding: 14px 16px;
}
.output-cards.single .output-actions {
grid-column: 2;
grid-row: 1;
justify-content: flex-end;
margin-top: 0;
}
.output-cards.single .output-card--stacked-actions {
grid-template-columns: minmax(0, 1fr);
align-items: start;
}
.output-cards.single .output-card--stacked-actions .output-actions {
grid-column: 1;
grid-row: auto;
justify-content: flex-start;
}
.output-cards.single .output-install-hint {
grid-column: 1 / -1;
margin-top: 0;
}
.code-card {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
min-width: 0;
max-width: 100%;
height: 100%;
background: var(--code-bg);
border: 1px solid var(--rule-soft);
padding: 18px;
transition: border-color 0.15s;
}
.code-card:hover {
border-color: var(--blueprint);
}
.code-card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
min-width: 0;
}
.code-lang-icon {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 2px 8px;
border: 1px solid var(--rule-soft);
color: var(--ink-soft);
line-height: 1;
}
.code-card-name {
font-family: var(--font-mono);
font-size: 0.84rem;
color: var(--ink);
word-break: break-all;
text-transform: none;
}
.code-card-size {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--ink-mute);
margin-bottom: 0;
margin-left: auto;
white-space: nowrap;
}
.code-card-run {
min-width: 0;
max-width: 100%;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--blueprint);
background: var(--blueprint-tint);
padding: 10px 12px;
margin-bottom: 12px;
border: 1px solid var(--blueprint);
text-transform: none;
line-height: 1.4;
text-align: left;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.code-card-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.output-cards:not(.single) .output-card:last-child:nth-child(odd),
.code-cards:not(.single) .code-card:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
.code-cards.single .code-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px 16px;
padding: 16px 18px;
}
.code-cards.single .code-card-header,
.code-cards.single .code-card-run {
margin-bottom: 0;
}
.code-cards.single .code-card-run { grid-column: 1 / -1; }
.code-cards.single .code-card-actions {
grid-column: 2;
grid-row: 1;
}
.learning-timeline {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(132px, 100%), 1fr));
gap: 0;
overflow: hidden;
margin: 14px 0 0;
border: 1px solid var(--rule-soft);
position: relative;
}
.timeline-item {
display: block;
min-width: 0;
padding: 12px;
position: relative;
text-decoration: none;
background: var(--bg);
transition:
background-color var(--motion-feedback) var(--ease-out),
box-shadow var(--motion-feedback) var(--ease-out);
}
.timeline-item + .timeline-item,
.timeline-ellipsis + .timeline-item,
.timeline-item + .timeline-ellipsis {
border-left: 1px solid var(--rule-soft);
}
.timeline-item:hover { background: var(--blueprint-tint); }
.timeline-item.current {
background: var(--blueprint-tint);
box-shadow: inset 0 3px 0 var(--blueprint);
}
.timeline-step-meta {
display: flex;
align-items: center;
gap: 7px;
min-height: 16px;
margin-bottom: 7px;
}
.timeline-dot {
width: 10px;
height: 10px;
background: transparent;
border: 1px solid var(--ink-mute);
flex: 0 0 auto;
transition: transform 0.15s, background 0.15s, border-color 0.15s;
}
.timeline-item.current .timeline-dot {
background: var(--blueprint);
border-color: var(--blueprint);
}
.timeline-item.prev .timeline-dot,
.timeline-item.done .timeline-dot {
background: var(--blueprint);
border-color: var(--blueprint);
}
.timeline-line {
display: none;
}
.timeline-line.done,
.timeline-line.active {
background: var(--blueprint);
}
.timeline-label {
font-family: var(--font-body);
font-size: 0.82rem;
letter-spacing: 0;
text-transform: none;
color: var(--ink-mute);
text-align: left;
max-width: none;
line-height: 1.35;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.timeline-item.current .timeline-label {
color: var(--blueprint);
font-weight: 500;
}
.timeline-index {
font-family: var(--font-mono);
font-size: 0.66rem;
color: var(--ink-mute);
letter-spacing: 0.08em;
}
.timeline-item.current .timeline-index,
.timeline-item.done .timeline-index { color: var(--blueprint); }
.timeline-ellipsis {
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--ink-mute);
letter-spacing: 0.08em;
padding: 12px;
white-space: nowrap;
background: var(--code-bg);
}
.path-panel-context {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
.path-panel-context .ai-panel-subtitle {
margin: 0;
color: var(--ink);
font-weight: 500;
}
.path-position {
flex: 0 0 auto;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.08em;
color: var(--ink-mute);
white-space: nowrap;
}
.phase-progress-bar {
width: 100%;
height: 3px;
background: var(--rule-soft);
overflow: hidden;
margin: 12px 0 7px;
}
.phase-progress-fill {
width: 100%;
height: 100%;
background: var(--blueprint);
transform: scaleX(var(--lesson-progress, 0));
transform-origin: left center;
transition: transform var(--motion-enter) var(--ease-out);
}
.phase-progress-text {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.06em;
color: var(--ink-soft);
text-align: left;
}
.path-completion-actions {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--rule-soft);
}
.path-completion-actions a {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--blueprint);
text-decoration: none;
}
.path-completion-actions a:hover { text-decoration: underline; }
.learning-path-preflights {
display: grid;
gap: 10px;
margin: 18px 0 2px;
padding-top: 16px;
border-top: 1px solid var(--rule-soft);
}
.learning-path-preflight {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px 18px;
align-items: center;
padding: 14px 16px;
border: 1px solid var(--rule-soft);
background: var(--code-bg);
}
.learning-path-preflight-copy { min-width: 0; }
.learning-path-preflight-label,
.learning-path-preflight-status {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.learning-path-preflight-label {
color: var(--blueprint);
margin-bottom: 5px;
}
.learning-path-preflight-title {
color: var(--ink);
font-family: var(--font-body);
font-size: 1rem;
font-weight: 600;
line-height: 1.3;
}
.learning-path-preflight-description {
color: var(--ink-soft);
font-family: var(--font-body);
font-size: 0.88rem;
line-height: 1.45;
margin-top: 4px;
}
.learning-path-preflight-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.learning-path-preflight-status { color: var(--ink-mute); }
.learning-path-preflight.confirmed { border-color: var(--blueprint); }
.learning-path-preflight.confirmed .learning-path-preflight-status { color: var(--blueprint); }
.learning-path-confirm[disabled] {
cursor: default;
opacity: 0.7;
}
.learning-path-locked {
cursor: not-allowed;
opacity: 0.48;
}
.learning-path-locked:hover {
color: var(--ink-mute);
border-color: transparent;
}
.prerequisite-callout:focus {
outline: 2px solid var(--blueprint);
outline-offset: 3px;
}
.phase-complete-callout {
position: relative;
overflow: hidden;
font-family: var(--font-mono);
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--blueprint);
text-align: center;
margin-top: 14px;
padding: 14px;
border: 1px solid var(--blueprint);
background: var(--blueprint-tint);
display: none;
}
.phase-complete-callout.visible {
display: block;
opacity: 1;
transform: none;
}
.phase-complete-callout.visible::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: var(--blueprint);
transform: scaleX(1);
transform-origin: left center;
}
.phase-complete-callout.visible.is-entering {
opacity: 0;
transform: translateY(8px);
}
.phase-complete-callout.visible.is-entering::before {
transform: scaleX(0);
}
.phase-complete-callout.visible.is-entered {
transition:
opacity var(--motion-enter) var(--ease-out),
transform var(--motion-enter) var(--ease-out);
}
.phase-complete-callout.visible.is-entered::before {
transition: transform var(--motion-drawer) var(--ease-out);
}
.continue-panel {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.phase-finished {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--bg);
background: var(--blueprint);
padding: 8px 16px;
}
.continue-links {
display: flex;
flex-direction: column;
gap: 6px;
}
.continue-link {
font-family: var(--font-body);
font-size: 0.96rem;
color: var(--ink);
text-decoration: none;
border-bottom: 1px solid transparent;
}
.continue-link:hover {
color: var(--blueprint);
border-bottom-color: var(--blueprint);
}
.continue-callout {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.06em;
color: var(--ink-soft);
background: var(--code-bg);
padding: 12px 18px;
border: 1px solid var(--rule-soft);
max-width: 480px;
}
.continue-callout code { color: var(--blueprint); }
.panel-loading {
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-mute);
text-align: center;
padding: 20px;
}
.panel-fallback {
text-align: left;
padding: 16px 0;
}
.panel-fallback a {
color: var(--blueprint);
text-decoration: none;
border-bottom: 1px solid var(--blueprint);
font-family: var(--font-body);
font-size: 0.95rem;
}
.lesson-cert-notice {
margin: 0 0 28px;
}
.lesson-cert-breadcrumb {
margin: 0 0 18px;
flex-wrap: wrap;
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
border-color: var(--rule-soft);
border-top-color: var(--blueprint);
}
.lesson-sidebar,
.phase-progress-fill {
transition: none;
}
.phase-complete-callout.visible,
.phase-complete-callout.visible.is-entering,
.phase-complete-callout.visible.is-entered {
opacity: 1;
transform: none;
transition: none;
}
.phase-complete-callout.visible::before,
.phase-complete-callout.visible.is-entering::before,
.phase-complete-callout.visible.is-entered::before {
transform: scaleX(1);
transition: none;
}
.quiz-explanation.visible,
.quiz-score.visible {
animation: lesson-feedback-fade var(--motion-feedback) ease both;
}
.lesson-sidebar-toggle:active,
.quiz-option:active:not(.disabled),
.quiz-retry:active,
.lesson-nav-btn:active,
.output-btn:active,
.code-card-btn:active,
.code-copy:active,
.mermaid-btn:active {
transform: none;
}
}
.toc-sidebar {
position: fixed;
top: 64px;
right: 0;
bottom: 0;
width: 240px;
background: transparent;
overflow-y: auto;
z-index: 40;
padding: 56px 24px 80px;
display: none;
scrollbar-width: thin;
scrollbar-color: var(--rule-soft) transparent;
}
.toc-sidebar[aria-hidden="true"] { display: none; }
.toc-sidebar::-webkit-scrollbar { width: 3px; }
.toc-sidebar::-webkit-scrollbar-thumb { background: var(--rule-soft); }
.toc-header {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 500;
color: var(--blueprint);
text-transform: uppercase;
letter-spacing: 0.16em;
margin-bottom: 18px;
}
.toc-nav {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0;
position: relative;
border-left: 1px solid var(--rule-soft);
}
.toc-item { line-height: 1.4; }
.toc-link {
font-family: var(--font-body);
font-size: 0.84rem;
color: var(--ink-soft);
text-decoration: none;
display: block;
border-left: 2px solid transparent;
padding: 5px 16px;
margin-left: -1px;
transition: color 0.15s, border-color 0.15s, background 0.15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toc-h3 .toc-link {
padding-left: 28px;
font-size: 0.78rem;
}
.toc-link:hover {
color: var(--ink);
background: var(--blueprint-tint);
}
.toc-link:focus-visible {
outline: 2px solid var(--blueprint);
outline-offset: 2px;
}
.toc-link.active {
color: var(--blueprint);
border-left-color: var(--blueprint);
font-weight: 500;
}
@media (min-width: 1280px) {
.lesson-main { padding-right: 240px; }
.toc-sidebar { display: block; }
}
@media (max-width: 1279px) {
.lesson-main { padding-right: 32px; }
}
@media (min-width: 1440px) {
.lesson-article { max-width: 760px; }
.lesson-sidebar { width: 280px; min-width: 280px; }
.lesson-main { margin-left: 280px; padding-right: 280px; }
.toc-sidebar { width: 280px; }
}
@media (max-width: 900px) {
html,
body {
overflow-x: hidden;
}
.lesson-layout { padding-top: 56px; }
.lesson-sidebar { transform: translateX(-100%); }
.lesson-sidebar.open { transform: translateX(0); }
.lesson-sidebar-toggle {
display: flex;
top: auto;
right: 16px;
bottom: 18px;
left: auto;
width: auto;
height: 44px;
padding: 0 14px;
gap: 8px;
border-color: var(--blueprint);
background: var(--modal-bg);
color: var(--blueprint);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.lesson-sidebar {
top: 56px;
width: min(360px, 88vw);
padding: 18px 0 88px;
border-right-color: var(--blueprint);
box-shadow: 16px 0 40px rgba(0, 0, 0, 0.22);
}
.sidebar-phase-header {
padding: 18px 18px 8px;
}
.sidebar-lesson-link {
padding: 10px 18px;
font-size: 1rem;
line-height: 1.35;
}
.lesson-main {
margin-left: 0;
justify-content: stretch;
padding: 44px 20px 80px;
}
.lesson-content { max-width: none; }
}
@media (max-width: 768px) {
.lesson-main { padding: 40px 16px 60px; }
.lesson-article { max-width: 100%; }
.lesson-article h1 {
font-size: clamp(2.05rem, 11vw, 3rem);
line-height: 0.95;
margin-bottom: 16px;
padding-bottom: 14px;
}
.lesson-article h2 {
margin-top: 42px;
font-size: clamp(1.35rem, 8vw, 1.9rem);
}
.lesson-article h3 {
margin-top: 28px;
font-size: 0.84rem;
line-height: 1.35;
}
.lesson-article p,
.lesson-article ul li,
.lesson-article ol li { font-size: 1rem; line-height: 1.65; }
.lesson-article p {
text-align: left;
hyphens: none;
-webkit-hyphens: none;
}
.lesson-article > p:first-of-type::first-letter { font-size: 3rem; }
.lesson-article pre code { font-size: 0.78rem; padding: 14px 14px; }
.lesson-article table { font-size: 0.84rem; display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.lesson-article table th,
.lesson-article table td { padding: 8px 10px; white-space: nowrap; }
.lesson-article .motto { font-size: 1.05rem; padding: 14px 16px; }
.learning-objectives,
.quiz-section,
.quiz-container {
padding: 18px 16px;
}
.lesson-meta {
gap: 6px;
margin-bottom: 24px;
}
.lesson-meta-tag {
font-size: 0.66rem;
letter-spacing: 0.07em;
padding: 3px 8px;
}
.lesson-action-head {
flex-direction: column;
padding: 16px;
}
.lesson-action-loop {
grid-template-columns: 1fr;
}
.lesson-action-step {
min-height: 0;
border-right: 0;
border-bottom: 1px solid var(--rule-soft);
}
.lesson-action-step:last-child { border-bottom: 0; }
.lesson-action-body { padding: 16px; }
.lesson-action-command-row { flex-direction: column; }
.lesson-action-copy { width: 100%; }
.ai-panels { margin-top: 40px; gap: 20px; }
.ai-panel { padding: 22px 16px; }
.ai-panel--compact,
.ai-panel--path,
.ai-panel--continue { padding: 16px; }
.ai-panel-header {
align-items: flex-start;
gap: 10px;
}
.ai-panel-title {
font-size: 1.22rem;
line-height: 1.05;
}
.output-cards,
.code-cards { grid-template-columns: 1fr; }
.output-cards.single .output-card,
.code-cards.single .code-card {
grid-template-columns: minmax(0, 1fr);
}
.output-cards.single .output-card > *,
.code-cards.single .code-card > * {
grid-column: 1;
grid-row: auto;
}
.output-cards.single .output-actions {
justify-content: flex-start;
}
.learning-timeline { grid-template-columns: minmax(0, 1fr); }
.timeline-item {
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
align-items: center;
gap: 10px;
padding: 10px 12px;
}
.timeline-step-meta { margin-bottom: 0; }
.timeline-item + .timeline-item,
.timeline-ellipsis + .timeline-item,
.timeline-item + .timeline-ellipsis {
border-left: 0;
border-top: 1px solid var(--rule-soft);
}
.timeline-label { -webkit-line-clamp: 2; }
.timeline-ellipsis {
justify-content: flex-start;
border-left: 0;
border-top: 1px solid var(--rule-soft);
}
.path-panel-context {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.lesson-nav-bottom { grid-template-columns: minmax(0, 1fr); }
.lesson-nav-btn + .lesson-nav-btn {
border-left: 0;
border-top: 1px solid var(--rule-soft);
}
.lesson-nav-btn { max-width: 100%; min-height: 78px; }
.lesson-nav-btn.next { text-align: left; }
}
@media (max-width: 480px) {
.lesson-main { padding: 36px 12px 56px; }
.lesson-sidebar-toggle {
right: 12px;
bottom: 14px;
height: 44px;
padding: 0 12px;
}
.lesson-article p,
.lesson-article ul li,
.lesson-article ol li { font-size: 0.96rem; }
.lesson-article h1 { font-size: clamp(1.9rem, 12vw, 2.55rem); }
.lesson-article > p:first-of-type::first-letter { font-size: 2.6rem; }
.lesson-article ul { padding-left: 18px; }
.lesson-article ol { padding-left: 24px; }
.learning-objectives,
.quiz-section,
.quiz-container {
padding: 16px 12px;
}
.learning-objectives-title,
.quiz-title {
align-items: flex-start;
font-size: 0.66rem;
line-height: 1.35;
}
.quiz-option {
gap: 10px;
padding: 10px 12px;
}
.ai-panel { padding: 18px 12px; }
.code-card,
.output-card {
padding: 14px;
}
.code-card-actions,
.output-actions {
display: grid;
grid-template-columns: 1fr;
}
.code-card-btn,
.output-btn {
text-align: center;
min-height: 44px;
}
.learning-path-preflight {
grid-template-columns: 1fr;
}
.learning-path-preflight-actions {
align-items: stretch;
}
}
</style>
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
<div class="scroll-progress" id="scrollProgress"></div>
<header class="site-header">
<div class="header-inner">
<a href="index.html" class="logo">
<span class="logo-icon" aria-hidden="true"></span> AI / FROM SCRATCH
</a>
<nav class="header-nav">
<a href="index.html#contents">Contents</a>
<a href="catalog.html">Catalog</a>
<a href="prereqs.html">Roadmap</a>
<a href="glossary.html">Glossary</a>
<a href="about.html">About</a>
<a href="https://github.com/rohitg00/ai-engineering-from-scratch" target="_blank" rel="noopener" class="header-github">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
<svg class="star-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.896l-7.416 3.517 1.48-8.279L0 9.306l8.332-1.151z"/></svg>
<span class="star-count" data-loading="true" aria-label="GitHub stars">…</span>
</a>
</nav>
<button class="search-toggle" type="button" data-cmd-palette
aria-label="Search (⌘K)" title="Search (⌘K)">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</button>
<div id="langPicker"></div>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" type="button">
<span class="theme-icon" id="themeIcon">N</span>
</button>
</div>
</header>
<button class="lesson-sidebar-toggle" id="sidebarToggle" aria-label="Toggle lesson menu" aria-controls="lessonSidebar" aria-expanded="false" type="button">
<span aria-hidden="true">&#9776;</span>
<span>Lessons</span>
</button>
<div class="mermaid-modal-overlay" id="mermaidModalOverlay" aria-hidden="true">
<div class="mermaid-modal" role="dialog" aria-modal="true" aria-label="Expanded diagram">
<div class="mermaid-modal-header">
<div class="mermaid-modal-title" id="mermaidModalTitle">Diagram</div>
<div class="mermaid-modal-actions">
<button class="mermaid-btn" type="button" id="mermaidModalClose">Close</button>
</div>
</div>
<div class="mermaid-modal-body" id="mermaidModalBody"><div class="mermaid-modal-center" id="mermaidModalCenter"></div></div>
</div>
</div>
<div class="lesson-layout">
<aside class="lesson-sidebar" id="lessonSidebar"></aside>
<main class="lesson-main" id="main">
<div class="lesson-content" id="lessonContent">
<!-- AIFS:LESSON-FALLBACK:START -->
<div class="lesson-loading" id="lessonLoading">
<div class="spinner"></div>
<div class="lesson-loading-text">Loading lesson...</div>
</div>
<!-- AIFS:LESSON-FALLBACK:END -->
</div>
</main>
<aside class="toc-sidebar" id="tocSidebar" aria-hidden="true"></aside>
</div>
<script src="build-meta.js"></script>
<script src="langs.js?v=20260802a"></script>
<script src="lang-picker.js?v=20260808d"></script>
<script src="data.js?v=20260821c"></script>
<script>
(function () {
var path = '';
var hasTrackContext = false;
try {
var lessonParams = new URLSearchParams(window.location.search);
path = lessonParams.get('path') || '';
hasTrackContext = !!(lessonParams.get('track') || lessonParams.get('fromTrack'));
} catch (_) {}
if (path.indexOf('certifications/claude/lessons/') !== 0 && !hasTrackContext) return;
window.__AIFS_CERTIFICATION_DATA_READY = new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = 'certification-data.js?v=20260809c';
script.onload = resolve;
script.onerror = function () { reject(new Error('certification-data.js failed to load')); };
document.head.appendChild(script);
});
}());
</script>
<script src="content-source.js?v=20260821a"></script>
<script src="progress.js?v=20260822a"></script>
<script id="learningPathProgressRuntime">
(function (root) {
'use strict';
var STORAGE_KEY = 'aifs:learning-path-progress:v1';
function emptyState() {
return { version: 1, paths: {} };
}
function createStore(storage) {
var memoryState = emptyState();
function read() {
try {
var raw = storage && storage.getItem(STORAGE_KEY);
var parsed = raw ? JSON.parse(raw) : null;
if (parsed && parsed.version === 1 && parsed.paths && typeof parsed.paths === 'object') {
memoryState = parsed;
}
} catch (_) {}
return memoryState;
}
function write(state) {
memoryState = state;
try {
if (storage) storage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (_) {}
return true;
}
function checkIds(entry) {
if (!entry || !Array.isArray(entry.prerequisiteChecks)) return [];
return entry.prerequisiteChecks.filter(function (id) {
return typeof id === 'string' && id.length > 0;
});
}
function prerequisitePaths(entry) {
if (!entry || !Array.isArray(entry.prerequisitePaths)) return [];
return entry.prerequisitePaths.filter(function (lessonPath) {
return typeof lessonPath === 'string' && lessonPath.length > 0;
});
}
function isConfirmed(pathId, checkId) {
var state = read();
return !!(state.paths[pathId] && state.paths[pathId].checks && state.paths[pathId].checks[checkId]);
}
function confirm(pathId, checkId) {
if (typeof pathId !== 'string' || !pathId || typeof checkId !== 'string' || !checkId) return false;
var state = read();
if (!state.paths[pathId] || typeof state.paths[pathId] !== 'object') state.paths[pathId] = {};
if (!state.paths[pathId].checks || typeof state.paths[pathId].checks !== 'object') state.paths[pathId].checks = {};
state.paths[pathId].checks[checkId] = true;
return write(state);
}
function unmetChecks(path, entry) {
if (!path || typeof path.id !== 'string') return [];
var prerequisites = Array.isArray(path.prerequisites) ? path.prerequisites : [];
return checkIds(entry).filter(function (checkId) {
return !isConfirmed(path.id, checkId);
}).map(function (checkId) {
return prerequisites.find(function (candidate) {
return candidate && candidate.id === checkId;
});
}).filter(Boolean);
}
function unmetPaths(entry, isLessonComplete) {
return prerequisitePaths(entry).filter(function (lessonPath) {
return typeof isLessonComplete !== 'function' || !isLessonComplete(lessonPath);
});
}
return {
storageKey: STORAGE_KEY,
isConfirmed: isConfirmed,
confirm: confirm,
unmetChecks: unmetChecks,
unmetPaths: unmetPaths,
firstUnmetCheckId: function (path, entry) {
var first = unmetChecks(path, entry)[0];
return first ? first.id : '';
},
canEnter: function (path, entry, isLessonComplete) {
return unmetChecks(path, entry).length === 0 &&
unmetPaths(entry, isLessonComplete).length === 0;
},
createForStorage: createStore
};
}
root.AIFSLearningPathProgress = createStore(root.localStorage);
}(window));
</script>
<script src="header.js?v=20260829b" defer></script>
<script src="cmdpalette.js?v=20260821a" defer></script>
<script src="lesson-figures.js?v=843871fd2942"></script>
<script src="figure-manifest.js?v=0980822a99ac"></script>
<script>
(function () {
var root = document.documentElement;
var stored = '';
try { stored = localStorage.getItem('theme') || ''; } catch (_) {}
if (stored) {
root.setAttribute('data-theme', stored);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.setAttribute('data-theme', 'dark');
} else {
root.setAttribute('data-theme', 'light');
}
updateThemeIcon();
function updateThemeIcon() {
var icon = document.getElementById('themeIcon');
if (!icon) return;
var theme = root.getAttribute('data-theme');
icon.textContent = theme === 'light' ? 'N' : 'D';
}
var btn = document.getElementById('themeToggle');
if (btn) {
btn.addEventListener('click', function () {
var current = root.getAttribute('data-theme');
var next = current === 'light' ? 'dark' : 'light';
root.setAttribute('data-theme', next);
try { localStorage.setItem('theme', next); } catch (_) {}
updateThemeIcon();
updateMermaidThemeAndRerender();
});
}
// lang-picker.js renders the switcher into #langPicker; when the reader
// chooses a language, re-fetch the current lesson in it (quiz stays English).
window.AIFS_onLangChange = function () {
if (typeof lessonPath !== 'undefined' && lessonPath) fetchLesson(lessonPath);
};
var params = new URLSearchParams(window.location.search);
var lessonPath = params.get('path');
var requestedTrackId = params.get('track') || '';
var originatingTrackId = params.get('fromTrack') || '';
var LEARNING_PATH_ID_ALIASES = { 'mcp-engineering': 'model-context-protocol' };
var incomingLearningPathId = params.get('learningPath') || '';
var requestedLearningPathId = LEARNING_PATH_ID_ALIASES[incomingLearningPathId] || incomingLearningPathId;
function replaceLearningPathInUrl(pathId) {
if (!pathId || !window.history || !window.history.replaceState) return;
var canonicalLearningPathUrl = new URL(window.location.href);
canonicalLearningPathUrl.searchParams.set('learningPath', pathId);
window.history.replaceState(null, '', canonicalLearningPathUrl.pathname + canonicalLearningPathUrl.search + canonicalLearningPathUrl.hash);
}
if (incomingLearningPathId && incomingLearningPathId !== requestedLearningPathId) replaceLearningPathInUrl(requestedLearningPathId);
function preferredScrollBehavior() {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
}
if (!lessonPath) {
showError('No lesson path specified', 'Add ?path=phases/01-math-foundations/01-linear-algebra-intuition to the URL.');
return;
}
if (window.AIFSProgress) {
window.AIFSProgress.recordVisit(lessonPath);
}
var phaseMatch = lessonPath.match(/phases\/(\d+-[^/]+)/);
var lessonMatch = lessonPath.match(/phases\/\d+-[^/]+\/(\d+-[^/]+)/);
var currentPhaseSlug = phaseMatch ? phaseMatch[1] : null;
var currentLessonSlug = lessonMatch ? lessonMatch[1] : null;
var currentPhaseIndex = -1;
var currentLessonIndex = -1;
var flatLessons = [];
var ACTIVE_REF = (typeof window !== 'undefined' && window.__AIFS_REF) ? window.__AIFS_REF : 'main';
var SOURCE_CONFIG = (typeof window !== 'undefined' && window.__AIFS_SOURCE) ? window.__AIFS_SOURCE : {};
var SOURCE_OWNER = /^[A-Za-z0-9-]+$/.test(SOURCE_CONFIG.owner || '') ? SOURCE_CONFIG.owner : 'rohitg00';
var SOURCE_REPO = /^[A-Za-z0-9_.-]+$/.test(SOURCE_CONFIG.repo || '') ? SOURCE_CONFIG.repo : 'ai-engineering-from-scratch';
var REPO_TREE = 'https://github.com/' + SOURCE_OWNER + '/' + SOURCE_REPO + '/tree/' + encodeURIComponent(ACTIVE_REF) + '/';
var certificationTrack = null;
var certificationOriginTrack = null;
var certificationLesson = null;
var certificationMode = false;
var certificationSupplementalMode = false;
var learningPath = null;
var learningPathMode = false;
var PROGRESS_STORAGE_KEY = 'aifs:progress:v2';
function certificationData() {
return typeof CERTIFICATIONS !== 'undefined' && CERTIFICATIONS ? CERTIFICATIONS : null;
}
function certificationRefPath(ref) {
return typeof ref === 'string' ? ref : ref && ref.path ? ref.path : '';
}
function certificationTrackContains(track, path) {
return !!(track && Array.isArray(track.lessons) && track.lessons.some(function (ref) {
return certificationRefPath(ref) === path;
}));
}
function findCertificationTrack(id) {
var certs = certificationData();
if (!certs || !Array.isArray(certs.tracks)) return null;
return certs.tracks.find(function (track) {
return track.id === id || track.slug === id || track.examCode === id;
}) || null;
}
function certificationContextTrack() {
return certificationTrack || certificationOriginTrack || null;
}
function certificationTrackLabel(track) {
return track && (track.examCode || track.shortName || track.credential || track.title || track.id) || 'Certification';
}
function phaseLessonForPath(path) {
if (typeof PHASES === 'undefined') return null;
for (var pi = 0; pi < PHASES.length; pi++) {
for (var li = 0; li < PHASES[pi].lessons.length; li++) {
var phaseLesson = PHASES[pi].lessons[li];
var match = phaseLesson.url && phaseLesson.url.match(/(phases\/[^/?#]+\/[^/?#]+)/);
if (match && match[1] === path) return { phase: PHASES[pi], lesson: phaseLesson, phaseIndex: pi, lessonIndex: li };
}
}
return null;
}
function learningPathEntryPath(entry) {
return typeof entry === 'string' ? entry : entry && entry.path ? entry.path : '';
}
function findLearningPath(id) {
if (!id || typeof LEARNING_PATHS === 'undefined' || !Array.isArray(LEARNING_PATHS)) return null;
return LEARNING_PATHS.find(function (path) { return path && path.id === id; }) || null;
}
function inferLearningPath(target) {
if (!target || typeof LEARNING_PATHS === 'undefined' || !Array.isArray(LEARNING_PATHS)) return null;
var preferredIds = ['agent-skills', 'model-context-protocol'];
for (var i = 0; i < preferredIds.length; i++) {
var preferred = findLearningPath(preferredIds[i]);
if (preferred && learningPathContains(preferred, target)) return preferred;
}
return LEARNING_PATHS.find(function (path) { return learningPathContains(path, target); }) || null;
}
function learningPathEntries(path, includeOptional) {
if (!path) return [];
var required = Array.isArray(path.lessons) ? path.lessons : [];
var optional = includeOptional && Array.isArray(path.optionalLessons) ? path.optionalLessons : [];
return required.concat(optional);
}
function learningPathContains(path, target) {
return learningPathEntries(path, true).some(function (entry) {
return learningPathEntryPath(entry) === target;
});
}
function learningPathLessonHref(path) {
return 'lesson?path=' + encodeURIComponent(path) +
'&learningPath=' + encodeURIComponent(learningPath.id);
}
function learningPathPrerequisiteLabel(path) {
var core = phaseLessonForPath(path);
if (!core) return path;
return 'Phase ' + String(core.phase.id).padStart(2, '0') + ' · ' + core.lesson.name;
}
function unmetLearningPathPrerequisites(entry) {
var prerequisites = entry && Array.isArray(entry.prerequisitePaths)
? entry.prerequisitePaths
: [];
return prerequisites.filter(function (prerequisitePath) {
return !(window.AIFSProgress && window.AIFSProgress.isLessonComplete(prerequisitePath));
});
}
function learningPathPrerequisiteChecks(entry) {
if (!learningPathMode || !window.AIFSLearningPathProgress) return [];
return window.AIFSLearningPathProgress.unmetChecks(learningPath, entry);
}
function learningPathCheckIds(entry) {
return entry && Array.isArray(entry.prerequisiteChecks)
? entry.prerequisiteChecks.filter(function (id) { return typeof id === 'string' && id.length > 0; })
: [];
}
function learningPathPrerequisitePaths(entry) {
return entry && Array.isArray(entry.prerequisitePaths)
? entry.prerequisitePaths.filter(function (path) { return typeof path === 'string' && path.length > 0; })
: [];
}
function learningPathEntryLocked(entry) {
return unmetLearningPathPrerequisites(entry).length > 0 ||
learningPathPrerequisiteChecks(entry).length > 0;
}
function learningPathGateClass(entry) {
return learningPathEntryLocked(entry) ? ' learning-path-locked' : '';
}
function learningPathGateAttributes(entry) {
var ids = learningPathCheckIds(entry);
var paths = learningPathPrerequisitePaths(entry);
if (!ids.length && !paths.length) return '';
var locked = learningPathEntryLocked(entry);
return ' data-learning-path-id="' + escapeAttr(learningPath.id) + '"' +
' data-learning-path-gate="true"' +
' data-prerequisite-checks="' + escapeAttr(ids.join(' ')) + '"' +
' data-prerequisite-paths="' + escapeAttr(paths.join(' ')) + '"' +
(locked ? ' aria-disabled="true"' : '');
}
function learningPathPreflightDefinitions() {
if (!learningPathMode) return [];
var referenced = {};
for (var lessonIndex = 0; lessonIndex < flatLessons.length; lessonIndex++) {
var ids = learningPathCheckIds(flatLessons[lessonIndex]);
for (var checkIndex = 0; checkIndex < ids.length; checkIndex++) referenced[ids[checkIndex]] = true;
}
var definitions = Array.isArray(learningPath.prerequisites) ? learningPath.prerequisites : [];
return Object.keys(referenced).map(function (id) {
return definitions.find(function (entry) { return entry && entry.id === id; });
}).filter(Boolean);
}
function learningPathPreflightHtml() {
var checks = learningPathPreflightDefinitions();
if (!checks.length) return '';
var html = '<div class="learning-path-preflights" aria-label="Knowledge preflights">';
for (var index = 0; index < checks.length; index++) {
var check = checks[index];
var confirmed = window.AIFSLearningPathProgress && window.AIFSLearningPathProgress.isConfirmed(learningPath.id, check.id);
html += '<div class="learning-path-preflight' + (confirmed ? ' confirmed' : '') + '" data-learning-path-preflight="' + escapeAttr(check.id) + '">';
html += '<div class="learning-path-preflight-copy"><div class="learning-path-preflight-label">Required knowledge preflight</div>';
html += '<div class="learning-path-preflight-title">' + escapeHtml(check.title || check.id) + '</div>';
if (check.description) html += '<div class="learning-path-preflight-description">' + escapeHtml(check.description) + '</div>';
html += '</div><div class="learning-path-preflight-actions">';
html += '<div class="learning-path-preflight-status" role="status" aria-live="polite">' + (confirmed ? 'Confirmed' : 'Confirmation required') + '</div>';
html += '<button type="button" class="output-btn learning-path-confirm" data-learning-path-check-id="' + escapeAttr(check.id) + '" aria-pressed="' + (confirmed ? 'true' : 'false') + '" onclick="confirmLearningPathCheck(this)"' + (confirmed ? ' disabled' : '') + '>' + (confirmed ? 'Confirmed' : 'Confirm preflight') + '</button>';
html += '</div></div>';
}
html += '</div>';
return html;
}
function learningPathPrerequisiteCallout(entry, heading) {
var unmet = unmetLearningPathPrerequisites(entry);
var unmetChecks = learningPathPrerequisiteChecks(entry);
if (!unmet.length && !unmetChecks.length) return '';
var html = '<div class="continue-callout prerequisite-callout" role="status" tabindex="-1" data-learning-path-prerequisite-callout="true"' +
' data-prerequisite-paths="' + escapeAttr(unmet.join(' ')) + '"' +
' data-prerequisite-checks="' + escapeAttr(unmetChecks.map(function (check) { return check.id; }).join(' ')) + '"><strong>' +
escapeHtml(heading || 'Prerequisite check') + '</strong><br>';
for (var index = 0; index < unmet.length; index++) {
var prerequisitePath = unmet[index];
var label = learningPathPrerequisiteLabel(prerequisitePath);
if (prerequisitePath === lessonPath) {
html += '<span>Complete this lesson: ' + escapeHtml(label) + '.</span>';
} else {
var insideFocusedPath = learningPathContains(learningPath, prerequisitePath);
var href = insideFocusedPath
? learningPathLessonHref(prerequisitePath)
: 'lesson?path=' + encodeURIComponent(prerequisitePath);
html += '<a href="' + href + '"' + (insideFocusedPath ? '' : ' target="_blank" rel="noopener"') + '>Detour: ' + escapeHtml(label) + '</a>';
}
if (index < unmet.length - 1) html += '<br>';
}
if (unmet.some(function (prerequisitePath) { return !learningPathContains(learningPath, prerequisitePath); })) {
html += '<br><span>The external prerequisite opens in a new tab so this focused route stays in place.</span>';
}
for (var checkIndex = 0; checkIndex < unmetChecks.length; checkIndex++) {
var check = unmetChecks[checkIndex];
if (unmet.length || checkIndex > 0) html += '<br>';
html += '<span>Knowledge preflight: ' + escapeHtml(check.title || check.id) + '.</span>';
html += '<button type="button" class="output-btn learning-path-confirm" data-learning-path-check-id="' + escapeAttr(check.id) + '" aria-pressed="false" onclick="confirmLearningPathCheck(this)">Confirm preflight</button>';
}
html += '</div>';
return html;
}
function linkUnmetLearningPathCheckIds(link) {
if (!learningPathMode || !link || !window.AIFSLearningPathProgress) return [];
var ids = (link.getAttribute('data-prerequisite-checks') || '').split(/\s+/).filter(Boolean);
return ids.filter(function (id) {
return !window.AIFSLearningPathProgress.isConfirmed(learningPath.id, id);
});
}
function linkUnmetLearningPathPrerequisitePaths(link) {
if (!learningPathMode || !link) return [];
var paths = (link.getAttribute('data-prerequisite-paths') || '').split(/\s+/).filter(Boolean);
return paths.filter(function (path) {
return !(window.AIFSProgress && window.AIFSProgress.isLessonComplete(path));
});
}
function linkHasUnmetLearningPathPrerequisite(link) {
if (!learningPathMode || !link) return false;
return linkUnmetLearningPathPrerequisitePaths(link).length > 0 ||
linkUnmetLearningPathCheckIds(link).length > 0;
}
function syncLearningPathGateLinks() {
var links = document.querySelectorAll('a[data-learning-path-gate]');
for (var index = 0; index < links.length; index++) {
var locked = linkHasUnmetLearningPathPrerequisite(links[index]);
links[index].classList.toggle('learning-path-locked', locked);
if (locked) links[index].setAttribute('aria-disabled', 'true');
else links[index].removeAttribute('aria-disabled');
var label = links[index].querySelector('[data-learning-path-gate-label]');
if (label) label.textContent = locked ? 'Locked' : 'Next \u2192';
}
}
function findLearningPathConfirmButton(checkId) {
var buttons = document.querySelectorAll('[data-learning-path-check-id]');
for (var index = 0; index < buttons.length; index++) {
if (buttons[index].getAttribute('data-learning-path-check-id') === checkId && !buttons[index].disabled) return buttons[index];
}
return null;
}
function findLearningPathPrerequisiteCallout(paths) {
if (!paths.length) return null;
var callouts = document.querySelectorAll('[data-learning-path-prerequisite-callout]');
for (var index = 0; index < callouts.length; index++) {
var describedPaths = (callouts[index].getAttribute('data-prerequisite-paths') || '').split(/\s+/).filter(Boolean);
if (paths.some(function (path) { return describedPaths.indexOf(path) >= 0; })) return callouts[index];
}
return null;
}
function ensureLearningPathPrerequisiteCallout(link) {
var paths = linkUnmetLearningPathPrerequisitePaths(link);
var callout = findLearningPathPrerequisiteCallout(paths);
if (callout || !paths.length) return callout;
var panel = document.getElementById('continuePathPanel') || document.getElementById('learningPathPanel');
if (!panel) return null;
var wrapper = document.createElement('div');
wrapper.innerHTML = learningPathPrerequisiteCallout({ prerequisitePaths: paths }, 'Required before this lesson');
callout = wrapper.firstElementChild;
if (!callout) return null;
var body = panel.querySelector('.continue-panel') || panel;
body.appendChild(callout);
return callout;
}
function initLearningPathGateNavigation() {
document.addEventListener('click', function (event) {
var link = event.target && event.target.closest ? event.target.closest('a[data-learning-path-gate]') : null;
if (!link || !linkHasUnmetLearningPathPrerequisite(link)) return;
event.preventDefault();
var firstId = linkUnmetLearningPathCheckIds(link)[0] || '';
var button = findLearningPathConfirmButton(firstId);
var pathCallout = button ? null : ensureLearningPathPrerequisiteCallout(link);
var feedbackTarget = button || pathCallout;
if (feedbackTarget) {
feedbackTarget.scrollIntoView({ behavior: preferredScrollBehavior(), block: 'center' });
feedbackTarget.focus();
}
});
}
window.confirmLearningPathCheck = function (button) {
if (!learningPathMode || !button || !window.AIFSLearningPathProgress) return false;
var checkId = button.getAttribute('data-learning-path-check-id') || '';
if (!window.AIFSLearningPathProgress.confirm(learningPath.id, checkId)) return false;
refreshProgressUi();
return true;
};
function lessonProgressClass(path, readable) {
if (!readable) return 'planned';
if (!window.AIFSProgress || !path) return '';
if (window.AIFSProgress.isLessonComplete(path)) return 'complete';
if (window.AIFSProgress.hasLessonActivity(path)) return 'in-progress';
return '';
}
function currentCertificationQuizVersion() {
if (!certificationLesson) return '';
var value = certificationLesson.quizVersion;
if (typeof value !== 'string' && typeof value !== 'number') return '';
return String(value).trim();
}
function mutateLessonProgress(mutator) {
if (!lessonPath || typeof mutator !== 'function') return false;
try {
var raw = localStorage.getItem(PROGRESS_STORAGE_KEY);
var state = raw ? JSON.parse(raw) : { schemaVersion: 2, lessons: {}, updatedAt: 0 };
if (!state || typeof state !== 'object' || !state.lessons || typeof state.lessons !== 'object') return false;
var lesson = state.lessons[lessonPath];
if (!lesson || typeof lesson !== 'object') {
lesson = { answers: {}, quizPassedAt: null, checkpoints: {}, completedAt: null, completionSource: '', visitedAt: Date.now() };
state.lessons[lessonPath] = lesson;
}
if (!mutator(lesson)) return false;
state.updatedAt = Date.now();
localStorage.setItem(PROGRESS_STORAGE_KEY, JSON.stringify(state));
return true;
} catch (_) {
return false;
}
}
function mutateCertificationLessonProgress(mutator) {
if (!certificationLesson) return false;
return mutateLessonProgress(mutator);
}
function syncCertificationQuizProgressVersion() {
var currentVersion = currentCertificationQuizVersion();
if (!currentVersion) return;
mutateCertificationLessonProgress(function (lesson) {
var storedVersion = lesson.quizVersion === undefined || lesson.quizVersion === null
? ''
: String(lesson.quizVersion);
if (storedVersion === currentVersion) return false;
lesson.answers = {};
lesson.quizPassedAt = null;
lesson.quizVersion = currentVersion;
return true;
});
}
function clearQuizSectionProgress(section) {
if (!section) return false;
var questionIds = Array.prototype.map.call(section.querySelectorAll('.quiz-question[id]'), function (question) {
return question.id;
});
if (!questionIds.length) return false;
var currentVersion = currentCertificationQuizVersion();
var changed = mutateLessonProgress(function (lesson) {
if (!lesson.answers || typeof lesson.answers !== 'object') lesson.answers = {};
for (var i = 0; i < questionIds.length; i++) delete lesson.answers[questionIds[i]];
lesson.quizPassedAt = null;
if (currentVersion) lesson.quizVersion = currentVersion;
return true;
});
if (changed) refreshProgressUi();
return changed;
}
function initializeLessonData() {
var certs = certificationData();
certificationLesson = certs && certs.lessonsByPath ? certs.lessonsByPath[lessonPath] || null : null;
certificationOriginTrack = originatingTrackId ? findCertificationTrack(originatingTrackId) : null;
certificationTrack = null;
if (certificationOriginTrack && certificationTrackContains(certificationOriginTrack, lessonPath)) {
certificationTrack = certificationOriginTrack;
} else if (!certificationOriginTrack) {
var requestedTrack = requestedTrackId ? findCertificationTrack(requestedTrackId) : null;
if (requestedTrack && certificationTrackContains(requestedTrack, lessonPath)) {
certificationTrack = requestedTrack;
} else if (requestedTrack) {
certificationOriginTrack = requestedTrack;
}
}
if (!certificationTrack && !certificationOriginTrack && certificationLesson && Array.isArray(certificationLesson.trackIds)) {
certificationTrack = findCertificationTrack(certificationLesson.trackIds[0]);
}
certificationMode = !!(certificationTrack && certificationTrackContains(certificationTrack, lessonPath));
certificationSupplementalMode = !!(certificationOriginTrack && !certificationMode);
learningPath = !certificationMode && !certificationSupplementalMode
? (requestedLearningPathId ? findLearningPath(requestedLearningPathId) : inferLearningPath(lessonPath))
: null;
learningPathMode = !!(learningPath && learningPathContains(learningPath, lessonPath));
if (learningPathMode && !incomingLearningPathId) replaceLearningPathInUrl(learningPath.id);
syncCertificationQuizProgressVersion();
if (certificationLesson) {
document.documentElement.lang = 'en';
document.documentElement.dir = 'ltr';
var langPicker = document.getElementById('langPicker');
if (langPicker) langPicker.hidden = true;
}
if (certificationMode) {
for (var ci = 0; ci < certificationTrack.lessons.length; ci++) {
var certRef = certificationTrack.lessons[ci];
var certPath = certificationRefPath(certRef);
if (!certPath) continue;
var embedded = certs.lessonsByPath[certPath] || null;
var core = embedded ? null : phaseLessonForPath(certPath);
var display = embedded || (core && core.lesson) || {};
flatLessons.push({
lessonIndex: ci,
lessonName: display.name || display.title || certPath.split('/').pop().replace(/^\d+-/, '').replace(/-/g, ' '),
lessonSlug: certPath.split('/').pop(),
status: display.status || 'complete',
type: certRef.role || display.type || 'Certification',
lang: display.languages || display.lang || '—',
url: display.url || '',
path: certPath,
isReadable: true,
domains: Array.isArray(certRef.domains) ? certRef.domains : [],
required: certRef.required !== false,
});
if (certPath === lessonPath) currentLessonIndex = flatLessons.length - 1;
}
} else if (learningPathMode) {
var requiredCount = Array.isArray(learningPath.lessons) ? learningPath.lessons.length : 0;
var pathEntries = learningPathEntries(learningPath, true);
for (var lp = 0; lp < pathEntries.length; lp++) {
var pathEntry = pathEntries[lp];
var pathValue = learningPathEntryPath(pathEntry);
var coreLesson = phaseLessonForPath(pathValue);
if (!coreLesson) continue;
var corePhaseSlug = extractPhaseSlug(coreLesson.phase, coreLesson.phaseIndex);
var coreLessonSlug = extractLessonSlug(coreLesson.lesson, coreLesson.lessonIndex);
flatLessons.push({
phaseIndex: coreLesson.phaseIndex,
lessonIndex: coreLesson.lessonIndex,
phaseName: coreLesson.phase.name,
phaseId: coreLesson.phase.id,
phaseSlug: corePhaseSlug,
lessonName: pathEntry && typeof pathEntry === 'object' && pathEntry.title
? String(pathEntry.title)
: coreLesson.lesson.name,
lessonSlug: coreLessonSlug,
status: coreLesson.lesson.status,
type: coreLesson.lesson.type,
lang: coreLesson.lesson.lang,
url: coreLesson.lesson.url,
path: pathValue,
isReadable: true,
required: lp < requiredCount && (!pathEntry || typeof pathEntry === 'string' || pathEntry.required !== false),
minutes: pathEntry && typeof pathEntry === 'object' ? Number(pathEntry.minutes || 0) : 0,
group: pathEntry && typeof pathEntry === 'object' ? String(pathEntry.group || '') : '',
checkpoint: pathEntry && typeof pathEntry === 'object' ? String(pathEntry.checkpoint || '') : '',
checkpointEvidence: pathEntry && typeof pathEntry === 'object' && Array.isArray(pathEntry.checkpointEvidence)
? pathEntry.checkpointEvidence.slice()
: [],
prerequisitePaths: pathEntry && typeof pathEntry === 'object' && Array.isArray(pathEntry.prerequisitePaths)
? pathEntry.prerequisitePaths.slice()
: [],
prerequisiteChecks: pathEntry && typeof pathEntry === 'object' && Array.isArray(pathEntry.prerequisiteChecks)
? pathEntry.prerequisiteChecks.slice()
: []
});
if (pathValue === lessonPath) {
currentPhaseIndex = coreLesson.phaseIndex;
currentLessonIndex = flatLessons.length - 1;
}
}
} else if (typeof PHASES !== 'undefined') {
for (var i = 0; i < PHASES.length; i++) {
var p = PHASES[i];
var pSlug = extractPhaseSlug(p, i);
for (var j = 0; j < p.lessons.length; j++) {
var l = p.lessons[j];
var lSlug = extractLessonSlug(l, j);
flatLessons.push({
phaseIndex: i,
lessonIndex: j,
phaseName: p.name,
phaseId: p.id,
phaseSlug: pSlug,
lessonName: l.name,
lessonSlug: lSlug,
status: l.status,
type: l.type,
lang: l.lang,
url: l.url,
path: 'phases/' + pSlug + '/' + lSlug,
isReadable: l.status === 'complete' || !!l.url
});
if (pSlug === currentPhaseSlug && lSlug === currentLessonSlug) {
currentPhaseIndex = i;
currentLessonIndex = flatLessons.length - 1;
}
}
}
}
}
function extractPhaseSlug(phase, index) {
if (phase.url) {
var m = phase.url.match(/phases\/([^/]+)/);
if (m) return m[1];
}
if (phase.lessons && phase.lessons.length > 0 && phase.lessons[0].url) {
var m2 = phase.lessons[0].url.match(/phases\/([^/]+)/);
if (m2) return m2[1];
}
return String(phase.id).padStart(2, '0') + '-' + phase.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
function extractLessonSlug(lesson, index) {
if (lesson.url) {
var m = lesson.url.match(/phases\/[^/]+\/([^/]+)/);
if (m) return m[1].replace(/\/$/, '');
}
return String(index + 1).padStart(2, '0') + '-' + lesson.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
function buildSidebar() {
var sidebar = document.getElementById('lessonSidebar');
if (!sidebar) return;
if (certificationSupplementalMode) {
var originLabel = certificationTrackLabel(certificationOriginTrack);
var supplementalCore = phaseLessonForPath(lessonPath);
var supplementalLesson = certificationLesson || (supplementalCore && supplementalCore.lesson) || {};
var lessonLabel = supplementalLesson.name || supplementalLesson.title || lessonPath.split('/').pop().replace(/^\d+-/, '').replace(/-/g, ' ');
var supplementalProgress = lessonProgressClass(lessonPath, true);
var supplementalHtml = '<div class="sidebar-phase-nav"><a href="certification?id=' + encodeURIComponent(certificationOriginTrack.id) + '">&larr; Back to ' + escapeHtml(originLabel) + '</a></div>';
supplementalHtml += '<div class="sidebar-phase-header">Supplemental lesson</div>';
supplementalHtml += '<span class="sidebar-lesson-link active"><span class="sidebar-lesson-dot ' + supplementalProgress + '"></span>' + escapeHtml(lessonLabel) + '</span>';
sidebar.innerHTML = supplementalHtml;
return;
}
if (certificationMode) {
var certHtml = '<div class="sidebar-phase-nav"><a href="certification?id=' + encodeURIComponent(certificationTrack.id) + '">&larr; Back to ' + escapeHtml(certificationTrack.examCode || certificationTrack.shortName || 'track') + '</a></div>';
certHtml += '<div class="sidebar-phase-header">' + escapeHtml(certificationTrack.examCode || 'Certification') + ' · ' + escapeHtml(certificationTrack.shortName || certificationTrack.credential || certificationTrack.title || 'Study path') + '</div>';
for (var ci = 0; ci < flatLessons.length; ci++) {
var certLesson = flatLessons[ci];
var certActive = certLesson.path === lessonPath;
var certProgress = lessonProgressClass(certLesson.path, true);
certHtml += '<a class="sidebar-lesson-link' + (certActive ? ' active' : '') + '" href="lesson?path=' + encodeURIComponent(certLesson.path) + '&track=' + encodeURIComponent(certificationTrack.id) + '">';
certHtml += '<span class="sidebar-lesson-dot ' + certProgress + '"></span>';
certHtml += escapeHtml(certLesson.lessonName) + '</a>';
}
sidebar.innerHTML = certHtml;
return;
}
if (learningPathMode) {
var pathReturnHref = 'catalog.html';
var pathReturnLabel = 'Full course catalog';
if (learningPath.kind === 'career-route') {
pathReturnHref = 'learning-paths.html#career-route-' + encodeURIComponent(learningPath.id);
pathReturnLabel = 'Back to career route';
}
var pathHtml = '<div class="sidebar-phase-nav"><a href="' + pathReturnHref + '">&larr; ' + pathReturnLabel + '</a></div>';
var requiredPathLessons = flatLessons.filter(function (item) { return item.required !== false; });
var optionalPathLessons = flatLessons.filter(function (item) { return item.required === false; });
pathHtml += '<div class="sidebar-phase-header">' + escapeHtml(learningPath.title || learningPath.id) + ' · ' + requiredPathLessons.length + ' lessons</div>';
for (var lpi = 0; lpi < requiredPathLessons.length; lpi++) {
var pathLesson = requiredPathLessons[lpi];
var pathActive = pathLesson.path === lessonPath;
pathHtml += '<a class="sidebar-lesson-link' + (pathActive ? ' active' : '') + learningPathGateClass(pathLesson) + '" href="' + learningPathLessonHref(pathLesson.path) + '"' + learningPathGateAttributes(pathLesson) + '>';
pathHtml += '<span class="sidebar-lesson-dot ' + lessonProgressClass(pathLesson.path, true) + '"></span>';
pathHtml += escapeHtml(pathLesson.lessonName) + '</a>';
}
if (optionalPathLessons.length) {
pathHtml += '<div class="sidebar-phase-header">Optional capstone</div>';
for (var lpo = 0; lpo < optionalPathLessons.length; lpo++) {
var optionalLesson = optionalPathLessons[lpo];
var optionalActive = optionalLesson.path === lessonPath;
pathHtml += '<a class="sidebar-lesson-link' + (optionalActive ? ' active' : '') + learningPathGateClass(optionalLesson) + '" href="' + learningPathLessonHref(optionalLesson.path) + '"' + learningPathGateAttributes(optionalLesson) + '>';
pathHtml += '<span class="sidebar-lesson-dot ' + lessonProgressClass(optionalLesson.path, true) + '"></span>';
pathHtml += escapeHtml(optionalLesson.lessonName) + '</a>';
}
}
sidebar.innerHTML = pathHtml;
return;
}
if (typeof PHASES === 'undefined') return;
var html = '';
var prevPhaseIdx = currentPhaseIndex > 0 ? currentPhaseIndex - 1 : -1;
var nextPhaseIdx = (currentPhaseIndex >= 0 && currentPhaseIndex < PHASES.length - 1) ? currentPhaseIndex + 1 : -1;
if (prevPhaseIdx >= 0 || nextPhaseIdx >= 0) {
html += '<div class="sidebar-phase-nav">';
if (prevPhaseIdx >= 0) {
var pp = PHASES[prevPhaseIdx];
var ppSlug = extractPhaseSlug(pp, prevPhaseIdx);
var ppFirstLesson = pp.lessons[0] ? extractLessonSlug(pp.lessons[0], 0) : '';
html += '<a href="lesson?path=phases/' + ppSlug + '/' + ppFirstLesson + '">&larr; Phase ' + String(pp.id).padStart(2, '0') + ': ' + escapeHtml(pp.name) + '</a>';
}
if (nextPhaseIdx >= 0) {
var np = PHASES[nextPhaseIdx];
var npSlug = extractPhaseSlug(np, nextPhaseIdx);
var npFirstLesson = np.lessons[0] ? extractLessonSlug(np.lessons[0], 0) : '';
html += '<a href="lesson?path=phases/' + npSlug + '/' + npFirstLesson + '">Phase ' + String(np.id).padStart(2, '0') + ': ' + escapeHtml(np.name) + ' &rarr;</a>';
}
html += '</div>';
}
if (currentPhaseIndex >= 0) {
var phase = PHASES[currentPhaseIndex];
html += '<div class="sidebar-phase-header">Phase ' + String(phase.id).padStart(2, '0') + ' · ' + escapeHtml(phase.name) + '</div>';
for (var k = 0; k < phase.lessons.length; k++) {
var les = phase.lessons[k];
var lesSlug = extractLessonSlug(les, k);
var pSlugCurrent = extractPhaseSlug(phase, currentPhaseIndex);
var isActive = lesSlug === currentLessonSlug;
var lesReadable = les.status === 'complete' || !!les.url;
var lesPath = 'phases/' + pSlugCurrent + '/' + lesSlug;
if (lesReadable) {
html += '<a class="sidebar-lesson-link' + (isActive ? ' active' : '') + '" href="lesson?path=' + lesPath + '">';
} else {
html += '<span class="sidebar-lesson-link disabled">';
}
html += '<span class="sidebar-lesson-dot ' + lessonProgressClass(lesPath, lesReadable) + '"></span>';
html += escapeHtml(les.name);
html += lesReadable ? '</a>' : '</span>';
}
}
sidebar.innerHTML = html;
}
function initSidebarToggle() {
var toggle = document.getElementById('sidebarToggle');
var sidebar = document.getElementById('lessonSidebar');
if (!toggle || !sidebar) return;
function setOpen(open) {
sidebar.classList.toggle('open', open);
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
}
toggle.addEventListener('click', function () {
setOpen(!sidebar.classList.contains('open'));
});
document.addEventListener('click', function (e) {
if (window.innerWidth <= 900 && sidebar.classList.contains('open')) {
if (!sidebar.contains(e.target) && !toggle.contains(e.target)) {
setOpen(false);
}
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && window.innerWidth <= 900 && sidebar.classList.contains('open')) {
setOpen(false);
toggle.focus();
}
});
}
function initScrollProgress() {
var bar = document.getElementById('scrollProgress');
if (!bar) return;
var frame = 0;
function update() {
var scrollTop = window.scrollY || document.documentElement.scrollTop;
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
var ratio = docHeight > 0 ? scrollTop / docHeight : 0;
bar.style.transform = 'scaleX(' + Math.max(0, Math.min(1, ratio)) + ')';
frame = 0;
}
function schedule() {
if (frame) return;
frame = window.requestAnimationFrame(update);
}
window.addEventListener('scroll', schedule, { passive: true });
window.addEventListener('resize', schedule);
schedule();
}
var lessonQuizPromise = null;
var lessonQuizCorrectAnswers = Object.create(null);
// language state (current lang + RTL direction) is owned by lang-picker.js
function currentLang() {
return window.AIFS_currentLang ? window.AIFS_currentLang() : 'en';
}
function applyRenderedLanguage(lang) {
var actual = lang || 'en';
if (window.AIFS_applyLangDir) {
window.AIFS_applyLangDir(actual);
return;
}
document.documentElement.lang = actual;
document.documentElement.dir = /^(ar|he|fa|ur)$/i.test(actual) ? 'rtl' : 'ltr';
}
function fetchRepositoryFile(relativePath) {
var source = window.AIFSContentSource;
var localPreview = !!(source && typeof source.isLocal === 'function' && source.isLocal());
var fetchOptions = localPreview ? { cache: 'no-store' } : undefined;
var primary = source && source.repoUrl
? source.repoUrl(relativePath)
: 'https://raw.githubusercontent.com/' + SOURCE_OWNER + '/' + SOURCE_REPO + '/' + ACTIVE_REF + '/' + relativePath;
var fallback = source && source.rawRepoUrl ? source.rawRepoUrl(relativePath) : primary;
return fetch(primary, fetchOptions).then(function (response) {
if (response.ok) return response;
throw new Error('fetch-failed');
}).catch(function (error) {
if (!fallback || fallback === primary) throw error;
return fetch(fallback, fetchOptions).then(function (response) {
if (!response.ok) throw new Error('fetch-failed');
return response;
});
});
}
var lessonFetchSequence = 0;
function fetchLesson(path, retried, requestSequence) {
if (!retried || !requestSequence) requestSequence = ++lessonFetchSequence;
if (certificationLesson && path === certificationLesson.path && typeof certificationLesson.markdown === 'string') {
applyRenderedLanguage('en');
lessonQuizPromise = Promise.resolve(certificationLesson.quiz || null);
try {
renderLesson(certificationLesson.markdown);
} catch (embeddedError) {
console.error('renderLesson failed', embeddedError);
showError('Render error', 'Loaded the certification lesson but failed to render it. Details are in the browser console.');
return;
}
lessonQuizPromise.then(function (embeddedQuiz) {
if (!embeddedQuiz || requestSequence !== lessonFetchSequence) return;
try { renderQuiz(embeddedQuiz); }
catch (quizError) { console.error('renderQuiz failed', quizError); }
});
return;
}
// Local previews read the checked-out repository first. Deploys and a
// missing local file use the branch-aware raw source from build-meta.
var enPath = path + '/docs/en.md';
var lang = currentLang();
var translatedUrl = 'https://raw.githubusercontent.com/rohitg00/ai-engineering-from-scratch/translations/i18n/'
+ lang + '/' + path + '/docs/' + lang + '.md';
if (!retried || !lessonQuizPromise) {
lessonQuizPromise = fetchRepositoryFile(path + '/quiz.json')
.then(function (res) { return res.json(); })
.catch(function () { return null; });
}
var quizPromiseForRequest = lessonQuizPromise;
var lessonDocument = lang === 'en'
? fetchRepositoryFile(enPath).then(function (response) {
return { response: response, lang: 'en' };
})
: fetch(translatedUrl).then(function (response) {
if (!response.ok) throw new Error('fetch-failed');
return { response: response, lang: lang };
}).catch(function () {
return fetchRepositoryFile(enPath).then(function (response) {
return { response: response, lang: 'en' };
});
});
lessonDocument
.then(function (documentResult) {
return documentResult.response.text().then(function (markdown) {
return { markdown: markdown, lang: documentResult.lang };
});
})
.then(function (documentResult) {
if (requestSequence !== lessonFetchSequence) return;
applyRenderedLanguage(documentResult.lang);
try {
renderLesson(documentResult.markdown);
} catch (err) {
console.error('renderLesson failed', err);
showError('Render error', 'Loaded the lesson markdown but failed to render it. Details in the browser console.');
return;
}
quizPromiseForRequest
.then(function (data) {
if (!data || requestSequence !== lessonFetchSequence) return;
try { renderQuiz(data); }
catch (err) { console.error('renderQuiz failed', err); }
});
})
.catch(function () {
if (requestSequence !== lessonFetchSequence) return;
if (!retried) {
lessonQuizPromise = null;
setTimeout(function () { fetchLesson(path, true, requestSequence); }, 600);
return;
}
var ghLink = ' <a href="' + REPO_TREE + escapeAttr(path) + '" target="_blank" rel="noopener">Read it on GitHub</a>.';
var listed = currentLessonIndex >= 0 && flatLessons[currentLessonIndex].isReadable;
if (listed) {
showError('Lesson temporarily unavailable', 'Could not load the lesson at <code>' + escapeHtml(path) + '</code> right now. Try reloading the page.' + ghLink);
} else {
showError('Lesson not found', 'Could not fetch the lesson at <code>' + escapeHtml(path) + '</code>. It may not have been written yet.' + ghLink);
}
});
}
function showError(title, msg) {
var el = document.getElementById('lessonContent');
disposeRenderedLesson(el);
var contextTrack = certificationContextTrack();
var backHref = contextTrack ? 'certification?id=' + encodeURIComponent(contextTrack.id) : 'index.html';
var backLabel = contextTrack ? 'Back to Track' : 'Back to Home';
el.innerHTML = '<div class="lesson-error"><h2>' + title + '</h2><p>' + msg + '</p><a href="' + backHref + '" class="btn btn-primary">' + backLabel + '</a></div>';
}
function lessonDescription(md) {
var lines = md.split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var t = lines[i].trim();
if (t.indexOf('>') === 0) return t.replace(/^>\s*/, '').replace(/[*_`]/g, '').slice(0, 200);
}
for (var j = 0; j < lines.length; j++) {
var p = lines[j].trim();
if (p && p[0] !== '#' && p[0] !== '>' && p[0] !== '|' && p[0] !== '`' && p.indexOf('**') !== 0) {
return p.replace(/[*_`]/g, '').slice(0, 200);
}
}
return certificationLesson
? 'Independent, open-source certification preparation built around practical engineering decisions.'
: '523 lessons. 20 phases. Build it from raw math, by hand.';
}
function updateLessonSeo(title, md) {
var ORIGIN = 'https://aiengineeringfromscratch.com';
var path = new URLSearchParams(location.search).get('path') || '';
var url = ORIGIN + '/lesson?path=' + encodeURIComponent(path);
var contextTrack = certificationContextTrack();
var desc = lessonDescription(md);
var courseName = certificationLesson
? (contextTrack && (contextTrack.credential || contextTrack.title || contextTrack.shortName) || 'Certification preparation')
: learningPathMode ? (learningPath.title || 'Focused learning path') : 'AI Engineering from Scratch';
var fullTitle = title + ' · ' + courseName;
var set = function (sel, val) { var m = document.querySelector(sel); if (m) m.setAttribute('content', val); };
var canon = document.querySelector('link[rel="canonical"]');
if (canon) canon.setAttribute('href', url);
set('meta[name="description"]', desc);
set('meta[property="og:title"]', fullTitle);
set('meta[property="og:description"]', desc);
set('meta[name="twitter:title"]', fullTitle);
set('meta[name="twitter:description"]', desc);
var ogu = document.querySelector('meta[property="og:url"]');
if (!ogu) { ogu = document.createElement('meta'); ogu.setAttribute('property', 'og:url'); document.head.appendChild(ogu); }
ogu.setAttribute('content', url);
var prev = document.getElementById('lessonJsonLd');
if (prev) prev.remove();
var breadcrumbs;
if (certificationLesson) {
breadcrumbs = [
{ '@type': 'ListItem', position: 1, name: 'Home', item: ORIGIN },
{ '@type': 'ListItem', position: 2, name: 'Certifications', item: ORIGIN + '/certifications.html' }
];
if (contextTrack) {
breadcrumbs.push({ '@type': 'ListItem', position: 3, name: courseName, item: ORIGIN + '/certification?id=' + encodeURIComponent(contextTrack.id) });
}
breadcrumbs.push({ '@type': 'ListItem', position: breadcrumbs.length + 1, name: title, item: url });
} else {
breadcrumbs = [
{ '@type': 'ListItem', position: 1, name: 'Home', item: ORIGIN },
{ '@type': 'ListItem', position: 2, name: 'Catalog', item: ORIGIN + '/catalog.html' },
{ '@type': 'ListItem', position: 3, name: title, item: url }
];
}
var ld = {
'@context': 'https://schema.org',
'@graph': [
{ '@type': 'LearningResource', name: title, description: desc, url: url,
inLanguage: document.documentElement.lang || 'en', isAccessibleForFree: true,
isPartOf: { '@type': 'Course', name: courseName, url: contextTrack ? ORIGIN + '/certification?id=' + encodeURIComponent(contextTrack.id) : ORIGIN } },
{ '@type': 'BreadcrumbList', itemListElement: breadcrumbs }
]
};
var s = document.createElement('script');
s.type = 'application/ld+json';
s.id = 'lessonJsonLd';
s.textContent = JSON.stringify(ld);
document.head.appendChild(s);
}
var headingHashRestoreFrame = 0;
var mermaidRenderSeq = 0;
var mermaidPromise = null;
function disposeTocRuntime() {
var sidebar = document.getElementById('tocSidebar');
if (!sidebar) return;
if (sidebar._tocObserver && sidebar._tocObserver.disconnect) sidebar._tocObserver.disconnect();
if (sidebar._tocScrollHandler) window.removeEventListener('scroll', sidebar._tocScrollHandler);
if (sidebar._tocResizeHandler) window.removeEventListener('resize', sidebar._tocResizeHandler);
sidebar._tocObserver = null;
sidebar._tocScrollHandler = null;
sidebar._tocResizeHandler = null;
}
function disposeRenderedLesson(root) {
if (window.AIFSFigureRuntime && root) window.AIFSFigureRuntime.disposeRoot(root);
disposeTocRuntime();
mermaidRenderSeq++;
if (headingHashRestoreFrame) {
window.cancelAnimationFrame(headingHashRestoreFrame);
headingHashRestoreFrame = 0;
}
closeMermaidModal();
}
function restoreHeadingHash() {
if (!window.location.hash || window.location.hash.length < 2) return;
var id = window.location.hash.slice(1);
try { id = decodeURIComponent(id); } catch (_) {}
var target = document.getElementById(id);
if (!target) return;
if (headingHashRestoreFrame) window.cancelAnimationFrame(headingHashRestoreFrame);
headingHashRestoreFrame = window.requestAnimationFrame(function () {
headingHashRestoreFrame = 0;
target.scrollIntoView({ behavior: 'auto', block: 'start' });
});
}
function renderLesson(md) {
var el = document.getElementById('lessonContent');
disposeRenderedLesson(el);
var html = parseMd(md);
html += '<div class="ai-panels" id="aiPanels"></div>';
html = addBottomNav(html);
el.innerHTML = certificationDisclaimerHtml() + '<article class="lesson-article">' + html + '</article>';
var contextTrack = certificationContextTrack();
document.title = extractTitle(md) + (certificationLesson
? ' - ' + certificationTrackLabel(contextTrack)
: ' - AI Engineering from Scratch');
updateLessonSeo(extractTitle(md), md);
var article = el.querySelector('.lesson-article');
renderLessonActionPanel(article);
initCodeCopy();
renderMermaidBlocks(article);
var assetPromise = window.AIFS_loadFigureProviders
? window.AIFS_loadFigureProviders(article)
: Promise.resolve([]);
assetPromise.then(function () {
if (!article || !article.isConnected) return;
if (window.mountLessonFigures) window.mountLessonFigures(article);
});
renderAIPanels();
buildTOC();
restoreHeadingHash();
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
link.addEventListener('click', function (e) {
if (link.classList && link.classList.contains('toc-link')) return;
var target = document.querySelector(link.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: preferredScrollBehavior() });
}
});
});
}
function currentFocusedPathEntry() {
if (!learningPathMode || currentLessonIndex < 0) return null;
return flatLessons[currentLessonIndex] && flatLessons[currentLessonIndex].path === lessonPath
? flatLessons[currentLessonIndex]
: flatLessons.find(function (item) { return item.path === lessonPath; }) || null;
}
function lessonActionPathHtml() {
if (!learningPathMode || !learningPath) return '';
var entry = currentFocusedPathEntry();
var quickStart = learningPath.quickStart || null;
var html = '<div class="lesson-action-path"><div class="lesson-action-label">Focused path context</div>';
if (entry && entry.checkpointEvidence && entry.checkpointEvidence.length) {
html += '<p>Capture this checkpoint evidence:</p><ul>';
for (var i = 0; i < entry.checkpointEvidence.length; i++) {
html += '<li>' + escapeHtml(String(entry.checkpointEvidence[i])) + '</li>';
}
html += '</ul>';
} else if (entry && entry.checkpoint) {
html += '<p><strong>Checkpoint:</strong> ' + escapeHtml(entry.checkpoint) + '</p>';
}
if (quickStart && quickStart.lessonPath === lessonPath) {
html += '<p><strong>Quick start:</strong> ' + escapeHtml(quickStart.goal || 'Complete the first executable checkpoint.') + '</p>';
if (quickStart.estimatedMinutes) html += '<p>Allow about ' + escapeHtml(String(quickStart.estimatedMinutes)) + ' minutes for this first run.</p>';
var quickStartCommand = quickStart.command || quickStart.installCommand || '';
if (quickStartCommand) html += '<p><strong>Quick start command:</strong> <code>' + escapeHtml(quickStartCommand) + '</code></p>';
if (quickStart.expectedEvidence && quickStart.expectedEvidence.length) {
html += '<p>Quick start evidence:</p><ul>';
for (var q = 0; q < quickStart.expectedEvidence.length; q++) {
html += '<li>' + escapeHtml(String(quickStart.expectedEvidence[q])) + '</li>';
}
html += '</ul>';
}
}
html += '</div>';
return html;
}
function lessonActionRunFile(files) {
if (!Array.isArray(files)) return null;
var supported = /\.(py|ts|js|rs|jl|sh)$/i;
for (var i = 0; i < files.length; i++) {
var name = files[i] && String(files[i].name || '').trim();
if (/^main\.(py|ts|js|rs|jl|sh)$/i.test(name)) return files[i];
}
for (var j = 0; j < files.length; j++) {
var fallbackName = files[j] && String(files[j].name || '').trim();
if (files.length === 1 && supported.test(fallbackName)) return files[j];
}
return null;
}
function setLessonActionRun(panel, files) {
if (!panel || !panel.isConnected) return;
var commandEl = panel.querySelector('.lesson-action-command');
var copyBtn = panel.querySelector('.lesson-action-copy');
var note = panel.querySelector('.lesson-action-note');
var file = lessonActionRunFile(files);
if (!file) {
panel._lessonRunCommand = '';
if (commandEl) commandEl.textContent = 'No standalone main file detected';
if (copyBtn) copyBtn.hidden = true;
if (note) note.textContent = 'Use the worked examples as guidance. Mark Run only after you execute an appropriate example or exercise. A code fence is not automatically a runnable program.';
return;
}
var filePath = file.path || file.file || (lessonPath + '/code/' + file.name);
var command = repoRootCommand(file.name, filePath);
panel._lessonRunCommand = command;
if (commandEl) commandEl.textContent = command;
if (copyBtn) copyBtn.hidden = false;
if (note) note.textContent = 'Run from the repository root, the folder containing README.md.';
}
function resolveLessonActionRun(panel) {
if (certificationLesson && certificationLesson.files) {
setLessonActionRun(panel, certificationLesson.files.code || []);
return;
}
repositoryDirectoryFetch(lessonPath + '/code', function (err, files) {
setLessonActionRun(panel, err ? [] : files);
});
}
function renderLessonActionPanel(article) {
if (!article || !window.AIFSProgress) return;
var entry = currentFocusedPathEntry();
var panel = document.createElement('aside');
panel.id = 'lessonActionPanel';
panel.className = 'lesson-action-panel';
panel.setAttribute('aria-labelledby', 'lessonActionTitle');
var tags = '<span class="lesson-action-tag">Local progress</span>';
if (learningPathMode && learningPath) {
tags += '<span class="lesson-action-tag">' + escapeHtml(learningPath.title || learningPath.id) + '</span>';
if (learningPath.estimatedMinutes) tags += '<span class="lesson-action-tag">Path ' + escapeHtml(String(learningPath.estimatedMinutes)) + ' min</span>';
if (entry && entry.minutes) tags += '<span class="lesson-action-tag">Lesson ' + escapeHtml(String(entry.minutes)) + ' min</span>';
if (entry && entry.group) tags += '<span class="lesson-action-tag">Group ' + escapeHtml(entry.group) + '</span>';
}
panel.innerHTML =
'<div class="lesson-action-head"><div><h2 class="lesson-action-title" id="lessonActionTitle">Act on this lesson</h2>' +
'<p class="lesson-action-intro">Use this loop to turn reading into evidence. Each checkpoint is yours to mark and stays separate from quiz correctness.</p></div>' +
'<div class="lesson-action-status" role="status" aria-live="polite">0 of 5 checkpoints</div></div>' +
'<div class="lesson-action-loop" aria-label="Lesson workflow checkpoints">' +
'<button class="lesson-action-step" type="button" data-checkpoint="read"><span class="lesson-action-step-num">01</span><span class="lesson-action-step-title">Read</span><span class="lesson-action-step-copy">Understand the claim and constraints.</span></button>' +
'<button class="lesson-action-step" type="button" data-checkpoint="built"><span class="lesson-action-step-num">02</span><span class="lesson-action-step-title">Build</span><span class="lesson-action-step-copy">Create or modify the lesson artifact.</span></button>' +
'<button class="lesson-action-step" type="button" data-checkpoint="ran"><span class="lesson-action-step-num">03</span><span class="lesson-action-step-title">Run</span><span class="lesson-action-step-copy">Execute the relevant program or exercise.</span></button>' +
'<button class="lesson-action-step" type="button" data-checkpoint="evidence"><span class="lesson-action-step-num">04</span><span class="lesson-action-step-title">Prove</span><span class="lesson-action-step-copy">Capture output that supports the claim.</span></button>' +
'<button class="lesson-action-step" type="button" data-lesson-complete="true"><span class="lesson-action-step-num">05</span><span class="lesson-action-step-title">Continue</span><span class="lesson-action-step-copy">Mark the lesson complete when you are ready.</span></button>' +
'</div><div class="lesson-action-body"><div class="lesson-action-tags">' + tags + '</div>' +
'<div class="lesson-action-run"><div class="lesson-action-label">Run from the repository</div>' +
'<div class="lesson-action-command-row"><code class="lesson-action-command">Checking for a standalone runnable file</code><button class="lesson-action-copy" type="button" hidden>Copy command</button></div>' +
'<p class="lesson-action-note">The page is checking the lesson code directory for a standalone entry point.</p></div>' +
lessonActionPathHtml() + '</div>';
var metaTags = article.querySelectorAll('.lesson-meta-tag');
var anchor = metaTags.length ? metaTags[metaTags.length - 1] : article.querySelector('.learning-objectives, .motto, h1');
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(panel, anchor.nextSibling);
else article.insertBefore(panel, article.firstChild);
panel.querySelectorAll('[data-checkpoint]').forEach(function (button) {
button.addEventListener('click', function () {
window.AIFSProgress.toggleCheckpoint(lessonPath, button.getAttribute('data-checkpoint'));
});
});
var completeButton = panel.querySelector('[data-lesson-complete]');
if (completeButton) {
completeButton.addEventListener('click', function () {
if (window.AIFSProgress.isLessonComplete(lessonPath)) window.AIFSProgress.unmarkLessonComplete(lessonPath);
else window.AIFSProgress.markLessonComplete(lessonPath, 'learner');
});
}
var copyButton = panel.querySelector('.lesson-action-copy');
if (copyButton) {
copyButton.addEventListener('click', function () {
if (!panel._lessonRunCommand || !navigator.clipboard) return;
navigator.clipboard.writeText(panel._lessonRunCommand).then(function () {
copyButton.textContent = 'Copied';
window.setTimeout(function () {
if (copyButton.isConnected) copyButton.textContent = 'Copy command';
}, 1200);
});
});
}
syncLessonActionProgress();
resolveLessonActionRun(panel);
}
function syncLessonActionProgress() {
var panel = document.getElementById('lessonActionPanel');
if (!panel || !window.AIFSProgress || !lessonPath) return;
var progress = window.AIFSProgress.getLessonProgress(lessonPath);
var checkpoints = progress && progress.checkpoints ? progress.checkpoints : {};
var doneCount = 0;
panel.querySelectorAll('[data-checkpoint]').forEach(function (button) {
var done = !!checkpoints[button.getAttribute('data-checkpoint') + 'At'];
button.classList.toggle('is-done', done);
button.setAttribute('aria-pressed', done ? 'true' : 'false');
if (done) doneCount++;
});
var completeButton = panel.querySelector('[data-lesson-complete]');
var complete = !!(progress && progress.completedAt);
if (completeButton) {
completeButton.classList.toggle('is-done', complete);
completeButton.setAttribute('aria-pressed', complete ? 'true' : 'false');
}
if (complete) doneCount++;
var status = panel.querySelector('.lesson-action-status');
if (status) status.textContent = doneCount + ' of 5 checkpoints · Quiz ' + (progress && progress.quizPassedAt ? 'passed' : 'not yet passed');
}
function certificationDisclaimerHtml() {
if (!certificationLesson) return '';
var certs = certificationData();
var program = certs && certs.program ? certs.program : {};
var contextTrack = certificationContextTrack();
var disclaimer = program.disclaimer || 'This is independent community preparation and is not affiliated with, endorsed by, sponsored by, or authorized by Anthropic.';
var scoring = program.scoringNotice || 'Course practice percentages are not official certification scores and cannot guarantee a passing result.';
var breadcrumb = '<div class="cert-breadcrumb lesson-cert-breadcrumb" aria-label="Breadcrumb"><a href="certifications.html">Certifications</a>';
if (contextTrack) {
breadcrumb += '<span>/</span><a href="certification?id=' + encodeURIComponent(contextTrack.id) + '">' + escapeHtml(certificationTrackLabel(contextTrack)) + '</a>';
}
breadcrumb += '<span>/</span><span>' + (certificationSupplementalMode ? 'Supplemental lesson' : 'Lesson') + '</span></div>';
return breadcrumb + '<aside class="cert-notice lesson-cert-notice" aria-label="Independent certification preparation"><strong>Independent preparation</strong><p>' + escapeHtml(disclaimer) + '</p><p>' + escapeHtml(scoring) + '</p></aside>';
}
function buildTOC() {
var sidebar = document.getElementById('tocSidebar');
if (!sidebar) return;
var article = document.querySelector('.lesson-article');
if (!article) {
sidebar.innerHTML = '';
sidebar.removeAttribute('aria-hidden');
return;
}
var headings = article.querySelectorAll('h2, h3');
if (!headings.length) {
sidebar.innerHTML = '';
sidebar.removeAttribute('aria-hidden');
return;
}
var used = Object.create(null);
function ensureHeadingId(heading, idx) {
if (!heading) return '';
var base = '';
if (heading.id) {
base = heading.id;
} else if (typeof slugify === 'function') {
base = slugify(heading.textContent || '');
} else {
base = String(heading.textContent || '').toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
}
if (!base) base = 'section-' + String(idx + 1);
var candidate = base;
var n = 2;
while (used[candidate] || (document.getElementById(candidate) && document.getElementById(candidate) !== heading)) {
candidate = base + '-' + n;
n++;
}
used[candidate] = true;
heading.id = candidate;
return candidate;
}
var html = '<div class="toc-header">On this page</div>';
html += '<nav aria-label="On this page"><ul class="toc-nav">';
for (var i = 0; i < headings.length; i++) {
var heading = headings[i];
var level = heading.tagName.toLowerCase();
var id = ensureHeadingId(heading, i);
if (!id) continue;
var fullText = (heading.textContent || '').trim();
var text = fullText.replace(/^[^\p{L}\p{N}]+/u, '').trim();
if (!text) continue;
html += '<li class="toc-item toc-' + level + '"><a href="#' + id + '" class="toc-link" data-toc-id="' + id + '" title="' + escapeHtml(fullText) + '">' + escapeHtml(text) + '</a></li>';
}
html += '</ul></nav>';
sidebar.innerHTML = html;
sidebar.removeAttribute('aria-hidden');
sidebar.querySelectorAll('.toc-link').forEach(function (a) {
a.addEventListener('click', function (e) {
var href = a.getAttribute('href');
if (!href || href.charAt(0) !== '#') return;
var id = href.slice(1);
var target = document.getElementById(id);
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: preferredScrollBehavior(), block: 'start' });
history.replaceState(null, '', '#' + id);
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
target.addEventListener('blur', function blurHandler() {
target.removeAttribute('tabindex');
target.removeEventListener('blur', blurHandler);
});
}
target.focus({ preventScroll: true });
});
});
initScrollspy(headings, sidebar);
}
function initScrollspy(headings, sidebar) {
sidebar = sidebar || document.getElementById('tocSidebar');
if (!sidebar) return;
var links = sidebar.querySelectorAll('.toc-link');
if (!links.length || !headings || !headings.length) return;
var headerOffsetStr = getComputedStyle(document.documentElement).getPropertyValue('--header-offset').trim();
var headerOffset = parseInt(headerOffsetStr, 10) || 92;
if (sidebar._tocObserver && sidebar._tocObserver.disconnect) {
sidebar._tocObserver.disconnect();
sidebar._tocObserver = null;
}
if (sidebar._tocScrollHandler) {
window.removeEventListener('scroll', sidebar._tocScrollHandler);
sidebar._tocScrollHandler = null;
}
if (sidebar._tocResizeHandler) {
window.removeEventListener('resize', sidebar._tocResizeHandler);
sidebar._tocResizeHandler = null;
}
function setActive(id) {
if (!id || sidebar._tocActiveId === id) return;
sidebar._tocActiveId = id;
links.forEach(function (link) {
var href = link.getAttribute('href');
var match = href && href.charAt(0) === '#' ? href.slice(1) : '';
if (match === id) link.classList.add('active');
else link.classList.remove('active');
});
var activeLink = sidebar.querySelector('.toc-link.active');
if (activeLink) {
var linkTop = activeLink.offsetTop;
var sidebarHeight = sidebar.clientHeight;
var sidebarScroll = sidebar.scrollTop;
if (linkTop < sidebarScroll + 50 || linkTop > sidebarScroll + sidebarHeight - 100) {
sidebar.scrollTo({ top: Math.max(0, linkTop - sidebarHeight / 3), behavior: preferredScrollBehavior() });
}
}
}
function fallbackByScroll() {
var y = (window.scrollY || document.documentElement.scrollTop || 0) + headerOffset + 18;
var active = '';
for (var i = 0; i < headings.length; i++) {
var h = headings[i];
if (!h || !h.id) continue;
var top = h.getBoundingClientRect().top + window.scrollY;
if (top <= y) active = h.id;
else break;
}
if (!active && headings[0] && headings[0].id) active = headings[0].id;
if (active) setActive(active);
}
if (typeof IntersectionObserver !== 'undefined') {
var marginOffset = headerOffset - 4;
var obs = new IntersectionObserver(function (entries) {
var best = null;
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
if (!e.isIntersecting) continue;
if (!best) best = e;
else if (e.boundingClientRect.top < best.boundingClientRect.top) best = e;
}
if (best && best.target && best.target.id) setActive(best.target.id);
}, {
root: null,
threshold: [0.01, 0.1],
rootMargin: '-' + marginOffset + 'px 0px -70% 0px'
});
Array.from(headings).forEach(function (h) {
if (h && h.id) obs.observe(h);
});
sidebar._tocObserver = obs;
var initial = (location.hash && location.hash.charAt(0) === '#') ? location.hash.slice(1) : '';
if (initial && document.getElementById(initial)) setActive(initial);
else if (headings[0] && headings[0].id) setActive(headings[0].id);
return;
}
sidebar._tocScrollHandler = function () {
if (window._tocTicking) return;
window._tocTicking = true;
window.requestAnimationFrame(function () {
fallbackByScroll();
window._tocTicking = false;
});
};
window.addEventListener('scroll', sidebar._tocScrollHandler, { passive: true });
sidebar._tocResizeHandler = function () {
fallbackByScroll();
};
window.addEventListener('resize', sidebar._tocResizeHandler);
fallbackByScroll();
}
function extractTitle(md) {
var m = md.match(/^# (.+)/m);
return m ? m[1] : 'Lesson';
}
function parseMd(md) {
var lines = md.split(/\r?\n/);
var out = '';
var inCodeBlock = false;
var codeLang = '';
var codeLines = [];
var inTable = false;
var tableRows = [];
var inList = false;
var listType = '';
var listItems = [];
var firstBlockquoteDone = false;
var firstParaAfterMottoDone = false;
var inBlockquote = false;
var blockquoteLines = [];
var mermaidBlocks = 0;
var inLabChallenge = false;
function flushLabChallenge() {
if (!inLabChallenge) return '';
inLabChallenge = false;
return '</div>';
}
function flushList() {
if (!inList) return '';
inList = false;
var tag = listType === 'ol' ? 'ol' : 'ul';
var h = '<' + tag + '>';
for (var li = 0; li < listItems.length; li++) {
h += '<li>' + inlineFormat(listItems[li]) + '</li>';
}
h += '</' + tag + '>';
listItems = [];
return h;
}
function flushTable() {
if (!inTable) return '';
inTable = false;
if (tableRows.length < 2) return '';
var headers = tableRows[0];
var isKeyTerms = false;
for (var th = 0; th < headers.length; th++) {
if (headers[th].toLowerCase().indexOf('what people say') >= 0 || headers[th].toLowerCase().indexOf('people say') >= 0) {
isKeyTerms = true;
}
}
var h = '<div class="table-wrap' + (isKeyTerms ? ' key-terms-table' : '') + '"><table><thead><tr>';
for (var ti = 0; ti < headers.length; ti++) {
h += '<th>' + inlineFormat(headers[ti].trim()) + '</th>';
}
h += '</tr></thead><tbody>';
for (var ri = 2; ri < tableRows.length; ri++) {
var cells = tableRows[ri];
h += '<tr>';
for (var ci = 0; ci < headers.length; ci++) {
var cls = '';
if (isKeyTerms) {
var headerLower = (headers[ci] || '').toLowerCase();
if (headerLower.indexOf('say') >= 0) cls = ' class="col-says"';
}
h += '<td' + cls + '>' + inlineFormat((cells[ci] || '').trim()) + '</td>';
}
h += '</tr>';
}
h += '</tbody></table></div>';
tableRows = [];
return h;
}
function flushBlockquote() {
if (!inBlockquote) return '';
inBlockquote = false;
var text = blockquoteLines.join(' ');
blockquoteLines = [];
if (!firstBlockquoteDone) {
firstBlockquoteDone = true;
return '<div class="motto">' + inlineFormat(text) + '</div>';
}
return '<blockquote><p>' + inlineFormat(text) + '</p></blockquote>';
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (inCodeBlock) {
if (line.match(/^```\s*$/)) {
inCodeBlock = false;
var raw = codeLines.join('\n');
if (codeLang === 'figure') {
out += '<div class="lesson-figure" data-figure="' + escapeHtml(raw.trim()) + '"></div>';
} else if (codeLang === 'mermaid') {
mermaidBlocks++;
out += '<div class="mermaid-container">';
out += '<div class="mermaid-block" data-mermaid-index="' + mermaidBlocks + '">';
out += '<div class="mermaid-toolbar">';
out += '<button type="button" class="mermaid-btn mermaid-expand" data-mermaid-index="' + mermaidBlocks + '">Expand</button>';
out += '</div>';
out += '<pre class="mermaid mermaid-source" id="mermaid-' + mermaidBlocks + '">' + escapeHtml(raw) + '</pre>';
out += '<div class="mermaid-render" id="mermaid-render-' + mermaidBlocks + '"></div>';
out += '</div>';
out += '</div>';
} else {
out += renderCodeBlock(raw, codeLang);
}
codeLines = [];
codeLang = '';
} else {
codeLines.push(line);
}
continue;
}
var codeStart = line.match(/^```(\w*)/);
if (codeStart) {
out += flushList();
out += flushTable();
out += flushBlockquote();
inCodeBlock = true;
codeLang = codeStart[1] || '';
codeLines = [];
continue;
}
if (line.match(/^\s*\|.*\|\s*$/)) {
out += flushList();
out += flushBlockquote();
if (!inTable) inTable = true;
tableRows.push(splitTableRow(line));
continue;
} else if (inTable) {
out += flushTable();
}
if (line.match(/^>\s/)) {
out += flushList();
out += flushTable();
if (!inBlockquote) inBlockquote = true;
blockquoteLines.push(line.replace(/^>\s?/, ''));
continue;
} else if (inBlockquote) {
out += flushBlockquote();
}
var h1 = line.match(/^# (.+)/);
if (h1) {
out += flushList();
out += flushLabChallenge();
var slug = slugify(h1[1]);
out += '<h1 id="' + slug + '">' + inlineFormat(h1[1]) + '</h1>';
continue;
}
var h2 = line.match(/^## (.+)/);
if (h2) {
out += flushList();
var slug2 = slugify(h2[1]);
var sectionClass = '';
var txt = h2[1].toLowerCase();
if (txt.indexOf('build it') >= 0 || txt.indexOf('build ') === 0) sectionClass = ' section-build';
else if (txt.indexOf('use it') >= 0 || txt.indexOf('use ') === 0) sectionClass = ' section-use';
else if (txt.indexOf('ship it') >= 0 || txt.indexOf('ship ') === 0) sectionClass = ' section-ship';
if (txt.indexOf('learning objectives') >= 0) {
var objItems = [];
for (var lo = i + 1; lo < lines.length; lo++) {
var loLine = lines[lo];
if (loLine.match(/^[-*]\s+(.+)/)) {
objItems.push(loLine.replace(/^[-*]\s+/, ''));
} else if (loLine.trim() === '') {
continue;
} else {
break;
}
}
out += '<div class="learning-objectives"><div class="learning-objectives-title">&#127919; Learning Objectives</div><ul>';
for (var oi = 0; oi < objItems.length; oi++) {
out += '<li>' + inlineFormat(objItems[oi]) + '</li>';
}
out += '</ul></div>';
i = lo - 1;
continue;
}
if (txt.indexOf('lab challenge') >= 0) {
out += flushLabChallenge();
inLabChallenge = true;
out += '<div class="lab-challenge">';
out += '<h2 id="' + slug2 + '">&#128171; Lab Challenge</h2>';
continue;
}
out += flushLabChallenge();
out += '<h2 id="' + slug2 + '" class="' + sectionClass + '">' + inlineFormat(h2[1]) + '</h2>';
continue;
}
var h3 = line.match(/^### (.+)/);
if (h3) {
out += flushList();
var slug3 = slugify(h3[1]);
out += '<h3 id="' + slug3 + '">' + inlineFormat(h3[1]) + '</h3>';
continue;
}
if (line.match(/^---+$/)) {
out += flushList();
out += '<hr>';
continue;
}
var ulMatch = line.match(/^[-*]\s+(.+)/);
if (ulMatch) {
out += flushTable();
out += flushBlockquote();
if (!inList || listType !== 'ul') {
out += flushList();
inList = true;
listType = 'ul';
}
listItems.push(ulMatch[1]);
continue;
}
var olMatch = line.match(/^\d+\.\s+(.+)/);
if (olMatch) {
out += flushTable();
out += flushBlockquote();
if (!inList || listType !== 'ol') {
out += flushList();
inList = true;
listType = 'ol';
}
listItems.push(olMatch[1]);
continue;
}
if (inList) {
out += flushList();
}
if (line.trim() === '') {
continue;
}
var paraClass = '';
if (firstBlockquoteDone && !firstParaAfterMottoDone) {
var isMetaLine = line.match(/^\*\*[^*]+\*\*\s*:/);
if (!isMetaLine) {
paraClass = ' class="drop-cap"';
firstParaAfterMottoDone = true;
}
}
var metaMatch = line.match(/^\*\*([^*]+)\*\*:\s*(.+)/);
if (metaMatch && !firstParaAfterMottoDone) {
out += '<div class="lesson-meta-tag"><strong>' + escapeHtml(metaMatch[1]) + ':</strong> ' + inlineFormat(metaMatch[2]) + '</div>';
continue;
}
out += '<p' + paraClass + '>' + inlineFormat(line) + '</p>';
}
out += flushList();
out += flushTable();
out += flushBlockquote();
out += flushLabChallenge();
return out;
}
function splitTableRow(line) {
// Split a markdown table row on cell delimiters while respecting
// escaped pipes (P(X\|Y)) and pipes inside inline-code spans (`a | b`).
var cells = [];
var cur = '';
var inCode = false;
for (var i = 0; i < line.length; i++) {
var ch = line.charAt(i);
if (ch === '\\' && line.charAt(i + 1) === '|') {
cur += '|';
i++;
continue;
}
if (ch === '`') {
inCode = !inCode;
cur += ch;
continue;
}
if (ch === '|' && !inCode) {
cells.push(cur);
cur = '';
continue;
}
cur += ch;
}
cells.push(cur);
if (cells.length && cells[0].trim() === '') cells.shift();
if (cells.length && cells[cells.length - 1].trim() === '') cells.pop();
return cells.map(function (c) { return c.trim(); });
}
function inlineFormat(text) {
text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (m, label, href) {
if (/^https?:\/\/|^mailto:/i.test(href)) {
return '<a href="' + href + '" target="_blank" rel="noopener">' + label + '</a>';
}
return label;
});
return text;
}
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
function renderCodeBlock(code, lang) {
var highlighted = highlightSyntax(escapeHtml(code), lang);
var langLabel = lang ? '<span class="code-lang">' + escapeHtml(lang) + '</span>' : '';
return '<pre>' + langLabel + '<button class="code-copy" data-code="' + escapeAttr(code) + '">Copy</button><code>' + highlighted + '</code></pre>';
}
function highlightSyntax(code, lang) {
var keywords, commentPattern;
if (lang === 'python' || lang === 'py') {
keywords = /\b(def|class|return|if|elif|else|for|while|in|import|from|as|with|try|except|finally|raise|yield|lambda|not|and|or|is|None|True|False|print|self|pass|break|continue|assert|global)\b/g;
commentPattern = /(#[^\n]*)/g;
} else if (lang === 'julia') {
keywords = /\b(function|end|if|elseif|else|for|while|in|using|import|return|struct|mutable|abstract|type|module|export|let|const|begin|do|try|catch|finally|throw|true|false|nothing|println)\b/g;
commentPattern = /(#[^\n]*)/g;
} else if (lang === 'javascript' || lang === 'js' || lang === 'typescript' || lang === 'ts') {
keywords = /\b(function|const|let|var|return|if|else|for|while|class|new|this|import|export|from|async|await|try|catch|throw|typeof|instanceof|true|false|null|undefined|console)\b/g;
commentPattern = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g;
} else if (lang === 'rust') {
keywords = /\b(fn|let|mut|struct|enum|impl|trait|pub|use|mod|self|super|crate|return|if|else|for|while|loop|match|async|await|true|false|None|Some|Ok|Err|println)\b/g;
commentPattern = /(\/\/[^\n]*)/g;
} else {
keywords = /\b(function|def|class|return|if|else|for|while|import|from|const|let|var|true|false|null|None|print|println)\b/g;
commentPattern = /(#[^\n]*|\/\/[^\n]*)/g;
}
var tokens = [];
function stash(match) {
var id = '\x00TOK' + tokens.length + '\x00';
tokens.push(match);
return id;
}
code = code.replace(commentPattern, function (m) { return stash('<span class="syn-comment">' + m + '</span>'); });
code = code.replace(/(&quot;(?:[^&]|&(?!quot;))*?&quot;|&#39;(?:[^&]|&(?!#39;))*?&#39;|&quot;&quot;&quot;[\s\S]*?&quot;&quot;&quot;)/g, function (m) { return stash('<span class="syn-string">' + m + '</span>'); });
code = code.replace(/"([^"]*?)"/g, function (m) { return stash('<span class="syn-string">' + m + '</span>'); });
code = code.replace(keywords, '<span class="syn-keyword">$1</span>');
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="syn-number">$1</span>');
for (var ti = 0; ti < tokens.length; ti++) {
code = code.replace('\x00TOK' + ti + '\x00', tokens[ti]);
}
return code;
}
function initCodeCopy() {
document.querySelectorAll('.code-copy').forEach(function (btn) {
btn.addEventListener('click', function () {
var code = btn.getAttribute('data-code');
navigator.clipboard.writeText(code).then(function () {
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
});
});
});
}
function initializeMermaid(mermaid) {
mermaid.initialize({
startOnLoad: false,
theme: mermaidTheme(),
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
themeVariables: { fontSize: '13px' },
flowchart: { useMaxWidth: true, htmlLabels: true, nodeSpacing: 40, rankSpacing: 50, padding: 12 }
});
window._mermaidReady = mermaid;
return mermaid;
}
function loadMermaid() {
if (window._mermaidReady) return Promise.resolve(window._mermaidReady);
if (!mermaidPromise) {
mermaidPromise = import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs')
.then(function (module) { return initializeMermaid(module.default || module); })
.catch(function (error) {
mermaidPromise = null;
throw error;
});
}
return mermaidPromise;
}
function renderMermaidBlocks(root) {
var sources = (root || document).querySelectorAll('pre.mermaid.mermaid-source');
if (!sources.length) return Promise.resolve();
var fontsReady = (document.fonts && document.fonts.load)
? Promise.all([
document.fonts.load('400 13px "JetBrains Mono"'),
document.fonts.load('500 13px "JetBrains Mono"'),
document.fonts.load('700 13px "JetBrains Mono"')
]).catch(function () {})
: Promise.resolve();
return loadMermaid().then(function () {
return fontsReady.then(function () {
if (root && !root.isConnected) return;
rerenderMermaidBlocks();
bindMermaidToolbar();
initMermaidModal();
});
}).catch(function () {
sources.forEach(function (source) {
var block = source.closest('.mermaid-block');
var target = block && block.querySelector('.mermaid-render');
if (target) target.innerHTML = '<p>Diagram could not be rendered.</p>';
});
});
}
function mermaidTheme() {
return document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark';
}
// Lessons hard-code node fills (light pastels that vanish on the dark
// default theme, and dark navies that vanish in light mode). Rather than
// maintain a hex table, flip the lightness band while preserving hue, so
// color-coding survives and labels stay legible in either theme.
function hexToHsl(hex) {
var r = parseInt(hex.slice(1, 3), 16) / 255;
var g = parseInt(hex.slice(3, 5), 16) / 255;
var b = parseInt(hex.slice(5, 7), 16) / 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b), h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h /= 6;
}
return [h, s, l];
}
function hslToHex(h, s, l) {
function hue(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var r, g, b;
if (s === 0) {
r = g = b = l;
} else {
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue(p, q, h + 1 / 3);
g = hue(p, q, h);
b = hue(p, q, h - 1 / 3);
}
function c(x) {
x = Math.round(Math.max(0, Math.min(1, x)) * 255);
return ('0' + x.toString(16)).slice(-2);
}
return '#' + c(r) + c(g) + c(b);
}
function mermaidPreprocess(def) {
var dark = mermaidTheme() === 'dark';
return def.replace(/fill\s*:\s*(#[0-9a-fA-F]{6})/g, function (m, hex) {
var hsl = hexToHsl(hex.toLowerCase());
if (dark && hsl[2] > 0.65) return 'fill:' + hslToHex(hsl[0], hsl[1], 0.26);
if (!dark && hsl[2] < 0.30) return 'fill:' + hslToHex(hsl[0], Math.min(hsl[1], 0.5), 0.86);
return m;
});
}
function updateMermaidThemeAndRerender() {
if (!window._mermaidReady) return;
try {
window._mermaidReady.initialize({
startOnLoad: false,
theme: mermaidTheme(),
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
themeVariables: { fontSize: '13px' },
flowchart: { useMaxWidth: true, htmlLabels: true, nodeSpacing: 40, rankSpacing: 50, padding: 12 }
});
} catch (_) {}
rerenderMermaidBlocks();
rerenderMermaidModal();
}
function rerenderMermaidBlocks() {
if (!window._mermaidReady) return;
var sources = document.querySelectorAll('pre.mermaid.mermaid-source');
sources.forEach(function (pre) {
var idxMatch = pre.id && pre.id.match(/mermaid-(\d+)/);
var idx = idxMatch ? idxMatch[1] : null;
if (!idx) return;
var target = document.getElementById('mermaid-render-' + idx);
if (!target) return;
var def = mermaidPreprocess(pre.textContent);
target.innerHTML = '<div class="panel-loading">Rendering diagram...</div>';
var token = ++mermaidRenderSeq;
target._mermaidRenderToken = token;
window._mermaidReady.render('mermaid-' + idx + '-svg', def).then(function (result) {
if (target._mermaidRenderToken !== token) return;
target.innerHTML = result.svg;
}).catch(function () {
if (target._mermaidRenderToken !== token) return;
target.innerHTML = '<p style="color:var(--text-muted);font-style:italic;">Diagram could not be rendered.</p>';
});
});
}
function bindMermaidToolbar() {
document.querySelectorAll('.mermaid-expand').forEach(function (btn) {
if (btn._bound) return;
btn._bound = true;
btn.addEventListener('click', function () {
var idx = btn.getAttribute('data-mermaid-index');
openMermaidModal(idx, btn);
});
});
}
function initMermaidModal() {
var overlay = document.getElementById('mermaidModalOverlay');
var closeBtn = document.getElementById('mermaidModalClose');
if (!overlay || !closeBtn) return;
if (overlay._bound) return;
overlay._bound = true;
closeBtn.addEventListener('click', closeMermaidModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeMermaidModal();
});
document.addEventListener('keydown', function (e) {
if (!overlay.classList.contains('open')) return;
if (e.key === 'Escape') {
e.preventDefault();
closeMermaidModal();
return;
}
if (e.key === 'Tab') {
var focusables = getMermaidModalFocusables();
if (!focusables.length) return;
var first = focusables[0];
var last = focusables[focusables.length - 1];
var active = document.activeElement;
if (e.shiftKey) {
if (active === first || active === overlay) {
e.preventDefault();
last.focus();
}
} else {
if (active === last) {
e.preventDefault();
first.focus();
}
}
}
});
}
var lastMermaidTrigger = null;
var openMermaidIndex = null;
function getMermaidModalFocusables() {
var modal = document.querySelector('#mermaidModalOverlay .mermaid-modal');
if (!modal) return [];
return Array.prototype.slice.call(modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
.filter(function (el) { return !el.disabled && el.offsetParent !== null; });
}
function openMermaidModal(idx, triggerEl) {
var overlay = document.getElementById('mermaidModalOverlay');
var body = document.getElementById('mermaidModalBody');
var center = document.getElementById('mermaidModalCenter');
var title = document.getElementById('mermaidModalTitle');
var closeBtn = document.getElementById('mermaidModalClose');
var pre = document.getElementById('mermaid-' + idx);
if (!overlay || !body || !center || !pre || !closeBtn) return;
lastMermaidTrigger = triggerEl || document.activeElement;
openMermaidIndex = idx;
if (title) title.textContent = 'Diagram';
overlay.classList.add('open');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
closeBtn.focus();
rerenderMermaidModal();
}
function rerenderMermaidModal() {
var overlay = document.getElementById('mermaidModalOverlay');
var center = document.getElementById('mermaidModalCenter');
if (!overlay || !center) return;
if (!overlay.classList.contains('open')) return;
if (!openMermaidIndex) return;
if (!window._mermaidReady) return;
var pre = document.getElementById('mermaid-' + openMermaidIndex);
if (!pre) return;
var def = mermaidPreprocess(pre.textContent);
center.innerHTML = '<div class="panel-loading">Rendering diagram...</div>';
var token = ++mermaidRenderSeq;
center._mermaidRenderToken = token;
window._mermaidReady.render('mermaid-modal-svg', def).then(function (result) {
if (center._mermaidRenderToken !== token) return;
center.innerHTML = result.svg;
}).catch(function () {
if (center._mermaidRenderToken !== token) return;
center.innerHTML = '<p style="color:var(--text-muted);font-style:italic;">Diagram could not be rendered.</p>';
});
}
function closeMermaidModal() {
var overlay = document.getElementById('mermaidModalOverlay');
var center = document.getElementById('mermaidModalCenter');
if (!overlay) return;
overlay.classList.remove('open');
overlay.setAttribute('aria-hidden', 'true');
if (center) center.innerHTML = '';
document.body.style.overflow = '';
openMermaidIndex = null;
if (lastMermaidTrigger && typeof lastMermaidTrigger.focus === 'function') {
lastMermaidTrigger.focus();
}
}
function renderQuiz(data) {
var questions = data.questions || data;
if (!questions || !questions.length) return;
lessonQuizCorrectAnswers = Object.create(null);
var article = document.querySelector('.lesson-article');
if (!article) return;
var conceptH2 = article.querySelector('#the-concept, #concept');
var buildH2 = article.querySelector('#build-it, #build');
var keyTermsH2 = article.querySelector('#key-terms');
var preQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'pre'; }), 'pre', 'Pre-Lesson Check');
var checkQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'check'; }), 'check', 'Mid-Lesson Check');
var postQuizHtml = buildQuizHtml(questions.filter(function (q) { return q.stage === 'post'; }), 'post', 'Post-Lesson Quiz');
if (!preQuizHtml && !checkQuizHtml && !postQuizHtml) {
var allHtml = buildQuizHtml(questions, 'all', 'Quiz');
if (allHtml && keyTermsH2) {
keyTermsH2.insertAdjacentHTML('beforebegin', allHtml);
} else if (allHtml) {
article.insertAdjacentHTML('beforeend', allHtml);
}
restoreQuizAnswers();
restoreHeadingHash();
return;
}
if (preQuizHtml && conceptH2) {
conceptH2.insertAdjacentHTML('beforebegin', preQuizHtml);
} else if (preQuizHtml && buildH2) {
buildH2.insertAdjacentHTML('beforebegin', preQuizHtml);
}
// Mid-lesson check: land between Build It and Use It if possible,
// otherwise fall back to before Use It / Ship It / Key Terms.
var useH2 = article.querySelector('#use-it, #use');
var shipH2 = article.querySelector('#ship-it, #ship');
if (checkQuizHtml && useH2) {
useH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
} else if (checkQuizHtml && shipH2) {
shipH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
} else if (checkQuizHtml && keyTermsH2) {
keyTermsH2.insertAdjacentHTML('beforebegin', checkQuizHtml);
} else if (checkQuizHtml) {
article.insertAdjacentHTML('beforeend', checkQuizHtml);
}
if (postQuizHtml && keyTermsH2) {
keyTermsH2.insertAdjacentHTML('beforebegin', postQuizHtml);
} else if (postQuizHtml) {
article.insertAdjacentHTML('beforeend', postQuizHtml);
}
restoreQuizAnswers();
restoreHeadingHash();
}
function buildQuizHtml(questions, id, title) {
if (!questions.length) return '';
var html = '<div class="quiz-section" id="quiz-' + id + '">';
html += '<div class="quiz-title">&#9989; ' + escapeHtml(title) + '</div>';
html += '<div class="quiz-reset-status" role="status" aria-live="polite"></div>';
for (var qi = 0; qi < questions.length; qi++) {
var q = questions[qi];
var qid = id + '-q' + qi;
var questionLabelId = qid + '-label';
var explanationId = qid + '-exp';
var hasExplanation = !!q.explanation || !!certificationLesson;
lessonQuizCorrectAnswers[qid] = q.correct;
html += '<div class="quiz-question" id="' + qid + '">';
html += '<div class="quiz-question-text" id="' + questionLabelId + '">' + (qi + 1) + '. ' + escapeHtml(q.question) + '</div>';
html += '<div class="quiz-options" role="group" aria-labelledby="' + questionLabelId + '">';
for (var oi = 0; oi < q.options.length; oi++) {
html += '<button type="button" class="quiz-option" data-index="' + oi + '" aria-pressed="false"' + (hasExplanation ? ' aria-describedby="' + explanationId + '"' : '') + ' onclick="handleQuizClick(this)">';
html += '<span class="quiz-marker" aria-hidden="true"></span>';
html += '<span class="quiz-option-text">' + escapeHtml(q.options[oi]) + '</span>';
html += '</button>';
}
html += '</div>';
if (hasExplanation) {
html += '<div class="quiz-explanation" id="' + explanationId + '" role="status" aria-live="polite">' + escapeHtml(q.explanation || '') + '</div>';
}
html += '</div>';
}
html += '<div class="quiz-score" id="quiz-' + id + '-score"><div class="quiz-score-result" role="status" aria-live="polite"></div><button type="button" class="quiz-retry" onclick="resetLessonQuizSection(this)">Retry this check</button></div>';
html += '</div>';
return html;
}
window.handleQuizClick = function (el) {
var questionDiv = el.closest('.quiz-question');
if (questionDiv.classList.contains('answered')) return;
applyQuizAnswer(questionDiv, parseInt(el.getAttribute('data-index')), true);
};
function applyQuizAnswer(questionDiv, chosen, persist) {
if (!questionDiv || isNaN(chosen)) return;
var correct = lessonQuizCorrectAnswers[questionDiv.id];
var options = questionDiv.querySelectorAll('.quiz-option');
if (chosen < 0 || chosen >= options.length || !Number.isInteger(correct)) return;
questionDiv.classList.add('answered');
for (var i = 0; i < options.length; i++) {
var option = options[i];
var optionText = option.querySelector('.quiz-option-text');
var accessibleLabel = optionText ? optionText.textContent : option.textContent;
option.classList.add('disabled');
option.disabled = true;
option.setAttribute('aria-disabled', 'true');
option.setAttribute('aria-pressed', i === chosen ? 'true' : 'false');
if (i === correct) {
option.classList.add('correct');
var mc = option.querySelector('.quiz-marker');
if (mc) mc.textContent = '\u2713';
}
if (i === chosen) option.classList.add('selected');
if (i === chosen && i === correct) {
option.setAttribute('aria-label', accessibleLabel + ', selected, correct');
} else if (i === chosen) {
option.setAttribute('aria-label', accessibleLabel + ', selected, incorrect');
} else if (i === correct) {
option.setAttribute('aria-label', accessibleLabel + ', correct answer');
}
}
if (chosen !== correct) {
var picked = questionDiv.querySelector('.quiz-option[data-index="' + chosen + '"]');
if (picked) {
picked.classList.add('incorrect');
var mp = picked.querySelector('.quiz-marker');
if (mp) mp.textContent = '\u2717';
}
}
var exp = questionDiv.querySelector('.quiz-explanation');
if (exp) {
if (certificationLesson) {
if (!exp.hasAttribute('data-base-explanation')) exp.setAttribute('data-base-explanation', exp.textContent || '');
exp.textContent = (chosen === correct ? 'Correct. ' : 'Not quite. ') + exp.getAttribute('data-base-explanation');
}
exp.classList.add('visible');
}
if (persist && window.AIFSProgress && lessonPath) {
if (certificationLesson) syncCertificationQuizProgressVersion();
window.AIFSProgress.recordAnswer(lessonPath, questionDiv.id, chosen, chosen === correct);
}
updateSectionScore(questionDiv.closest('.quiz-section'));
maybeMarkLessonComplete();
}
function updateSectionScore(section) {
if (!section) return;
var allQs = section.querySelectorAll('.quiz-question');
var answeredQs = section.querySelectorAll('.quiz-question.answered');
var correctCount = 0;
for (var j = 0; j < answeredQs.length; j++) {
if (!answeredQs[j].querySelector('.quiz-option.incorrect')) correctCount++;
}
var complete = allQs.length === answeredQs.length;
var hasWrongAnswer = !!section.querySelector('.quiz-option.incorrect');
if (!complete && !hasWrongAnswer) return;
var scoreEl = section.querySelector('.quiz-score');
if (scoreEl) {
var resultEl = scoreEl.querySelector('.quiz-score-result');
if (!resultEl) return;
resultEl.textContent = complete
? correctCount + '/' + allQs.length + ' correct'
: answeredQs.length + '/' + allQs.length + ' answered. Review the feedback, then retry when ready.';
scoreEl.classList.add('visible');
}
}
function maybeMarkLessonComplete() {
if (!window.AIFSProgress || !lessonPath) return;
var sections = document.querySelectorAll('.quiz-section');
if (!sections.length) return;
for (var s = 0; s < sections.length; s++) {
var qs = sections[s].querySelectorAll('.quiz-question');
var answered = sections[s].querySelectorAll('.quiz-question.answered');
if (qs.length === 0 || qs.length !== answered.length) return;
if (sections[s].querySelector('.quiz-option.incorrect')) return;
}
if (certificationLesson) syncCertificationQuizProgressVersion();
window.AIFSProgress.markQuizPassed(lessonPath);
}
function resetLessonQuizSection(trigger) {
var section = trigger && trigger.closest ? trigger.closest('.quiz-section') : null;
if (!section) return;
clearQuizSectionProgress(section);
var questions = section.querySelectorAll('.quiz-question');
for (var qi = 0; qi < questions.length; qi++) {
questions[qi].classList.remove('answered');
var options = questions[qi].querySelectorAll('.quiz-option');
for (var oi = 0; oi < options.length; oi++) {
options[oi].classList.remove('disabled', 'selected', 'correct', 'incorrect', 'wrong', 'is-answer');
options[oi].disabled = false;
options[oi].removeAttribute('aria-disabled');
options[oi].setAttribute('aria-pressed', 'false');
options[oi].removeAttribute('aria-label');
var marker = options[oi].querySelector('.quiz-marker');
if (marker) marker.textContent = '';
}
var explanation = questions[qi].querySelector('.quiz-explanation');
if (explanation) {
explanation.classList.remove('visible');
if (explanation.hasAttribute('data-base-explanation')) {
explanation.textContent = explanation.getAttribute('data-base-explanation');
}
}
}
var score = section.querySelector('.quiz-score');
if (score) {
score.classList.remove('visible');
var result = score.querySelector('.quiz-score-result');
if (result) result.textContent = '';
}
var status = section.querySelector('.quiz-reset-status');
if (status) status.textContent = 'This check was reset. Choose a new answer for each question.';
var firstOption = section.querySelector('.quiz-option:not(:disabled)');
if (firstOption) firstOption.focus();
}
window.resetLessonQuizSection = resetLessonQuizSection;
window.resetCertificationLessonQuiz = resetLessonQuizSection;
function restoreQuizAnswers() {
if (!window.AIFSProgress || !lessonPath) return;
if (certificationLesson) syncCertificationQuizProgressVersion();
var lp = window.AIFSProgress.getLessonProgress(lessonPath);
if (!lp || !lp.answers) return;
for (var qid in lp.answers) {
var q = document.getElementById(qid);
if (q && !q.classList.contains('answered')) {
applyQuizAnswer(q, lp.answers[qid].picked, false);
}
}
}
function addBottomNav(html) {
if (currentLessonIndex < 0) return html;
var prev = null;
for (var pi = currentLessonIndex - 1; pi >= 0; pi--) {
if (learningPathMode && flatLessons[pi].required === false) continue;
if (flatLessons[pi].isReadable) { prev = flatLessons[pi]; break; }
}
var next = null;
for (var ni = currentLessonIndex + 1; ni < flatLessons.length; ni++) {
if (learningPathMode && flatLessons[ni].required === false) continue;
if (flatLessons[ni].isReadable) { next = flatLessons[ni]; break; }
}
var nav = '<nav class="lesson-nav-bottom" aria-label="Lesson navigation">';
var trackSuffix = certificationMode
? '&track=' + encodeURIComponent(certificationTrack.id)
: learningPathMode ? '&learningPath=' + encodeURIComponent(learningPath.id) : '';
if (prev) {
nav += '<a class="lesson-nav-btn prev" href="lesson?path=' + encodeURIComponent(prev.path) + trackSuffix + '">';
nav += '<span class="nav-label">&larr; Previous</span>';
nav += '<span class="nav-title">' + escapeHtml(prev.lessonName) + '</span>';
nav += '</a>';
}
if (next) {
var nextLocked = learningPathMode && learningPathEntryLocked(next);
nav += '<a class="lesson-nav-btn next' + (learningPathMode ? learningPathGateClass(next) : '') + '" href="lesson?path=' + encodeURIComponent(next.path) + trackSuffix + '"' + (learningPathMode ? learningPathGateAttributes(next) : '') + '>';
nav += '<span class="nav-label" data-learning-path-gate-label>' + (nextLocked ? 'Locked' : 'Next &rarr;') + '</span>';
nav += '<span class="nav-title">' + escapeHtml(next.lessonName) + '</span>';
nav += '</a>';
}
nav += '</nav>';
return html + nav;
}
function renderAIPanels() {
var container = document.getElementById('aiPanels');
if (!container || !lessonPath) return;
renderOutputsPanel(container);
renderCodePanel(container);
renderQuizPanel(container);
renderLearningPathPanel(container);
renderContinuePanel(container);
}
var repositoryDirectoryCache = {};
function githubDirectoryFetch(apiPath) {
return fetch('https://api.github.com/repos/' + SOURCE_OWNER + '/' + SOURCE_REPO + '/contents/' + apiPath + '?ref=' + encodeURIComponent(ACTIVE_REF), {
headers: { 'Accept': 'application/vnd.github.v3+json' }
}).then(function (res) {
if (!res.ok) throw new Error(res.status);
return res.json();
});
}
function repositoryDirectoryFetch(apiPath, cb) {
if (repositoryDirectoryCache[apiPath]) {
cb(null, repositoryDirectoryCache[apiPath]);
return;
}
var source = window.AIFSContentSource;
var request = source && typeof source.isLocal === 'function' && source.isLocal() && typeof source.localDirectoryFiles === 'function'
? source.localDirectoryFiles(apiPath).catch(function () { return githubDirectoryFetch(apiPath); })
: githubDirectoryFetch(apiPath);
request.then(function (data) {
repositoryDirectoryCache[apiPath] = data;
cb(null, data);
}).catch(function (err) {
cb(err, null);
});
}
function classifyOutput(name) {
var lower = String(name || '').toLowerCase();
if (lower.indexOf('prompt') >= 0) return 'prompt';
if (lower.indexOf('skill') >= 0) return 'skill';
if (lower.indexOf('agent') >= 0) return 'agent';
return 'other';
}
function outputFileName(file) {
return file && typeof file.name === 'string' ? file.name.trim() : '';
}
function isRenderableOutput(file) {
var name = outputFileName(file);
return !!name && name.charAt(0) !== '.' && (!file.type || file.type !== 'dir');
}
function isMarkdownOutput(file) {
return /\.md(?:own)?$/i.test(outputFileName(file));
}
function outputType(file) {
var declared = file && typeof file.kind === 'string' ? file.kind.toLowerCase() : '';
return ['prompt', 'skill', 'agent'].indexOf(declared) >= 0
? declared
: classifyOutput(outputFileName(file));
}
function outputBadgeLabel(type, filename) {
if (type === 'prompt') return 'Prompt';
if (type === 'skill') return 'Skill';
if (type === 'agent') return 'Agent';
var extension = String(filename || '').match(/\.([a-z0-9]+)$/i);
return extension ? extension[1].toUpperCase() + ' output' : 'Output';
}
function langIcon(filename) {
var ext = filename.split('.').pop().toLowerCase();
var map = { py: 'PY', python: 'PY', ts: 'TS', js: 'JS', rust: 'RS', rs: 'RS', jl: 'JL', julia: 'JL', sh: 'SH', bash: 'SH' };
return map[ext] || '··';
}
function repoRootCommand(filename, path) {
var ext = filename.split('.').pop().toLowerCase();
var relativePath = String(path).replace(/^\/+/, '');
var pathArg = /^[A-Za-z0-9_./-]+$/.test(relativePath)
? relativePath
: "'" + relativePath.replace(/'/g, "'\\''") + "'";
if (ext === 'py') return 'python3 ' + pathArg;
if (ext === 'ts') return 'npx tsx ' + pathArg;
if (ext === 'js') return 'node ' + pathArg;
if (ext === 'rs') return 'rustc ' + pathArg + ' -o /tmp/aiefs-lesson-main && /tmp/aiefs-lesson-main';
if (ext === 'jl') return 'julia ' + pathArg;
if (ext === 'sh') return 'bash ' + pathArg;
return 'test -e ' + pathArg;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
return (bytes / 1024).toFixed(1) + ' KB';
}
function extractMarkdownDesc(content) {
var match = content.match(/^---[\s\S]*?description:\s*(.+?)[\r\n]/m);
if (match) return match[1].replace(/^["']|["']$/g, '').trim();
var firstLine = content.split('\n').find(function (l) { return l.trim() && !l.match(/^[#-]/) && !l.match(/^---/); });
return firstLine ? firstLine.trim().substring(0, 120) : '';
}
function lessonFileViewUrl(file) {
var filePath = file && (file.path || file.file);
if (file && file.html_url) return file.html_url;
if (filePath && window.AIFSContentSource) return window.AIFSContentSource.repoUrl(filePath);
return REPO_TREE + (filePath || lessonPath);
}
function lessonFileSourceUrl(file) {
var filePath = file && (file.path || file.file);
if (file && file.download_url) return file.download_url;
if (filePath && window.AIFSContentSource) return window.AIFSContentSource.repoUrl(filePath);
return '';
}
function renderPanelFallback(target, message, href, label) {
target.classList.remove('panel-loading');
target.textContent = '';
var fallback = document.createElement('div');
fallback.className = 'panel-fallback';
var copy = document.createElement('p');
copy.textContent = message;
fallback.appendChild(copy);
if (href) {
var link = document.createElement('a');
link.href = href;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = label || 'Open source';
fallback.appendChild(link);
}
target.appendChild(fallback);
}
function renderOutputFiles(outputsEl, data) {
outputsEl.classList.remove('panel-loading');
var visibleOutputs = Array.isArray(data) ? data.filter(isRenderableOutput) : [];
if (visibleOutputs.length === 0) {
renderPanelFallback(outputsEl, 'This lesson has no separate output artifact. The lesson itself is the deliverable.', '', '');
return;
}
var descriptionRequests = [];
var html = '<div class="output-cards' + (visibleOutputs.length === 1 ? ' single' : '') + '">';
visibleOutputs.forEach(function (file, idx) {
var filename = outputFileName(file);
var type = outputType(file);
var badgeClass = type;
var badgeText = type === 'skill' && file.bundle ? 'Skill bundle' : outputBadgeLabel(type, filename);
var installId = 'install-hint-' + idx;
var installHint = '';
var installCommand = '';
var viewUrl = lessonFileViewUrl(file);
var viewLabel = file.bundle ? 'Open SKILL.md' : 'Open file';
var bundleUrl = '';
var bundleLabel = '';
if (file.bundle && file.bundlePath) {
var source = window.AIFSContentSource;
bundleUrl = source && typeof source.isLocal === 'function' && source.isLocal()
? source.repoUrl(file.bundlePath + '/')
: REPO_TREE + file.bundlePath;
bundleLabel = 'Browse bundle (' + (Array.isArray(file.files) ? file.files.length : 0) + ' files)';
}
if (type === 'prompt') {
installHint = 'Paste into Claude, Cursor, Codex, OpenClaw, Hermes, or any agent that reads prompts';
} else if (type === 'skill') {
var skillName = file.kind === 'skill'
? file.name
: file.name.replace(/\.md$/, '').replace(/^skill-/, '');
installCommand = 'npx skills add rohitg00/ai-engineering-from-scratch --skill ' + skillName + ' --full-depth';
installHint = installCommand;
}
var actionCount = 1 + (bundleUrl ? 1 : 0) + (installHint ? 1 : 0) + (installCommand ? 1 : 0);
html += '<div class="output-card' + (actionCount > 2 ? ' output-card--stacked-actions' : '') + '">';
html += '<div class="output-card-main">';
html += '<div class="output-card-heading">';
html += '<span class="output-badge ' + badgeClass + '">' + badgeText + '</span>';
html += '<span class="output-card-name">' + escapeHtml(filename) + '</span>';
html += '</div>';
if (file.description) {
html += '<div class="output-desc" data-output-description="' + idx + '">' + escapeHtml(file.description) + '</div>';
} else if (isMarkdownOutput(file)) {
html += '<div class="output-desc" data-output-description="' + idx + '" hidden></div>';
descriptionRequests.push({ file: file, index: idx });
}
html += '</div>';
html += '<div class="output-actions">';
html += '<a class="output-btn" href="' + escapeAttr(viewUrl) + '" target="_blank" rel="noopener">' + viewLabel + '</a>';
if (bundleUrl) {
html += '<a class="output-btn" href="' + escapeAttr(bundleUrl) + '" target="_blank" rel="noopener">' + escapeHtml(bundleLabel) + '</a>';
}
if (installHint) {
html += '<button class="output-btn output-install-toggle" type="button" aria-expanded="false" aria-controls="' + installId + '">Install</button>';
}
if (installCommand) {
html += '<button class="output-btn output-install-copy" data-command="' + escapeAttr(installCommand) + '">Copy install</button>';
}
html += '</div>';
if (installHint) {
html += '<div class="output-install-hint" id="' + installId + '">' + escapeHtml(installHint) + '</div>';
}
html += '</div>';
});
html += '</div>';
outputsEl.innerHTML = html;
descriptionRequests.forEach(function (request) {
var sourceUrl = lessonFileSourceUrl(request.file);
if (!sourceUrl) return;
fetch(sourceUrl).then(function (response) {
if (!response.ok) throw new Error('fetch-failed');
return response.text();
}).then(function (content) {
if (!outputsEl.isConnected) return;
var desc = extractMarkdownDesc(content);
var descEl = outputsEl.querySelector('[data-output-description="' + request.index + '"]');
if (!desc || !descEl) return;
descEl.textContent = desc;
descEl.hidden = false;
}).catch(function () {});
});
outputsEl.querySelectorAll('.output-install-toggle').forEach(function (btn) {
btn.addEventListener('click', function () {
var hint = document.getElementById(btn.getAttribute('aria-controls'));
if (!hint) return;
var expanded = hint.classList.toggle('visible');
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
});
});
outputsEl.querySelectorAll('.output-install-copy').forEach(function (btn) {
btn.addEventListener('click', function () {
navigator.clipboard.writeText(btn.getAttribute('data-command')).then(function () {
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = 'Copy install'; }, 1500);
});
});
});
}
function renderOutputsPanel(container) {
var panel = document.createElement('div');
panel.className = 'ai-panel ai-panel--compact ai-panel--outputs';
panel.innerHTML = '<div class="ai-panel-header"><div class="ai-panel-icon">O</div><div class="ai-panel-title">What This Lesson Ships</div></div><div class="ai-panel-subtitle">Prompts, skills, and artifacts you can use right now</div><div id="outputsContent" class="panel-loading">Loading outputs...</div>';
container.appendChild(panel);
var outputsEl = panel.querySelector('#outputsContent');
if (certificationLesson && certificationLesson.files) {
renderOutputFiles(outputsEl, certificationLesson.files.outputs || []);
return;
}
repositoryDirectoryFetch(lessonPath + '/outputs', function (err, data) {
if (!outputsEl || !outputsEl.parentNode) return;
var source = window.AIFSContentSource;
var directoryEntries = Array.isArray(data) ? data : [];
var artifacts = typeof ARTIFACTS !== 'undefined' && Array.isArray(ARTIFACTS) ? ARTIFACTS : [];
var outputs = source && typeof source.mergeLessonOutputs === 'function'
? source.mergeLessonOutputs(lessonPath, directoryEntries, artifacts)
: directoryEntries;
if (outputs.length === 0) {
renderPanelFallback(outputsEl, 'No separate output artifacts were found for this lesson.', REPO_TREE + lessonPath, 'View lesson source');
return;
}
renderOutputFiles(outputsEl, outputs);
});
}
function renderCodeFiles(codeEl, data) {
codeEl.classList.remove('panel-loading');
if (!data || !Array.isArray(data) || data.length === 0) {
renderPanelFallback(codeEl, 'This lesson has no separate runnable file. Use the worked examples in the lesson.', '', '');
return;
}
var html = '<div class="code-cards' + (data.length === 1 ? ' single' : '') + '">';
data.forEach(function (file) {
var icon = langIcon(file.name);
var filePath = file.path || (lessonPath + '/code/' + file.name);
var runCmd = repoRootCommand(file.name, filePath);
html += '<div class="code-card">';
html += '<div class="code-card-header"><span class="code-lang-icon">' + icon + '</span><span class="code-card-name">' + escapeHtml(file.name) + '</span><span class="code-card-size">' + formatSize(Number(file.size || 0)) + '</span></div>';
html += '<div class="code-card-run">' + escapeHtml(runCmd) + '</div>';
html += '<div class="code-card-actions">';
html += '<a class="code-card-btn" href="' + escapeAttr(lessonFileViewUrl(file)) + '" target="_blank" rel="noopener">Open file</a>';
html += '<button class="code-card-btn code-card-copy" data-command="' + escapeAttr(runCmd) + '">Copy command</button>';
html += '</div>';
html += '</div>';
});
html += '</div>';
codeEl.innerHTML = html;
codeEl.querySelectorAll('.code-card-copy').forEach(function (btn) {
btn.addEventListener('click', function () {
var command = btn.getAttribute('data-command');
navigator.clipboard.writeText(command).then(function () {
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = 'Copy command'; }, 1500);
});
});
});
}
function renderCodePanel(container) {
var panel = document.createElement('div');
panel.className = 'ai-panel ai-panel--compact ai-panel--code';
panel.innerHTML = '<div class="ai-panel-header"><div class="ai-panel-icon">C</div><div class="ai-panel-title">Run the Code</div></div><div class="ai-panel-subtitle">Requires a local clone. Run copied commands from the repository root, the directory containing README.md and phases/.</div><div id="codeContent" class="panel-loading">Loading code files...</div>';
container.appendChild(panel);
var codeEl = panel.querySelector('#codeContent');
if (certificationLesson && certificationLesson.files) {
renderCodeFiles(codeEl, certificationLesson.files.code || []);
return;
}
repositoryDirectoryFetch(lessonPath + '/code', function (err, data) {
if (!codeEl || !codeEl.parentNode) return;
if (err || !data || !Array.isArray(data) || data.length === 0) {
renderPanelFallback(codeEl, 'No separate runnable files were found for this lesson.', REPO_TREE + lessonPath, 'View lesson source');
return;
}
renderCodeFiles(codeEl, data);
});
}
function getQuizQuestions() {
var pathLower = (lessonPath || '').toLowerCase();
var lessonName = '';
if (currentLessonIndex >= 0) lessonName = flatLessons[currentLessonIndex].lessonName;
if (pathLower.indexOf('math') >= 0 || pathLower.indexOf('linear-algebra') >= 0 || pathLower.indexOf('calculus') >= 0) {
return [
{ q: 'What does a dot product measure between two vectors?', opts: ['Their sum', 'How aligned they are', 'Their cross product', 'The distance between them'], answer: 1, explain: 'The dot product measures the similarity or alignment between two vectors. When it is zero, the vectors are orthogonal.' },
{ q: 'What does the gradient of a function point toward?', opts: ['The minimum', 'The steepest ascent', 'The nearest saddle point', 'A random direction'], answer: 1, explain: 'The gradient always points in the direction of steepest increase. Gradient descent moves opposite to the gradient to find minima.' },
{ q: 'A matrix with shape (3, 5) multiplied by (5, 2) produces what shape?', opts: ['(3, 2)', '(5, 5)', '(3, 5)', '(2, 3)'], answer: 0, explain: 'Matrix multiplication: (m, n) x (n, p) = (m, p). So (3, 5) x (5, 2) = (3, 2).' }
];
}
if (pathLower.indexOf('ml') >= 0 || pathLower.indexOf('regression') >= 0 || pathLower.indexOf('classification') >= 0 || pathLower.indexOf('supervised') >= 0) {
return [
{ q: 'What is the purpose of a loss function in machine learning?', opts: ['To generate data', 'To measure how wrong predictions are', 'To select features', 'To split data'], answer: 1, explain: 'A loss function quantifies the difference between predicted and actual values. The optimizer minimizes this value during training.' },
{ q: 'Why do we split data into train and test sets?', opts: ['To save memory', 'To evaluate generalization on unseen data', 'To make training faster', 'To reduce the dataset size'], answer: 1, explain: 'The test set acts as unseen data, revealing whether the model memorized training data or learned generalizable patterns.' },
{ q: 'What does overfitting mean?', opts: ['The model is too simple', 'The model memorizes training data but fails on new data', 'The model trains too slowly', 'The loss is too low'], answer: 1, explain: 'Overfitting occurs when a model performs well on training data but poorly on new data because it learned noise rather than signal.' }
];
}
if (pathLower.indexOf('deep-learning') >= 0 || pathLower.indexOf('neural') >= 0 || pathLower.indexOf('backprop') >= 0 || pathLower.indexOf('activation') >= 0) {
return [
{ q: 'What does backpropagation compute?', opts: ['Forward predictions', 'The gradient of the loss with respect to each weight', 'The learning rate', 'New training data'], answer: 1, explain: 'Backpropagation uses the chain rule to compute how much each weight contributed to the error, then adjusts weights accordingly.' },
{ q: 'Why do neural networks need non-linear activation functions?', opts: ['To speed up training', 'Without them, stacking layers is equivalent to a single linear layer', 'To reduce memory usage', 'To normalize outputs'], answer: 1, explain: 'Without non-linearities, any composition of linear layers collapses to a single linear transformation. Activations like ReLU let the network learn complex patterns.' },
{ q: 'What does the learning rate control?', opts: ['How many epochs to train', 'The size of each weight update step', 'The number of layers', 'The batch size'], answer: 1, explain: 'The learning rate scales the gradient update. Too large causes divergence, too small causes slow or stuck training.' }
];
}
if (pathLower.indexOf('llm') >= 0 || pathLower.indexOf('transformer') >= 0 || pathLower.indexOf('attention') >= 0 || pathLower.indexOf('tokeniz') >= 0 || pathLower.indexOf('fine-tun') >= 0) {
return [
{ q: 'What does self-attention allow a transformer to do?', opts: ['Process tokens in order', 'Weight the importance of every token relative to every other token', 'Reduce vocabulary size', 'Compress the model'], answer: 1, explain: 'Self-attention computes pairwise relevance scores across all positions, allowing the model to relate distant tokens without recurrence.' },
{ q: 'Why do LLMs use tokenizers instead of raw characters?', opts: ['Characters are too large', 'Tokens compress frequent patterns into single units, reducing sequence length', 'Tokenizers are faster to train', 'Characters cannot be embedded'], answer: 1, explain: 'Subword tokenizers like BPE balance vocabulary size against sequence length, making common words single tokens while handling rare words as pieces.' },
{ q: 'What is the key difference between pre-training and fine-tuning?', opts: ['Pre-training uses labeled data', 'Pre-training learns general language; fine-tuning adapts to a specific task', 'Fine-tuning uses more data', 'There is no difference'], answer: 1, explain: 'Pre-training learns language patterns from massive unlabeled text. Fine-tuning takes that foundation and specializes it on smaller, task-specific data.' }
];
}
if (pathLower.indexOf('rag') >= 0 || pathLower.indexOf('retrieval') >= 0 || pathLower.indexOf('embed') >= 0 || pathLower.indexOf('vector') >= 0) {
return [
{ q: 'What is the core idea behind RAG?', opts: ['Training a larger model', 'Retrieving relevant context before generating a response', 'Using more GPUs', 'Reducing model size'], answer: 1, explain: 'RAG (Retrieval-Augmented Generation) grounds LLM responses in retrieved documents, reducing hallucination and enabling knowledge updates without retraining.' },
{ q: 'What do embedding models produce?', opts: ['Text summaries', 'Dense vector representations of text', 'Token counts', 'Grammar corrections'], answer: 1, explain: 'Embedding models map text into fixed-dimensional vectors where semantic similarity corresponds to geometric proximity.' },
{ q: 'Why use cosine similarity for comparing embeddings?', opts: ['It is the only metric', 'It measures angular similarity regardless of vector magnitude', 'It is faster than dot product', 'It works with integers only'], answer: 1, explain: 'Cosine similarity normalizes for magnitude, focusing on direction. Two texts about the same topic will have high cosine similarity regardless of length.' }
];
}
if (pathLower.indexOf('agent') >= 0 || pathLower.indexOf('tool') >= 0 || pathLower.indexOf('mcp') >= 0) {
return [
{ q: 'What distinguishes an AI agent from a simple chatbot?', opts: ['Agents are faster', 'Agents can take actions and use tools autonomously', 'Agents use bigger models', 'There is no difference'], answer: 1, explain: 'Agents have a loop: observe, decide, act. They can call tools, read files, search the web, and chain multiple steps to complete complex tasks.' },
{ q: 'What is MCP (Model Context Protocol)?', opts: ['A model training format', 'A standardized protocol for connecting AI models to tools and data sources', 'A compression algorithm', 'A testing framework'], answer: 1, explain: 'MCP provides a universal interface between AI assistants and external tools/data, replacing one-off integrations with a standard protocol.' },
{ q: 'Why is tool-use important for LLMs?', opts: ['It reduces cost', 'It lets LLMs access real-time information and take actions beyond text generation', 'It makes responses shorter', 'It removes hallucinations entirely'], answer: 1, explain: 'Tools extend LLMs beyond their training data cutoff, enabling real-time lookups, calculations, code execution, and interaction with external systems.' }
];
}
if (pathLower.indexOf('eval') >= 0 || pathLower.indexOf('safety') >= 0 || pathLower.indexOf('alignment') >= 0 || pathLower.indexOf('deploy') >= 0) {
return [
{ q: 'Why are evals critical for production AI systems?', opts: ['To save money', 'To measure performance objectively before and after changes', 'To make the model bigger', 'Evals are optional'], answer: 1, explain: 'Evals are the tests of AI engineering. Without them, you cannot know if a change improved or regressed quality. They should run on every code change.' },
{ q: 'What does "alignment" mean in AI safety?', opts: ['Aligning text on screen', 'Ensuring AI systems act according to human intentions and values', 'Making models faster', 'Using the same training data'], answer: 1, explain: 'Alignment ensures that as AI systems become more capable, they remain helpful, honest, and harmless, acting in accordance with human goals.' },
{ q: 'What is a guardrail in the context of deployed LLMs?', opts: ['A physical barrier', 'A check that prevents harmful, off-topic, or policy-violating outputs', 'A backup model', 'A caching layer'], answer: 1, explain: 'Guardrails filter inputs and outputs at runtime, catching toxicity, prompt injection, PII leaks, and other risks before they reach the user.' }
];
}
return [
{ q: 'What does "from scratch" mean in this course?', opts: ['Using no computer', 'Building each concept by implementing it yourself, not just reading theory', 'Starting from assembly language', 'Only using pen and paper'], answer: 1, explain: 'This course follows a Build-Use-Ship methodology: first understand by building it, then apply it, then ship it as a real artifact.' },
{ q: 'Why does this course combine math, ML, and engineering?', opts: ['To make it longer', 'Because real AI engineering requires all three to build production systems', 'Math is just for fun', 'Engineering is optional'], answer: 1, explain: 'Production AI systems require mathematical foundations (for understanding), ML knowledge (for modeling), and engineering skills (for deployment and reliability).' },
{ q: 'What is the "Ship" step in the Build-Use-Ship framework?', opts: ['Mailing physical goods', 'Creating a reusable artifact like a prompt, skill, or tool from what you learned', 'Publishing a paper', 'Deleting your code'], answer: 2, explain: 'The Ship step turns your learning into something tangible: a prompt, skill file, MCP tool, or CLI utility that others (or future you) can use immediately.' }
];
}
function lessonQuizPanelQuestions(data) {
var source = data && (data.questions || data);
if (!Array.isArray(source)) return [];
var valid = source.filter(function (q) {
return q && q.question && Array.isArray(q.options) &&
typeof q.correct === 'number' && q.correct >= 0 && q.correct < q.options.length;
});
var post = valid.filter(function (q) { return q.stage === 'post'; });
return (post.length ? post : valid).slice(0, 3).map(function (q) {
return { q: q.question, opts: q.options, answer: q.correct, explain: q.explanation || '' };
});
}
function renderQuizPanel(container) {
var panel = document.createElement('div');
panel.className = 'ai-panel';
panel.innerHTML = '<div class="ai-panel-header"><div class="ai-panel-icon">Q</div><div class="ai-panel-title">Test Your Understanding</div></div><div id="quizContent"><div class="panel-loading">Loading questions...</div></div>';
container.appendChild(panel);
if (lessonQuizPromise) {
lessonQuizPromise.then(function (data) {
var questions = lessonQuizPanelQuestions(data);
if (!questions.length) questions = getQuizQuestions();
fillQuizPanel(panel.querySelector('#quizContent'), questions);
});
} else {
fillQuizPanel(panel.querySelector('#quizContent'), getQuizQuestions());
}
}
function fillQuizPanel(body, questions) {
var phase = '';
if (currentLessonIndex >= 0) {
var fl = flatLessons[currentLessonIndex];
phase = fl.phaseSlug || '';
}
var html = '<div class="ai-panel-subtitle">Did you get it?</div>';
html += '<div class="quiz-container" id="quizContainer">';
var letters = ['A', 'B', 'C', 'D'];
questions.forEach(function (q, qi) {
var panelQuestionLabel = 'quiz-q-' + qi + '-label';
var panelExplanation = 'quiz-explain-' + qi;
html += '<div class="quiz-question" id="quiz-q-' + qi + '">';
html += '<div class="quiz-question-num">Question ' + (qi + 1) + ' of ' + questions.length + '</div>';
html += '<div class="quiz-question-text" id="' + panelQuestionLabel + '">' + escapeHtml(q.q) + '</div>';
html += '<div class="quiz-options" role="group" aria-labelledby="' + panelQuestionLabel + '">';
q.opts.forEach(function (opt, oi) {
html += '<button type="button" class="quiz-option" data-qi="' + qi + '" data-oi="' + oi + '" aria-pressed="false" aria-describedby="' + panelExplanation + '" onclick="handleQuizAnswer(' + qi + ',' + oi + ')">';
html += '<span class="opt-letter" aria-hidden="true">' + letters[oi] + '</span>';
html += '<span class="quiz-option-text">' + escapeHtml(opt) + '</span>';
html += '</button>';
});
html += '</div>';
html += '<div class="quiz-explanation" id="' + panelExplanation + '" role="status" aria-live="polite">' + escapeHtml(q.explain) + '</div>';
html += '</div>';
});
html += '<div class="quiz-score" id="quizScore"><div class="quiz-score-number" id="quizScoreNum">0/' + questions.length + '</div><div class="quiz-score-label">Complete all questions to see your score</div></div>';
var phaseArgumentMatch = phase.match(/^(\d+)/);
var phaseArgument = phaseArgumentMatch ? phaseArgumentMatch[1] : phase || 'current phase';
html += '<div class="quiz-deeper">Want a deeper quiz? In Codex use <code>check-understanding ' + escapeHtml(phaseArgument) + '</code> or choose it from <code>/skills</code>. In Claude Code use <code>/check-understanding ' + escapeHtml(phaseArgument) + '</code>. In another compatible host say: <code>Use check-understanding to quiz me on Phase ' + escapeHtml(phaseArgument) + '.</code></div>';
html += '</div>';
body.innerHTML = html;
window._quizData = questions;
window._quizAnswered = 0;
window._quizCorrect = 0;
}
window.handleQuizAnswer = function (qi, oi) {
var questions = window._quizData;
if (!questions) return;
var qEl = document.getElementById('quiz-q-' + qi);
if (!qEl) return;
var opts = qEl.querySelectorAll('.quiz-option');
if (opts[0].classList.contains('disabled')) return;
var correct = questions[qi].answer;
opts.forEach(function (opt, i) {
var optionText = opt.querySelector('.quiz-option-text');
var accessibleLabel = optionText ? optionText.textContent : opt.textContent;
opt.classList.add('disabled');
opt.disabled = true;
opt.setAttribute('aria-disabled', 'true');
opt.setAttribute('aria-pressed', i === oi ? 'true' : 'false');
if (i === oi) {
opt.classList.add('selected');
opt.classList.add(i === correct ? 'correct' : 'wrong');
opt.setAttribute('aria-label', accessibleLabel + (i === correct ? ', selected, correct' : ', selected, incorrect'));
}
if (i === correct) {
opt.classList.add('is-answer');
if (i !== oi) opt.setAttribute('aria-label', accessibleLabel + ', correct answer');
}
});
var explain = document.getElementById('quiz-explain-' + qi);
if (explain) {
explain.textContent = (oi === correct ? 'Correct. ' : 'Not quite. ') + explain.textContent;
explain.classList.add('visible');
}
window._quizAnswered++;
if (oi === correct) window._quizCorrect++;
if (window._quizAnswered === questions.length) {
var scoreEl = document.getElementById('quizScore');
var scoreNum = document.getElementById('quizScoreNum');
if (scoreEl && scoreNum) {
scoreNum.textContent = window._quizCorrect + '/' + questions.length;
scoreEl.querySelector('.quiz-score-label').textContent =
window._quizCorrect === questions.length ? 'Perfect score!' :
window._quizCorrect >= 2 ? 'Great work!' : 'Keep studying!';
scoreEl.classList.add('visible');
}
}
};
function refreshProgressUi() {
buildSidebar();
syncLessonActionProgress();
var container = document.getElementById('aiPanels');
if (container) {
renderLearningPathPanel(container);
if (learningPathMode) renderContinuePanel(container);
}
syncLearningPathGateLinks();
}
function learningPanelReducedMotion() {
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
}
function timelineItemKey(item, index) {
return item.getAttribute('data-path-step') || item.getAttribute('data-timeline-key') || item.tagName + ':' + index;
}
function copyOptionalAttribute(target, source, name) {
if (source.hasAttribute(name)) target.setAttribute(name, source.getAttribute(name));
else target.removeAttribute(name);
}
function syncLearningTimeline(currentTimeline, nextTimeline) {
if (!currentTimeline || !nextTimeline) return;
var currentItems = Array.prototype.slice.call(currentTimeline.children);
var nextItems = Array.prototype.slice.call(nextTimeline.children);
var compatible = currentItems.length === nextItems.length && currentItems.every(function (item, index) {
return item.tagName === nextItems[index].tagName && timelineItemKey(item, index) === timelineItemKey(nextItems[index], index);
});
if (!compatible) {
currentTimeline.replaceWith(nextTimeline.cloneNode(true));
return;
}
for (var i = 0; i < currentItems.length; i++) {
var currentItem = currentItems[i];
var nextItem = nextItems[i];
currentItem.className = nextItem.className;
copyOptionalAttribute(currentItem, nextItem, 'href');
copyOptionalAttribute(currentItem, nextItem, 'title');
copyOptionalAttribute(currentItem, nextItem, 'aria-label');
copyOptionalAttribute(currentItem, nextItem, 'aria-current');
var currentIndex = currentItem.querySelector('.timeline-index');
var nextIndex = nextItem.querySelector('.timeline-index');
if (currentIndex && nextIndex) currentIndex.textContent = nextIndex.textContent;
var currentLabel = currentItem.querySelector('.timeline-label');
var nextLabel = nextItem.querySelector('.timeline-label');
if (currentLabel && nextLabel) currentLabel.textContent = nextLabel.textContent;
if (!currentLabel && !nextLabel) currentItem.textContent = nextItem.textContent;
}
}
function revealPhaseCompletion(panel, animate) {
var callout = panel && panel.querySelector('.phase-complete-callout.visible');
if (!callout) return;
callout.classList.remove('is-entering', 'is-entered');
if (!animate || learningPanelReducedMotion()) {
callout.classList.add('is-entered');
return;
}
callout.classList.add('is-entering');
window.requestAnimationFrame(function () {
if (!callout.isConnected) return;
callout.classList.remove('is-entering');
callout.classList.add('is-entered');
});
}
function syncLearningPathPanel(currentPanel, nextPanel) {
currentPanel.className = nextPanel.className;
var currentTitle = currentPanel.querySelector('.ai-panel-title');
var nextTitle = nextPanel.querySelector('.ai-panel-title');
if (currentTitle && nextTitle) currentTitle.textContent = nextTitle.textContent;
var currentIcon = currentPanel.querySelector('.ai-panel-icon');
var nextIcon = nextPanel.querySelector('.ai-panel-icon');
if (currentIcon && nextIcon) currentIcon.textContent = nextIcon.textContent;
var currentSubtitle = currentPanel.querySelector('.path-panel-context .ai-panel-subtitle');
var nextSubtitle = nextPanel.querySelector('.path-panel-context .ai-panel-subtitle');
if (currentSubtitle && nextSubtitle) currentSubtitle.textContent = nextSubtitle.textContent;
var currentPosition = currentPanel.querySelector('.path-position');
var nextPosition = nextPanel.querySelector('.path-position');
if (currentPosition && nextPosition) currentPosition.textContent = nextPosition.textContent;
syncLearningTimeline(
currentPanel.querySelector('.learning-timeline'),
nextPanel.querySelector('.learning-timeline')
);
var currentFill = currentPanel.querySelector('.phase-progress-fill');
var nextFill = nextPanel.querySelector('.phase-progress-fill');
if (currentFill && nextFill) {
currentFill.style.setProperty('--lesson-progress', nextFill.style.getPropertyValue('--lesson-progress'));
}
var currentCounter = currentPanel.querySelector('.phase-progress-text');
var nextCounter = nextPanel.querySelector('.phase-progress-text');
if (currentCounter && nextCounter) currentCounter.textContent = nextCounter.textContent;
var currentTail = currentPanel.querySelector('.path-panel-tail');
var nextTail = nextPanel.querySelector('.path-panel-tail');
var hadCompletion = !!(currentTail && currentTail.querySelector('.phase-complete-callout.visible'));
if (currentTail && nextTail) currentTail.innerHTML = nextTail.innerHTML;
var hasCompletion = !!(currentTail && currentTail.querySelector('.phase-complete-callout.visible'));
if (hasCompletion) revealPhaseCompletion(currentPanel, !hadCompletion);
}
function mountLearningPathPanel(container, panel, pathKey) {
panel.id = 'learningPathPanel';
panel.setAttribute('data-learning-path-key', pathKey);
var existing = document.getElementById('learningPathPanel');
if (existing && existing.getAttribute('data-learning-path-key') === pathKey) {
syncLearningPathPanel(existing, panel);
return existing;
}
if (existing && existing.parentNode) existing.parentNode.replaceChild(panel, existing);
else container.appendChild(panel);
revealPhaseCompletion(panel, true);
return panel;
}
function mountContinuePanel(container, panel) {
panel.id = 'continuePathPanel';
var existing = document.getElementById('continuePathPanel');
if (existing && existing.parentNode) existing.parentNode.replaceChild(panel, existing);
else container.appendChild(panel);
}
function renderLearningPathPanel(container) {
if (certificationMode) {
if (currentLessonIndex < 0) {
var staleCertPanel = document.getElementById('learningPathPanel');
if (staleCertPanel) staleCertPanel.remove();
return;
}
var certPanel = document.createElement('div');
certPanel.className = 'ai-panel ai-panel--path';
var certTotal = flatLessons.length;
var certDone = 0;
for (var cp = 0; cp < flatLessons.length; cp++) {
if (window.AIFSProgress && window.AIFSProgress.isLessonComplete(flatLessons[cp].path)) certDone++;
}
var certPct = certTotal ? Math.round((certDone / certTotal) * 100) : 0;
var certStart = Math.max(0, currentLessonIndex - 1);
var certEnd = Math.min(certTotal, certStart + 3);
certStart = Math.max(0, certEnd - 3);
var certHtml = '<div class="ai-panel-header"><div class="ai-panel-icon">C</div><div class="ai-panel-title">Certification Path</div></div>';
certHtml += '<div class="path-panel-context"><div class="ai-panel-subtitle">' + escapeHtml(certificationTrack.examCode || certificationTrack.shortName || 'Certification') + ': ' + escapeHtml(certificationTrack.credential || certificationTrack.title || 'Study path') + '</div><span class="path-position">Lesson ' + (currentLessonIndex + 1) + ' of ' + certTotal + '</span></div>';
certHtml += '<div class="learning-timeline">';
var certLessonDoneAt = function (index) {
return !!(window.AIFSProgress && flatLessons[index] && window.AIFSProgress.isLessonComplete(flatLessons[index].path));
};
if (certStart > 0) {
certHtml += '<span class="timeline-ellipsis" data-timeline-key="before">' + certStart + ' earlier</span>';
}
for (var cv = certStart; cv < certEnd; cv++) {
var certItem = flatLessons[cv];
var certItemDone = certLessonDoneAt(cv);
var certClass = cv === currentLessonIndex ? 'current' : certItemDone ? 'done' : '';
certHtml += '<a class="timeline-item ' + certClass + '" data-path-step="' + escapeAttr(certItem.path) + '" href="lesson?path=' + encodeURIComponent(certItem.path) + '&track=' + encodeURIComponent(certificationTrack.id) + '" title="' + escapeAttr(certItem.lessonName) + '" aria-label="Lesson ' + (cv + 1) + ' of ' + certTotal + ': ' + escapeAttr(certItem.lessonName) + '"' + (cv === currentLessonIndex ? ' aria-current="step"' : '') + '><div class="timeline-step-meta"><div class="timeline-dot"></div><span class="timeline-index">' + String(cv + 1).padStart(2, '0') + '</span></div><div class="timeline-label">' + escapeHtml(certItem.lessonName) + '</div></a>';
}
if (certEnd < certTotal) certHtml += '<span class="timeline-ellipsis" data-timeline-key="after">' + (certTotal - certEnd) + ' later</span>';
certHtml += '</div><div class="phase-progress-bar"><div class="phase-progress-fill" style="--lesson-progress:' + (certPct / 100) + '"></div></div>';
certHtml += '<div class="phase-progress-text">' + certDone + ' of ' + certTotal + ' lessons completed in this certification path</div><div class="path-panel-tail"></div>';
certPanel.innerHTML = certHtml;
mountLearningPathPanel(container, certPanel, 'certification:' + certificationTrack.id);
return;
}
if (learningPathMode) {
var focusedPanel = document.createElement('div');
focusedPanel.className = 'ai-panel ai-panel--path';
var focusedLessons = flatLessons.filter(function (item) { return item.required !== false; });
var focusedOptionalLessons = flatLessons.filter(function (item) { return item.required === false; });
var focusedIndex = focusedLessons.findIndex(function (item) { return item.path === lessonPath; });
var focusedEntry = flatLessons.find(function (item) { return item.path === lessonPath; }) || null;
var focusedDone = 0;
for (var fp = 0; fp < focusedLessons.length; fp++) {
if (window.AIFSProgress && window.AIFSProgress.isLessonComplete(focusedLessons[fp].path)) focusedDone++;
}
var focusedPct = focusedLessons.length ? Math.round((focusedDone / focusedLessons.length) * 100) : 0;
var focusedHtml = '<div class="ai-panel-header"><div class="ai-panel-icon">P</div><div class="ai-panel-title">Focused Learning Path</div></div>';
focusedHtml += '<div class="path-panel-context"><div class="ai-panel-subtitle">' + escapeHtml(learningPath.title || learningPath.id) + '</div><span class="path-position">';
focusedHtml += focusedIndex >= 0
? 'Lesson ' + (focusedIndex + 1) + ' of ' + focusedLessons.length
: 'Optional lesson';
focusedHtml += '</span></div>';
focusedHtml += learningPathPreflightHtml();
focusedHtml += '<div class="learning-timeline">';
var focusedWindowSize = 3;
var focusedStart = focusedIndex >= 0
? Math.max(0, focusedIndex - 1)
: Math.max(0, focusedLessons.length - focusedWindowSize);
var focusedEnd = Math.min(focusedLessons.length, focusedStart + focusedWindowSize);
focusedStart = Math.max(0, focusedEnd - focusedWindowSize);
if (focusedStart > 0) focusedHtml += '<span class="timeline-ellipsis" data-timeline-key="before">' + focusedStart + ' earlier</span>';
for (var fpi = focusedStart; fpi < focusedEnd; fpi++) {
var focusedLesson = focusedLessons[fpi];
var focusedLessonDone = !!(window.AIFSProgress && window.AIFSProgress.isLessonComplete(focusedLesson.path));
var focusedClass = focusedLesson.path === lessonPath ? 'current' : focusedLessonDone ? 'done' : '';
focusedHtml += '<a class="timeline-item ' + focusedClass + learningPathGateClass(focusedLesson) + '" data-path-step="' + escapeAttr(focusedLesson.path) + '" href="' + learningPathLessonHref(focusedLesson.path) + '" title="' + escapeAttr(focusedLesson.lessonName) + '"' + learningPathGateAttributes(focusedLesson) + ' aria-label="Lesson ' + (fpi + 1) + ' of ' + focusedLessons.length + ': ' + escapeAttr(focusedLesson.lessonName) + '"' + (focusedLesson.path === lessonPath ? ' aria-current="step"' : '') + '>';
focusedHtml += '<div class="timeline-step-meta"><div class="timeline-dot"></div><span class="timeline-index">' + String(fpi + 1).padStart(2, '0') + '</span></div><div class="timeline-label">' + escapeHtml(focusedLesson.lessonName) + '</div></a>';
}
if (focusedEnd < focusedLessons.length) focusedHtml += '<span class="timeline-ellipsis" data-timeline-key="after">' + (focusedLessons.length - focusedEnd) + ' later</span>';
focusedHtml += '</div><div class="phase-progress-bar"><div class="phase-progress-fill" style="--lesson-progress:' + (focusedPct / 100) + '"></div></div>';
focusedHtml += '<div class="phase-progress-text">';
focusedHtml += focusedIndex >= 0
? focusedDone + ' of ' + focusedLessons.length + ' lessons completed in this path.'
: 'Optional lesson. ' + focusedDone + ' of ' + focusedLessons.length + ' required lessons completed.';
var focusedChecks = learningPathPreflightDefinitions();
var focusedChecksDone = focusedChecks.filter(function (check) {
return window.AIFSLearningPathProgress && window.AIFSLearningPathProgress.isConfirmed(learningPath.id, check.id);
}).length;
if (focusedChecks.length) focusedHtml += ' ' + focusedChecksDone + ' of ' + focusedChecks.length + ' knowledge preflights confirmed.';
focusedHtml += '</div>';
focusedHtml += '<div class="path-panel-tail">';
if (focusedEntry) {
focusedHtml += learningPathPrerequisiteCallout(focusedEntry, 'Required before this lesson');
}
var nextRequired = focusedIndex >= 0 && focusedIndex < focusedLessons.length - 1
? focusedLessons[focusedIndex + 1]
: null;
if (nextRequired) {
focusedHtml += learningPathPrerequisiteCallout(nextRequired, 'Before the next lesson');
} else {
focusedHtml += '<div class="path-completion-actions">';
for (var focusedOptionalIndex = 0; focusedOptionalIndex < focusedOptionalLessons.length; focusedOptionalIndex++) {
var focusedOptionalLesson = focusedOptionalLessons[focusedOptionalIndex];
if (focusedOptionalLesson.path === lessonPath) continue;
var focusedOptionalLocked = learningPathEntryLocked(focusedOptionalLesson);
focusedHtml += '<a class="path-completion-link' + learningPathGateClass(focusedOptionalLesson) + '" href="' + learningPathLessonHref(focusedOptionalLesson.path) + '"' + learningPathGateAttributes(focusedOptionalLesson) + '>' + (focusedOptionalLocked ? 'Locked optional capstone: ' : 'Optional capstone: ') + escapeHtml(focusedOptionalLesson.lessonName) + '</a>';
}
if (focusedLessons.length) focusedHtml += '<a href="' + learningPathLessonHref(focusedLessons[0].path) + '">Restart path</a>';
focusedHtml += '<a href="catalog.html">Full course catalog</a></div>';
}
focusedHtml += '</div>';
focusedPanel.innerHTML = focusedHtml;
mountLearningPathPanel(container, focusedPanel, 'learning-path:' + learningPath.id);
return;
}
if (currentLessonIndex < 0 || typeof PHASES === 'undefined') {
var stalePanel = document.getElementById('learningPathPanel');
if (stalePanel) stalePanel.remove();
return;
}
var panel = document.createElement('div');
panel.className = 'ai-panel ai-panel--path';
var current = flatLessons[currentLessonIndex];
var phaseIdx = current.phaseIndex;
var phase = PHASES[phaseIdx];
var lessonsInPhase = [];
var currentIdxInPhase = -1;
for (var i = 0; i < flatLessons.length; i++) {
if (flatLessons[i].phaseIndex === phaseIdx) {
if (i === currentLessonIndex) currentIdxInPhase = lessonsInPhase.length;
lessonsInPhase.push({ flat: flatLessons[i], flatIdx: i });
}
}
var html = '<div class="ai-panel-header"><div class="ai-panel-icon">P</div><div class="ai-panel-title">Learning Path</div></div>';
html += '<div class="path-panel-context"><div class="ai-panel-subtitle">Phase ' + String(phase.id).padStart(2, '0') + ': ' + escapeHtml(phase.name) + '</div><span class="path-position">Lesson ' + (currentIdxInPhase + 1) + ' of ' + lessonsInPhase.length + '</span></div>';
// Keep the route readable in the available column: one before, current, one after.
var windowSize = 3;
var startIdx = Math.max(0, currentIdxInPhase - 1);
var endIdx = Math.min(lessonsInPhase.length, startIdx + windowSize);
startIdx = Math.max(0, endIdx - windowSize);
var visible = lessonsInPhase.slice(startIdx, endIdx);
var hiddenBefore = startIdx;
var hiddenAfter = lessonsInPhase.length - endIdx;
var coreLessonDoneAt = function (index) {
return !!(window.AIFSProgress && lessonsInPhase[index] && window.AIFSProgress.isLessonComplete(lessonsInPhase[index].flat.path));
};
html += '<div class="learning-timeline">';
if (hiddenBefore > 0) {
html += '<span class="timeline-ellipsis" data-timeline-key="before" title="' + hiddenBefore + ' earlier lessons">' + hiddenBefore + ' earlier</span>';
}
visible.forEach(function (item, vIdx) {
var idx = startIdx + vIdx;
var isCurrent = idx === currentIdxInPhase;
var isDone = coreLessonDoneAt(idx);
var cls = (isCurrent ? 'current ' : '') + (isDone ? 'done' : '');
if (item.flat.isReadable) {
html += '<a class="timeline-item ' + cls + '" data-path-step="' + escapeAttr(item.flat.path) + '" href="lesson?path=' + item.flat.path + '" title="' + escapeAttr(item.flat.lessonName) + '" aria-label="Lesson ' + (idx + 1) + ' of ' + lessonsInPhase.length + ': ' + escapeAttr(item.flat.lessonName) + '"' + (isCurrent ? ' aria-current="step"' : '') + '>';
} else {
html += '<span class="timeline-item ' + cls + ' disabled" data-path-step="' + escapeAttr(item.flat.path) + '" title="' + escapeAttr(item.flat.lessonName) + '">';
}
html += '<div class="timeline-step-meta"><div class="timeline-dot"></div><span class="timeline-index">' + String(idx + 1).padStart(2, '0') + '</span></div>';
html += '<div class="timeline-label">' + escapeHtml(item.flat.lessonName) + '</div>';
html += item.flat.isReadable ? '</a>' : '</span>';
});
if (hiddenAfter > 0) {
html += '<span class="timeline-ellipsis" data-timeline-key="after" title="' + hiddenAfter + ' later lessons">' + hiddenAfter + ' later</span>';
}
html += '</div>';
var completedCount = 0;
for (var completedIndex = 0; completedIndex < lessonsInPhase.length; completedIndex++) {
if (coreLessonDoneAt(completedIndex)) completedCount++;
}
var totalCount = lessonsInPhase.length;
var pct = totalCount ? Math.round((completedCount / totalCount) * 100) : 0;
html += '<div class="phase-progress-bar"><div class="phase-progress-fill" style="--lesson-progress:' + (pct / 100) + '"></div></div>';
html += '<div class="phase-progress-text">You\'ve completed ' + completedCount + ' of ' + totalCount + ' lessons in this phase</div>';
html += '<div class="path-panel-tail">';
if (completedCount >= totalCount && phaseIdx < PHASES.length - 1) {
var nextPhase = PHASES[phaseIdx + 1];
html += '<div class="phase-complete-callout visible">Ready for Phase ' + String(nextPhase.id).padStart(2, '0') + ': ' + escapeHtml(nextPhase.name) + '</div>';
}
html += '</div>';
panel.innerHTML = html;
mountLearningPathPanel(container, panel, 'phase:' + phase.id);
}
function renderContinuePanel(container) {
if (currentLessonIndex < 0) return;
if (certificationMode) {
var certContinue = document.createElement('div');
certContinue.className = 'ai-panel ai-panel--continue';
var certNext = null;
for (var cn = currentLessonIndex + 1; cn < flatLessons.length; cn++) {
if (flatLessons[cn].isReadable) { certNext = flatLessons[cn]; break; }
}
var continueHtml = '<div class="ai-panel-header"><div class="ai-panel-icon">N</div><div class="ai-panel-title">Continue Certification Prep</div></div><div class="continue-panel">';
if (!certNext) continueHtml += '<div class="phase-finished">You reached the end of this lesson path.</div>';
continueHtml += '<div class="continue-links">';
if (certNext) continueHtml += '<a class="continue-link" href="lesson?path=' + encodeURIComponent(certNext.path) + '&track=' + encodeURIComponent(certificationTrack.id) + '">Next: ' + escapeHtml(certNext.lessonName) + '</a>';
continueHtml += '<a class="continue-link" href="certification?id=' + encodeURIComponent(certificationTrack.id) + '">Track overview and practice</a>';
continueHtml += '<a class="continue-link" href="certifications.html">All certification tracks</a></div>';
continueHtml += '<div class="continue-callout">Practice results are course percentages, not official scaled exam scores. Use them to find the next domain to study.</div></div>';
certContinue.innerHTML = continueHtml;
container.appendChild(certContinue);
return;
}
if (learningPathMode) {
var staleFocusedContinue = document.getElementById('continuePathPanel');
if (staleFocusedContinue) staleFocusedContinue.remove();
return;
}
var panel = document.createElement('div');
panel.className = 'ai-panel ai-panel--continue';
var next = null;
for (var ci = currentLessonIndex + 1; ci < flatLessons.length; ci++) {
if (flatLessons[ci].isReadable) { next = flatLessons[ci]; break; }
}
var current = flatLessons[currentLessonIndex];
var phaseIdx = current.phaseIndex;
var phase = PHASES[phaseIdx];
var html = '<div class="ai-panel-header"><div class="ai-panel-icon">N</div><div class="ai-panel-title">Continue Learning</div></div>';
html += '<div class="continue-panel">';
if (!next || !next.path) {
html += '<div class="phase-finished">\u2705 You finished this phase!</div>';
}
html += '<div class="continue-links">';
var phaseSlug = extractPhaseSlug(phase, phaseIdx);
var firstLesson = phase.lessons[0] ? extractLessonSlug(phase.lessons[0], 0) : '';
html += '<a class="continue-link" href="lesson?path=phases/' + phaseSlug + '/' + firstLesson + '">Browse all Phase ' + String(phase.id).padStart(2, '0') + ' lessons</a>';
html += '<a class="continue-link" href="catalog.html">Full course catalog</a>';
html += '</div>';
html += '<div class="continue-callout">Run <code>/find-your-level</code> in Claude, Cursor, Codex, OpenClaw, Hermes, or any agent with the curriculum skills installed for a personalized learning path</div>';
html += '</div>';
panel.innerHTML = html;
container.appendChild(panel);
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Start only after every module-level cache and promise has been
// initialized. The large certification bundle is loaded only for
// certification lesson paths, then the same rendering path continues.
function startLesson() {
initializeLessonData();
buildSidebar();
initSidebarToggle();
initScrollProgress();
initLearningPathGateNavigation();
if (window.AIFSProgress) window.AIFSProgress.onChange(refreshProgressUi);
fetchLesson(lessonPath);
}
var certificationDataReady = window.__AIFS_CERTIFICATION_DATA_READY;
if (certificationDataReady && typeof certificationDataReady.then === 'function') {
certificationDataReady.then(startLesson).catch(function (error) {
console.error('Certification data failed to load', error);
showError('Certification data unavailable', 'Reload the page after the certification bundle is available.');
});
} else {
startLesson();
}
})();
</script>
</body>
</html>