Technical guide

Design

Tokens and layout

The numbers every game shares (space, type, the page’s frame, buttons, motion) and the color jobs each game maps its own palette onto, read live from the stylesheets.

Checked against the code on 2026-10-31.

On this page
  1. Space: one rhythm
  2. The page’s frame
    1. Safe areas and the app
  3. Type roles
  4. Buttons, panels and sheets
  5. What check:layout holds
  6. Motion
  7. Color jobs
    1. The paper
    2. Choosing isn’t doing

A token is a named value in CSS, a custom property such as --gutter or --ink, that a page’s rules read instead of writing a number or a color themselves. Tokens come in two kinds. The house’s set sizes, spacing and motion, and they’re the same in every game. A game can’t change them. The look’s set colors, faces and corners, and each game declares its own, mapped onto the same jobs.

Every value in this chapter is read from the stylesheets as the page is drawn, so it’s always the one in use.

Space: one rhythm#

Every root declares the same five spaces (One skeleton). Here they are as the hub declares them:

TokenValue
--s14px
--s28px
--s316px
--s424px
--s540px
Live from static/css/site.css

--s2 goes inside a group, --s3 between rows and groups, --s4 between sections and --s5 around a page. npm run check:tokens fails any stylesheet that gives one of them another value.

The page’s frame#

The layout tokens are declared once, in games/static/games/house/house.css, and every page on the house kit inherits them. No other stylesheet may set one.

TokenComputerPhone (640px or narrower)Phone on its side
--guttermax(24px, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))max(16px, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))
--frame10px0px
--bar-h44px30px
--bar-top12px8px
--bar-gap24px16px
--page-max960px
--page-max-wide1180px
Live from games/static/games/house/house.css
  • --gutter is where the page’s words and controls start, from each side of the window. It grows to clear a phone’s notch where there is one (Every screen fits).
  • --frame is the margin round a framed table (the felt), inside the gutter. On a phone the felt is the screen.
  • --bar-h, --bar-top and --bar-gap are the top bar’s height, its distance from the top, and the room under it on a home or lobby.
  • --page-max is a home’s or lobby’s column. --page-max-wide is for pages of panels side by side, such as a waiting room.

Safe areas and the app#

Every page asks for viewport-fit=cover, so on a notched phone on its side the page may run to the glass, and keeps clear of the notch, the rounded corners and the home bar itself (ADR 0029):

  • --gutter is the larger of its size and the side insets, so everything drawn from it clears the notch with nothing to remember.
  • A dock or a foot at the bottom pads by the larger of its padding and the bottom inset; a bar or a felt that fills a side pads by the side insets the same way.
  • The app’s status bar is black and opaque on every page, since the pages run from the cream paper to the dark felt, and a translucent bar would lose its clock on the paper.
  • In the app, the arrow at the top left is the only way home, so every screen has it, and nothing links outside the site.

Type roles#

Each role has one size, weight and line height (Type roles). A game picks the faces; the sizes are the house’s.

Role Size Weight Line height Used for
Display --t-display (the family’s root sets it; check:layout holds it, below) the game’s --display-weight 1.1 The page’s one title
Title 28px display A sheet’s title
Heading 20px display 1.2 A section’s or panel’s heading
Bar 17px (15px on a phone) display What you’re playing, in the bar
Body 16px 400 1.45 Text
Lead 16px 400 1.45 The line under a title, in soft ink
Label 16px 600 1.45 Names a control or a value
Tool 15px 700 The bar’s quiet tools
Small 14px A small button’s words
Note 14px 400 1.4 Hints, captions, a status line
Kicker 12px 700 Over a title, upper case, letter-spaced .14em
Figure 20px (18px on a phone) 800 The glance’s numbers: the countdown and the score, in tabular figures

The Heading, Body, Label and Note sizes are declared by every root (here, the hub’s), and check:tokens holds them all to one value. The rest are house.css’s alone.

Fine print at 12–13px is only for table furniture (Readable).

