Yes, we have one, and it is stricter than you might expect. Below: what is locked today, what the code actually does (they differ), and the five things I would change, each next to what it would replace.
The guide lives at buildchain/DESIGN-SYSTEM.md in Nathan's repo. It is titled Design System v1 and marked reference-locked 2026-07-17. Twenty-seven dense lines: a full token set, a type scale, button anatomy, ten numbered composition laws and a list of hard bans.
Its stated references are Duolingo for layout grammar and chunky buttons, Euka for saturated colour blocking and the navy anchor band, and godslittlelightkeepers.com for warmth, wave dividers and the trust strip. The instruction to implementers is blunt: “Workers implement exactly; zero improvisation.”
One caveat before you design against it. buildchain/ is off-limits to our lane without raising it in a PR first. Changing the design system is a conversation with Nathan, not something I can just do.
Rendered from the real values in the spec, not described.
Contrast law, verbatim: text on --sun is always --navy; body text on white is --ink-body; text on navy is --on-navy. And: no gradients anywhere.
The Duolingo signature: a hard bottom edge, no blur. Radius 16px, padding 15px 30px, weight 800. Hover lifts 1px, active drops 3px and the edge collapses to 1px.
Real values, live. This is what the spec produces.
Everything is Nunito Variable, self-hosted. h1 is clamp(2.5rem, 5.2vw, 3.9rem) at weight 900. h2 is clamp(1.85rem, 3.4vw, 2.5rem) at 800. Body is 1.0625rem/1.65 at weight 500, capped at 62 characters. Eyebrows are 13px, weight 800, uppercase.
One rule worth knowing because it shapes every headline you write: exactly one accent-coloured word or short phrase per headline. Never more.
Hard bans: gradients; diagonal, rotated or scattered elements; floating cut-out people; text over photos; any second display font; invented numbers or testimonials; section padding outside spec.
I read the stylesheets rather than trusting the spec. The gap matters before you design against it.
There are five separate token sets live in the repo, and four typefaces loading, against a spec that bans a second display font. Two generations of brand are present and the older one was never removed.
| File | What it is | Palette | Status |
|---|---|---|---|
ds-system.css | The locked system. --ds-* | navy / sun / teal, Nunito | Canonical Canonical. Applied via a .ds class in 18 places |
marketing.css | Homepage only. Bare names under .mkt | Identical hex values to the ds set | Fork A straight fork. Same colours, second copy |
lesson.css | Lesson player only. --lds-* | Third copy of the same hex values | Fork The lesson player never applies .ds at all |
app-tokens.css | The “hearth” set. --hearth-* | #1B2740, #F0A945, #1F3B33 | Should not exist Uses radial gradients, which the spec bans outright |
tokens.css | The old brand. --paper / --forest / --amber | #FBF7F0, #2F4A43, #8D5E19, Fraunces + Inter | Should not exist Still sets body and h1,h2,h3 globally |
The four faces loading are Nunito Variable (the canonical one, its @font-face declared three separate times), Bricolage Grotesque, Fraunces and Inter. The last two come in through both layouts via npm.
The locked system only applies where an author remembered to type a .ds class on a wrapper. Where they didn't, the page falls back to tokens.css, which is the old brand, which the buildchain itself marks as a failure condition.
So the fallback state is the banned state. The lesson player is the clearest case: it runs its own private fork of the tokens and never applies .ds at all.
Five items, each against what it replaces. One is a keep, three are changes, one is a fix. Only the second is a matter of taste.
Locked 2026-07-17. Navy anchor, sun primary, teal on dark. Contrast law already written: text on sun is always navy, body on white is ink-body.
Do not rebrand. It is coherent, the contrast rules are already thought through, and it is implemented across 18 surfaces. A palette change here buys nothing and costs a rebuild.
Nunito does every job: wordmark, headline, body and UI. The spec bans “any second display font”.
Add a display face for headlines and the wordmark. Nunito stays for body and UI, unchanged. My pick is Fredoka: it is the pairing Khan Academy Kids already uses, it is OFL and free, it loads the same way Nunito does, and it has the single-storey a children are taught to handwrite.
The same sixteen hex values are declared in four places under four different names, plus a fifth “hearth” set that uses banned gradients.
ds-system.css survives. Fold marketing.css and the --lds-* block in lesson.css into it. Migrate forms.css off the old tokens.
--ds-sun moves neither the homepage nor the lesson player. The file comments are candid that the duplication was deliberate lane isolation while parallel agents built. That tradeoff has been paid for; the isolation is now just cost.The split is real in the writing (“child space is play, parent space is calm”) but in the code it is enforced only by which class an author remembers to type. The actual child session state drives no styling at all. It renders one banner.
Drive the theme off the session. The app already knows whether it is in child mode; one attribute on the body from that state, and every token can shift underneath it.
tokens.css still sets body background, colour and font globally, and assigns Fraunces to every h1,h2,h3. Both layouts still import Fraunces and Inter.
Remove the global rules and the two font imports. Anything still depending on them gets moved onto the ds tokens first.
.ds wrapper falls back to the old brand, which the buildchain explicitly marks as a failure condition. The fallback state is the banned state. That is backwards.None of this should constrain the concepts you generate this week. The design prompts are deliberately brand-free for exactly that reason: if you feed a tool navy and sun, you get navy and sun back, and you learn nothing.
Use this page afterwards instead. When a concept comes back that you like, the question becomes how far it sits from the locked system, and whether that distance is worth a conversation with Nathan. Small distance, we adopt it inside the existing tokens. Large distance and genuinely better, that is a v2 conversation and it needs evidence, which is what this page is for.