<!-- https://getkonjo.com/design/agents/skill · source: .claude/skills/design/SKILL.md -->

---
name: design
description: Konjo's design language, in operational form — the tokens, screen formulas, and hard laws that make a screen look and feel like Konjo on the first try. Read it BEFORE writing any user-visible UI in src/ or apps/studio/, and again before claiming a screen is done. Covers layout, spacing, type, colour, empty/loading/error states, progress, and copy voice. Not for backend, migration, or infrastructure work.
---

# Designing for Konjo

> This skill bundles `references/`. `SKILL.md` carries the laws, the budgets and the
> formulas; §0 routes to the chapter for whatever you are actually building, and the
> generated references carry every value and every component that exists. The reasoning
> behind all of it lives in
> [docs/design/konjo-design-language.md](../../../docs/design/konjo-design-language.md).

> **Kata (2026-09-29) supersedes parts of this skill** — the brief is
> [the Kata interview](../../../docs/design/2026-09-29-kata-founder-interview.md). Red is the
> **action colour** (not a one-per-screen role); the budgets are **guidance** the reviewer
> judges; type is **Archivo**, with **Archivo ExtraCondensed** for display; cards are **16**
> with **pill** buttons; the **ninja** is the mark and may appear in empty states, errors and
> celebrations. Where this file still says otherwise, the interview wins.

**Loudness is a budget, and each screen gets one purchase.** Konjo is athletic and dojo
together: Nike-grade energy at the big moments, dojo calm in daily use. The failure mode of loud is that everything shouts and
nothing does — which is exactly what the current app gets wrong. Spend the volume on one
element per screen and make everything else quiet enough to disappear.

```
read the screen's job → pick the formula → build with tokens only
   → check the three budgets → walk it as the persona who'd quit
   → fix what you find → then say it's done
        ↑____________ iterate ____________|
```

## 0. Where to go for more

**Read the pattern chapter for the shape you are building — always, before you start.** This
file carries the laws, the budgets and the sketches; the pattern chapter carries the decisions
that only make sense for that shape, and it is short.

Then load a foundation chapter for anything on your screen you do not have a rule for. A real
screen usually needs two or three: a student detail is a *detail* + *rank* + *content-format*,
a booking flow is a *form* + *sheet-picker* + *destructive*. That is normal and it is what the
router is for — do not ration yourself down to one and then invent the rest. Inventing is the
failure this system exists to prevent.

What you should not do is read the whole design language front to back. Take the rows you need.

| Building | Read |
|---|---|
| A tab home | [tab-home](../../../docs/design/patterns/tab-home.md) |
| A list or index | [index-list](../../../docs/design/patterns/index-list.md) |
| A grid of people | [tile-grid](../../../docs/design/patterns/tile-grid.md) |
| A detail screen | [detail](../../../docs/design/patterns/detail.md) |
| Deciding push vs modal, or a header | [navigation](../../../docs/design/patterns/navigation.md) |
| Anything typed into | [form](../../../docs/design/patterns/form.md) · [sheet-picker](../../../docs/design/patterns/sheet-picker.md) |
| Something irreversible | [destructive](../../../docs/design/patterns/destructive.md) |
| Anything dragged, dropped, or reordered | [direct-manipulation](../../../docs/design/patterns/direct-manipulation.md) — **and its keyboard path is not optional** |
| Columns of cards, one per stage | [board](../../../docs/design/patterns/board.md) — the container; the gesture is the row above |
| A search field or filter chips | [search-filter](../../../docs/design/patterns/search-filter.md) |
| Any button, anywhere | [buttons](../../../docs/design/patterns/buttons.md) |
| Empty / loading / error | [states](../../../docs/design/patterns/states.md) |
| Push, a banner, a toast, a badge | [notifications](../../../docs/design/patterns/notifications.md) |
| Streaks, badges, promotion | [progress](../../../docs/design/patterns/progress.md) · [celebration](../../../docs/design/patterns/celebration.md) |
| Anything in Studio | [studio/dialect](../../../docs/design/studio/dialect.md) **and [studio/metrics](../../../docs/design/studio/metrics.md)** — the foundations row below is *mobile* metrics, and building a Studio screen to a 44pt target and a 20pt gutter is the most expensive wrong turn on offer |
| A Studio dashboard or snapshot cards | [studio/dashboard](../../../docs/design/studio/dashboard.md) |
| A table | [studio/tables](../../../docs/design/studio/tables.md) |
| A drawer, menu, dropdown or toast in Studio | [studio/overlays](../../../docs/design/studio/overlays.md) — a floating surface's boundary is `border.control`, not the hairline |
| A chart | [studio/charts](../../../docs/design/studio/charts.md) — **and load the `dataviz` skill** |
| Which Studio screen this is, and what is on it | [studio/ia-and-inventory](../../../docs/design/studio/ia-and-inventory.md) |
| Studio's navigation rail | [studio/metrics](../../../docs/design/studio/metrics.md#the-rail) |