Buttons, panels and sheets#

TokenComputerPhone400px or narrower560px or narrowerTouch screen
--btn-h44px
--btn-pad16px12px
--tool-h36px44px
--tool-pad10px8px
--panel-pad16px
--panel-gap16px
--panel-radius12px
--sheet-pad24px16px
Live from games/static/games/house/house.css

--btn-h and --btn-pad are a move’s or a page’s button. --tool-h and --tool-pad are a quiet tool in the bar or a dock’s small button, a finger’s height on a touch screen. --panel-radius is the one layout token a game may set: corners are personality.

What check:layout holds#

npm run check:layout measures the pages as drawn, in a browser, against these numbers. That includes the page’s one title, whose size each family’s root sets but every family keeps to the same two sizes:

tools/house/layout.mjsRULES · lines 40–61
export const RULES = {
  gutter: w => (phone(w) ? 16 : 24),
  barHeight: 44,
  type: {
    title: { size: w => (phone(w) ? 32 : 44) },
    barTitle: { size: w => (phone(w) ? 15 : 17) },
    section: { size: () => 20 },
    lead: { size: () => 16 },
    column: { size: () => 16, name: 'body' },
    note: { size: () => 14 },
    label: { size: () => 16, weight: () => 600 },
    kicker: { size: () => 12, weight: () => 700 },
    tool: { size: () => 15, weight: () => 700 },
  },
  // The way up, top left of every bar (house/nav.js): a finger square, one
  // per screen, at the bar's start.
  back: { size: 44 },
  button: { height: 44, padX: w => (phone(w) ? 12 : 16) },
  tool: { height: w => (w < 768 ? 44 : 36) },
  panelPad: 16,
  sheet: { pad: w => (w <= 560 ? 16 : 24) },
};

The Register’s masthead is the one title off the scale, listed in EXCEPTIONS beside it.

Motion#

Things move from one vocabulary of named motions (Motion explains), each a token here and the same number in MOTION in games/static/games/house/motion.js. npm run check:house fails if the two disagree. Under reduced motion, everything that travels takes no time, and short fades stay.

TokenValueReduced motion
--m-press120ms
--m-lift150ms
--m-quick150ms
--m-glide250ms0ms
--m-stagger22ms0ms
--m-reflow220ms0ms
--m-flight380ms0ms
--m-flight-long580ms0ms
--m-deal250ms0ms
--m-deal-gap40ms0ms
--m-flip240ms0ms
--m-sweep-hold900ms
--m-sweep-hold-relaxed1350ms
--m-sweep350ms0ms
--m-arrive350ms0ms
--m-pop200ms0ms
--m-nope360ms0ms
--m-pulse1200ms
--m-sheet-in150ms
--m-sheet-out150ms
--m-banner2000ms
--m-announce2000ms
--m-toast4500ms
--m-toast-action6000ms
--m-splash3400ms
--m-prompt2600ms
--m-beckon1600ms
--m-beckon-idle3000ms
--m-celebrate1000ms0ms
--m-hold1200ms
--m-hold-key2000ms
--m-catch-up1500ms
--m-emote3200ms
Live from games/static/games/house/house.css
Motion For
press, lift A button going down; a box or card lifting on hover
glide, stagger, reflow Your own move, place to place; a run’s stagger; a hand closing up
flight, flight-long Someone else’s (or a helper’s) move across the table
deal, deal-gap Cards from the dealer to each place
flip A card turning over, through its edge
sweep-hold, sweep A finished trick held, then gathered to its winner
arrive, pop, nope, pulse A card joining your hand; a mark placed; a refused move; a place that just changed
sheet-in, sheet-out, toast, toast-action Sheets and toasts coming and going, and how long a toast stays
banner, announce, splash, prompt A game moment; a moment everyone must see; a new hand; something new that’s yours to do
beckon, beckon-idle Your move’s ring, and how long a routine turn waits before it
celebrate A solved puzzle settling
hold, hold-key, catch-up How long each move by someone else holds, a deciding one longer; when your turn catches up
emote A reaction rising up the screen

