Technical guide

Design

Fine details

The smallest rules, measured in a real browser on every screen of every game: a glyph in the middle of its ring, nothing over anything, no words cut off, room to breathe, a finger’s reach, and words you can read.

Checked against the code on 2026-10-31.

On this page
  1. Centered by ink
  2. Nothing over anything, and words that fit
  3. Room to breathe
  4. Rows in line, and alike looks alike
  5. Readable
  6. A finger’s reach
  7. How it’s measured

A screen can follow every big rule and still feel wonky: a number a pixel high in its ring, a label touching the board, a button a finger can’t quite hit. David’s playtest found exactly that (“circled alphanumeric characters can sometimes be off-center”), so the finest details are rules too (ADR 0033). This chapter is how each one is met, and how npm run check:details measures it. The rules themselves are in Principles.

Centered by ink#

A glyph in a box sits in its middle by its own ink, not its line box: within 0.75px, or 4% of the box’s smaller side, across and down (Centered by ink). Words in a button or a pill are centered across by their ink and down by their capitals, so a descender or a lowercase word doesn’t pull them.

Why boxes miss. A face’s ascent and descent are rarely even (in a centered line, Nunito’s capitals sit .03em high, Courier Prime’s .07em). A symbol (✕ ★ ‹ …) comes from a fallback font with metrics of its own. dominant-baseline: central centers the em box, not the capitals, and Safari and Chrome put it in different places. A hand nudge right for one face is wrong for the next.

How to center one:

  • Icons are drawn, not typed. Close is closeIcon(), the way up backArrow(), and a game’s arrows and glyph buttons are SVG on a 24px grid.
  • A glyph in a box takes .house-glyph (in house.css): text-box: trim-both cap alphabetic trims the line to its capitals, so equal space above and below is equal space. Put it on the box itself when that’s a block, an inline-block or a button, never on a flex or grid box, whose words sit in an anonymous box the trim doesn’t reach.
  • Key caps are trimmed the same way and padded evenly.
  • A number or a letter in an SVG ring is set with text-anchor: middle on the default baseline and dy="0.35em" (the puzzle kit’s MID), half a capital below the middle, in any face and any engine. A shape’s middle is its fill’s: a triangle’s and a drop’s are low.
  • A glyph that isn’t the middle of its own advance (a bracket in a monospace face) is an exception, with why. A dot on the baseline is the wrong glyph for a middle: ⋯, not ….
  • Nothing hidden keeps space on one side. A key cap stays on a disabled button, dimmed with it; on a touch screen caps take no space.

Nothing over anything, and words that fit#

  • Nothing draws over anything else but what’s meant to: a badge on a corner (the dealer’s chip), cards in a hand or a pile, a sticky column over the rows that scroll under it, a chessboard’s coordinates under its pieces. A layer that passes (a toast, a banner, a splash) isn’t an overlap.
  • Words aren’t cut off by a box that clips them. An ellipsis is for a name in a seat, a long title in a bar, or a dock’s line kept to its two lines so the moves under it never jump; never for something you need to choose.
  • Words stay inside the box they’re drawn in (a card, a panel, a button, a chip) even where nothing clips them. A line kept on one line that’s wider than its card is a bug: let it wrap, or let what sits beside it go under it. A word too long for its share of a choice row gets more of the row.
  • Nothing runs off the window’s side.

Room to breathe#

Not overlapping isn’t enough. Words keep at least 8px (--s2) from any edge, line or mark that isn’t theirs (a board’s spaces, a ring, a card’s edge, the next control), and 4px from their own box’s edge. Words in a round shape keep inside the shape, not only its box: a column of words in a circle is no wider at its top and foot than the circle is there (Room to breathe).

Rows in line, and alike looks alike#

  • Things in a row share a middle (boxes) or a baseline (words), within a pixel. Alike things in a row are evenly spaced; a group set apart on purpose is an exception, with why.
  • Things the code gives one role (one class, in one group: a dock’s moves, a choice’s options, a row of seats) look alike within a pixel: one height, one fill, one weight, one type size, and, where they share a row as equals, one width (Look follows meaning). A row of equals takes the widest one’s width for each, or shares the row evenly.
  • A difference that’s meant comes from a class that says so (.main isn’t one: it names a slot’s place, not a meaning). A state (.on, .mine, a seat’s color) may change a fill or a weight. Only an emphasis (.primary, .alt, .danger) may change a width. Nothing changes a height or a type size.

