utword DS · v3.25.7
Outword · Design System
fig.00 — the swipe, spelling the name

The design language of a fast multiplayer word game. A symmetric light/dark system where roles swap, two hues carry every meaning, and each screen is quiet around one thing — the letter tile.

Sourceapp.jsx · v3.25.7
Grounds2  dark default · light swap
Hues2 semantic + 1 system (focus)
Color58 base values · 2 modes

01  Principles

Principles

Five beliefs, in priority order — when two conflict, the higher one wins. The mechanical choices under them aren't separate rules; they're these, made concrete. Written to be used in a critique, not admired on a wall.

01

The experience is the whole point

Judge every decision by how the round plays — never how it looks or how clever it is. Speed and clarity are the biggest levers inside the experience, but the small joys — a word landing, the trace snapping shut — are part of it too, and don't get traded away to shave milliseconds.

so "does this make the round better to play?" is the only question that settles an argument. Pretty, novel, and distinctive are all downstream of it.

02

Fast and clear, before anything on screen

The player is racing a clock: every read instant, every control obvious, nothing between them and the next word. When speed or legibility fights beauty, novelty, or personality, speed wins — every time.

so this governs the look, never the feel in 01 — a treatment that costs even a beat of legibility loses, however good it looks.

03

Calm does double duty — speed and feel

A quiet interface keeps fewer things between the player and their word, and gives the game a composed, premium calm. Low stimulation isn't restraint for its own sake; it's how Outword stays fast and feels good at once.

so a muted palette, one thing moving at a time, and nothing on screen competing with the board for the eye.

04

Craft is restraint; over-design is the failure

Craft here is precision, not richness — the truest colour, the tightest spacing, the cleanest state. The quickest way to be wrong is a flourish that's trying. If it looks "designed," cut it back.

so the strongest move is usually to remove something — new emphasis competes with existing emphasis, and more signal is rarely more clarity.

05

Unmistakably Outword — earned, never decorated

It reads as itself the instant you see it: the tile, the swipe-trace, the two grounds. Identity is the last thing to protect in a conflict — clarity always outranks it — but it's always present, and it comes from the mechanic, not from ornament.

so distinctiveness is a byproduct of the game's own shapes, never a treatment added to look unique.

How they show up

The recurring mechanical choices in this document — each one is a driver above, made concrete.

  • The board is the hero; everything else stays quiet. One primary action per screen, controls in outline, and no decoration that pulls the eye off the letters.
  • One hue, one meaning · one pill, one live value. Green is earned, rose is danger, plus one system hue for focus; a pill holds a single changing number, never a static label.
  • Motion belongs to menus, not gameplay. Ambient movement on idle screens; the instant a round goes live, everything freezes.
  • Two grounds, symmetric. Light and dark are one system with the roles swapped; every colour is a token, so a hardcoded value can't survive a mode flip.

02  Foundations

Color

Two grounds. The color that is the page in one mode is the ink in the other. The ground/ink pair is a true inversion; the softer tiers — soft, faint, lit — are hand-tuned per mode rather than blindly swapped, so each reads correctly.

Tokens are built in three tiers: a base reference (--dk-* / --lt-*) holding the only literal values; an active layer (--ground, --fg…) that components reference; and a mode swap on :root[data-mode="light"] that repoints the active layer. Flipping that one attribute is the whole mechanism — a hardcoded hex can't follow it, which is why it's forbidden.

Aa
ground #202845 · fg #F2F0EA
Aa
ground #F2F0EA · fg #202845

fig.01 — the same two roles, grounds swapped

The two semantic hues

Green (--earned) and rose (--danger) are the only colors that carry meaning — and the only ones that don't invert. Each means exactly one thing. Green is never a selection colour: a word shown or traced on a board, and the pill that shows it, use the pressed-tile state. Shown in context, on the word pill they live on:

SPARROW QXZ PLOVER TRAC

fig.02 — earned · danger · dup · neutral

The one hard rule

Type on any semantic fill is always --ground — never white, never black. In dark mode --danger is a light rose, so white on it lands at ≈1.7:1 — unreadable — while --ground ink clears 8:1. Green is positive only; rose is negative only. If green ever also meant "selected," the system would be broken.


03  Foundations

Typography

Three families as tokens — display, body, and word — the same split this document is set in. Arial Black carries the loud moments; a system face carries the reading; the word face is the tiles.

RSTL
Room code · 54px display · +14px
Create game
Heading · 16px display · caps +1px
SPARROW
Word pill · 17px / 800 · +2px
plover — 8 pts
Body / list · 16px / 600
Longest word
Eyebrow · 11px / 700 · caps +2px

Weights: 500, 600, 700, 800 — nothing else. Live numbers use tabular-nums. The eyebrow (uppercase, +2px, --fg-soft) is the recurring structural label — reuse it, don't invent section headers.


04  Foundations

Radius

Three radii carry the system. Reach past them only with a reason.

12 · buttons · fields · cards
999 · pills
14 · tiles · rows

Also present: 8 (brand die), 10 (segmented), 18 (sheet top), 2 (rails). Prefer 12 / 999 / 14.


05  Foundations

Spacing

A 4px base grid. Common gaps: 8 / 12 / 16 / 20 / 24. The modal head is the reference rhythm — 20 above the title, 4 to the sub, 16 to the rule, 12 under.

8 · 12 · 16 · 20 · 24

06  Foundations

Motion

Motion in Outword has one job: make the game feel alive without slowing anyone down. It explains where something came from, confirms a tap, or celebrates something rare. A move that does none of those gets cut.

Principles

  • Purpose first. Every move answers one question: where did that come from, did that work, or is this worth celebrating.
  • Size it to the moment. Things that happen often stay small and quick. Only things that happen once get a story (see the moment map).
  • The round is sacred. While a clock is running, only feedback moves: tile state and the word pill. No ambient motion and no celebration on the board.
  • Physical, not magic. Things arrive from somewhere and leave to somewhere. Nothing pops out of nothing, and nothing stretches.
  • Calm by default. Motion settles and stops. Attention is asked for once per visit, then the screen goes still.

Vocabulary

Seven moves cover everything that ships. New motion uses one of these before it invents anything. Press a move to replay it.

MoveCurveDuration · used for
feedbacklinear70–80ms · tile state, button press. Feels instant; it only confirms.
popcubic-bezier(.3,1.4,.5,1)240–300ms · something small landing: the key dot, the crown, a badge. Starts at ×0.4–0.9, never from zero.
enter / movecubic-bezier(.2,.8,.2,1)500–700ms · pieces travelling, toasts, rows, captions, sheets.
risecubic-bezier(.22,1,.36,1)700–820ms · podium columns sliding up out of a baseline mask.
exit / fallcubic-bezier(.5,0,.75,0)520ms · things leaving, the crown dropping, the board falling away.
attentionease-out900ms × 2 · a ring grows from the dot to ×2.6 and fades. Once per visit, then still.
kinetic textenter curve460ms a word, 60ms apart, rising 10px out of a 4px blur. Sub line follows 300ms later. Stories only (the update reel).

Ambient · the brand-mark trace runs a 5700ms cycle on menus only (see Brand mark) · mode cross-fade eases the ground over 0.5s · chevron 0.15s

The moment map

Positive · commonSmall and quick

Copy turns into a check. "Saved". Never in the way.

Positive · rareCelebrate

The podium and crown, the update reel, welcome back. Confetti lives here only.

Negative · commonPlain and fast

A wrong code turns the slots --danger. No shake.

Negative · rareEmpathy, not decoration

A fresh phone asks "Played before?" Words do the work.

