← Home

Design System

Tokens live in src/styles.css. Components live in src/components/ui/. This page renders the actual production components — what you see is what ships. Open experiments →

Typography

Korean Typing
Fraunces 700 · display
Drill the hangul reading
Fraunces 500
Manrope 800
font-sans · 800
Manrope 700
font-sans · 700
Manrope 600
font-sans · 600
Manrope 500 — body
font-sans · 500
Manrope 400 — secondary text
font-sans · 400

Manrope is default on body via --font-sans. Fraunces is opt-in via .display-title: home h1, hanja prompt, end-screen heading.

Brand palette

Ink
--ink
primary text
Ink soft
--ink-soft
secondary text
Indigo
--indigo
Indigo deep
--indigo-deep
primary action
Seal
--seal
stamp ink
Paper light
--paper-light
Paper cream
--paper-cream
Paper
--paper
page background

Surfaces

Surface
--surface
Surface strong
--surface-strong
Header bg
--header-bg
Chip bg
--chip-bg
Link bg hover
--link-bg-hover

Lines & glints

Line
--line
Chip line
--chip-line
Inset glint
--inset-glint

shadcn tokens

Background
--background
Foreground
--foreground
Card
--card
Primary
--primary
= --indigo-deep
Primary fg
--primary-foreground
Stamp ink
--stamp-ink
= --seal · click flash
Muted
--muted
Muted fg
--muted-foreground
Border
--border
Ring
--ring
Destructive
--destructive

Shadcn tokens are remapped to the brand palette in :root, so bg-primary etc. inherit our colors without patching the components themselves.

Button — variants

From src/components/ui/button.tsx. Recolor via :root tokens (--primary, --background, …), not by patching the component.

Button — sizes

Navigation

From src/components/nav.tsx. Rendered at the top of every page by __root.tsx. Active route gets text-foreground font-medium; others use text-muted-foreground.

Icon buttons

·

Pattern: <Button size="icon-sm"><ChevronLeft /></Button>. Icons from lucide-react. Building block — for prev/next page UI use <Pagination /> below. Always set aria-label since there's no visible text.

Pagination

1 / 7

From src/components/pagination.tsx. Controlled: pass page, pageCount, and onChange. Disables prev at 0 and next at the last page. Used by the drill recap modal.

Segmented tabs

From src/components/segmented-tabs.tsx. Controlled: pass value, onChange, options. The active option takes bg-primary; inactive ones use text-muted-foreground. Used on /import to pick between hanja and vocab imports.

Deck card

Hanja

hanja

0cardsRecall: 0CPM

Balatro

vocab

0wordsRecall: 0CPMTyping: 0CPM

Empty deck

vocab

0wordsRecall: CPMTyping: CPM

From src/components/deck-card.tsx. Title (display font), optional subtitle (uppercase badge), an array of stat chips (label, value, optional suffix — leave the label empty for a prefixless stat like 181 cards), a primary action button, and an optional delete button in the top-right.

Collectible card

Browse mode
one
사랑
love
Compact mode — used in the drill recap modal
one
안녕
hello
사랑
love
커피
coffee

From src/components/collectible-card.tsx. Renders a Card as a hero + reading + gloss tile keyed off DeckKind: hanja gets the royal (deep indigo) shell with hangul as the reading line; vocab gets the hwatu shell in one of three palettes (red, blue, green) deterministically hashed from the hangul, with no reading line. Default mode is the full browse register (idle hwatu breathing, entrance sparkles, hover ripple + glow) used on /decks/$slug. Pass compact for the calm review register (kind-coded shell, 3D tilt, hover lift only) used inside the drill recap modal.

Line chart

Full
Sparkline

From src/components/line-chart.tsx, wrapping src/components/ui/chart.tsx (shadcn) over Recharts. Two modes: full (grid + axes + tooltip) and sparkline (line only). Pass color as a CSS var so brand tokens flow through. Points with muted: true render hollow — used to distinguish aborted drill sessions in the /stats view.