Color jobs#

The house has six color jobs (Colors have jobs). Every look maps its palette onto them, under its own kit’s names. The names differ by kit, but the jobs don’t, and the house kit reads either through its fallbacks (below).

Job Means The room kit and card room The paper (the hub, this guide, the puzzles) The house kit reads
Ink Words, outlined buttons --ink --ink --house-ink
Soft Supporting words, 4.5:1 on its surface --soft --soft --house-soft
Accent The one primary fill --btn (its words --btn-ink) --accent (its words --accent-ink), each puzzle’s own the button classes a game gives configure()
Turn Yours, your turn, picked --turn (its words --turn-ink) --pick --house-turn
Good Solved, done --good --good
Bad A mistake, a danger --bad --bad

The surfaces and faces line up the same way:

What The room kit and card room The paper The house kit reads
A sheet’s or card’s ground --sheet --paper-2 --house-surface
A container’s edge --rail, --panel-edge --paper-edge --house-edge
The words’ face --ui-font --ui --house-font
The display face --display --display --house-display

A game whose names differ declares the --house-* ones itself (the puzzle kit and the hub do). Every name the house kit reads falls back to the room’s, so a table game declares nothing extra:

games/static/games/house/house.cssfallbacks · lines 131–149
:where(.house-scrim, .house-toasts, .house-banner, .house-splash, .house-announce, .house-cheer, .house-options, .house-section, .house-setting, .house-seg) {
  --_surface: var(--house-surface, var(--sheet));
  --_ink: var(--house-ink, var(--ink));
  --_soft: var(--house-soft, var(--soft));
  --_edge: var(--house-edge, var(--rail));
  --_radius: var(--house-radius, var(--rail-radius, 14px));
  --_line: var(--house-line, color-mix(in srgb, var(--_ink) 14%, transparent));
  --_field-edge: var(--house-field-edge, var(--field-edge, 1.5px solid color-mix(in srgb, currentColor 50%, transparent)));
  --_btn-radius: var(--house-btn-radius, var(--btn-radius, 8px));
  --_display: var(--house-display, var(--display));
  --_display-weight: var(--house-display-weight, var(--display-weight, 400));
  --_font: var(--house-font, var(--ui-font));
  --_focus: var(--house-focus, var(--focus, var(--_ink)));
  --_turn: var(--house-turn, var(--turn, var(--_ink)));
  --_turn-ink: var(--house-turn-ink, var(--turn-ink, var(--_surface)));
  --_toast-edge: var(--house-toast-edge, var(--chip-edge, var(--_edge)));
  --_scrim: var(--house-scrim, rgba(0, 0, 0, .55));
  --_shadow: var(--house-shadow, 0 24px 64px rgba(0, 0, 0, .45));
}

The paper#

The hub, this guide and the pencil puzzles are printed on one palette, declared once and never set again (ADR 0036). It stays light whatever the reader’s dark-mode setting.

TokenValue
--paper#f4efe3
--paper-2#fffdf7
--paper-edge#ddd5c3
--desk#e9e2d0
--ink#22262b
--soft#5b5f66
--line#c9c1ae
--line-bold#22262b
--good#2f7a57
--bad#b5372c
Live from games/static/games/house/paper.css

Choosing isn’t doing#

Every root declares how a pick looks, drawn in the color of the words around it, so a pick never wears the fill (Choosing isn’t doing). On the paper:

TokenValue
--pick-tintcolor-mix(in srgb, currentColor 9%, transparent)
--pick-ringinset 0 0 0 2px currentColor
--pick-weight700
Live from games/static/games/puzzles/kit/puzzles.css

The card room’s tint is a little stronger, for its darker felt:

TokenValue
--pick-tintcolor-mix(in srgb, currentColor 16%, transparent)
--pick-ringinset 0 0 0 2px currentColor
--pick-weight700
Live from games/static/games/cards/themes.css