Rules

  1. Only feedback moves during a live round. Celebration, attention and ambient motion all wait for Home or results.
  2. Never scale from zero. Pops start at ×0.4–0.9. Things fade and grow in; they don't appear from a point.
  3. Grow with a mask, never a stretch. Anything with text, a radius or a border slides out of a clipping mask (translateY(101%) → 0). scaleY on those is banned, because it squashes corners and type.
  4. Enter slow, leave fast. Entries decelerate and exits accelerate. An exit is never longer than the entry it undoes.
  5. One move per area at a time. Groups stagger 40–60ms apart so the eye can follow. Two things never compete in one spot.
  6. Ask for attention once. A ring or dot animates on the first view of a visit (sessionStorage), then holds still. Nothing loops forever except the brand mark on menus.
  7. Confetti is earned, and it clears. Rare positive moments only, the full fall (4–6s), and it's gone before any button needs a tap.
  8. Errors don't shake. Colour plus plain words. Shaking reads as scolding, and kids use this too.
  9. Reading time is part of the motion. In a story, hold 3s after each caption lands before the next beat starts.
  10. Only transform and opacity. No animating width, height, top or box-shadow, so motion stays smooth on older phones.
  11. Reduced motion is a full design, not an off switch. See below.

Reduced motion

MoveUnder prefers-reduced-motion: reduce
feedbackUnchanged: colour changes are not motion
pop · enter · riseThe final frame, or a ≤150ms opacity fade where feedback is needed
exitRemoved instantly
attentionThe dot shows with no ring. The cue is the dot, not the movement
confettiNone. The podium opens built, crown on
update reelOpens on the recap close. Nothing it said is lost
brand markStatic on its resting hook
The test