Readable#

  • Text reads at 4.5:1 against what’s actually behind it, as painted: the felt, the velvet, a painting, a gradient, not only the surface its token was checked on. Large text (24px, or 19px bold) and a control’s outline read at 3:1 (Readable).
  • Words over a picture or a texture get a ground: a scrim, a plate, a shadow that makes the 4.5:1.
  • No words under 12px, and 12–13px only for table furniture. Words that shrink to fit stop at 12px and wrap or shorten instead.
  • Words drawn on a board scale with it. On a phone they’re as large as their space allows, never smaller than they need to be, and zoom reads the rest; they aren’t hidden behind a tap (David’s call, October 30, 2026). A seat mark’s letter is as large as its shape allows.

A finger’s reach#

  • On a touch screen everything you press is at least 44px each way (A finger’s reach). Make it that big, or, where it’s drawn smaller to fit (a link in a line, a compact switch), give it .house-reach: an invisible ::before, 44px round its middle.
  • A reach is measured from the control itself, so the control must be positioned (a rule that starts from all: unset puts it back to static, and its reach then spans whatever positioned box holds it). A box that clips clips its reach too: use overflow: clip with an overflow-clip-margin, or, in Safari, pad the control and give the padding back with a negative margin.
  • Every press inside a control is that control’s. Nothing you can’t see (a reach, a pseudo-element) answers in its place.
  • A board’s cells, a hand’s cards and a chessboard’s squares are as big as the table allows, and are listed as exceptions.

How it’s measured#

npm run check:details (with the site running at UO_URL) opens every game’s screens: each home with its How to play and Options, a table game’s lobby and waiting room and its table mid-hand against bots, a puzzle mid-solve and in its lit, met, wrong and chosen looks, every result sheet, the Dailies page, the Stats tabs and the account pages. It opens each at seven sizes:

tools/house/details.mjsSIZES · line 46
export const SIZES = ['1440x900', '1280x800', '1920x1080', '390x844', '375x812', '320x568', '844x390'];

In each, tools/house/inspect.js runs a window at a time down the page or the open sheet, and finds each kind of problem: center and icon (a glyph off its middle), overlap, clip, overflow, offscreen, align (rows out of line), tap, taken and stray (a finger’s reach, and a press answered by something else), alike, room and readable. Two kinds still only report, until what’s left of the October 30 audit is fixed or allowed (--strict makes them fail):

tools/house/details.mjsREPORT_ONLY · lines 60–65
// Two of the newer kinds (room, readable: docs/guide/design/fine-details.md,
// "Room to breathe" and "Readable") are report-only until what's left of the audit that
// introduced them (docs/audits/2026-10-30-alike-room-readable.md) is fixed or allowed:
// printed and written to --report, but they don't fail the run. --strict
// makes them fail. Alike fails since the audit's fixes (October 30, 2026).
export const REPORT_ONLY = new Set(['room', 'readable']);

A glyph is also measured as painted (tools/house/ink.mjs): each glyph in a ring, badge or box is painted magenta, the window photographed, and the magenta’s box compared with the middle of what it sits in, within a pixel’s snap. The other checks believe each engine’s own boxes, and engines disagree about where an SVG text is; pixels don’t.

Readable is measured as painted too: every word hidden at once, the window photographed, and each word’s color laid over the pixels under it. The tenth percentile must reach the contrast, so words on a painting are as readable as their worst part. Words on a disabled control or a dimmed space are left out, as named states.

npm run check:details:webkit runs the same in Safari’s engine, as an iPhone. npm run check:details:self plants each kind of mistake (a glyph set high or low in each kind of ring, Auction House’s two old bugs) beside a right one, in both engines, to show the check still finds them.

A finding that’s meant goes in ALLOW in tools/house/details.mjs, with why (Exceptions). Anything else fails. --shots dir crops each finding, --frames dir saves each whole window, and --report file writes the table. Testing and checks has how to run it.