<!-- https://getkonjo.com/design/start/language · source: docs/design/konjo-design-language.md -->

# Konjo design language

> The definitive reference for how Konjo looks and behaves, on both surfaces.
> Supersedes the visual sections of
> [2026-06-11-design-refresh-dark-mode-design.md](../superpowers/specs/2026-06-11-design-refresh-dark-mode-design.md)
> and folds in [the Studio brief](studio/ia-and-inventory.md).
> Built from a 46-app / 315-screenshot study plus a founder calibration session — the
> reasoning is in [2026-08-22-design-calibration.md](2026-08-22-design-calibration.md),
> the reference screens are in [reference/](reference/).
>
> **Kata (2026-09-29).** The founder reopened every decision below; the brief is
> [the Kata interview](2026-09-29-kata-founder-interview.md). Where the two disagree, the interview
> wins. So far that means: red is the action colour and **L4 is retired**; the budgets below are
> **guidance judged by the reviewer, not a hard count**; type is Archivo (display: Archivo
> ExtraCondensed); cards are 16 with pill buttons; the ninja is the mark and appears beyond
> onboarding (see [imagery](foundations/imagery.md)).
>
> **Agents: start at [the design skill](../../.claude/skills/design/SKILL.md), not here.** It
> carries the same laws in operational form and routes to the chapter for whatever you are
> building. Come back for **Part 1 — the laws**, which every chapter defers to and which the
> skill states without arguing for; the rest of this file is the argument, and the index at the
> bottom is the map.

---

## The one idea

**Loudness is a budget, and each screen gets one purchase.**

Konjo is loud. Bold athletic, high-contrast, heavy condensed caps — the founder's call,
and the right one for a martial-arts product. The failure mode of loud is that everything
shouts and therefore nothing does. Nike Run Club is loud, but its information design is
trivially simple: one enormous number, three supporting stats, one chart, nothing else.
The volume is spent on a single element and everything around it is near-silent.

That is the whole trick, and it is the thing Konjo currently gets wrong. Train home today
stacks up to eight containers at identical visual weight under a band that shouts three
times. Nothing on it can be second-most-important.

Every rule below is downstream of this one:

| The screen gets | At most | Counted as |
|---|---|---|
| Loud block (hero, inverted, display type) | 1, **or none** | Instances |
| Filled primary button | 1 | Instances |
| Uppercase **label** register | 1 | Type tokens |

**Kata: these are guidance, not a hard count.** The reviewer judges whether a screen has one clear
focus; a screen that breaks a line here needs a reason, not a rewrite. Red is no longer budgeted:
it is the action colour (see L4).

Everything else is quiet enough to disappear.

**Uppercase counts a *register*, not instances.** The hero's `statement` is display voice, not a
label, and does not count — a hero with a caps eyebrow above a caps statement is legal. What is not
legal is two competing label tokens (`eyebrow` and a second uppercase style) on one screen.

The loud block and the filled primary are literal counts: one each.

**These are ceilings, not quotas.** A screen with no loud block, no red, and no filled
primary is perfectly legal — an index screen usually has exactly that. "At most one" is the
rule; "exactly one" was never the rule and any wording implying it is wrong.

**When the subject is chosen by the data, the emphasis goes in a slot — or nowhere.** Every
budget above assumes a screen with a *fixed subject*, so the designer picks the loud thing once
and it is the same thing on every render. A dashboard, a board, a queue, an inbox and a filtered
index are not like that: the most important fact is an uncovered class on Tuesday, three unsigned
waivers on Wednesday, and nothing at all on Thursday — and loudness in this system is a property
of a component, not of a fact. Two rules follow, and they generalise past the shape that found
them:

- **Allocate the emphasis to one designated slot**, not to a region. One place, always in the
  same position, that renders only when a stated bar is met. What clears the bar is a
  [product fact](product-facts.md) — "which costs the dojo more, an uncovered class or an unsigned
  waiver" is a business answer, not a design one — and where no bar is written down, the slot
  renders only for facts whose urgency is unambiguous.
- **When nothing clears the bar, the screen has no loud element, and that is the right answer.**
  Never manufacture urgency to fill the slot: no "all clear" banner, no green celebration, no
  region promoted to loud because nothing else was. The absence *is* the answer to the screen's
  question.

