<!-- https://getkonjo.com/design/process/kata-migration · source: docs/design/kata-migration.md -->

# Kata migration — bringing every screen onto the new look

**Status:** in progress (2026-10-01) · **Brief:** [the Kata interview](2026-09-29-kata-founder-interview.md)
· **Brand core:** [galenjauss/Konjo#179](https://github.com/galenjauss/Konjo/pull/179)

The brand core changed the tokens and the shared primitives. Every screen that builds its own button, chip,
band or label still shows the old look. This document is the one recipe for moving a screen onto Kata.
Every person or agent doing the migration follows it, so the result is one design rather than many
interpretations.

**The prime directive: change how it looks, never what it does.** A migrated screen has the same
destinations, the same data, the same order of steps, the same tap targets and the same accessibility as
before.

## What must not change

These are hard rules. A diff that breaks one is wrong, however good it looks.

- **Behaviour.** No change to an `onPress`, a navigation call, a mutation, a query, a conditional that
  decides *whether* something renders, or the order of a flow.
- **Selectors.** Every `testID` survives, on the same element. If a bespoke control is replaced by a
  primitive, its `testID` is passed through.
- **Accessibility.** `accessibilityLabel`, `accessibilityRole`, `accessibilityState` and `accessibilityHint`
  survive. A replacement primitive must expose the same role and label.
- **Copy.** Words stay the same. The one exception is capitals typed into a string: those become sentence
  case with `textTransform: 'uppercase'` on the style, so the screen looks identical and VoiceOver stops
  spelling it out. **Before changing any visible string, grep `e2e/` and `.maestro/` for it.** If a test
  matches it, update the test's matcher in the same change (case-insensitive, or the new string), and say
  so in your report.
- **Touch targets.** Nothing that was ≥44pt gets smaller. `hitSlop` stays.
- **Layout that holds data.** Don't remove a field, collapse a section or hide content to make a screen
  calmer. Calm comes from type, colour and space, not from deleting things.

## The recipes

### 1. Selected and active states → red

Anything that expresses "this one is chosen" fills **`colors.brand.red`** with label/icon
**`colors.brand.onRed`**. This applies to chips, filter pills, tabs inside a screen, segmented buttons,
toggled day pickers and choice cards. It does **not** apply to the hero block or the Studio rail.
Unselected stays as it was: a `surface.card` fill with a 1px `border.control` edge.

```ts
chipActive: { backgroundColor: colors.brand.red, borderColor: colors.brand.red },
chipActiveText: { color: colors.brand.onRed },
```

### 2. Primary buttons → red pill

A filled call-to-action uses `<Button>` from `src/components/ui` when the swap is clean: same label, same
handler, same `testID`, same loading behaviour. If the bespoke button has things `<Button>` cannot express
(an icon-only control, a two-line label, a timer), keep it bespoke and restyle it:

```ts
primaryButton: { backgroundColor: colors.brand.red, borderRadius: radius.pill, minHeight: controlHeight.primary },
primaryButtonText: { color: colors.brand.onRed },
```

- **Secondary buttons** are an ink outline pill: no fill, `borderWidth: 1`, `borderColor: colors.border.control`,
  `borderRadius: radius.pill`, label `colors.text.primary`.
- **Destructive buttons are never red.** They use the secondary style with a plain verb ("Delete event").
  If a destructive button is currently red, make it the outline.
- **Links** are `colors.accent.red` text (never `brand.red` as text).

### 3. Red only means "tap here" or "selected"

Red on a label, eyebrow, section header, count, caption or decorative number becomes `colors.text.primary`
(or `text.secondary` for metadata). Red stays on:

- links and tappable text;
- live or urgent status ("Starts in 5 min", "Past due"), which is the one non-tappable exception, and only
  when it is genuinely urgent;
- errors, which use `colors.feedback.errorText` with an icon and words.

### 4. The retired diagonal band → Hero, rows, or nothing

`DiagonalBand` and every `band.*` token are retired. A band strip becomes one of three things:

- the screen's single **`<Hero>`** block, if it is the screen's one most important thing;
- an ordinary **row or card**, keeping its title, subtitle, arrow and destination;
- nothing, if the same destination already appears elsewhere in the same viewport.

Every destination a band linked to must still be reachable from the same screen.

### 5. Text reflows, it doesn't clip

`numberOfLines={1}` clips. Remove it, or raise it to 2, wherever the container can grow. Keep a single line
only where the layout genuinely cannot grow, and say why on the same line with
`{/* kata-allow: <reason> */}`. Examples where one line is right: a tab-bar label, a chip inside a
horizontal scroller, a segmented-control label, a fixed-size tile. Never truncate a person's name, a class
or event title, or a date that a person needs to read.

### 6. Shape

- Cards and tiles: `radius.card` (16).
- Buttons, chips, pills, segmented tracks and avatars: `radius.pill`.
- Inputs: `radius.input` (8).
- Sheets: `radius.sheet` on the top corners.
- No raw radius numbers.

### 7. Type

Archivo is applied automatically. **Never set `fontFamily`** except via a token.

- A screen's large title uses `typography.display`.
- A hero statement uses `typography.statement`.
- A big number uses `typography.metric` or `typography.hero`.

Those four carry the condensed display cut. Body, rows and labels stay on the text tokens.

### 8. The mechanical rules

Fix every `design-check` finding in the files you own:

- **Spacing:** move spacing onto the ramp `0 2 4 6 8 12 16 20 24 32 40 48 64 80`, nearest value, and
  prefer the token (`spacing.s12`).
- **Borders:** `border.control` on a control edge, never `border.default` or `border.input`.
- **Colour:** no `text.quaternary` as a text colour, and no hardcoded hex (use the theme).
- **Type size:** nothing under 11pt (tab labels excepted).
- **Depth:** no shadows or elevation. A floating surface uses a hairline and a scrim.
- **Caps:** no capitals typed into strings.

## How to check your work

```bash
npx tsc --noEmit                                     # must stay clean
node scripts/verify/design-check.mjs <your folders>  # drive it to zero, or to stated kata-allow lines
grep -rn "<any string you changed>" e2e .maestro      # keep the tests matching
```

Then re-read your diff with one question per change: *does this alter what happens when someone taps,
or only how it looks?* If you cannot answer "only how it looks", revert that change.

## The surfaces pass (wave 2)

`surface.white` is deprecated. In dark mode it resolves to the page colour, so a card painted with it
separates from nothing. As a light-mode screen root it puts white cards on a white page, which is the
opposite of the separation law. `design-check` flags it as `kata-surface-white`. Replace every use by
what the element *is*:

| The element is… | Use | And check |
|---|---|---|
| A screen root, safe area, scroll container or list background | `colors.surface.page` | Text sitting **directly** on it: `text.tertiary` fails there (4.23:1), so make it `text.secondary`. A `surface.tag` or `surface.muted` chip or field directly on it vanishes (≈1.02:1), so make it `surface.card` with a 1px `border.control` edge |
| A card, tile, row group, sheet body or modal | `colors.surface.card` | Drop a border that only existed to separate white-on-white; keep `border.control` on anything tappable |
| An input, search field or select | `colors.surface.card` + 1px `border.control` | Inputs sit on the page, not inside a card |
| A full-screen media viewer or camera surface | leave dark surfaces dark: `brand.ink` / `surface.hero` as they were | — |
| Something that must be white in both themes (a QR code field, a printed document preview) | the theme-invariant token that already exists (`brand.onRed` is #FFFFFF in both) | Say why in a comment |

A screen that was a white page with bare rows on it becomes a tinted page with those rows grouped
into a white card. That is a change of container, not of content: same rows, same order, same
handlers.

Look at both themes: `surface.page` is #F2F2F3 in light and #0E0E10 in dark, and `surface.card` is
#FFFFFF in light and #1C1C22 in dark.
