<!-- https://getkonjo.com/design/foundations/rank · source: docs/design/foundations/rank.md -->

# Rank

Belts as data. The one foundation that is Konjo's and nobody else's.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them.

Rank is the most identity-defining element in the product. It is the thing a student checks,
the thing an instructor confers, and the thing the [certificate](../patterns/celebration.md)
exists to celebrate. It is also, until this chapter, the least specified: twenty raw colour
constants living outside the theme, two incompatible renderer models, three copies of the
data, and eight rank-and-theme combinations that fail contrast outright.

## Rank is a ladder, and the ladder belongs to the dojo

**Konjo is multi-style by design.** Cuong Nhu is the launch community, not the model. The
schema already reflects this — `styles` and `style_ranks` carry each dojo's ladder, Studio has
an authoring surface with an approval flow, and `user_style_ranks` records what a person holds
in each art they practise.

So the design rule is simple and absolute:

> **Never hardcode a rank ladder, a rank name, or a rank colour into a component.** A screen
> renders whatever ladder the dojo has. Nineteen is a Cuong Nhu number; ten is an ATA number;
> the next dojo will have neither.

That extends to language. "Kyu" and "dan" are Japanese-lineage terms; a BJJ academy has
neither, and a Taekwondo school uses "gup". Shared components say **rank**; the dojo's own
labels come from its ladder rows. Same for honorifics — see
[content format](content-format.md).

## The swatch, and the ring that makes it legible

A rank renders as a small circular swatch: [`BeltDot`](../../../src/components/ui/BeltDot.tsx)
on mobile, `BeltSwatch` in Studio.

**Every swatch carries a 1px `border.control` ring — every rank, both themes, no exceptions.**

This is not a nicety. Measured against `surface.card`, at the 3:1 that SC 1.4.11 requires of a
meaningful non-text mark:

| | Light | Dark |
|---|---|---|
| Fails | white 1.00, yellow 1.65, orange 2.70 | black 1.02, purple 1.80, camo 2.06, brown 2.38, blue 2.95 |
| Count | 3 of 10 | 5 of 10 |

Eight failures, at both ends of every ladder — the white belt who just started and the black
belt who has been there twenty years. Conditional rings do not solve this: a rule like "ring
it if the fill is white" leaves the black belt invisible in dark mode, which is exactly what
shipped. A ring on *everything* solves all eight at once, because `border.control` itself
clears the threshold against the card in both themes (3.53 light, 3.16 dark) no matter what it
surrounds.

The ring width is `borderWidth.hairline`. Half-pixel borders vanish at dpr 1 and round
inconsistently at dpr 3.

Rendering all nineteen ranks at 10 / 16 / 28pt in both themes confirms the arithmetic and
shows what it costs to skip: without the ring, in dark, the black belt is three faint smudges
and the entire dan progression reads as **red stripes floating on nothing**, because the black
belt behind them has merged into the card. With it, every rank has an edge and the dan belts
are belts again.

## Size carries the ladder, and 10pt does not

The same render surfaced something the arithmetic could not. `BeltDot`'s default size is
**10pt**, and at 10pt a nineteen-rank ladder collapses: shodan, nidan, sandan, yondan and
rokudan are all a small dark circle with a hint of red, and nothing distinguishes them.

That is not a bug in the component — 10pt is fine for what it usually does, which is signal
*that* a person has a rank next to their name. But it means:

- **10pt says "ranked", not "which rank".** Legal only where the exact rank is also present as
  text, or is not the point.
- **A rank is never chosen from chips.** [Pickers](../patterns/sheet-picker.md) prefer chips at
  six options or fewer, but a chip is 32pt tall and cannot carry a 16pt swatch with its ring
  and stay a chip. A short ladder still uses the sheet: rows are the only control where the
  swatch is legible and the rank's name sits beside it.
- **Where the specific rank matters — a roster tile, a profile, a promotion — use 16pt or
  larger**, and 28pt where the rank is the subject.
- **The rank's name is always available as text or in the containing label.** The swatch is
  never the sole carrier at any size; see the screen-reader contract below.

## Rank colour is data, not theme

Belt colours are the dojo's, not Konjo's. They do not live in the palette, they do not invert
by theme, and they are not subject to the red budget — a red belt is a fact about a person,
not an accent.

But that means they get no protection from the theme either, so:

- **The ring is what guarantees legibility**, not the colour. See above.
- **Never derive a UI colour from a rank.** No belt-tinted row backgrounds, no belt-coloured
  avatar rings, no gradient seeded from a rank. A representative colour extracted from a
  ladder row has no contrast contract with anything, and using it as a surface or a text
  colour is how a screen becomes unreadable for exactly one student.
- **Rank appears once per element.** A row with a belt dot does not also carry a belt-coloured
  border and the rank spelled out in a coloured chip. Dot plus text, or dot alone with the
  rank in the `accessibilityLabel`.

## Screen-reader contract

A swatch is a coloured circle: it says nothing on its own, and colour is the only channel it
uses — which is a failure of SC 1.4.1 if it is the sole carrier of the fact.

- **The rank's label always reaches the screen reader**, either as adjacent text or inside the
  containing row's `accessibilityLabel`. On the [tile grid](../patterns/tile-grid.md), where
  the dot is the only visible rank marker, the tile's label spells it: *"Ana Torres, white
  belt, not checked in, waiver missing."*
- **The swatch itself is decorative** once its meaning is in the label —
  `accessibilityElementsHidden`, so the rank is not announced twice.

## Two ranks must never render the same

A ladder where two rows produce an identical swatch has lost information. In the shipped Cuong
Nhu ladder, `nidan` and `rokudan` are both *two red stripes on black* and are indistinguishable
— a sixth-degree black belt renders exactly like a second-degree.

**Rendering distinctness is a property of the ladder, and the authoring surface is where it is
enforced.** Studio's ladder editor should refuse to save two rows with the same visual, the
same way it would refuse two rows with the same name. Downstream, a swatch is never the only
thing distinguishing two ranks in a list — the label is always there too.

## The visual model

`style_ranks.belt_visual` is jsonb. Today it carries `{color, stripeColor?, stripeCount?}`,
which expresses solid and striped belts and nothing else.

That is narrower than the Cuong Nhu ladder actually needs. Three of its nineteen ranks —
godan's split belt, hachidan's end cap, kudan's alternating bands — cannot be expressed in that
shape at all, so a dojo authoring the same ladder through Studio silently gets three ranks that
render as plain stripes. **Mobile and Studio can therefore show different belts for the same
person**, which is the sharpest form of the drift this design system exists to prevent.

The target is one model that both surfaces read, expressive enough for the ladders that exist:

| Kind | Shape |
|---|---|
| `solid` | one fill |
| `stripes` | base plus 1–4 evenly spaced bands |
| `split` | two halves |
| `end-cap` | base plus a band at one end |
| `alternating` | N equal bands of two colours |

One schema, one renderer contract, one set of colours per ladder row — and the built-in
ladders seeded through the same path as a dojo's custom one, rather than living as constants
in two apps.

## Never

- A hardcoded ladder in a component.
- A conditional ring.
- A UI colour derived from a rank.
- Rank conveyed by colour alone.
- Two ladder rows that render identically.
- A rank name assumed to be Japanese.