A shape with no slot — a board, where the urgent thing is one card among forty — spends nothing,
and the closing bar of [the skill](../../.claude/skills/design/SKILL.md) (*"if you cannot point
at the one loud thing within a second, it isn't finished"*) is asked only of screens with a fixed
subject. Worked in full for cards in [the dashboard chapter](studio/dashboard.md).

**Chrome is exempt.** The tab bar persists across every screen, so its active state does not
count against the screen. The same goes for the status bar, the header icon row, and **the screen
title** — a `display` title is not the screen's loud block, which is why an index screen ships
one and still spends zero. What counts is the content inside the screen's own scroll view.

**Control states are exempt too.** A selected chip filled with `surface.hero` on `surface.card`, or a
checked-in tile, is a control expressing state — not a loud block. Otherwise the tile grid,
which fills a dozen tiles with ink, would be illegal by its own document.

## The user we lose

Design decisions get settled by whoever we'd lose first.

- **The white belt in week two.** Quits when something makes them feel stupid, or an empty
  screen doesn't say what to do next.
- **The student logging one-handed, sweaty, standing, with 40 seconds.**
- **The instructor mid-class, 20 students, one free hand.** Quits at anything needing a
  paragraph of reading or more than ~3 taps.
- **The member deciding what to sign up for.** Browsing camps and tests, weighing cost and
  travel, not yet committed to anything. Quits when a screen makes them hunt for what is on.
- **The owner at a desktop in the evening doing admin they resent.** Wants the task done and
  the tab closed. Quits at anything that makes an evening chore longer.
- **The front desk, mid-afternoon, three minutes between walk-ins.** Works a queue all day —
  leads, check-ins, the shop — and is measured on nothing falling through. Quits at a screen
  that hides which item has been sitting untouched.
- **The same owner, the next morning, opening Studio with coffee before anyone arrives.** Not
  resentful — *triaging*. The question is "what needs me today?", the answer changes every
  day, and the design's job is to make today's answer findable in seconds without them
  reading five cards to find it. This persona was missing from the catalog until round 8
  specified Studio's Home screen and found there was nobody in it who opens a dashboard.

If a design serves the founder's taste but loses one of these, it loses.

---

# Part 1 — The laws

Non-negotiable. Evidence-backed, no taste involved. An agent may not trade these away for
a nicer-looking screen, and a reviewer should reject a diff that breaks one.

## L1 · Accessibility floor

| Rule | Value | Source |
|---|---|---|
| Text contrast | ≥ 4.5:1 body, ≥ 3:1 for ≥24px or ≥18.5px bold | WCAG 2.1 SC 1.4.3 |
| Non-text contrast | ≥ 3:1 for anything that identifies a control or its state | WCAG 2.1 SC 1.4.11 |
| Touch target | ≥ 44×44pt for any control | Apple HIG; WCAG 2.5.5 (AAA) |
| Minimum type | ≥ 11pt, ever, anywhere | Apple HIG |
| Text spacing tolerance | No clipping at 1.5× line height, 0.12em letter spacing | WCAG 2.1 SC 1.4.12 |
| Dynamic Type | No fixed-height row containing text | body reaches ~46pt at AX5 |
| Reduce motion | Replace movement with a cross-fade; never just delete the transition | WCAG 2.3.3; MDN |

Contrast is computed, never eyeballed. `2.999:1` fails.

**Live contrast failures in the current tree — fix these, don't propagate them:**

Ratios are written in the `A on B — N.NN:1` form so the doc checker recomputes every one of
them. They were once written as bare numbers, and eight of them had drifted by 0.01 from the
generated table — small, and corrosive, because the one claim this system rests on is that
the prose and the code agree.

| Pair | Ratio | Where it hurts |
|---|---|---|
| the old `text.quaternary`, #BBBBBB on #FFFFFF | **1.91:1** | It was the inactive tab label. 4 of 5 tabs were effectively invisible. |
| the old dark `text.quaternary`, #5A5A60 on #0E0E10 | **2.81:1** | Same, dark. |
| the old `gold.text`, #B8860B on #FFF4D6 | **2.96:1** | The achievement moment — the thing the brand cares most about — was the least readable thing in the app. **Fixed:** ships `#8A6300`, and #8A6300 on #FFF4D6 is 4.95:1. |
| the old `text.tertiary`, #888888 on #FFFFFF | **3.54:1** | Failed for body copy; used everywhere as one. |
| `band.steelSub` #777777 on #ECECEC | 3.79:1 | Band is retired (L6), but the tokens live on. |
| 0.85 white on `brand.red` | 3.97:1 | Every red-strip subtitle. Alpha, so it is composited rather than recomputed here. |
| the old `feedback.successText`, #1F8A4C on #E6F4EC | **3.85:1** | Every success banner and pill. Missed by both the research and the first critic pass — one checked the colour against *white* and called it "just under", nobody checked it against the fill it is actually painted on. Fixed to `#1A7A42`, and #1A7A42 on #E6F4EC is 4.73:1. |
| the shipping `border.focus` on `surface.hero` | **1.14:1** light, **1.00:1** dark | Found in round 8. Every filled primary button and all ~25 items in Studio's rail had a focus ring nobody could see. **Fixed:** `border.focusOnHero`, and `border.focusOnHero on surface.hero` is 16.86:1 light / 14.72:1 dark. |

## L2 · The spacing ramp

```
0 · 2 · 4 · 6 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80
```

Fourteen values — **Atlassian's spacing table exactly**. **8 is the spine**
(8/16/24/32/40/48/64/80). **4, 6, 12, 20 are the legal half-steps**, with 6 reserved for
gaps inside a component (icon to label), not for layout. **2 is for optical correction
only** — baseline nudges, hairline compensation, badge insets — never a layout gap or page
padding.

Why not a strict 8pt grid: only 34% of Konjo's 3,782 existing spacing declarations survive
it, and it kills `12`, the single most-used value in the app (858 uses). That is a rewrite
of the app's visual rhythm, not a tokenization. Why not a full 2pt ramp: `4,6,8,10,12,14,16`
is a number line, not a scale — seven near-identical choices in the 8–16 band is exactly
what produced an 83% magic-number rate.

**This ramp keeps 75% of existing declarations** and moves ~1,155. The bulk of the migration
is two values: 10 (420 uses) and 14 (338), each moving 2px.

Precedent: IBM Carbon (`spacing-01` = 2px) and Atlassian (`space.025` = 2px), both of which
state an 8px base and ship 2/4/6 anyway. An earlier draft of this ramp dropped `6` for
tidiness; the adversarial review pointed out that it was citing Atlassian as precedent while
deleting the value Atlassian ships, and that keeping `6` is 24% cheaper to migrate
(1,155 declarations vs 1,522). Keeping it.

**The ramp is only 20% of the fix.** `packages/design-tokens/tokens.ts` exposes seven *semantic* spacing
names and zero numeric steps, so there is nothing to reach for when you need "the gap
between a chip and its icon" — you type a number. The other 80% is primitives that own
their own spacing (`Stack`, `Card`, `Row`, `Button`), so most screens stop declaring
spacing at all. Ramp and primitives together, or neither works. See
[the follow-up plan](../superpowers/plans/2026-08-22-design-primitives.md).

## L3 · Separation: tint the page, keep cards white

**No shadows. No blur. No glow. No gradients.** The law survives — but it stops leaning on
the hairline, and it runs in the direction the whole reference corpus runs.

| Need | Do this | Not this |
|---|---|---|
| A card on a page | white `surface.card` on a tinted `surface.page` | a grey card on a white page; a 1px border around white on white |
| A group of rows | 12pt of extra space between groups | a divider line |
| A row boundary in a long list | 1px inset hairline, starting at the text edge | 0.5px full-bleed |
| An interactive control's edge (chip, secondary button, input) | 1px `border.control` | a fill that doesn't separate |
| A floating surface (sheet, toast, menu) | scrim behind + 1px hairline | a drop shadow |

**Two border tokens, two jobs.** `border.hairline` divides rows inside a list. `border.control`
draws the edge of something you can tap. The distinction matters because WCAG 1.4.11 treats
them differently: a row divider identifies nothing and is presentational, but a chip's edge is
"visual information needed to identify a component", so it has to actually be visible. A
`surface.tag` chip on the tinted page is **1.017:1** and is not a control anyone can see.

> **This is a correction.** The first version of this law said the opposite — a
> `surface.muted` #FAFAFA card on a white page. That is arithmetically weaker than the thing
> it replaced: **#FAFAFA on #FFFFFF is 1.044:1**, against **1.225:1** for the
> `border.default` hairline. It would have made separation worse while claiming to fix it.
> Deepening the card fill instead doesn't work either — by the time the fill separates
> properly (#EDEDED, 1.171:1) `text.tertiary` on it has fallen to 4.05:1 and fails AA.
>
> Inverting solves both at once, and it is what Apple Health, Attio, Notion and Strong all
> actually ship: a tinted page at **1.119:1** against a white card, with body text sitting on
> white where `text.tertiary` holds **4.74:1**.
>
> **Dark has to be checked separately, and originally wasn't.** The first dark pair
> (`#18181C` card on `#0E0E10` page) was **1.089:1** — weaker than the light pair this law is
> defended with, and with borders and shadows both banned there was no legal recourse. The
> card is now `#1C1C22`: **1.137:1**, slightly ahead of light, with `text.tertiary` still at
> 5.33:1 on it. Publish both ratios whenever this law is restated; a separation law with only
> one theme's arithmetic is half a law. Social home already tries this
> ("white cards on a gray page") and the audit found it invisible at phone width — because
> the grey was #FAFAFA. Same idea, correct value.

Three reasons this is right, beyond taste:

1. **The old law was already broken 13 times** — six shadow/elevation declarations in
   `src/` (`PlansLibraryScreen` defines *two* levels, an elevation ramp reinvented locally)
   and seven gradients in Studio. When a law is routed around every time it binds, the law
   is wrong. Tonal fills give those cases somewhere legal to go.
2. **The hairline is the least reliable pixel on the device.** 356 of 487 border
   declarations are `0.5`, which vanishes or ghosts at dpr 1 and rounds inconsistently at
   dpr 3. **One hairline value: 1px.**
3. **The contrast arithmetic decides it, but read the standard correctly.** WCAG 1.4.11's
   3:1 applies to boundaries *required to identify a control or its state* — not to card
   grouping, which is presentational. No tonal fill on any surface anywhere reaches 3:1, so
   the broad reading would outlaw the founder's own answer by arithmetic and take Apple
   Health with it. What the numbers do settle is the *relative* question: 1.044:1 is
   invisible, 1.119:1 is what shipping products use, and a `border.default` hairline is
   1.225:1 but as a hard 1px edge rather than an area — which is why it survives as a row
   divider and fails as a card boundary.

Material 3 reaches the same conclusion from the other direction: it ships six elevation
levels and then instructs designers to prefer *tonal* elevation, using shadow "only for
elements that need more focus."

**Two gradient exemptions**, and only two: an image scrim (needed anyway for legible text
over an event photo), and exactly one brand moment (belt promotion / achievement).

## L4 · Red is the action colour

> **Retired and replaced (Kata, 2026-09-29).** L4 was "one red role per screen". The founder
> retired it: red is now the colour of **action**. The history of the old law is in the
> [August calibration](2026-08-22-design-calibration.md), round 12.

| Role | Colour |
|---|---|
| Filled primary button, selected segment, checked box, active tab | `brand.red` fill, `brand.onRed` label — 5.01:1 in both themes |
| Links and red text | `accent.red`, per theme — never `brand.red` as text (4.48:1 on the light page, 3.85:1 on the dark) |
| Everything structural (labels, eyebrows, chevrons, section titles) | `text.primary` / ink |
| Secondary and **destructive** actions | an ink outline (`border.control`) with `text.primary` label |
| Errors | `feedback.errorText` — a different red — always with an icon and words, never colour alone |

**Destructive is never red.** With red meaning "tap here", a red Delete would read as the thing to
tap. A destructive action is an ink outline and a plain verb ("Delete event"), behind the
confirmation the [serious actions](patterns/destructive.md) chapter requires.

**Red still means something.** Red on a surface should always be tappable or be the selected
state. A red block that does nothing, red decoration, and red body text are still wrong.

## L5 · The uppercase budget

Uppercase is a **label** treatment: **≤ 3 words, ≤ 20 characters, never a sentence.**

The evidence cuts both ways and the rule respects both halves:

- *For a glanced single word*, uppercase wins. NN/g's write-up of the MIT AgeLab study
  found lowercase needed **26% more time** for accurate reading of an isolated glanced
  word, and condensed needed 11.2% more than regular. `LIVE NOW` on a phone held by a
  sweaty student is exactly that task.
- *For anything read as a sentence*, uppercase loses badly. Reading speed drops 10–20%
  (Tinker 1955; replicated 2019 at >13%). Dyslexic readers slow a further 13–18%. Readers
  55+ were **29% more likely to misunderstand** terms set in capitals (Arbel & Toler, 2020).

So: one uppercase register per screen, on labels only. Konjo currently ships five
(`eyebrow` 11/800, `sectionLabel` 12/500, `bandLabel` 16/900, `bandLabelLead` 18/900, tri-button
labels 10/800) — uppercase used as ambient light instead of a spotlight.

**`eyebrow` is the uppercase register, and it is the only one.** `label` — buttons, chips,
badges — is sentence case. The single exception is a CTA *inside the hero block*, which may
take the hero's display voice, because the hero is one composition with one voice: eyebrow,
statement and CTA read as a single utterance. Outside the hero, a button says "Check in", not
"CHECK IN".

**Always via `textTransform`, never by typing capitals into the string.** VoiceOver reads a
literal `"ADD"` as "A. D. D."; a `textTransform`'d "Add" reads correctly.

## L6 · The diagonal band is retired

`DiagonalBand` and the `band.*` tokens are deprecated. The −2° skew goes; **the voice
survives** — full-bleed ink block, uppercase 800–900 letterspaced label, live subtitle, red
arrow, all at skew 0.

Why, specifically:

- It only reads as intentional when ≥2 strips stack. Events home ships one and it reads as
  a crooked black rectangle.
- It costs unpredictable vertical space. Overhang is `(width/2)·tan 2°` computed at
  runtime: 6.8pt on a 390pt phone, **22.3pt at 1280pt web** — the same component reserves
  3.3× more dead space on desktop. This is why Events has ~90pt of nothing under it.
- It cannot hold data. One `numberOfLines={1}` uppercase label (long event names silently
  clip) and one 12px subtitle at 3.79–3.97:1. Every actual payload — the countdown, the
  class name, "1 video awaiting feedback" — lives in the illegible half.
- It cannot be nested, cropped, or reused inside a list or card, so it duplicates the
  destination it points at. Events shows the same event three times in one viewport; Learn
  twice.
- It forces negative horizontal margins on every host, which already needed a documented
  contract (`bandWrap` in `EventsHomeScreen` exists purely to restore padding the band
  cancels).
- Rotated text cannot use sub-pixel antialiasing — the skew softens every glyph edge.

Squaring it deletes `SKEW_DEG`, `SKEW_TAN`, `BAND_TOP_GAP`, `BAND_BOTTOM_GAP` and the
runtime width maths in one commit.

## L7 · Motion

| Token | Duration | Use |
|---|---|---|
| `instant` | 100ms | Press feedback, checkbox, toggle |
| `quick` | 150ms | Chip select, small expansion |
| `base` | 200ms | Default transition |
| `enter` | 250ms | Sheet in, modal in, screen push |
| `exit` | 180ms | Anything leaving — exits are faster than entrances |
| `slow` | 350ms | Large expansion. The ceiling. |

Easing: `cubic-bezier(0.2, 0, 0, 1)` standard; `cubic-bezier(0, 0, 0, 1)` for entering;
`cubic-bezier(0.3, 0, 1, 1)` for exiting. Prefer ease-out.

Nothing exceeds 400ms except one deliberate celebration (belt promotion), which is
opt-out. NN/g: "at 500ms, animations start to feel like a real drag."

Reduce motion is live-listened via `AccessibilityInfo` and **replaces** movement with a
cross-fade rather than removing it.

## L8 · Never ship

- A screen with no empty state, a mutation with no error path, a list with no loading state.
- Truncation used as a layout strategy. If it doesn't fit, reflow it — don't clip it.
- The same destination twice in one viewport.
- Two controls of equal weight competing to be primary.
- A control under 44pt without `hitSlop` to make up the difference.
- Text on a gradient or a photograph without a scrim.
- Copy in developer voice.
- A number typed where a token exists.

---

# The rest of the system

The laws above are the part that does not vary. Everything else is a chapter, loaded when you
need it. **Agents: use [the design skill](../../.claude/skills/design/SKILL.md) as your entry
point** — it routes to the right chapter and carries the checklist. This spine is the argument;
the chapters are the detail; the skill is the instruction.

### Foundations — the raw material

| Chapter | What it settles |
|---|---|
| [Colour](foundations/colour.md) | The palette, why the hero inverts, the two recurring traps |
| [Type](foundations/type.md) | The scale, and two sizes per screen |
| [Space](foundations/space.md) | The ramp and its legal half-steps |
| [Radius and borders](foundations/borders.md) | Two border tokens, two jobs |
| [The small metrics](foundations/metrics.md) | Heights, glyphs, avatars, chips, insets |
| [Elevation](foundations/elevation.md) | Why the app is flat |
| [Motion](foundations/motion.md) | What moves, how far, and who opts out |
| [Iconography](foundations/iconography.md) | Weight, size, and when a glyph may stand alone |
| [Imagery](foundations/imagery.md) | Photos, avatars, thumbnails, and what happens when they fail |
| [Rank](foundations/rank.md) | Belts as data — the one Konjo-specific foundation |
| [Content format](foundations/content-format.md) | Dates, numbers, names, counts, truncation |
| [Accessibility](foundations/accessibility.md) | Beyond contrast: screen readers, text scaling, reduced motion |

### Patterns — the shapes a screen can take

| Chapter | When |
|---|---|
| [The tab home](patterns/tab-home.md) | One of the five tab roots |
| [The list screen](patterns/index-list.md) | A collection they came to browse |
| [The tile grid](patterns/tile-grid.md) | Triage plus one tap per person |
| [The detail screen](patterns/detail.md) | One record, in full |
| [Navigation](patterns/navigation.md) | Where a screen sits, and how you get back |
| [Forms](patterns/form.md) | Anything they type into |
| [Pickers and sheets](patterns/sheet-picker.md) | Choosing among options |
| [Serious actions](patterns/destructive.md) | Legal records, money, anything unrecoverable |
| [Buttons](patterns/buttons.md) | Any action, anywhere |
| [Search and filter](patterns/search-filter.md) | Finding one thing among many |
| [Empty, loading, error](patterns/states.md) | Every screen, every time |
| [Notifications](patterns/notifications.md) | Push, banners, toasts, badges |
| [Celebration](patterns/celebration.md) | The promotion certificate |
| [Progress](patterns/progress.md) | Streaks, badges, belts |

### Voice, surfaces, and the record

| Chapter | What it settles |
|---|---|
| [Voice](voice/voice.md) | The words the app uses, and the ones it never does |
| [Brand](brand/identity.md) | Wordmark, icon, splash, store — where Konjo presents itself |
| [Studio — the desktop dialect](studio/dialect.md) | Where the owner's desk deliberately differs |
| [Boards](patterns/board.md) | Columns of cards, one per stage — the container the gesture happens inside |
| [Direct manipulation](patterns/direct-manipulation.md) | Drag, drop, reorder — and the keyboard path that is not optional |
| [Studio — metrics](studio/metrics.md) | Every Studio number, the rail, the pointer model, and what a status *means* |
| [Studio — dashboards](studio/dashboard.md) | Snapshot cards, and how a screen spends its budgets when the important thing changes daily |
| [Studio — tables](studio/tables.md) | The owner's first-class layout |
| [Studio — overlays](studio/overlays.md) | Drawer, menu, dropdown, transient confirmation — and how a flat system separates a floating surface |
| [Studio — charts](studio/charts.md) | What to draw, and the palette that passed validation |
| [Studio — public surfaces](studio/public-surfaces.md) | The session-less pages a stranger sees, and the two exceptions the storefront is allowed |
| [Studio — IA and screen inventory](studio/ia-and-inventory.md) | Every Studio screen and what is on it |
| [Anti-patterns](../../.claude/skills/design/references/anti-patterns.md) | 21 named failures, with the product each was observed in |
| [Product facts](product-facts.md) | The business decisions the design cannot hold — answered once |
| [Governance](governance.md) | How a token is added, and how this document changes |
| [The completeness test](testing/design-completeness.md) | The standing gate, and why the blind test is not one |
| [Where this came from](provenance.md) | The research and the calibration rounds |

### Generated, never hand-written

| Reference | Generated from |
|---|---|
| [Token tables](../../.claude/skills/design/references/tokens.md) | `packages/design-tokens/tokens.ts` |
| [Component inventory](../../.claude/skills/design/references/components.md) | `src/components/ui/index.ts` |

Values in those two files are read out of the code on every run and every contrast ratio is
recomputed. If prose anywhere disagrees with them, `npm run design:check` fails — which is the
only reason this document can be trusted to still be true.
