Install
$ agentstack add skill-kevintsai1202-teaching-site-skills-teaching-site-design-system ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Teaching Site Design System
> Token starter (copy this, don't re-derive): [templates/tokens.css](templates/tokens.css) — full :root block + dark theme + signature components (day-hero, glass-card, prompt-card, task-list, learning-goal, concept, unit, material, table). Paste into the ` block of index.html (vanilla SPA pattern) or use as external style.css. > > **Schema authority**: this skill defines visual tokens; data field names live in [_shared/domain-primitives.md](../_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(...)):
- Perceptually uniform —
oklch(60% ...)andoklch(70% ...)look like an equal step apart; HSL doesn't (its 60→70 yellow jumps visually huge but blue barely changes). - Predictable dark mode — for each light token at
L=N%, the dark equivalent is roughlyL=(100-N)%with the same chroma+hue. Mechanical. 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.- 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-deep
Why 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:
- Latin + 中文 混排在 90%+ 的段落都會出現。每換一種字體就會出現「字體斷裂感」。
- The fallback chain handles bilingual rendering — Nunito for Latin glyphs, openhuninn for 中文.
- Mono / serif variants are mostly used for spacing / weight effects, not actually different typefaces. Implement via
font-feature-settings,letter-spacing,font-weightinstead 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
--accentfor 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
-softvariants — 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.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kevintsai1202
- Source: kevintsai1202/teaching-site-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.