Skip to main content
The mobile app has one design system — a six-step type scale, a 4px spacing rhythm, and two deliberately designed colour themes — driven entirely by CSS custom properties you reuse instead of writing literals.

Quick Start

1

Use a token

Style a component from tokens — never from raw hex, px, or font-size literals.
2

Follow the two themes

The same tokens switch automatically under prefers-color-scheme: dark. Your component needs no dark rule of its own — style once, both themes render.

How It Works

Four decisions define the identity — each one defends a specific cross-platform edge case.

Design Tokens

Every value below is verbatim from app.css. Reach for the token, never the literal.

Font stacks

Weights

Only these three exist by design.

Type scale — information band (12/13/14)

Type scale — hierarchy band (16/18/22)

Line-height & tracking

Spacing scale — 4px grid, in rem

Radius

Colour — both themes, side by side

Every token carries a light and a dark value in one comparison. --chrome is lighter than --ground in both themes (chrome floats above the page); --panel flips — below --ground in light, above it in dark, because a control reads as distinguished by sitting nearer the light source. Surfaces / neutrals Ink Accent & user message Status Tone — defined once, self-updating in dark because they reference the tokens above.

Two behaviours you can rely on

Focus ring — never suppressed for keyboard

The base ring is :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }. To drop the outline on programmatic focus (e.g. route navigation to .screen-heading), scope the removal so it never touches keyboard focus.
A bare .screen-heading:focus { outline: none } beats :focus-visible on specificity (0,2,0 vs 0,1,0) and takes the keyboard ring with it. src/praisonai-mobile/app/src/css.test.ts pins this by rejecting any :focus outline suppression that is not scoped with :not(:focus-visible).

WCAG AA in both themes

Every text pair in the stylesheet clears WCAG AA — the worst pair measures 4.5:1 in light and 6.3:1 in dark on the merged file. The sweep runs on the three screens and the real error row.

Chat message treatment

The user’s row keeps its bubble, its alignment, and gains one logical corner notch.
The notch is logical (border-end-end-radius), not physical — under dir="rtl" it follows the reader, not the LTR page. The assistant’s row loses its fill: a 100%-wide filled box is a container with prose in it, not a bubble, and stacked down a transcript it read as a column of grey slabs. Alignment, data-speaker, and the visually-hidden “You said:” span carry the speaker in the accessibility tree.

Tool-status chip — status as text and colour

A <span class="tool-status"> renders strings.toolStatus(row.status) as words (from dom.ts) and is coloured by --tone-* per data-status. Before this, unresolved had no rule at all and rendered pixel-for-pixel identically to running.

The gutter coupling (contributor note)

--gutter: var(--space-4) (= 12px) is the one number in app.css also written elsewhere: app/src/main.ts sets the composer’s padding-inline-start/end inline as calc(<inset> + .75rem). src/praisonai-mobile/app/src/css.test.ts has a remOf() / addendOf() helper pair that resolves --gutter through the :root token table (following the var() indirection through --space-4) and asserts the resolved rem equals the addend parsed from main.ts’s inline padding template. The same test asserts that .screen-settings, .screen-chats, .transcript, and .topbar each build both right and left inset padding as calc(var(--inset-<side>) + var(--gutter)). Change the gutter in one place; the test catches drift.

Common Patterns

Compose a component from tokens.
Colour by meaning, not by hue. Use the tone tokens so “success/failure/warning” can be re-tuned in one place.
Keep the keyboard ring when you quiet an outline.

Best Practices

Every colour, spacing, radius, and type step has a token; a literal skips both themes.
The css.test.ts gate will fail your PR — a bare :focus { outline: none } out-specifies :focus-visible and removes the keyboard ring.
When the meaning is “success/failure/warning”, use --tone-* so they can be re-tuned without touching every call site.
Requests outside 400/500/700 land unpredictably across platforms (Roboto vs SF).

Accessibility

Focus, RTL, and the accessibility tree that carries the speaker.

Boot Indicator

The wordmark and boot-mark tokens live in the same stylesheet.