| Needing a value or a rule | Read |
|---|---|
| Any token, any ratio | [references/tokens.md](references/tokens.md) — generated, always current |
| What already exists | [references/components.md](references/components.md) — generated from the barrel |
| Colour · type · space · borders · metrics | [foundations/](../../../docs/design/foundations/) — **mobile**. In Studio these are overridden by [studio/metrics](../../../docs/design/studio/metrics.md) |
| Anything that animates | [motion](../../../docs/design/foundations/motion.md) |
| An icon | [iconography](../../../docs/design/foundations/iconography.md) |
| A photo, avatar, or the mascot | [imagery](../../../docs/design/foundations/imagery.md) |
| A belt or rank | [rank](../../../docs/design/foundations/rank.md) |
| A date, number, name, or count | [content-format](../../../docs/design/foundations/content-format.md) |
| Screen readers, text scaling, focus | [accessibility](../../../docs/design/foundations/accessibility.md) |
| The wordmark, icon, splash, or store art | [brand](../../../docs/design/brand/identity.md) |
| Anything permanent — who's alerted, what's undoable | [product-facts](../../../docs/design/product-facts.md) — **read, don't ask** |
| Adding or changing a token | [governance](../../../docs/design/governance.md) |

## 1. Before you write a line

Answer these. If you can't, you're not ready to design — go back to the
[ship](../ship/SKILL.md) interview.

- **Whose screen is this?** The white belt in week two (quits when made to feel stupid),
  the student logging one-handed and sweaty with 40 seconds, the instructor mid-class with
  one free hand, or the owner doing admin they resent.
- **What is the one thing they came for?** That is the loud thing. Everything else is quiet.
- **What does it look like with no data? With bad data? For someone without access?**
- **What does this actually do?** For anything that writes a permanent record — a promotion, a
  waiver, a payment, a removal — you cannot write the screen without knowing what becomes
  permanent, who is alerted, who can read it afterwards, and whether it can be undone.

**Check [product-facts.md](../../../docs/design/product-facts.md) first — it is the answer, not
a question.** Promotions, incidents, removals, waivers and push categories are decided there.

**If it is not there, that is a blocker, not a blank to fill.** A consequence line invented to
satisfy a rule is worse than no consequence line: it is a confident false statement on the one
screen where people are trusting the text. Say which fact you are missing and stop. Everything
else on the screen can be built while you wait — but never guess what an irreversible action
does, and never soften the gap with vaguer copy.

## 2. The three budgets — guidance, judged per screen

| Budget | Allowance | Counted as |
|---|---|---|
| Loud block (hero, inverted surface, display type) | **1**, or none | Instances |
| Filled primary button | **1** — everything else is a text link or a bordered control | Instances |
| Uppercase **label** register | **1** — labels only, ≤3 words, ≤20 chars, never a sentence | Type tokens |

**Kata: guidance, not a hard count.** The reviewer asks whether the screen has one clear focus.
Going over needs a reason you can state; it is not an automatic fail.

**These are ceilings, not quotas.** A screen with **zero** loud blocks and zero filled primaries
is legal and common — an index screen usually looks exactly like that.

**Chrome is exempt** — tab bar, status bar, header icons, and the `display` screen title. A
title is not the screen's loud block, which is why an index screen ships one and still spends
zero. **Control states are exempt too** — a selected chip or a checked-in tile filled with
`surface.hero` is state, not a loud block.

