
Teaching Site Design System
- 48 installs
- 47 repo stars
- Updated May 15, 2026
- kevintsai1202/teaching-site-skills
Helps with design & ui/ux tasks.
About
teaching-site-design-system is a Claude Code skill for design & ui/ux. It helps solo builders move faster with AI-assisted coding.
- teaching-site-design-system
- Design & UI/UX
- AI-coding skill
Teaching Site Design System by the numbers
- 48 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,246 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill teaching-site-design-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 48 |
|---|---|
| repo stars | ★ 47 |
| Last updated | May 15, 2026 |
| Repository | kevintsai1202/teaching-site-skills ↗ |
What it does
Helps with design & ui/ux tasks.
Files
Teaching Site Design System
Token starter (copy this, don't re-derive): `templates/tokens.css` — full:rootblock + dark theme + signature components (day-hero, glass-card, prompt-card, task-list, learning-goal, concept, unit, material, table). Paste into the<style>block ofindex.html(vanilla SPA pattern) or use as externalstyle.css.
>
Schema authority: this skill defines visual tokens; data field names live in `_shared/domain-primitives.md`. When styling a component, read that file to know the data shape you're rendering.
>
Reference implementation: d:/GitHub/ai-workshop/index.html:11-1700 — the full production CSS this skill's tokens were extracted from.This skill defines the visual layer of a teaching site: color tokens, typography, geometry, components, dark mode strategy, and — critically — the reasoning behind each decision. The rationale matters more than the literal values; future sites can re-tune the values, but the why survives across projects.
When to Invoke
- Setting up a new teaching site (read tokens before writing first CSS).
- Adding a new visual element (badge, card, hero) — check whether an existing pattern covers it before inventing.
- Tuning dark mode (which token to override, which not to).
- Producing a corporate edition or ebook (same tokens flow to print CSS).
- Reviewing visual consistency complaints ("it looks inconsistent here").
The Foundational Decision: OKLCH, Not HEX/RGB
:root {
--bg: oklch(99% 0.002 240);
--surface: oklch(100% 0 0);
--fg: oklch(18% 0.012 250);
--accent: oklch(58% 0.18 255);
--accent-2: oklch(66% 0.14 65);
}Why OKLCH (not #1a73e8 or hsl(...)): 1. Perceptually uniform — oklch(60% ...) and oklch(70% ...) look like an equal step apart; HSL doesn't (its 60→70 yellow jumps visually huge but blue barely changes). 2. Predictable dark mode — for each light token at L=N%, the dark equivalent is roughly L=(100-N)% with the same chroma+hue. Mechanical. 3. `color-mix(in oklab, ...)` works natively — e.g. soft accent tints (color-mix(in oklab, var(--accent) 18%, transparent)) compose cleanly, no manual rgba math. 4. Modern browser support is fine (Chrome 111+, Safari 15.4+, Firefox 113+). For a static teaching site this is acceptable.
If a stakeholder hands you brand colors as hex: convert once to OKLCH and never look back.
Token Architecture (Two-Axis)
Axis 1: Semantic Surface Tokens
--bg /* page background */
--surface /* card / panel */
--surface-2 /* nested card / hover state */
--fg /* primary text */
--muted /* secondary text */
--muted-2 /* tertiary text / placeholder */
--border /* default border */
--border-strong /* emphasis border */These are structural — they describe role in the layout, not color. Dark mode swaps the values; component CSS doesn't change.
Axis 2: Accent Tokens (Dual Accent Strategy)
--accent /* primary action color — blue/violet typically */
--accent-soft /* faint tint, for hover backgrounds, badges */
--accent-deep /* high-contrast variant, for hover text */
--accent-2 /* secondary accent — orange/amber typically */
--accent-2-soft
--accent-2-deepWhy two accents (not one + grayscale): teaching sites have two equally important attention paths:
- Action path (
--accent): "click me", "copy this", "navigate here" — typically cool color (blue). - Content path (
--accent-2): "look here, this is rich content" — Day hero numerals, PROMPT badges, callout backgrounds. Typically warm color (orange/amber).
Single-accent designs end up using grayscale for content emphasis, which makes everything feel like a button. Dual-accent separates "things you click" from "things you read carefully".
Status Tokens
--success, --success-soft /* learning goals ✓, copy success */
--warning, --warning-soft /* gotchas, optional warnings */
--danger, --danger-soft /* errors, destructive actions */Always provide a -soft variant for backgrounds; the base is for text/icons.
Typography: One Font Stack to Rule Them All
--font-display: "Nunito", "jf-openhuninn-1.1", "jf-openhuninn",
"Gen Jyuu Gothic", "Taipei Sans TC Beta",
"PingFang TC", "Noto Sans TC",
system-ui, sans-serif;
/* All four roles point to the same stack */
--font-body: var(--font-display);
--font-serif: var(--font-display);
--font-mono: var(--font-display);Why one stack for everything on a Chinese-language teaching site: 1. Latin + 中文 混排在 90%+ 的段落都會出現。每換一種字體就會出現「字體斷裂感」。 2. The fallback chain handles bilingual rendering — Nunito for Latin glyphs, openhuninn for 中文. 3. Mono / serif variants are mostly used for spacing / weight effects, not actually different typefaces. Implement via font-feature-settings, letter-spacing, font-weight instead of swapping fonts.
Exception: ebook PDF for formal print may use a true serif (Source Han Serif) — that's the only place to break this rule.
Geometry Tokens
--r-sm: 6px; /* buttons, small chips */
--r-md: 10px; /* cards, panels */
--r-lg: 14px; /* heroes, large containers */
--sidebar-w: 268px;
--header-h: 56px;
--content-max: 880px; /* reading line length cap */
--shadow-lift:
0 6px 18px -10px oklch(20% 0.02 250 / 0.18),
0 2px 4px -2px oklch(20% 0.02 250 / 0.06);Three radii is enough — more produces visual chaos. Match radius to element scale: button → sm, card → md, hero → lg.
`--content-max: 880px` is deliberate — comfortable reading line length is ~60–75 CJK characters. Wider feels like documentation, narrower feels like mobile-stuck-on-desktop.
Double-layered shadows (--shadow-lift): one wide-soft + one tight-tighter. Single-layer shadows look digital; double-layer mimics physical card lift. Always negate the y-offset (-10px, -2px) so the shadow doesn't bleed below the element.
Signature Components
1. Day Hero Numeral
.day-hero-numeral {
font-family: var(--font-serif);
font-size: clamp(120px, 16vw, 200px);
font-weight: 500;
line-height: 0.85;
letter-spacing: -0.05em;
color: var(--accent-2);
text-shadow: 0 1px 0 color-mix(in oklab, var(--accent-2) 18%, transparent);
}
.day-hero:hover .day-hero-numeral {
color: var(--accent-deep);
text-shadow:
0 2px 0 color-mix(in oklab, var(--accent-2) 22%, transparent),
0 18px 40px color-mix(in oklab, var(--accent) 22%, transparent);
}Why a giant numeral instead of decorative SVG / illustration for Day headers: an abstract SVG looks like a placeholder ("they forgot to put a real image"). A 200px "D2" reads as intentional, confident, navigational. Specific > abstract.
Why `clamp(120px, 16vw, 200px)`: scales with viewport width but with floor and ceiling so it never feels broken on mobile (would be 120px) or absurd on 4K (capped at 200px).
Why `@keyframes` fade-in, not `opacity: 0 → transition`: if IntersectionObserver fails to fire (e.g. CSS zoom is in play, see static-spa-interactions), the numeral stays at opacity: 0 forever. With @keyframes, the default state is visible; animation only "plays once if triggered".
2. PROMPT Block (Orange Badge + Dark Code)
.prompt-block {
background: oklch(15% 0.02 250); /* always dark, even in light mode */
color: oklch(92% 0.005 250);
border-left: 4px solid var(--accent-2);
padding: 14px 16px;
border-radius: var(--r-md);
}
.prompt-block::before {
content: 'PROMPT';
display: inline-block;
background: var(--accent-2);
color: white;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-family: var(--font-mono);
letter-spacing: 0.1em;
margin-bottom: 8px;
}Why orange (--accent-2) for PROMPT: blue (--accent) is reserved for "clickable / navigation". Orange marks "this is a recipe — read carefully and copy verbatim". Three months in, learners associate orange = prompt block automatically.
Why dark background even in light mode: code-y content reads as code-y. Switching light/dark would feel like the prompt is "less serious" in light mode.
3. Glass Card (glass-card)
.glass-card {
background: color-mix(in oklab, var(--surface) 60%, transparent);
backdrop-filter: blur(12px) saturate(140%);
border: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
border-radius: var(--r-md);
padding: 16px 20px;
box-shadow: var(--shadow-lift);
}Why translucent + backdrop-filter: on a page with rich background (gradient, dotted pattern, illustrations), opaque cards feel heavy. Glass cards let content breathe, signal "this floats on top of the document".
Caveat: requires backdrop-filter browser support. Safari needs -webkit-backdrop-filter prefix. On unsupported browsers, the fallback is just the 60%-opacity color — degrades gracefully.
4. Task Checkbox (Three-State Visual)
.task-checkbox {
width: 18px; height: 18px;
border: 1.5px solid var(--border-strong);
border-radius: var(--r-sm);
display: grid; place-items: center;
transition: all 120ms ease;
}
.task-item:hover .task-checkbox { border-color: var(--accent); }
.task-item.done .task-checkbox {
background: var(--success);
border-color: var(--success);
}
.task-item.done .task-label { text-decoration: line-through; color: var(--muted); }Three states: default (empty + strong border), hover (accent border, hint of intent), done (filled green + strikethrough). The hover state matters — without it, learners aren't sure the checkbox is clickable.
5. Goal Badge (Green ✓)
.learning-goal::before {
content: '✓';
display: inline-block;
width: 20px; height: 20px;
background: var(--success-soft);
color: var(--success);
border-radius: 999px;
text-align: center;
line-height: 20px;
margin-right: 8px;
font-weight: bold;
}Use the green-on-light-green disc consistently for "learning outcomes" / "you'll be able to ...". Don't reuse for "completed" — that's task-checkbox's job. Green has two distinct semantic uses which must look different: goal (badge prefix) vs done (filled checkbox).
Dark Mode Strategy
:root { /* light tokens here */ }
[data-theme="dark"] {
--bg: oklch(16% 0.014 250);
--surface: oklch(19.5% 0.014 250);
--fg: oklch(96% 0.005 250);
--border: oklch(28% 0.014 250);
/* accents usually stay the same — accent-deep may flip to brighter */
--accent-deep: oklch(72% 0.18 255);
}Use `[data-theme]` attribute, not `prefers-color-scheme`: a manual toggle is the source of truth. Auto-following OS theme means the user can't decide per-site. The toggle reads/writes localStorage.
What flips in dark mode:
- ✅ All surface tokens (bg / surface / fg / muted / border)
- ✅
accent-deep(the high-contrast variant) — flip to a brighter L value - ❌ Status colors (success / danger / warning) — these are semantic, keep their hue; the
-softvariant naturally darkens because of the surface change - ❌
accent-2— the dual-accent identity should survive dark mode (the orange brand stays orange)
What stays the same:
- All geometry tokens (radii, sidebar width, shadow definitions — though shadow opacity may need tuning)
- All component layouts and structure
The rule of thumb: token values change, component CSS does not. If you find yourself writing [data-theme="dark"] .task-checkbox { ... }, the abstraction has leaked — refactor the component to use tokens for the property that's varying.
Cross-Format Token Reuse (Web → Ebook)
The ebook print CSS (course-ebook-publishing) reads the same tokens. Specifically:
/* style-ebook.css (print only) */
@media print {
:root {
/* Override only what print needs differently */
--bg: white;
--surface: white;
/* But colors of badges, prompts, headings stay the same */
}
.prompt-block { /* same .prompt-block style — looks identical to web */ }
.learning-goal::before { /* same green disc */ }
}Why: a learner who sees the web first, then opens the PDF handbook, should feel "same product". If web is orange-PROMPT and PDF is gray-PROMPT, the visual brand is shattered.
The corporate edition (course-corporate-edition) does the same — even with brand-customised assets, the design tokens remain.
Three Decisions That Sound Small But Aren't
Decision 1: color-mix(in oklab, X 18%, transparent) for soft tints
Don't hand-write rgba(...). Use color-mix so tints derive from the source token:
background: color-mix(in oklab, var(--accent-2) 18%, transparent);When you eventually re-tune --accent-2, every tint follows. With manual rgba, you'd have 50 places to update.
Decision 2: No grid system, just --content-max
Avoid bringing in a 12-column grid framework. For a teaching site with linear reading flow, max-width: var(--content-max); margin: 0 auto is enough. Columns appear locally via grid-template-columns inside specific components (figure grid, tool gallery) — but the page itself is a stream.
Decision 3: Animation budget = "almost none"
Allowed:
fade-inon scroll (one-time per element)- Hover micro-interactions (color / shadow shift, 120–200ms)
- Sidebar slide (transform, 200ms)
NOT allowed:
- Page transitions between sections
- Decorative idle animations
- Anything > 400ms
Teaching content is dense; animation is a distraction tax. Spend the attention budget on content, not motion.
Anti-Patterns
- Inventing a new color for "this one section" — pull from existing tokens or refactor to add a properly-named token. Drive-by hex values rot the system.
- Using `--accent` for content-emphasis (like PROMPT badge) — collapses the dual-accent semantic. Use
--accent-2. - Different font for code blocks — see typography section. Use the same stack with
font-weight/letter-spacingfor differentiation, or accept the visual cost. - Manually styling dark mode per component — see dark mode section. If a component needs special dark-mode handling, the component is using a non-tokenised value somewhere.
- Forgetting `-soft` variants — every accent / status color needs a soft variant. Without it, designers reach for ad-hoc opacity values.
Hand-off
When this skill is consumed by another skill (SPA / interactions / ebook / corporate):
- Reference
:roottokens, never raw values. - If you find yourself writing a literal
#...hex orrgb(...), you've broken the system — either it belongs as a new token, or you're recreating an existing one. - New components should compose existing patterns (glass-card + prompt-block + task-list) before inventing.
The token file (the :root { ... } block) is the single source of truth. Print it on the wall. Re-tune values when the brand evolves, but never widen the token set casually.
/* ============================================================
* tokens.css — Teaching Site Design System
*
* Paste this into <style> at the top of index.html (vanilla SPA pattern)
* OR link as <link rel="stylesheet" href="style.css"> if extracted.
*
* Schema authority: ../../_shared/domain-primitives.md
* Reference impl: d:/GitHub/ai-workshop/index.html (lines 11–1700)
* Design rationale: ../../teaching-site-design-system/SKILL.md
*
* Modern minimal direction (Linear / Vercel-ish):
* - OKLCH everywhere (perceptually uniform, mechanical dark mode)
* - Near-grayscale base + single saturated blue accent + warm amber accent-2
* - Hairline borders, double-layer shadows, no gradients
* - Single font stack handles bilingual (Latin + 中文)
* ============================================================ */
/* ===================== light tokens ===================== */
:root {
/* surfaces */
--bg: oklch(99% 0.002 240);
--surface: oklch(100% 0 0);
--surface-2: oklch(97.5% 0.004 250);
--fg: oklch(18% 0.012 250);
--muted: oklch(54% 0.012 250);
--muted-2: oklch(72% 0.01 250);
--border: oklch(92% 0.005 250);
--border-strong: oklch(82% 0.008 250);
/* primary accent — clickable / navigation */
--accent: oklch(58% 0.18 255);
--accent-soft: oklch(95% 0.04 255);
--accent-deep: oklch(45% 0.18 255);
/* secondary accent — content emphasis (Day numerals, PROMPT badges, callouts)
* Complementary to --accent so the page has rhythm without clashing */
--accent-2: oklch(66% 0.14 65);
--accent-2-soft: oklch(94% 0.045 65);
--accent-2-deep: oklch(48% 0.14 60);
/* status — always pair base + soft */
--success: oklch(58% 0.16 145);
--success-soft: oklch(95% 0.04 145);
--danger: oklch(60% 0.18 25);
--danger-soft: oklch(95% 0.05 25);
--warning: oklch(70% 0.13 75);
--warning-soft: oklch(95% 0.05 75);
/* dot-grid texture (sidebar brand + day-hero only — never on body) */
--dot-color: oklch(80% 0.005 250 / 0.6);
--dot-bg: radial-gradient(circle at 1px 1px, var(--dot-color) 1px, transparent 0);
/* typography — single stack handles Latin + CJK */
--font-display: "Nunito", "jf-openhuninn-1.1", "jf-openhuninn",
"Gen Jyuu Gothic", "Taipei Sans TC Beta",
"PingFang TC", "Noto Sans TC",
system-ui, sans-serif;
--font-body: var(--font-display);
--font-serif: var(--font-display);
--font-mono: var(--font-display);
/* geometry */
--r-sm: 6px; /* buttons, chips */
--r-md: 10px; /* cards, panels */
--r-lg: 14px; /* heroes, large containers */
--sidebar-w: 268px;
--header-h: 56px;
--content-max: 880px; /* reading line length cap (~60–75 CJK chars) */
/* double-layer shadow for hover lift; negate y-offset so shadow stays under */
--shadow-lift: 0 6px 18px -10px oklch(20% 0.02 250 / 0.18),
0 2px 4px -2px oklch(20% 0.02 250 / 0.06);
}
/* ===================== dark theme ===================== */
[data-theme="dark"] {
--bg: oklch(16% 0.014 250);
--surface: oklch(19.5% 0.014 250);
--surface-2: oklch(22.5% 0.014 250);
--fg: oklch(96% 0.005 250);
--muted: oklch(68% 0.012 250);
--muted-2: oklch(50% 0.012 250);
--border: oklch(28% 0.014 250);
--border-strong: oklch(38% 0.014 250);
--accent: oklch(70% 0.17 255);
--accent-soft: oklch(30% 0.08 255);
--accent-deep: oklch(82% 0.13 255);
--accent-2: oklch(78% 0.13 65);
--accent-2-soft: oklch(30% 0.075 65);
--accent-2-deep: oklch(86% 0.12 65);
--success: oklch(70% 0.16 145);
--success-soft: oklch(28% 0.07 145);
--danger: oklch(70% 0.18 25);
--danger-soft: oklch(28% 0.08 25);
--warning: oklch(78% 0.13 75);
--warning-soft: oklch(28% 0.08 75);
--shadow-lift: 0 8px 22px -10px oklch(0% 0 0 / 0.55),
0 2px 6px -2px oklch(0% 0 0 / 0.3);
color-scheme: dark;
}
/* ===================== reset & base ===================== */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: 15px;
font-weight: 600;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
letter-spacing: -0.005em;
transition: background-color 220ms ease, color 220ms ease;
}
h1, h2, h3, h4 {
font-family: var(--font-display);
letter-spacing: -0.02em;
line-height: 1.25;
margin: 0;
color: var(--fg);
}
h1 { font-size: 32px; font-weight: 660; }
h2 { font-size: 24px; font-weight: 600; }
h3 { font-size: 18px; font-weight: 600; }
h4 { font-size: 15px; font-weight: 600; }
p { margin: 0 0 12px; text-wrap: pretty; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }
::selection { background: var(--accent-soft); color: var(--accent-deep); }
button { font: inherit; color: inherit; cursor: pointer; }
/* ===================== layout shell ===================== */
/* IMPORTANT: do NOT add overflow-x:hidden on .app — browser implicitly sets
* overflow-y:auto, breaking window scroll and topbar sticky. See ai-workshop
* CLAUDE.md for the full diagnosis. */
.app { min-height: 100vh; }
.app.sidebar-closed .sidebar { transform: translateX(-100%); }
.sidebar {
position: fixed; left: 0; top: 0;
width: var(--sidebar-w); height: 100vh;
z-index: 10;
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
border-right: 1px solid var(--border);
background: var(--surface);
display: flex; flex-direction: column;
overflow: hidden;
}
.main {
margin-left: var(--sidebar-w);
transition: margin-left 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.app.sidebar-closed .main { margin-left: 0; }
.topbar {
position: sticky; top: 0; z-index: 5;
height: var(--header-h);
background: color-mix(in oklab, var(--surface) 92%, transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 24px;
}
.content {
max-width: var(--content-max);
margin: 0 auto;
padding: 32px 24px 80px;
/* zoom only here — never on html (would cascade with sidebar) */
}
/* ===================== mobile breakpoint ===================== */
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.app.sidebar-open .sidebar { transform: translateX(0); }
.main { margin-left: 0; }
}
/* ===================== signature: day-hero numeral ===================== */
.day-hero {
position: relative;
padding: 48px 32px;
border-radius: var(--r-lg);
background:
var(--dot-bg) 0 0 / 24px 24px,
var(--surface);
border: 1px solid var(--border);
margin-bottom: 32px;
display: grid;
grid-template-columns: auto 1fr;
gap: 32px;
align-items: center;
}
.day-hero-numeral {
font-family: var(--font-serif);
font-size: clamp(120px, 16vw, 200px);
font-weight: 500;
line-height: 0.85;
letter-spacing: -0.05em;
color: var(--accent-2);
text-shadow: 0 1px 0 color-mix(in oklab, var(--accent-2) 18%, transparent);
}
.day-hero:hover .day-hero-numeral {
color: var(--accent-2-deep);
text-shadow:
0 2px 0 color-mix(in oklab, var(--accent-2) 22%, transparent),
0 18px 40px color-mix(in oklab, var(--accent) 22%, transparent);
}
/* ===================== signature: glass card ===================== */
.glass-card {
background: color-mix(in oklab, var(--surface) 60%, transparent);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
border: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
border-radius: var(--r-md);
padding: 16px 20px;
box-shadow: var(--shadow-lift);
}
/* ===================== signature: prompt-card (RTFC) ===================== */
.prompt-card {
background: oklch(15% 0.02 250);
color: oklch(92% 0.005 250);
border-left: 4px solid var(--accent-2);
padding: 14px 16px;
border-radius: var(--r-md);
margin: 16px 0;
}
.prompt-card-head {
display: flex; justify-content: space-between; align-items: start;
margin-bottom: 8px;
}
.prompt-card-title { font-weight: 700; }
.prompt-card-note { font-size: 12px; opacity: 0.75; margin-top: 2px; }
.prompt-text {
background: oklch(10% 0.015 250);
padding: 12px;
border-radius: var(--r-sm);
font-size: 13px;
white-space: pre-wrap;
margin: 0;
}
.prompt-copy-btn {
background: var(--accent-2);
color: white;
border: 0;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.08em;
display: inline-flex; align-items: center; gap: 4px;
}
.prompt-copy-btn.ok { background: var(--success); }
/* ===================== signature: task-list (3-state checkbox) ===================== */
.task-list { list-style: none; padding: 0; margin: 0; }
.task-item {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 120ms ease;
}
.task-item:hover { background: var(--surface-2); }
.task-checkbox {
width: 18px; height: 18px;
border: 1.5px solid var(--border-strong);
border-radius: var(--r-sm);
display: grid; place-items: center;
transition: all 120ms ease;
flex-shrink: 0;
}
.task-item:hover .task-checkbox { border-color: var(--accent); }
.task-item.done .task-checkbox {
background: var(--success);
border-color: var(--success);
color: white;
}
.task-item.done .task-label { text-decoration: line-through; color: var(--muted); }
/* ===================== signature: learning-goal badge ===================== */
.learning-goal,
.list li {
list-style: none;
position: relative;
padding-left: 28px;
margin-bottom: 6px;
}
.learning-goal::before,
.list li::before {
content: '✓';
position: absolute; left: 0; top: 1px;
width: 20px; height: 20px;
background: var(--success-soft);
color: var(--success);
border-radius: 999px;
text-align: center;
line-height: 20px;
font-weight: bold;
font-size: 12px;
}
/* ===================== concept (teaching block) ===================== */
.concept {
margin: 20px 0;
padding: 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-md);
position: relative;
}
.concept-heading {
font-size: 17px;
font-weight: 700;
margin-bottom: 8px;
color: var(--fg);
}
.concept-body { margin-bottom: 12px; }
.kv {
display: grid;
grid-template-columns: minmax(120px, max-content) 1fr;
gap: 8px 16px;
margin: 12px 0;
}
.kv-key { font-weight: 600; color: var(--muted); }
.note {
margin-top: 12px;
padding: 10px 14px;
background: var(--accent-2-soft);
border-left: 3px solid var(--accent-2);
border-radius: var(--r-sm);
font-size: 14px;
}
/* ===================== unit accordion ===================== */
.unit {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-md);
margin: 16px 0;
}
.unit > summary {
padding: 16px 20px;
cursor: pointer;
display: flex; justify-content: space-between; align-items: center;
list-style: none;
}
.unit > summary::-webkit-details-marker { display: none; }
.unit-title { font-weight: 700; font-size: 16px; }
.unit-meta { font-size: 12px; color: var(--muted); }
.unit-body { padding: 0 20px 20px; }
.unit-progress-pill {
font-family: var(--font-mono);
font-size: 11px;
padding: 2px 8px;
background: var(--accent-soft);
color: var(--accent-deep);
border-radius: 999px;
}
/* ===================== material list ===================== */
.materials { display: flex; flex-direction: column; gap: 8px; }
.material-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
padding: 10px 12px;
background: var(--surface-2);
border-radius: var(--r-sm);
align-items: center;
}
.material-tag {
font-family: var(--font-mono);
font-size: 11px;
padding: 2px 8px;
background: var(--accent-soft);
color: var(--accent-deep);
border-radius: 999px;
}
.material-name { font-weight: 600; }
.material-desc { font-size: 13px; color: var(--muted); }
.material-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted-2); }
/* ===================== table ===================== */
.t {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 14px;
}
.t th, .t td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.t th {
background: var(--surface-2);
font-weight: 700;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ===================== chapter / section / common typography ===================== */
.chapter { margin-bottom: 56px; }
.eyebrow {
display: inline-block;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 8px;
}
.lead {
font-size: 16px;
color: var(--muted);
margin: 12px 0 24px;
text-wrap: pretty;
}
/* ===================== illustration (PNG-first / SVG-fallback) ===================== */
.illustration {
margin: 16px 0;
text-align: center;
}
.illustration img,
.illustration svg {
max-width: 100%;
height: auto;
border-radius: var(--r-md);
filter: drop-shadow(0 4px 12px oklch(20% 0.02 250 / 0.08));
}