Lightkeepers
← Back to the working file

The design system.

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.

Short answer

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.

What is locked today

Rendered from the real values in the spec, not described.

The palette

#1E2B4A
--navyAnchor. Headlines, the mid-page band, the footer
#16223C
--navy-deepDeeper navy
#F4F7FB
--on-navyText on navy
#4A5568
--ink-bodyBody text on white
#FFB703
--sunPrimary button. Text on it is always navy
#E59D00
--sun-deepSun, pressed
#D08700
--sun-edgeThe hard bottom edge under a primary button
#FFF1CC
--sun-softSoft tint for icon chips
#14B8A6
--tealButtons on navy, check icons
#0E9384
--teal-deepTeal edge
#3B82F6
--skySecondary accent
#FF6B57
--coralWarning, attention
#F59E0B
--gold-starReview stars only
#FFF7EA
--creamWarm section background
#E7ECF2
--lineBorders
#FFFFFF
--whiteDefault page background

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 buttons

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.

The type scale

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.

The composition laws, abridged

  1. Content maxes at 1120px, 24px side padding. Sections are 96px top and bottom, 60px on mobile. No other values.
  2. White section, then at most one saturated block, then white again. Never two saturated blocks touching. Exactly one navy band mid-page, plus the navy footer.
  3. Saturated meets white via one shared single-crest wave divider, the same curve reused and flipped. No diagonals, no blobs.
  4. Zig-zag feature rows, image side alternating. Headline six words or fewer, body 45 words or fewer. Illustrations sit directly on the background, never in a box.
  5. Cards are white, 24px radius, 28px padding, equal-height three-up grid.
  6. Characters only in zig-zag image slots or peeking over section seams. Never floating in empty space.
  7. Every section runs eyebrow, then h2, then one short paragraph, then content.
  8. Mobile: single column, image above text, full-width buttons.

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.

What the code actually does

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.

FileWhat it isPaletteStatus
ds-system.cssThe locked system. --ds-*navy / sun / teal, NunitoCanonical
Canonical. Applied via a .ds class in 18 places
marketing.cssHomepage only. Bare names under .mktIdentical hex values to the ds setFork
A straight fork. Same colours, second copy
lesson.cssLesson player only. --lds-*Third copy of the same hex valuesFork
The lesson player never applies .ds at all
app-tokens.cssThe “hearth” set. --hearth-*#1B2740, #F0A945, #1F3B33Should not exist
Uses radial gradients, which the spec bans outright
tokens.cssThe old brand. --paper / --forest / --amber#FBF7F0, #2F4A43, #8D5E19, Fraunces + InterShould 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 one that would bite you

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.

What I would change

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.

1
Keep

The palette. Navy, sun, teal.

Today

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.

Recommended

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.

Why: Nothing in either research pass found a problem with these colours. The problems found were about type and about copy.
2
Change

Add one display face. Headlines only.

Today

Nunito does every job: wordmark, headline, body and UI. The spec bans “any second display font”.

Recommended

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.

Why: This is the one real finding from the typography research. Every strong brand in this category has a display face. Duolingo has Feather Bold, Khan has Chalky, Prodigy has Prodigy Sans. We have one Google font doing four jobs, so we have no typographic asset at all.
Needs Nathan: This contradicts a hard ban in the locked spec, so it is Nathan's call, not mine. The ban exists to stop font drift, and four faces are loaded today, so it is already being broken by accident. I am proposing to break it once, on purpose, and then enforce it properly.
3
Change

Collapse five palettes into one.

Today

The same sixteen hex values are declared in four places under four different names, plus a fifth “hearth” set that uses banned gradients.

Recommended

ds-system.css survives. Fold marketing.css and the --lds-* block in lesson.css into it. Migrate forms.css off the old tokens.

Why: Right now changing --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.
4
Change

Make parent and child a mechanism, not a doctrine.

Today

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.

Recommended

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.

Why: This is exactly the room concept we already agreed on. Same brand, different room. The mechanism it needs does not exist yet, so today the difference is carried by whoever wrote the page.
5
Fix

Close the old-brand leak.

Today

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.

Recommended

Remove the global rules and the two font imports. Anything still depending on them gets moved onto the ds tokens first.

Why: A page that forgets its .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.

For the ideation you are doing right now

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.