**Red is not budgeted — it is the action colour.** A filled primary, a link, a selected
segment, a checked box and the active tab are red. Red that is not tappable and not a selected
state is still wrong: no red decoration, no red body text, no red block that does nothing.
**Destructive actions are never red** — an ink outline and a plain verb.

**Uppercase counts label tokens.** `eyebrow` is the only uppercase *label* register in the
scale, so the way this budget actually gets broken is not by adding a second token — it is by
pressing `statement` into service as a section label, or by typing capitals by hand. The hero's
`statement` is display voice: a caps eyebrow above a caps statement is legal. A caps
`statement` sitting over a list, doing an `eyebrow`'s job, is not.

## 3. Pick a formula

**First decide whether this tab home has a hero at all.** Hero-led when there is usually one
obviously-next thing for this person (Train, Teach). Index-led when they came to choose among
many (Social, Learn) — section header, then the list, no inverted block. Events is
conditional: hero when they're enrolled in something soon, index when they aren't. Applying
the hero shape to a browse tab buries the browsing, which is the most likely way to misuse
this skill.

**Tab home (index-led)** — Social, Learn, Events-without-an-enrolment:

```
Title (display 34/800)                    ◎ ⬤
[ ⌕ Search…            ]  Filters            input h44, border.control; search first
( chip ×) ( chip ×)                          h32, surface.card + border.control
┌ FOR YOU ──────────────────────────────┐    at most ONE personal card above the index
│ ● One Green Stripe                  › │
└───────────────────────────────────────┘
BROWSE                                       eyebrow, text.secondary, on the page
┌───────────────────────────────────────┐
│ Kata                            4   › │    the index = one card of hairline rows
│ ─────────────────────────────────────  │
│ Hand techniques                16   › │
└───────────────────────────────────────┘
```

No inverted block — the index is the point, and a hero above it is a wall in front of the
door. Two cards above the index and you have rebuilt a dashboard. Empty *search* keeps the
field and filters on screen; an empty *collection* replaces the index.

**Tab home (hero-led)** — one hero, then a little:

```
Title (display 34/800)                    ⌕ ◎ ⬤   24pt glyphs, 44pt targets
▓▓ full-bleed surface.hero ▓▓                        pad 20h / 24v, radius 0
▓ eyebrow 11/800 caps · accent.onHero        ▓
▓ statement 46/900 caps · text.onHero        ▓       reflow to 3 lines, then step to display
▓ meta 15/400 · text.onHeroMuted             ▓
▓ [ ONE RED CTA → ] 48pt pill, hugs left     ▓       the block itself is NOT pressable
Quiet row                                        →   rowTitle 16/600 + caption 13 secondary
Quiet row                                        →
░ one tonal tile ░
```

The hero is a **solid block carrying type, never a photograph** — it has to work for a dojo
that has never uploaded an image. If a sixth *section* wants on the screen, it goes one tap
away.

**`surface.hero` inverts by theme** — `#111111` light, `#F2F2F2` dark. It is the maximally
contrasting surface, not "the dark one". Painting it `brand.ink` makes it **1.021:1** on the
dark page and the screen's centrepiece vanishes. Text on it is `text.onHero` /
`text.onHeroMuted` / `accent.onHero` — never a hardcoded hex, and never `brand.red` (3.77:1
on ink).

**The hero block is not pressable.** Only its CTA navigates, or the same destination appears
twice in one viewport.

**List row** — the thing they're looking for is first and boldest:

```
Primary line              rowTitle 16/600
Secondary · metadata      caption 13/400 text.secondary       row ≥64pt
```

Never invert this. Hairlines are **inset to the text edge**; ≤5 rows get no hairlines at
all, just 12pt between groups.

**Tile grid** — for triage plus one tap per person (attendance, check-in, a queue).
111×92pt tiles, 3 across at 390pt, 8pt gaps. **Ink fill = done, white = still outstanding**
— never red, or you spend the budget once per tile. One bare status glyph top-right (a
tinted chip is 1.14:1 on white and disappears); belt dot top-left with a hairline ring, or a
white belt is invisible on a white tile. One glyph per tile by priority, with the
`accessibilityLabel` enumerating all of them. A tappable "N need attention" row above the
grid is the legend — filtering to those people teaches the glyphs without a key.

**Detail** — eyebrow → title → meta → primary action → content. The ID or category is an
eyebrow *above* the title, never a chip beside it.