Turn reduced motion on and walk the flow. If any information only came across through movement (where something went, what's new), the still version has a bug.


07  Foundations

Brand mark

A filled disc with a 3×3 lattice, and a line that traces a legal board path across it — the disc is the leading O of "utword." SVG on a 0 0 100 100 viewBox; geometry locked to the approved reference.

utword

fig.06 — the live mark, tracing legal board paths · disc --fg · dots --mark-dot · trace --ground

PartSpec
disccx/cy 50, r 34.2, fill --fg
lattice9 dots, r 3.15, --mark-dot, spaced by R_CELL 0.234 × diameter 68.4
tracepolyline, --ground, width 3.9, round caps, pathLength=1
motiondraw 1400 → hold 2800 → retract 1000 → pause 500 (5700ms), eased

Legal traces

The trace only ever draws a real board path — cells that touch, edges or corners, and never the same one twice. It's the exact rule you swipe by in a round: a word is just a legal trace with letters on it.

The rule allows 10,256 legal paths of 3 to 9 cells. Drawing them at random throws the occasional awkward shape at logo size, so the mark instead rotates through a curated set (the four below, the game's MARK_PATHS, chosen to read cleanly), starting from the canonical hook (1·4·7·6·3), which is also the app icon and the still frame under reduced-motion.

1·4·7·6·3
0·4·8·5·2
6·4·2·1·0
6·3·1·5·8

the rotation set · the live mark above cycles these in order · hook leads and is canonical

Motion — the lattice holds, the trace moves

Each trace runs the same beat: draw 1400ms → hold 2800ms → retract 1000ms → pause 500ms — a 5700ms cycle, eased cubic in-out — then the next in the rotation. The nine dots never move; only the line does. It's the game's own swipe gesture, slowed to an ambient pace.

Still during play

The instant a round starts, live=false and the mark freezes on its resting hook. Nothing moves while someone is racing the clock — ambient motion belongs to menus, not to the board, where it would only pull the eye off the tiles. It also pauses when scrolled off-screen (IntersectionObserver) and holds completely still under prefers-reduced-motion.

The NameGrid is the same primitive — as of v3.14.1 its letters recede, so its trace line stays visible while the game board's is hidden.


08  Components

Tiles

The heart of the UI; everything else is quiet around it. Square by construction, radius 14, 2.5px border, box-sizing border-box so a state change never shifts the grid.

Dark · resting + lit
S
P
A
R

lit = --select + --select-stroke + --select-ink

Light · resting + lit
S
P
A
R

resting is a solid fill — an outline read as "unfinished"

Trace-line rule · v3.12 / v3.14.1

On the game board the trace line is hidden — filled tiles carry the word, feedback lives in the pill. The name board is the sole exception: its letters recede, so the line earns its place. Tiles are the one control with no press-dip; it would fight the trace.

Showing a word · 3.21.1

On review boards (daily board, game results, practice results), tapping a word to show it lights its path in the same lit state as tracing in a round: --select fill, --select-stroke outline, --select-ink letter, with the normal --trace line. That includes "Longest on board". Showing a word earns nothing, so the path is never green.

Name board

On the home screen your name is spelled across a mini board — the reward for typing a real name. It's the same tile, the same selected treatment, and the same polyline as gameplay, so it reads as a board rather than decoration. The name sits on a random legal path (adjacent cells, no reuse); the rest is filler. The name letters wear the selected/recede treatment (--select fill + --select-stroke) while filler stays the bright resting tile — the inverse emphasis of gameplay, which is exactly why the trace line shows here.

Name board · types on, tap to re-lay

5 columns · tile radius 9 · trace --trace, width 2.4, 50% · draws at 190ms/letter

How it lays out

The board is 5 wide, 2 rows up to 10 letters and 3 beyond. A self-avoiding walk places the name; leftover cells take filler from a fixed set. Each open types the name on — 260ms in, then a tile every 190ms with the trace growing behind it — and every tap re-lays a fresh random arrangement.


09  Components

Buttons & controls

One primary per screen, and it's the action people are most likely to take — not the one listed first. Most players join rather than host, so Join is the filled primary and Create is the outline. Everything else is an outline too; there is no third style — a screen that seems to need one has too many actions.

Dark

primary · secondary · icon (44×44)

Light

icon buttons are always 44×44 · radius 12 · --fg-soft

ControlHeightRadiusFill / borderType
primary5212fill --fg · ink --ground · lift16 / 700
secondary52121.5px --rule outline15 / 600
icon-secondary4412--rule outline · --fg-soft14 / 600
icon44×4412--rule outline · --fg-soft20 glyph
segmented4410on-state fills --fg13 / 600
Icon-button exception

Icon buttons are 44×44 / radius 12 / --fg-soft — the modal close included; no size exceptions. Press: scale 0.98 + brightness 0.94 over 0.07s, on every button except tiles. Focus: 2px --gold (=--fg) ring, offset 2px — the home/mode icon button is the one that rings in --focus.

States

Primary · every state

default · pressed (scale .98 + brightness .94) · focus (2px --gold ring, offset 2) · disabled (.4 opacity)

Focus · most controls

2px --gold (=--fg) ring — 5.3:1, clears the 3:1 non-text target

Focus · mode toggle (exception)

the home/mode icon button rings in --focus (powder blue)


10  Components

Toggle

Two toggles. The segmented control switches between named options — the on segment fills with --fg, so the choice reads as loud as a primary. The mode toggle is a single icon button that flips the ground.

Segmented · dark

on = --fg fill · --ground ink · 44px · radius 10

Segmented · light

off segments are --fg-soft on transparent

Mode toggle · on dark

home-icon-btn · 44×44 · a half-filled disc reads as "appearance"

Mode toggle · on light

one glyph, not a sun/moon — only the ground flips (0.5s cross-fade)

One selected, always

The segmented control has no empty state — one option is always on. It's a choice between peers, not an on/off switch; there is no sliding-thumb switch in the system, and adding one would introduce a second selection metaphor competing with the tile's lit state.


11  Components

Pills

Radius 999, reserved for a single live value — the clock, the score, the word being traced. Not for recaps or badges; those use weight and ink.

1:240:07128

fig.10 — timer · urgent (rose, ground ink) · score

The word pill's resting state is boxless, so touching a tile has nothing to collapse; min-height holds the slot. Arriving fades grey→green; leaving is instant (.empty is transition:none) so no green blob lingers after the word is gone.

Tappable word pills

burgouts burgouts

resting · active (its word is showing on the board) — the pill flips exactly like a pressed tile · never green


12  Components

Cards & sheets

Exactly three elevation levels — depth comes from the tile's inset lip, not stacked shadows: --ground (page), --raise (on it), --lit (a tile in the trace).

Leaderboard rows
1 Ru longestsparrow 24
2 Sam 17
3 wordstap word to show on board
quartzunique ×218 pts
plover8 pts
otter5 pts

rank · name · longest pill · points · tap to expand → per-word points

Earned state · fill + stroke
sparrow SPARROW
--earned-card-bg fill · --earned-card-border stroke

green takes fill AND border — solid pills match; tinted surfaces keep a solid --earned stroke

Green all the way to the edge

An earned (green) or danger (rose) element takes the hue on both fill and border — never a coloured fill left with a neutral --rule stroke. Solid states match border to fill (--earned on --earned); tinted surfaces pair an --earned-wash/--earned-card-bg fill with a solid --earned-card-border stroke, so the edge still reads green.

Bottom sheet

The app's modal rises from the bottom — that's where thumbs already are. The scrim dismisses; the ✕ is for people who don't know that. Every gap in the head is a multiple of 4: 20 above the title, 4 to the sub, 16 to the rule, 12 under.

Create game

Set it up, then read the code out to everyone.

Mode
Board
Round
PartSpec
scrimfixed inset 0, rgba(6,16,32,.72), fade-in 160ms
sheetmax-width 520, bg --ground, 1px --rule, radius 18 18 0 0, shadow 0 -8px 40px --shadow, max-height 88vh (less the keyboard while one is up)
risesheet-up 220ms cubic-bezier(.22,1,.36,1) — none under reduced-motion
headtitle 800/17, sub 500/12.5, close 44×44 outline
footborder-top --rule, padding respects env(safe-area-inset-bottom)
no footthe body's bottom adds env(safe-area-inset-bottom), so its last line clears the home indicator (3.25.4)
keyboardwhile one is up the sheet sits on it: scrim padding-bottom var(--kb), from the visual viewport; the inset drops out (3.25.4)

fig.11 — scrim dismisses · sheet rises from the thumb line · 18px top radius only


13  Components

Fields

One field style. The name field is the default; the join field is the same field with a .code modifier — centered, uppercase, +6px — for typing a host's 4-letter code.

Dark

name + join input · height 52 · radius 12 · fill --raise · code +6px, centered

Light

same tokens, swapped ground

Codes aren't boxed

That .code field is only for entering a code to join a game. The room's own code — the one the host reads out — is never in a field. It's open display text: --font-display, 54px, +14px.

Room code · open display text
RSTL

States

Validation runs on submit, never while typing. The field itself doesn't change on error — the message does the work: names show an inline .field-err, the join code shows an .err banner. Fields never disable.

Name field · error is a message, not a border

Names need at least 2 letters.

Pick a different name.

default · focus (--gold ring) · too short (<2) · blocked (profanity). Also: "Use at least two different letters."

Join code · .code field

Get the 4-letter code from your host.

Room codes are 4 letters.

uppercase · +6px · centered · exactly 4 letters. 4 but unknown → "No game found with that code."

Where word validation lives

The field checks above are for names and codes. A traced word is validated in the word pill (see Pills): under the minimum length (3, climbing to 4/5 in Survival) reads short; not-a-word or a blocklisted word reads bad; already-found reads dup. Same profanity BAD_SET keeps bad words off the board and the leaderboard.


14  Components

Compete cards

Home's Compete section is a small bento: the monthly leaderboard row, the Daily board (full width), then Longest word and Trivia side by side. Every card is one tap target with its arrow tucked in the top-right corner. A card turns green once you've played that game today — green means earned, so it never shows before.

not played today
not played today
played · 1st today
played · 1st today
played · 3rd today
played · 3rd today
PartSpec
.cc-cardradius 14 · padding 14 · --tray-raise fill · 1px border. play: border --select-stroke. done: fill = 7% --ok-green over --tray-raise, border = 45% --ok-green, arrow turns green
arrow24×24, absolute top 8 / right 8, close to the corner. Rows reserve 36px on the right, tall cards 22px under the top row
.cc-rowleaderboard: 40px rank circle · eyebrow "Leaderboard · October" · meta "833 pts · 104 behind name" (number first)
.cc-wideDaily board: DAILY in selected tiles (24px) · "Daily board / 90 sec · 5×5" → done: eyebrow "Daily ✓" + big "46 pts"
.cc-pair / .cc-talltwo columns, gap 10 · min-height 140 · top row (cover + arrow, min 40px) · text pinned to the bottom
eyebrow / big / meta.cc-eb 700 11px, tracking 1.2px, uppercase (green + check when done) · .cc-big 800 22px, tabular · .cc-meta 600 13px --paper-dim
word tiles.cc-t size var (17px default), radius 28% of size · selected = --select / --select-stroke / --select-ink · empty = 1.5px --tray-line outline · max 5 per line, then wrap

Longest word card

Before: empty slots for the board's longest word (5 + 4) with a small lock, "Longest word / 60 sec · 9 letters". After: your word in selected tiles in the slots, "Longest ✓ / 6 of 9". The lock opens (green) only when you found the longest word possible; white when someone else did.

Trivia card · option K (3.23.2)

Before: the ? tile, "Trivia", "2 min · as many as you can" — nothing else, so it never reads text-heavy. After: your place today as a rank circle (circle = rank) with "2nd today / of 14", your count, and one quiet line. The number always leads, so a long name only ever truncates itself.

StateLine
behind"Top 7 · Samra-the real1"
tied"Tied with Alexandria-Kate" (+n when more than one) · 1st
leading"Ahead by 2" · or "Ahead on skips" when the count is equal
only playerno line
.cc-rank32px circle (1st uses .place-1) · "1st today" 700 12px, green when 1st · "of N" 600 --paper-dim
.cc-trline600 12.5px --paper-dim · one line, ellipsis · stretches the card width
nevera lock, "unlocked", empty tiles standing for a count, or the answer

15  Patterns

System states

Outword is realtime and multiplayer, so waiting and failure are core surfaces, not corners. Every state below ships in the app — the copy is verbatim, and it's where the Voice principles actually earn their keep.

Loading

the primary swaps its label and disables while Firebase is pending (create / join)

Offline

No connection. Playing solo works anyway — the board and the dictionary are on your phone.

multiplayer actions disable; solo stays live — board and dictionary are on-device

Errors · verbatim

Room codes are 4 letters.

No game found with that code. Double-check the letters with your host, or pick one from Public games below.

That room is full. 12 players max.

That game already started. Ask the host for a new round.

specific, actionable, never apologetic — always naming the next move

Empty

Nobody's scored yet this month

Nothing open right now. Ask your host for a code, or start your own game.

Looking for games…

leaderboard · public list (empty) · public list (loading) — each points to an action

The rule

Every waiting state disables and relabels rather than spinning a generic loader; every error is specific and names the fix; every empty state invites the next step instead of setting a mood. Nothing in this product is allowed to fail silently or block the one thing that always works — solo play.


16  Patterns

Help & recovery

A swipeable help deck that teaches the game in the game's own language (3.18), and a short recovery code so a cleared browser or a new phone never loses your name and rank (3.21).

Help sheet

Opened from the header ? — a swipeable deck across two sections: Playing and Competing. The chooser lists them with a thumbnail board that spells each name on a trace; open one and the cards demonstrate on a real mini board — the same tile and trace as gameplay — so the tutorial is the game, not a diagram of it. It auto-opens once on a true first visit, and never during a live round, where the clock would keep running behind it.

Help chooser
P
L
A
R
E
Y
O
S
T
PlayingTracing, real words, the clock. View →
C
O
M
T
E
P
E
R
S
CompetingDaily boards, the monthly climb. View →

the chooser · each thumbnail spells its section on a trace board · cards teach on real mini boards

Recovery code (3.21)

Identity lives under a hidden id. Every player gets a short code: six characters in two groups of three (K7P-RX4), drawn from 20 letters and 5 digits with everything a kid could misread left out (0/O, 1/I/L, 2/Z, 5/S, 8/B). At least two digits and never four letters in a row, so a code can't spell a word. It's made in the background the moment the player exists and stored in Firebase (rc/CODE → id, create-only and not listable; rcu/id → CODE). The old long code (otter-cobalt-a1b2c3d4) still restores forever, and is shown if the short code can't be made.

The code is shown as tiles, outlined in --fg, never a boxed input. The whole panel is the copy button (square = tap). Save is two buttons side by side: Save to Photos (primary) shares a picture of the code with instructions on it; Save to Notes (outline) shares a ready-made note. Each turns into an outlined "Saved to …" with a check. Text buttons carry no icons.

Dark · key sheet
K7PRX4Tap to copy
I already have a code

panel on --raise, 1px --rule, radius 14 · tiles 42px, 1.5px --fg · copied: check + "Copied" in --fg for 1.8s

Light · restore
K7P
Not saved yet: the key turns outlined and gets a dot

empty slots are dashed --rule-strong, like an unfilled board · a wrong code turns the slots --danger

MomentSpec
first askAfter the first real result, the key gets the "not saved" state: outlined key + 12px dot. The dot pops (240ms, overshoot) and rings twice (900ms) once per visit, then rests. Icon-only, so the header never changes width.
keep your spotOne sheet, once: on the third day played or the first top-10 spot. "You're 7th in October. That spot lives on this phone, so save your code to keep it." Not now is respected; after that it's only the dot.
home screenSafari players after three days get a card on Home, "Play from your home screen". The guide is four short steps, step 1 is saving the code. iOS 26: ••• by the address bar, then Share. Older iOS: Share at the bottom. Android: Chrome's own Install. Closing hides it for two weeks, once.
first openOpened from the home screen icon with nothing on the phone: "Played before?" with six empty slots, I have a code (primary) and I'm new, let's play. A first browser visit never sees it. The tutorial waits for the answer: I have a code skips it, I'm new opens it (3.25.4).
restoreSix slots fill as you type, or all at once from Paste under them, which takes the code out of whatever was copied, the whole saved note included (3.25.4). Five misses an hour, then a pause. "Is this you?" shows the name in tiles and warns when this phone has a player of its own. After switching, Home says "Welcome back, Zuhayr". A ?code= link opens restore filled in.
fallbacksNo share sheet (Photos): the picture full screen, press and hold, or Download. No share sheet (Notes): the note is copied with a toast. Clipboard blocked: the code or note turns selectable with a Copy button that tries the older copy method. Cancel in a share sheet changes nothing.
rulesOne ask per visit, only on Home, never mid-round. On the 1st the podium goes first. "Saved" lives on the account (rcs/id), so nudges never return after a restore. Copy alone doesn't count as saved.

17  Patterns

Celebration

One moment a month. On the 1st, last month's final standings open as a podium for everyone, the winner wears a crown for the rest of the month, and confetti falls. All of it is built from parts the game already owns: the three tile states, the trace line, and the brand's flared crown. No new hue, and nothing ever plays during a round.

The crown

The crown means one thing: won last month. It hangs off the top-left of that player's rank circle, tilted back, so it reads as a crown on the rank without hiding the number. It's a flared shape (three points, a dot on each, a base that narrows) because that's the one that reads as a champion at 16px.

Dark · season leaderboard
1Dev412
3ZuhayrSeptember winner301

crown on the circle · "September winner" under the name · no separate winner card

Light · same tokens
3ZuhayrSeptember winner301

the glyph · ink is --fg, so it flips with the mode

PartSpec
glyphviewBox 24×20 · body 1.8,6 7.4,11.2 12,4.6 16.6,11.2 22.2,6 19.6,17 4.4,17 · finials r1.6 / 1.9 / 1.6 · stroke 1.2, round joins · fill and stroke --fg
placement75% of the circle's width (21px on a 28px circle) · left −8 / top −10 · rotate −14° from the bottom center · scales with the circle (×0.27 left, ×0.36 top)
cutthe same shape behind it, stroke 5.6, in the surface color: --raise-solid on rows, --ground on the home banner and podium
whereseason leaderboard row, plus a "Month winner" line · your own circle on the home leaderboard banner if you won · the podium's 1st column
neveras decoration, as an icon for "first place this month", or anywhere during a round

The podium

Players asked for gold, silver and bronze. The system says it in tiles instead: 1st is a solid tile, 2nd a traced tile, 3rd a lit tile. The board's own vocabulary, so the top three stand apart without a fourth hue.

Dark · built
2
Maya3,980
1
ZuhayrYou · 4,210
3
Ru3,610

2 · 1 · 3 · solid, traced, lit · the viewer's plate reads "You"

Light · built
2
Maya3,980
1
Zuhayr4,210
3
Ru3,610

the same three states, swapped ground

StepSpec
columns72 wide, radius 12 · heights 150 / 104 / 66 · numerals --font-display 24 · 1st --tile · 2nd --select + 2px --select-stroke · 3rd --lit + 1.5px --select-stroke
build3rd, then 2nd, then 1st, at 380 / 820 / 1260ms · each grows 640ms on cubic-bezier(.3,1.25,.55,1) · plates fade up 280ms after their column
crowndrops onto the 1st column at 1900ms · 440ms, accelerating (cubic-bezier(.5,0,.75,0)) · the column dips to scaleY .95 on impact
impactconfetti fires from the top of the 1st column · rows below fade up 110ms later
pace×0.62 for anyone who wasn't on the podium or didn't play: it's a results moment for them, not a party

Who sees what

ViewerTitleWhat's different
winnerYou won September"You" on the 1st plate · sub: points, and that October starts at zero
2nd · 3rdSecond in September"You" on their plate · sub: points and the gap to the winner · nothing extra beyond the shared confetti
4th and belowZuhayr won Septemberfaster build · rows 4, then a gap, then the player above you, you (outlined), and the player below
didn't playZuhayr won Septembersame as 4th and below, without a row of their own

Confetti

The pieces · every one is a board part
Tile Traced tile ZWinner's letter Trace ribbon Crown · max 2
PartSpec
count36 · every third piece a ribbon · up to two crowns · the rest tiles, traced tiles and letter tiles spelling the winner's name
depththree layers · far: ×0.55–0.7, blur 1.6px, 50% · mid: ×0.95–1.15, blur 0.3px, 85% · near: ×1.45–1.8, crisp, 100%, painted last
arcone continuous move: rise 1.5–1.9s and slow to a stop (cubic-bezier(.12,.8,.2,1)), then fall from rest over 4.3–6.2s (cubic-bezier(.45,0,.8,.55)) · staggered up to 340ms so it pours, not pops
lifesway ±22px and ±7° over 3.4 / 4.2s · tiles flip slowly and brighten facing you, dim edge-on (brightness 1.18 → 0.5) · ribbons draw themselves out over 1.5s
color--tile, --tile-ink, --fg, --trace at 62% · nothing else, so light mode is automatic · never --earned
layera fixed layer above the sheet (z 61), so pieces fly into the scrim · pointer events pass through

When it shows

  • On the 1st, once. The sheet opens on its own the first time the app lands on home that day, and only if last month had a scorer. ow_podium_YYYY-MM marks it seen.
  • Rest of the 1st. Closing it leaves a "Today" card at the top of Compete: a tiny podium with the crown, who won, and who closed it out. Tapping reopens the sheet. Gone at midnight.
  • No action button. The X and the scrim close it. There's nothing to do next, so the sheet doesn't pretend there is.
  • All month. The crown stays on the winner's rank circle until the next 1st.
The rule

Celebration lives outside the round. It plays on home, never on a board with a clock running, which keeps principle 1 intact: the round is what matters. Under prefers-reduced-motion the sheet opens on its final frame, podium built and crown on, with no confetti.


18  Patterns

Update reel

The "What's new" popup that plays once after each release: one continuous story built from the app's real pieces, not screenshots or a phone frame. The format is fixed; only the chapters change. It's the place motion gets to tell a story, because it happens once. Its job: show players exactly what was added, how to use it, and where and when they'll see it.

Dark · the open
New in Outword · 3.21
SOTARMNEIDLAOWCPGYBK
New in Outword
Two new things in 3.21.
1 · Recovery code
2 · Podium

the board trace spells NEW, the rest falls away · the rail names each chapter and fills as it plays

PartWhat happens
open · 5sA full board cascades in, a trace spells NEW, the rest falls away. "New in Outword" plus how many things are new.
chapter · 8–10s a beatOne per new thing. Context first ("You climb the leaderboard"), then the feature with its real parts. Feature headlines start with "New:".
close · 3sA recap of where each new thing lives ("Your code · Under the key on Home"). "That's what's new". Continue playing returns to Home; Watch again replays.
containerA 350×640 card over Home, scaled to fit short screens. X the whole time. Bands: stage, captions, rail; the buttons replace the rail at the end.

Principles

  • Real pieces, not screens. Rows, the key, code tiles, the code card, the podium and the crown, lifted onto the ground bigger than life.
  • One hero object. The tile carries the story; each beat grows out of the last, like a match cut.
  • Make it theirs. The player's own name and code.
  • Say what's new. The open counts the changes, "New:" marks a feature, the rail names each chapter.
  • One move per beat (a deal, a fill, a drop), and a 3s hold after each caption so it can be read.
  • Kinetic captions. Headline of 5 words or fewer; each word rises 60ms after the last out of a 4px blur. One line of 14 words or fewer under it.
  • Earned celebration. The full confetti only where a chapter earns it, and it clears before the buttons.
  • Quiet close. Plain words and one useful pointer. Never a finale.

The template

Every release uses the same shape, so players learn how to watch it. Only what goes into the slots changes.

Open5s
Chapter 18–10s
Chapter 28–10s
Chapter 3optional
Close3s · waits

Total = 5s + 9s per chapter + close · 1 to 3 chapters · never longer than about 35s

Inside a chapter: four beats

Each chapter answers four questions in order, so a player leaves knowing exactly what was added, why it matters, how to use it, and where to find it. A simple feature can fold beats together: the 3.21 podium is one beat that answers New, How and When at once. Worked example: 3.21, chapter 1, with its real copy.

3Maya412
4Ru380
5Rio301
You climb the leaderboardEvery round moves you up.

1 · ContextWhat do they already know? Start there, so the new thing has a reason.

New: your recovery codeSix characters that bring back your name and scores.

2 · NewWhat was added? Name it. The headline starts with "New:" and shows the real piece.

K7PRX4
Save it onceOne tap puts it in Photos or Notes.

3 · HowHow do you use it? One verb, one move on screen.

Home · top right
Your codeUnder the key on Home

4 · Where & whenWhere will they see it, and when does it show up on its own? Answered in the close, one line per chapter.

Chapter spec

Fill one of these in for every chapter before anything is animated. If a field can't be filled, the change probably doesn't need a chapter.

FieldLimit3.21 · recovery code3.21 · podium
rail labeln · 1–2 words1 · Recovery code2 · Podium
context≤ 5 words + 1 line · optionalYou climb the leaderboard · But it lives on this phonenone: the podium is its own context
new"New:" + ≤ 4 wordsNew: your recovery codeNew: the monthly podium
howverb-first · ≤ 14 wordsSave it once · Back in, on any phoneOn the 1st, see last month's top three. The winner gets the crown.
wherea place in the appUnder the key on HomeOpens on Home
whenits triggerAny time. Asked once, on day 3 or a first top-10 spotOn the 1st of every month
hero objecta real componentkey → code tiles → code cardcolumns → crown
the moveone vocabulary movetiles deal in (enter)columns rise, crown drops
celebrate?rare positive onlynoyes · full confetti, clears

The close: where each thing lives

Dark · the close
New in Outword · 3.21
Your codeUnder the key on Home
1The podiumOn the 1st of every month
That's what's new
Both are live now.
Continue playingWatch again

one recap item per chapter: its hero in miniature, its name, then where or when

  • Recap, don't repeat. No replayed traces and no second confetti. The close is a calm summary.
  • Each item = icon · name · where or when. Pick whichever helps them find it: a place ("Under the key on Home") or a moment ("On the 1st of every month").
  • "That's what's new", with one line under it. Plain words, never a sign-off slogan.
  • Continue playing is the one primary button and returns to Home. Watch again is a text link that replays from the open.
  • The close waits. It never auto-dismisses. X still works.

What gets a chapter

Kind of changeIn the reel?Shape
new thing they'll see or touchYes, a chapterContext → New → How → Where & when
change to something they knowYes, a chapterBefore → After (the "New:" beat shows the old piece turning into the new one) → Where
more than 3 visible changesThe best 3The rest go in one recap line: "Plus smaller fixes"
fixes, speed, behind the scenesNoCHANGELOG only. A patch release plays no reel

Copy template

OPEN       eyebrow  New in Outword · {version}
           headline New in Outword
           sub      {N} new things in {version}.
CHAPTER n  rail     {n} · {Name}
           context  {What they already know, ≤5 words}
                    {why it matters, one line}
           new      New: {the thing, ≤4 words}
           how      {Verb} … {≤14 words}
           where    {Place in the app}
CLOSE      recap    {Name} · {Where or when}   (one per chapter)
           headline That's what's new
           sub      {Both are / It's / All three are} live now.
           buttons  [Continue playing → Home]   Watch again

Before it ships

  1. Every chapter's spec table is filled, with real copy and no placeholders.
  2. The open says how many things are new, and the rail names each one.
  3. Every hero is a real component from the app, at its real proportions.
  4. Each beat uses one move from the vocabulary, with a 3s hold after its caption.
  5. The player's own name and code appear wherever the story shows them.
  6. Confetti only where a chapter earns it, and it's gone before the close.
  7. Reduced motion opens on the close, and every recap line still makes sense alone.
  8. Screen readers hear one sentence per beat in a live region.
  9. It plays once per version, on Home, for returning players only. Never mid-round.
When it plays

Once per version (ow_reel), on Home, for players who have played before. Never mid-round; on the 1st the podium goes first. Under prefers-reduced-motion it opens on the recap with no movement. Screen readers get one sentence per beat in a live region.

Updated in 3.22

The reel is now a story on real screens: it starts on Home, taps into the new thing, uses it, and closes on the new Home with small green NEW tags. Close is the primary button, Watch again a link. No recap list. It's authored on the design canvas and exported as data the app plays. Full format: decisions/update-reel-format.md. Patches never replay the reel; 3.23.x shipped without one.


19  Patterns

Trivia

A question on top, its answer hidden on a 5×5 board as a path you swipe. The daily run is 2 minutes, as many questions as you can: solve one and the next is straight up. One hint for the whole game; skip is free. The same pieces as every other board — tiles, the trace, the word pill — so nothing new had to be learned.

Get ready…
Get ready…
3, 2, 1
3, 2, 1
question · Easy
question · Easy
tracing
tracing
not it
not it
hint: first two letters
hint: first two letters
skip → next
skip → next
last 10 seconds
last 10 seconds
PartSpec
layouteyebrow "Trivia · Sat, Oct 10" · HUD: timer pill (left) + "N solved" score pill (right) · question card ("Question N · Easy" + the question, centred) · slot · board · Hint + Skip
.tv-qn700 10.5px, tracking 1.5px, uppercase, --paper-dim · tiers read Easy / Intermediate / Difficult · in the card: no margin, 6px above the question
.tv-q800 20px/1.3 (18px under 760px tall), centred, inside the question card: 33px from each screen edge (16 gutter + 1 border + 16 padding)
.tv-slotheight 40 · 18px above and below · holds the word pill, or "Swipe the answer" (700 10px, tracking 2px) at rest
.tv-acts22px under the board · gap 10 · .tv-btn height 44, radius 12, 1.5px --paper outline · Hint shows "1 left", then "Hint used" (outline --tray-line, dim)
solvepill turns --ok-green for 650ms (the one green moment), haptic, then the next question
not it.bad pill (rose fill/line/ink) for 900ms; under 3 letters says nothing (a fumbled tap)
swipefirst touch and the lift point take the whole tile; mid-swipe keeps the 70% zone so clipped corners don't grab tiles · the word is read from the live trace on lift
countdown"Get ready…" then 3, 2, 1 (3.8s) before the clock starts, like the daily board

The question card

Questions run 1 to 4 lines, and anything above the board that changes height moves the board under the player's thumb. So the question sits in a card that keeps one height for the whole run: as tall as the run's longest question. Every question of the run is laid out, hidden, in the card's one grid cell, so the browser measures the height at the player's own width and font size. The label and question sit centred, with equal space above and below. Spare space reads as padding only when it belongs to a surface.

PartSpec
.tv-qsone grid cell, every child stacked in it (grid-area: 1 / 1), align-items: center · fill --raise · 1px --rule · radius 14 · padding 14 / 16 · margin-top 14 · under 760px tall: padding 12 / 14, margin-top 8
ghostsevery question in the run, each a label line + the question, visibility: hidden and aria-hidden · the tallest sets the height, so no number is hardcoded
contentthe visible label + question, centred: equal space above and below
behaviourcard, slot, board and buttons hold still across every question in a run, wherever the player has scrolled · a new run can be taller or shorter (its longest question differs)
measured14 skips at 320 / 375 / 390 / 430 wide: board moved 0px · card height constant · space above and below equal to the pixel
rejectedquestion at the top of the space (3.23.4: a gap under short questions) · at the bottom (3.23.5, preview only: the gap moved under the timer) · the header sliding away for long questions · no reserved space (the original jump)

The hint tag

The hint lights the first two path tiles and rides in the pill as a soft filled tag with a bulb. The tag sits tight against the rest of the word so PO·PCORN reads as one word, and it stays through wrong answers and the green solve. With the hint lit, tracing the whole word, starting on the second hint tile, or tracing only what's left all count.

PartSpec
.tv-chipfill 16% currentColor (reads on neutral, rose and green) · radius 999 · letter-spacing 2px
centring6px visible space on every side of bulb + letters (measured at 3×): padding 3.17 / 3.5 / 2.17 / 3.83 — uneven on purpose, the bulb icon carries its own space and caps sit high — plus translateY(-0.33px) on the contents (text snaps to whole pixels; transforms don't)
gap2px between the tag and the next letter · the tag never shrinks

End of the run

leaderboard
leaderboard

The summary shows the count big, "solved · N skipped · hint used", then every question you met in order: your answer in selected tiles + SOLVED (green), or dashed empty tiles + SKIPPED. If you have the most today the label reads "You solved the most" in green and the podium confetti fires — the only celebration in trivia, and it happens after the round. Otherwise "Solved in 2 minutes". 3.25.5: tap any answer and a sheet shows its question, the answer forming in the word pill, and the board it was on with the answer traced: the daily board leaderboard's small board, pressed tiles and soft trace. Tap the board to play it again; room results open the same sheet with who solved it. The leaderboard ranks most solved, then fewer skips; the leader reads "most today". No lock, no "unlocked".

ConfettiSpec
confetti36 pieces from the label: tiles, selected tiles, name tiles spelling the player's name, and trace ribbons · tile colours only, never green · clears on its own · never on Home
name tilesboard tiles scaled down: 20px, radius 22%, 500-weight letter at ~52% of the tile (10.5px), --tile / --tile-ink
pieces.cf-tile 12px, radius 22%, --tile · .cf-ring = the selected tile, --select + 1.5px --select-stroke · ribbons = the trace stroke at 62%

Trivia mode in Play

Create game · Trivia
Create game · Trivia

Trivia replaced Survival in Create game and Play solo. Settings: Round 1 / 2 / 3 min and Difficulty Mixed · Easy · Intermediate · Difficult (a full-width segmented control — the selected segment is --fg solid, never the tile treatment). The rules card says it in three lines, including "one hint for the whole game". In rooms everyone gets the same questions in the same order; results show standings (solved, skips) and every question with chips for who solved it (you in green, "nobody" in italic).


20  Patterns

Scoring

Every game scores in small numbers a player can add in their head. Rooms and solo boards rank the people in them; the three daily games also bank points into one monthly total, which is the leaderboard on Home. Points are banked once, when you finish, and never recomputed.

Word points

A word is worth its length minus two, with a floor of one: 3 letters = 1, 4 = 2, then letters − 2 (partyScore). Under 3 letters isn't a word.

Letters345678910
Points12345678

Each game

GameHow it scoresHow the day ranksBanks to the month
Daily board · 90 sec · 5×5word points for every word you findtotal points, then longest word, then more words, then who finished firstyour total · typical 40–50
Longest word · 60 sec · 5×5your longest word. The goal is the board's headline word, shown as "N letters" on the card and above the board; find a word that long (or longer) and the round ends on the findwhoever reached the goal first sits at the top; then longest word, then faster, then who finished first3 per letter of your longest word, +10 if it reached the goal (TOPWORD_PTS_PER_LETTER, UNLOCK_BONUS) · 6 letters = 18 · 9 of 9 = 37
Trivia · 2 minone per question solved. The hint is free; a skip costs nothing but timemost solved, then fewer skips, then who finished first5 per answer (TRIVIA_PTS) · 6 answers = 30
Rooms · 2 or more playersword points for every word, and a word nobody else in the room found counts double (marked ×2)total points. Survival ranks by how long you lasted, then words, then pointsno points; the game counts toward games played, and a win if you place first with points
Soloword points, never doubled: alone, every word would be "unique"just younothing

The month

Banked points add up across the month on the Compete leaderboard, ranked by points, then words. Only players who played a daily game that month appear. On the 1st the final standings open as the podium, and last month's winner wears the crown until the next 1st.

Your month, by game (3.24.0) and the month board's stats (3.25.0)

Every opened row on the month board says the same things in the same order, about this board's points only. Your own row opens by itself on three cells, the month's points from the Daily board, Longest word and Trivia, each with a swatch in the ink of --fg (Daily solid --fg, Longest --fg-soft, Trivia an outline of --fg since 3.25.3). Then up to five days with points, newest first, one bar each, every part carrying its points (since 3.25.6 a part is never narrower than its number plus 4px a side). Every row then shows the month, "12 of 31 days" and a mark a day (played days filled, today outlined), and the records said in full: Best Daily board, Longest word found first (the open lock stays --earned), Best Trivia run (3.25.1: Longest streak left; the strip shows the runs). Since 3.25.1 the line under the leaderboard's title is a text button, "How points work". Since 3.25.2 its sheet ("Tap a game to see it play.") says each game's rule in one line, and tapping a game plays it on a small board ("3 letters is 1 point, +1 a letter.": TAG +1, GATHER +4, GATHERED +6; "3 points a letter, +10 for the board's longest.": GARDENS climbing to +21, then the lock opening for +10; "5 points a right answer.": two questions, +5 each). Before a tap, and under reduced motion, each board shows how it ends. The Daily board keeps a point a letter, with no unique rule. Words found and Party games won are gone: one mixes two games' words, the other counts rooms, which bank no points here.

your row on the month board: points by game, the October note, three days as bars with each part's points, the month, four records
your row
another player's row: the month and four records
anyone else's row
the How points work sheet: the three games' rules with an example each
how points work

Nothing new is stored: each day is rebuilt from that day's own results with the rules in force since 2026-10-10, and a finished day is kept on the phone, so it's read once. A month that began before 2026-10-10 shows its older points as one earlier cell, so the cells always add up to the row's total.

Worked examples

  • Daily board: dealer, dearer, dilate, eaters and ideals are six letters each, 4 points each: 20 points, banked as 20.
  • Room: two players both find dealer (4 each); only one finds retailed, 8 letters, 6 points, doubled to 12.
  • Longest word: the card says 9 letters. A 7-letter best banks 21. A 9-letter word ends the round and banks 27 + 10 = 37; so does a 10-letter word if you find one (30 + 10 = 40).
  • Trivia: 6 solved, 2 skipped, hint used: 30 points. A player with 6 solved and 1 skip ranks above you.
Rules

Hints never cost points. Skips only break ties, never subtract. Count in fives and threes so a rule fits on a card. Rule changes apply from the day they ship; banked points stay as scored (3.23.2 applies from 2026-10-10; 3.23.7's Longest word goal from 2026-10-12).


21  Patterns

Question bank & rotation

197 questions in three scopes: the original 96 shared by daily and rooms, 50 daily-only and 51 room-only. Questions come only from Ru's own lists, filtered to one-word answers, no numbers, kid-safe and clear (see trivia/question-requirements.md).

WhereHow
Dailysame for everyone, so it can't use your history. Each level walks a fixed shuffle and steps forward daily by what a strong run uses (Easy 2, Intermediate 6, Difficult 8). Today and tomorrow never overlap; a question returns only after its level has gone round
Trivia modeeach phone counts every question it has met (ow_tq_counts). Unseen first, then least-seen; a question met 3 times waits until fewer than 8 in its level remain under the cap, then the level starts over. Rooms: the host deals from their counts and the list rides on the room (tseq)
order in a runMixed climbs: questions 1–2 Easy, 3–5 Intermediate, then mostly Difficult with every third Intermediate
addingnew questions append to the bank (existing refs never move) and join both rotations automatically

22  Standards

The rules, short

  1. Never hardcode a color — always a token, so it survives a mode flip.
  2. Type on --earned / --danger fills is --ground. Never white or black.
  3. Green means positive; rose means negative. One meaning each.
  4. Green never marks a selection. A shown or traced word, and its active pill, use the pressed-tile state.
  5. An earned/danger element carries the hue on fill and border — never a coloured fill with a neutral --rule stroke.
  6. Pills are for one live value. Recaps use weight and ink — not pills, not green.
  7. One primary per screen; everything else is an outline. No third style.
  8. Icon buttons are 44×44 / radius 12 / --fg-soft — modal close included, no exceptions.
  9. Three elevation levels: ground, raise, lit. Depth from the tile lip, not shadows.
  10. Radius: 12 (most), 999 (pills), 14 (tiles). Past these only with a reason.
  11. Tiles never press-dip. Trace hidden on the game board; name board excepted.
  12. Respect prefers-reduced-motion on anything that moves.
  13. The crown means one thing: won last month. Never decoration, never during a round.
  14. 1st, 2nd and 3rd are solid, traced and lit tiles. Never gold, silver and bronze.
  15. A code is shown as tiles and copied by tapping it. Text buttons never carry icons.
  16. One ask per visit, only on Home, never mid-round. "Not now" is respected.
  17. Every save path has a fallback, and the long code always still works.
  18. The update reel uses the app's real pieces, names exactly what's new, and closes quietly.
  19. Size motion to the moment: small for common, a story only for rare.

23  Standards

Do & don't

The rules above, shown. Each pair is a real component rendered right and wrong — the reason matters more than the rule, because it's what lets you judge the cases this doc doesn't cover.

Do
SPARROW
Green fill, green border. The edge carries the state too.
Don't
SPARROW
Hue fill with a neutral --rule stroke reads as a mistake up close and mutes the signal.
Do
128
A pill holds one live value — a score, a clock. It signals "this is changing."
Don't
Longest word
A static label in a pill makes people wait for a number that never comes. Labels are the eyebrow's job.
Do
One primary per screen — and it's the action people take most. Most players join, so Join is filled and Create is the outline.
Don't
Two primaries is no primary — they compete, and the screen loses its answer to "what now?"
Do
PLOVER
Green appears at the moment a word scores — that's the only thing it means.
Don't
14 words found
Green in a recap stops meaning "earned" and starts meaning "green." Recaps use weight and ink.
Do
MBUIburgouts
Showing a word uses the pressed-tile state, on the tiles and on the pill. It's a selection, not a result.
Don't
MBUIburgouts
A green path or pill says "someone found this." Nobody has; the player just tapped to look.

24  Standards

Accessibility

Target: WCAG 2.1 AA — 4.5:1 for body text, 3:1 for large text (≥24px, or ≥19px bold) and non-text UI. Measured from the tokens, not asserted; the failures are disclosed below, not hidden.

PairDarkLightTarget
--fg on --ground12.70 ✓12.70 ✓4.5 body
--fg-soft on --ground6.36 ✓4.78 ✓4.5 body
--fg-faint on --ground3.19 ✕2.29 ✕4.5 body
--fg-faint on --raise2.55 ✕2.48 ✕4.5 body
--fg-soft on --raise5.07 ✓5.17 ✓4.5 body
--ground on --earned8.98 ✓5.71 ✓4.5 body
--ground on --danger8.52 ✓7.54 ✓4.5 body
--focus vs --ground5.27 ✓5.38 ✓3.0 non-text
--rule vs --ground1.65 —1.30 —3.0 non-text
Known contrast deviation · --fg-faint

--fg-faint is the faintest tertiary ink (fig captions, hints like "tap word to show on board"). It does not meet 4.5:1 in either mode, and in light mode misses even the 3:1 large-text floor. It's a real gap, flagged not buried — the fix is to darken --dk-fg-faint / --lt-fg-faint until both clear 4.5:1, then re-verify. --rule (hairlines) sits below 3:1 by design — decorative, never load-bearing, and marked "—" not "✕".

  • Hit targets — 44px on every interactive element, the modal close included. No exceptions.
  • Focus — always-visible :focus-visible ring, 2px --gold (=--fg), offset 2px; the home/mode icon button rings in --focus. Passes 3:1 non-text (5.3:1).
  • Keyboard & AT — known gap. Spelling a word is a pointer-driven swipe across tiles; there is no keyboard or assistive-tech path to trace a word today. It's a real limitation for a swipe-native game, disclosed here rather than omitted; a keyboard model (arrow-to-move, space-to-select) is the open item.
  • Reduced motion — honored throughout; the mark and tiles hold still under prefers-reduced-motion.
  • Live numbers — tabular-nums so they don't shift width as they tick.

25  Standards

Voice & content

Plain verbs, sentence case, active voice. A control says what happens and keeps the same name through the flow. Errors don't apologise and name the fix; empty states invite the next step rather than set a mood. The shipped examples live in System states.

Instead ofOutword says
"Error: room capacity exceeded"That room is full. 12 players max.
"Submit"Create game
"No data"Nobody's scored yet this month
"Network error"No connection. Playing solo works anyway…
"Invalid input"Room codes are 4 letters.
One name per thing · a gap to close

The product currently mixes game and room: the button reads "Create game," its confirm reads "Create room," and errors use both ("That game already started" · "That room is full"). That breaks the rule above. Pick one mapping — room for the shared space, game for a single round — and use it end to end. This is a product fix, flagged here so the doc doesn't paper over it.


26  System

Governance

The system has one owner and one source of truth. That's not a limitation of being small — it's the thing that keeps two years of changes from turning into a pile of one-off exceptions.

Source of truth
app.jsx at its current APP_VERSION. This document is generated from it; where they disagree, the code wins and the doc is the bug. The built app.js is output — never edited by hand except for a verified one-value string change.
Add vs. reuse
Reuse first. A pattern earns a component only once it appears in roughly three places; before that it stays local. A new token or component needs a name, a one-line reason, and an entry here — no silent additions.
Naming
Tokens are semantic, not literal — --earned, not --green; --ground, not --navy. The name says the job, so the value can change without the name lying.
Versioning
Minor for features, patch for fixes. Every release bumps four things together — APP_VERSION, app.js?v=, sw.js VERSION, and CHANGELOG.md — or the service worker serves a stale shell and the change never ships. The design system's version moves with them (this page's badge, the README title, the spec's source line), and npm run check stops the release until all of them match.
Deprecation
Nothing is deleted out from under a caller. Old names resolve to canonical through the alias layer and are marked "don't add new ones"; superseded artifacts are labelled stale rather than removed until nothing points at them. The old root design-system.html went in 3.23.6, once nothing did.
Review
One change per release, and a visual mock is approved before any UI code is written. Unrelated changes never ride along unflagged.

Recent changes

3.25.7
One round, one clock: in a room, a phone that sees the start late joins the round in progress and ends with everyone; a phone still playing when the tally is out stops.
3.25.6
Every part of the month bars shows its points: a part is never narrower than its number plus 4px a side. Before, a thin part left its number off, so on a 375-wide phone Longest word's points were missing on most days.
3.25.5
Trivia answers open their question: tap an answer on the summary or in room results and it replays on the board it was on, with the daily board leaderboard's board and trace.
3.25.4
Coming back with a code: Paste under the code tiles, and the field over them is see-through instead of invisible; I have a code skips the tutorial. A sheet with no foot clears the home indicator, and sheets sit on top of a keyboard. The code tiles are 38px under 360px.
3.25.3
Trivia's part of the month bars is an outline of --fg, not --fg-faint: the three games are easier to tell apart, and its numbers read at 12.7:1. How points work starts empty.
3.25.2
How points work, said once and shown: each game says its rule in one line, and tapping a game plays it on a small board with the points it earns; before a tap, and under reduced motion, each board shows how it ends. The Daily board keeps a point a letter after Boggle's and Word Hunt's scoring were weighed; a score stays final when you finish.
3.25.1
October's "+744 from Oct 1–9" note is gone, to cut noise. How points work: a text button under the leaderboard's title, in place of "Points add up from every Compete board.", opens the three games' rules with an example each, written from the scoring constants. Longest streak leaves the records.
3.25.0
Stats on the month board: every opened row says the same things in order. Your row: the games, October's note in place of the "earlier" cell, your last days with each part's points on its bar. Every row: the month as a mark a day, then Best Daily board, Longest word found first, Best Trivia run (new) and Longest streak. Words found and Party games won are cut. A streak under a name ends when a day is missed.
3.24.0
Your month, by game: your own row on the month board opens on the month's points from each game and your last five days as bars, split in the inks of --fg; tap a day for its numbers. Rebuilt from each day's results, nothing new stored. Home's half cards fit 320px screens: smaller slots and place under 360px.
3.23.7
Longest word has one goal from start to finish: the board's headline word, the length on the card and above the board. Any word that long or longer wins and banks the +10; the win used to be judged against the full dictionary's longest word, a rare word on about half of days. Scoring is written down in full (word points, rooms, each daily game, the month, worked examples).
3.23.6
Trivia question card: the question sits centred in a fixed-size card (--raise, --rule, 14px corners) as tall as the run's longest question, so the board never moves between questions. The design system moved into the repo (design-system/): css/ and trivia/ are generated from app.jsx and checked on every release, and the stale root design-system.html was removed. See audits/.
3.23.4–5
3.23.4 reserved the question's space with the question at the top (a gap under short questions); 3.23.5, preview only, put it at the bottom (the gap moved under the timer). Both replaced by the card.
3.23.3
Hint tag centred (6px every side) with a 2px gap to the word. Confetti name tiles are board tiles scaled down. Question rotation: Trivia mode caps repeats at 3 per phone; the daily walks the whole bank.
3.23.2
Home trivia card shows your place today (option K), not a lock. Scoring rebalanced: Longest 3 per letter +10, Trivia 5 per answer. Swipe reads the live trace; whole-tile first touch and lift. Daily trivia: same questions for everyone, countdown, "You solved the most".
3.23
Trivia becomes an unlimited 2-minute run (one hint, free skips, most solved). Trivia mode replaces Survival in Play, with a difficulty toggle. Compete card arrows top-right.
3.22
Home refresh: the Compete bento (leaderboard row, Daily board, Longest + Trivia). Daily trivia. The update reel becomes a story on real screens, exported from the canvas. Podium confetti at the earned moment.
3.21.1
Green never marks a selection. A word shown on a review board (the longest-on-board word included) lights in the pressed-tile state with the normal trace, and active word pills flip like a pressed tile. The update reel is keyed to REEL_VERSION so patches never replay it.
3.21
Short recovery codes (six characters as tiles, Save to Photos or Notes, fallbacks for every path), the save nudges (outlined key with a dot, Keep your spot, home-screen guide, Played before?), the update reel format, and the first motion vocabulary. Then formalised: motion principles, eleven motion rules, a reduced-motion table and a live motion lab; the update reel template (open, chapters of four beats, recap close), its chapter spec, copy template and pre-ship checklist.
3.20
Monthly podium on the 1st (solid, traced, lit columns; the crown drops on 1st; trace-ribbon confetti for everyone, tuned per viewer) and the crown all month on last month's winner. New token --raise-solid.
3.18
Recovery codes (readable word-word-id, view/restore sheet reached from the header key icon). The help sheet stays two sections — Playing, Competing.
3.14.6
Brand-mark motion → curated rotation of twelve legal traces (was four).
3.14.1
Name-board letters recede, so its trace line stays visible while the game board's is hidden.
3.12
Game-board trace line hidden — filled tiles carry the word; feedback moved to the pill.

27  System

Deliberately omitted

What the system refuses to build is as load-bearing as what it does. Each omission traces to a principle; adding any of these would cost more in clarity than it returns.

  • Nothemes beyond the two grounds
    Light and dark already cover it. A third theme is surface area with no job — and every extra theme is another set of contrast pairs to keep honest. (Principle 3, 4)
  • Noshadow / elevation stack
    Depth comes from the tile's inset lip and three flat surface levels — ground, raise, lit. Stacked shadows would add rendering cost and visual noise for a hierarchy that's already legible. (Principle 3, 4)
  • Nofourth hue
    Two semantic hues (earned, danger) plus one system hue (focus) is the whole palette. A genuinely new hue would start diluting meaning and break "one hue, one meaning." (Principle 2, 4)
  • Nogold, silver or bronze
    The top three are told apart by the board's own tile states (solid, traced, lit), not by three metal colors that would each be a new hue with one job. (Principle 2)
  • Noicon library
    A handful of 20px stroked glyphs, drawn to one spec. More icons means more to misread and more to keep consistent, for a game that needs almost none. (Principle 4)
  • Nocomponent-count target
    A small, opinionated set beats a large generic one. Growth is demand-driven (the rule of three), never a roadmap goal. (Principle 4)
  • Noanimated mode switch
    Changing mode is a quiet 0.5s cross-fade, not a moment. Celebrating it would contradict low stimulation. (Principle 3)
This is a point of view, not a parts bin

Like the systems it's modelled on, this one isn't a reason to make every screen identical, and it isn't a substitute for product thinking. It removes the small decisions so the real ones get the attention.


28  System

Reference

Each active role resolves, per mode, to a value in the base tier (--dk-* / --lt-*) — the only hardcoded hexes in the system. The table shows those resolved values: 29 roles × 2 modes = 58 base literals. Components reference the active role, never a literal. Only colour is tokenised; durations, spacing and radii are raw constants (see those sections). Legacy aliases resolve to canonical in one place, so nothing breaks — prefer canonical names in new work.

TokenDarkLightRole
--ground#202845#F2F0EAthe page
--raisefg .08#FAF9F6cards, fields, rows
--raise-solid#313852#FAF9F6opaque twin of --raise · the crown's halo (3.20)
--fg#F2F0EA#202845primary text / fill
--fg-soft#A5ABC4#5F6987secondary text
--fg-faint#6C7597#98A0B8tertiary text
--rulefg .17ink .14hairlines, outlines
--rule-strongfg .32ink .30emphasised borders
--tile#F2F0EA#202845resting tile face
--tile-ink#202845#F2F0EAletter on resting tile
--die-edge#7E828F#9496A0tile edge detail
--select#202845#F2F0EAselected tile fill
--select-ink#F2F0EA#202845letter on selected tile
--select-strokefg .5ink .5selected tile border
--lit#3D445C#D5D4D3lit surface (non-tile)
--lit-ink#F2F0EA#202845ink on lit surface
--lit-edge#2D354F#E5E3E0lit edge detail
--trace#F2F0EA#202845connecting trace line
--earned#7BE0A3#276A43positive — valid / win
--danger#F2B8BC#8E2430negative — invalid / error
--focus#7E9BE0#2F58D0focus ring
--earned-washearned .12earned .10green tint bg
--danger-washdanger .14danger .10rose tint bg
--earned-card-bgearned .06#E6EEE3earned card fill
--earned-card-borderearned .45#9FC2A9earned card border
--mark-dot#9698A0#686D7Dbrand-mark lattice dots
--shadow#0A0E22 .38ink .16drop shadow
--shadow-lift#0A0E22 .28ink .20button lift shadow
--scrim#0A0E22 .72ink .55modal scrim
Legacy aliases — resolve to canonical, don't add new ones

--tray→--ground · --tray-raise→--raise · --tray-line→--rule · --die→--tile · --ink→--tile-ink · --paper→--fg · --paper-dim→--fg-soft · --gold→--fg · --ok-green→--earned · --bad-fill→--danger · --cta→--fg · --urgent→--danger · --error-surface→--danger-wash · --error-border→--danger · --error-text→--danger · --valid→--fg