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 fromapp.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.
.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.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.Best Practices
Use tokens, not literals
Use tokens, not literals
Every colour, spacing, radius, and type step has a token; a literal skips both themes.
Never suppress :focus without :not(:focus-visible)
Never suppress :focus without :not(:focus-visible)
The
css.test.ts gate will fail your PR — a bare :focus { outline: none } out-specifies :focus-visible and removes the keyboard ring.Reach for the tone tokens, not --bad / --warn directly
Reach for the tone tokens, not --bad / --warn directly
When the meaning is “success/failure/warning”, use
--tone-* so they can be re-tuned without touching every call site.Pick from three weights, not five
Pick from three weights, not five
Requests outside 400/500/700 land unpredictably across platforms (Roboto vs SF).
Related
Accessibility
Focus, RTL, and the accessibility tree that carries the speaker.
Boot Indicator
The wordmark and boot-mark tokens live in the same stylesheet.