**Form** — the full spec is [patterns/form.md](../../../docs/design/patterns/form.md); the load-bearing parts:

```
SECTION HEADER                  eyebrow · text.secondary
                          ↕12
One-line summary                label 13/700 SENTENCE case
┌────────────────────────┐ ↕6
│                        │      h≥44 · radius.input · 1px border.control · surface.card
└────────────────────────┘      ↕16 to the next field · ↕24 to the next section
Where              Optional     mark what is OPTIONAL, not what is required
```

**Inputs sit on the page, never inside a white card** — an input's fill *is* `surface.card`,
so an input in a card is white-on-white. Group with space and an `eyebrow`, not a bordered
box. Chips **wrap**, never scroll horizontally.

Validate a typed input **on blur, never on keystroke**. A **select trigger has no blur event** —
you tap it, a sheet opens, it closes — so a trigger validates **on submit**, then re-validates
live once it has been in an error state. Most Konjo forms are mostly triggers, so this is the
common case, not the exception. Error = border swaps to `feedback.errorBorder` (still 1px) +
`caption` in `feedback.errorText` 6pt below. **Focus = 2px `border.focus`** — the one
of three sanctioned uses of `borderWidth.emphasis` 2, and without it the form is unusable with a keyboard on
RN-web, which is the surface CI actually walks.

**Never disable submit** (there is no legal disabled text colour) — validate on tap and jump
to the first problem. In flight: fill stays, label → "Submitting…", 16pt spinner left, inputs
`editable={false}`, button `accessibilityState={{ busy: true }}`. **A failed submit never replaces the form** — a bordered block above the
button, and the button relabels "Try again". Success pops back to the list the record joins,
with a one-line confirmation naming who was affected.

**Pickers** — chips inline for 2–6 options (the choice stays visible); a sheet with a row
list for 7–20; a sheet with a pinned search for 20+; the native picker for dates and times,
always. Sheet: grabber, left-aligned `title`, rows ≥64pt with a **check** on the selected row
(a filled row inside a sheet reads as a hero), `radius.sheet` on the top corners, separated by
the `overlay.dim` scrim and nothing else. Single-select closes on tap — the tap is the
confirmation. A picker never validates; the field it feeds does.

**Serious screens** (legal records, alerts, money): state the consequence *before* the tap,
never pre-select a field that carries liability, and keep the copy clinical — roles not
names, no reassurance, no apology. Destructive actions arm on first tap, fire on second.

**Buttons** — use `<Button>`. Primary fills `brand.red` with `brand.onRed` label, h48,
`radius.pill`, label **`rowTitle` 16/600**. Secondary (and destructive): no fill, 1px
`border.control` pill in ink, h44, `label` 13/700. Link: `accent.red` text. `label` 13/700 is
for chips, secondary buttons and links — **a 48pt button carrying 13pt type is anti-pattern
#13**.

**Error** — the Empty anatomy: eyebrow → "Couldn't load events" → plain words about what
failed, no error codes → `[ Try again ]`. **The retry inherits the weight of what it
replaced**: a failed *screen* renders nothing else, so its retry is the filled primary; a
failed *section* sits under a screen whose primary is still on screen, so its retry is a
bordered secondary. That is how the budget survives a partial failure. Left-aligned, in
flow. Field errors sit inline below the field and fire **on blur, never on keystroke**.

**Empty** — `EmptyState` already exists (24 call sites); extend it, don't write another.
First ask whether the screen should be empty at all. Real data elsewhere that
belongs here? Show it. Can you suggest from a real source? Suggest it. Only if genuinely
nothing: eyebrow → title → what goes here and why → one filled primary → one text link,
left-aligned and in flow, **never centred in a void**. Demote the screen's other action
while the empty state shows.

**Loading** — under 1s show nothing. 1–10s a spinner; a skeleton only where the layout is
predictable and the user has seen it before. Over 10s, determinate progress.

## 4. Build with tokens only

Full tables in [references/tokens.md](references/tokens.md). The ones you'll reach for:

**Space** — `0 2 4 6 8 12 16 20 24 32 40 48 64 80`. 8 is the spine; 4/6/12/20 are the legal
half-steps (6 for gaps inside a component, not layout); **2 is optical correction only**,
never a layout gap.
Aliases: `page` 20, `section` 24, `card` 16, `tight` 8. Group with space, not lines.

**Three unrelated things are called hero.** `typography.hero` is a 64pt **numeral**;
`surface.hero` is the inverting **surface**; `Hero` is the **component** that paints one with
the other. A screen can use the surface without the type, and usually should.

**Type** — `hero` 64/900 (a numeral), `statement` 46/900 (the hero's words), `display` 34/800,
`title` 22/700, `rowTitle` 16/600, `bodyLarge` 16/400, `body` 15/400, `caption` 13/400,
`micro` 12/500, `eyebrow` 11/800 **caps**, `label` 13/700 **sentence case**. Every entry has a
line height — `eyebrow` included, because it is the only uppercase register and sits above
every section on every screen. Fourteen legacy entries still lack one; the
[token tables](references/tokens.md) name them. Whatever the entry, give a container holding
text room to grow and never a fixed height. `eyebrow` is the only uppercase **label** register — the one exception is a CTA inside
the hero block, which takes the hero's display voice.
**Two sizes should carry any screen.** A hero is *bigger than the page title* — 2pt larger
than its neighbours is not a hero.

**Separation** — **white cards on a tinted page** (`surface.card` #FFFFFF on `surface.page`
#F2F2F3 — dark: `#1C1C22` on `#0E0E10`), the way Apple Health and Attio do it. Not the other
way round: a #FAFAFA card on a white page is 1.044:1 and invisible. **No shadow, no blur, no
glow, no gradient.**

Two border tokens, two jobs: `border.hairline` 1px divides rows inside a list (inset to the
text edge); `border.control` 1px draws the edge of something tappable — chip, secondary
button, input. A chip with only a `surface.tag` fill is **1.017:1** on the page and is not a
control anyone can see.

**The card-only family** — legal on a white card, illegal on the tinted page:
`text.tertiary` (`text.tertiary on surface.page` is 4.23:1 → use `text.secondary`) and `surface.tag`
(1.017:1 → use a white chip with `border.control`).

**Everything named here ships.** The tokens are in
[`packages/design-tokens/tokens.ts`](../../../packages/design-tokens/tokens.ts), re-exported
by `src/theme/tokens.ts` so either import path works, and the primitives — `Text`, `Stack`, `Card`, `Row`, `Button`,
`Badge`, `Hero`, `EmptyState` — are exported from
[`src/components/ui`](../../../src/components/ui/index.ts). **Reach for the primitive before
you compose one by hand.** The full list, including what is retired, is in
[references/components.md](references/components.md).

**Colour** — `brand.red` #D62828 is the **action fill** (white on it, 5.01:1, both themes).
`accent.red` is **red as text** (links) — #D62828 as text is 4.48:1 on the light page and 3.85:1
on the dark one, and fails. Ink for section labels, eyebrows, arrows and chevrons. `text.tertiary` is `#737373` and is **card-only** — `text.tertiary on surface.page` is 4.23:1 and
fails, so text sitting directly on the page uses `text.secondary`. **`text.quaternary` is not
a text colour** — never put type in it. **Red on the hero surface is `accent.onHero`, never
`accent.red`** — `brand.red` on ink is 3.77:1 and fails AA. The two tokens **swap by theme**,
because the hero inverts and the page does not: `accent.red` is #C42020 light / #FF5252 dark,
`accent.onHero` is #FF5252 light / #C42020 dark. Name the surface you are painting on and let
the token resolve; typing either hex yourself gets one theme wrong.

**Radius** — `input` 8, `card` 16, `large` 16, `sheet` 24, `pill` 999. Nothing else.
Buttons and chips are `pill`.

**Type** — **Archivo** for every word; the app applies it automatically (a `fontWeight`
becomes the matching Archivo family — see `src/theme/brandFont.ts`), so never set a
`fontFamily` by hand. The display tokens (`hero`, `statement`, `metric`, `display`) carry
**Archivo ExtraCondensed** already. Bebas Neue is the `KONJO` wordmark and nothing else.

**The small metrics** — inputs h44, `radius.input`, `border.control`, 16pt inline glyph.
Icons 16 inline / 24 standalone / 19 status glyph; icon colour `text.primary`, or
`text.tertiary` for a chevron. Avatars 32 row and
header (header gets a 1px hairline ring), 88 profile. Chips 32 tall, 12 pad-h, `radius.pill`.
Scroll inset above the tab bar `insets.bottom + 80`. Skeleton fill `border.input` — a placeholder, on `surface.card` or `surface.page` alike. Pressed
opacity 0.85. Belt dots always carry a 1px `border.control` ring — without it a white belt is
1.2:1 on a white card and a black belt is 1.01:1 on a dark one.

**Motion** — `instant` 100, `quick` 150, `base` 200, `enter` 250, `exit` 180, `slow` 350.
Exits are faster than entrances. Nothing over 400ms except a belt promotion. Reduce-motion
replaces movement with a cross-fade.

Never type a number a token covers. Never import colours except through `useTheme()` /
`createThemedStyles`. Never use `DiagonalBand` or `band.*` — both are retired.

## 5. Write the copy plain

Say the thing. "Nothing graded yet / Add what you're teaching to start grading." Not "Nothing
here yet — let's fix that!" No exclamation marks outside a real celebration, no emoji in
chrome, no first person from the app. Use the art's words: dojo, belt, rank, promotion, test,
kata, sensei — never gym-generic synonyms. **But a shared component says "rank", not "kyu" or
"dan"**: those are one lineage's words, and Konjo is multi-style. Use the art's vocabulary in
copy about *this* dojo; use neutral vocabulary in anything rendered for every dojo.

For progress: **Duolingo's mechanics, Konjo's voice.** Streaks and badges are the display;
belts are the highest tier of badge because a human conferred them. A streak is *stated*, a
rank is *celebrated*. No mascots in progress UI, no confetti for showing up, and never a
loss-aversion nudge — a dojo does not guilt people into attending.

## 6. Check it before you claim it

Run this list against your own screen. Every item is a real defect found in the shipped app.

- Contrast computed, not eyeballed: **4.5:1** body, **3:1** for ≥24px or any control boundary.
- Every tappable thing **≥44×44pt**, or has `hitSlop` making up the difference. **In Studio: ≥32pt, and no `hitSlop`** — that is a touch remedy and Studio is pointer input.
- Nothing under **11pt** — with one exemption, `tabLabelActive` / `tabLabelInactive` at 10,
  which matches the platform's own tab bar. No fixed-height row containing text (Dynamic Type
  reaches ~46pt).
- The three budgets in §2 each spent **at most** once, or the reason written in the PR — zero is fine and normal on an index.
- Red only on things you can tap or on a selected state; nothing destructive is red.
- Empty, loading, and error states all exist and were looked at.
- Nothing truncates. If it doesn't fit, it reflows.
- The same destination does not appear twice in one viewport.
- Uppercase applied with `textTransform`, never by typing capitals (VoiceOver reads a literal
  `"ADD"` as "A. D. D.").
- Interactive elements and the screen root have kebab-case `testID`s.
- Verified in **both** themes. Light is where you design; dark is where you check. **Studio is light only** — the ink rail is brand, not a dark theme — so there is nothing to check there.

Then look at the reference screens in
[docs/design/reference/](../../../docs/design/reference/) — open a `.jpg` with Read and you
will actually see it, including the Konjo targets in `reference/konjo/` — two tab homes, a tile grid, the promotion
certificate, and a **form**, all in both themes. Compare yours to the closest one. "Roughly similar" is not the bar;
find the specific difference in spacing, weight, or ratio and close it.

## What never ships

A screen with no empty state. A mutation with no error path. A list with no loading state.
Two controls of equal weight competing to be primary. A centred button floating in a void.
Truncation used as a layout strategy. Text over a photo or gradient with no scrim. A
gradient nav bar. A decorative accent that means nothing. A row whose metadata is larger
than its subject. Copy in developer voice. A hardcoded hex. A magic number where a token
exists.

More, with the product each was observed in: [references/anti-patterns.md](references/anti-patterns.md).

## The bar

Not "it matches the tokens." The bar is that the person you designed it for — the white belt
who feels stupid, the instructor with one free hand — gets what they came for on the first
screen, without reading a paragraph, and would do it again next week without being told to.

If you cannot point at the one loud thing on your screen within a second of looking at it,
it isn't finished.
