# Composition grammar

Which shape carries which meaning. The numbers are [constitution.md](./constitution.md) and this
file never restates one; which entry answers which role is [catalog.md](./catalog.md) and this file
never lists entries. What is here is the law a screen can break while every test passes.

**A rule that prevents a known HARM binds** — a page scrolling sideways on a phone, an act that
cannot be undone, a figure typed where it should be computed, a machine's guess rendered like a
person's decision, a control a keyboard cannot reach. **A rule that picks among valid FORMS
advises**: the space of jobs is unbounded and the best answer is regularly one no document named.
[reviewing.md](./reviewing.md) is how you find out whether a screen you built holds these.

## The form comes before the treatment

**If the domain already has a standard shape, that is the answer — build it.** A parcel's journey,
a ledger, a calendar, a seating chart, a boarding pass: each is a form the reader learned
elsewhere, so matching it means they arrive knowing how to read the screen. Look at how the
industry does it first.

**Name what the subject IS, then reach for parts.** A set of records is a register; a thing moving
through ordered places is a rail; work assigned to finite resources over time is a board; a
sequence of events is a timeline; a population whose point is its deviant minority is a queue
sorted by deviation. Getting this wrong is not recoverable later — a register of objects that
should have been a rail says nothing about order: a status column reports one word and leaves the
reader to know which word comes after which.

**A RUN'S SPINE IS ONE COMPOUND, and it is the kit's.** A rail through a list — a trip's stops, a
delivery round — is a `Stepper`: one node per item, centred on what that item LEADS with, the
group's head its own first rung, the line unbroken between them. Never dots beside a band: a node
placed off the ITEM floats mid-air the moment a name wraps. **The air goes BETWEEN the items, never
inside them** — the peers rung — because the rail reaches the next node through it.

**A NODE IS A POSITION, and it SAYS which one.** A run that is merely ORDERED wears none of the
reporting faces — a tick ticks a journey nobody has taken — so its rung carries its ORDINAL within
the group: an unnumbered disc is a bullet nobody can name without counting the rail. A rung
CONTINUING the one above is no node, a group head takes no number, and no rung carries a hue. WHERE
an item stands is a `Status` on its line; one struck off is struck, title and figure.

**A ROW'S LEAD IS DECLARED** — a tile where the surface is TOLD to, else the title; a glyph dealt
from a category marks nothing, so the category is a word on the line. Told to, a row with nothing on
file takes the placeholder beside one on file; where none is, no row leads. **That line is FOUR runs, each WHOLE or not there**
— one the width cannot seat leaves the line, the first too; a stage or figure moved off the title
takes a line of its own — and **its box is the tallest
mark it may carry**, or its clip cuts the mark. **Every gap is a rung or nothing, and a NAMED gap is
one number on every row-shaped surface.**

**A hierarchy in the DATA does not oblige a hierarchy in the UI.** Ask what the reader DOES with
the set: a population they walk is a tree or a drill; one they scan and search is a register, the
path on the row's supporting line. The distinction is who maintains the structure — where an
importer, an integration or an agent writes it, a drill-down makes the reader navigate a shape
nobody chose.

## Canvas & content column

**Canvas**: a full-bleed scroller on the page ground around a padded box stating `flexShrink: 0` —
a content box squeezed to the viewport has nothing left to scroll.

**Content column**: one centred column, `width: 100%`, a peer rung between bands, capped
by the JOB the screen does — `PageContent size`, whose keys and widths are `PAGE_SIZES`. Unnamed,
it is a `register`; only `fullscreen` is uncapped.

**An overlay's gutter belongs to a CONTENT primitive, never to the caller.**
`DialogScrollArea` and `DrawerScrollArea` each carry their surface's inset; only the surface knows
the number. Inside a `Dialog` the responsive one is READ, never re-derived —
`var(--lotics-dialog-gutter)`, declared at the root so a band reads the same rung wherever it is
rendered: a band measuring its own box measures the wrong box, and two honest answers to one
question put a pane's rows out of line with the dialog's title.

**ANY scroller whose body is SWAPPED opens the new content at the top.** Pass `scrollKey` — the
content's identity. An ABSENT key names the root content rather than opting out, so the offset
comes back when the child closes. Without it a swapped-in record opens part-way down itself, its
heading off-screen.

**Overlays paint in the order they were OPENED, and no call site has to know it.** Mount them
wherever composition wants and toggle `open`: a kit overlay puts nothing at body level until it
opens, so open order IS DOM order. One of your own holding a body-level box while closed is
covered by anything opened later — dead to every press.

**Never wrap a scroller in a padded box.** The box insets the scroller's VIEWPORT, so the list ends
short of the surface with its last row clipped. A pinned heading above a scrolling list takes the
overlay's BARE slot and puts the gutter on the pinned part and the scroller's content container.

**A row that navigates PAINTS.** A link paints nothing, so a row tall enough to hold an identity
mark is a large silent target. Reach for the row surface, never a hand-rolled hover on the anchor:
the wash spans the WHOLE row, trailing controls included, and the anchor stays INSIDE as the door,
because a row of places to go owes middle-click and copy-link.

**ONE LEFT EDGE — the whole page, and then the whole APP.** Headings, detail labels, prose, a
table's first column, a footer's totals, a dialog's title. A list and the record it opens are one
reading column seen twice, both derived from ONE constant rather than happening to match. Two
exceptions: a self-contained overlay has nothing to align with, and an indent something VISIBLE
occupies is explained by the thing occupying it.

**A two-pane row that STACKS must drop its `flex` weights with the direction.** `flex` distributes
along the MAIN axis, so width shares become HEIGHT shares the moment a row becomes a column: the
pane that asked for less is clipped mid-content. Compute the share from the same condition that
picks the direction — and check WHICH: stacking on `small || medium` collapses the layout for
nearly every reader while measuring perfectly at the one width nobody uses.

## The heading ramp — one construct per surface

**The ramp is FIXED — the markdown ladder, no size knobs.** One outline on every surface, never
restyled per page. **A heading is WORDS, the verb beside them, and no slot for a mark**: a subject's
picture leads the surface that NAMES it.

| Rank | `Text` size | Construct |
|---|---|---|
| `#` | `xxl` semibold | Page title — `PageHeader` / `RecordHeader` |
| `##` | `xl` semibold | Section title on a PAGE — `SectionHeadingTitle` |
| `###` | `lg` semibold | Subsection title — `SubsectionHeadingTitle` |
| `####` | `md` semibold | Section title in a PANE — the SAME `SectionHeadingTitle` |
| body | `sm` | Everything else |

**A section title's rung is the SURFACE's answer, never the call site's.** The CONTAINER publishes it
and one `SectionHeadingTitle` answers: a PAGE surface docked in an overlay keeps the page rung in the
container's BARE slot; a pane you build declares its own (`HeadingSurfaceContext.Provider
value="panel"`). In a pane `####` is the FLOOR — a named group inside a pane's section is `Text
size="sm" weight="semibold"`, or `SubsectionHeadingTitle` out-ranks it.

### The add-placement law

**A section's ADD rides its heading row, right edge, at `primary`; a verb INSIDE the body is
`secondary`** — a day head's add, a line's add, moves within what it creates. The SECTION, not the
page, is where an act is primary. Two verbs for one act is the defect: where the READ kept nothing
(not a `narrowed` lens) the toolbar stands down and the act moves into the empty state. Out of
width the VERB wraps; the title never gives way. A header CHECKBOX is legible only over a COLUMN of
them.

## Bands, tabs and lenses

**A partitioning control states its band's size as a PROP**, never by counting rows itself. A
grouping control CLEARS; it never offers "no grouping".

**Only a LIFECYCLE earns a strip** — each tab a queue of work at one stage, with its own columns and
act. A kind, a book, a channel is a LENS beside the search. **A switcher's pick may be OPTIONAL,
and then it states a `placeholder`** — falling back to the first item names a subject nobody
chose.

**ONE tab strip per surface, and a `Tabs` never opens onto another `Tabs`.** The second is a filter
in navigation's clothes. Below a strip the switch is a lens or a mode. The one that may sit under a `Tabs` is `order="sequence"`: its stages change the register's
columns and act. The app's own SCREENS are outside it altogether: an app is one register and the
records it opens, so a second register is another app rather than a second rung here.

**Layout**: search and secondary filters LEFT, the primary CTA RIGHT, in ONE wrapping band. A
one-of-N lens is fine only when the band has no search; with a search present the status filter
becomes a dropdown rather than a row of pills competing with it.

**A fixed-height child of a control band** — an avatar, a swatch, a thumbnail — **is sized against
the band's CONTENT budget**, and a surface needing more than it is not a control.

## Time

**A report's comparator is DERIVED from the selected range, never chosen.** `previousPeriod` gives
the comparison; show it ON the figure. A grain control beside the date field re-adds the mode the
field removed. A period still RUNNING is compared TO DATE (`periodToDate(value, now)`), or the
comparison reads as a collapse.

**A calendar answers WHEN, and only when the position on a timeline is the question** — which
entry draws which time shape is [catalog.md](./catalog.md). A grid is unreadable in TWO directions
— too narrow or too short — so month and week fall back to the agenda on their own and nothing
wraps one in a horizontal scroller. **A time window is a viewport, never a filter**: the reader
moves it, and the events outside it still exist.

## Keyboard and focus

**A tab stop is `tabIndex`, and nothing else.** A `role` without one is announced and unreachable.

**NEVER let a FOCUSED control unmount.** Focus falls to the body, the reader's place is gone and a
screen reader goes silent. A control that disappears on its own press has to hand focus somewhere
first.

**Focus rings are per-component, never global** — `:focus-visible` on `--ring`.

## Hover grammar — intensify the control's own signature

**ONE signal per species, strong enough to carry alone.** A control changes PAINT across its
states and never GEOMETRY: a border that appears on hover, a padding that grows, a shadow that
lifts — each moves the thing under the pointer.

**A control riding a hoverable surface must be DARKER than that surface at the same state**, or
reaching the control looks like leaving the row. **A SELECTED control needs a DOUBLED dark EDGE**,
not a one-pixel border change: selection is state and has to survive being looked at from across
the screen.

## Cards, summaries and facts

**Cards are banded and composable** — a header, a body, a footer, each carrying the gutter itself.

**A card either IS a control or CONTAINS them — never both.** A `CardGrid` card is one anatomy —
picture, title, caption, `badge` then `notes`, then `figure` and facts pinned to its foot, a row's
cards one height.
**Never total counts over sets that OVERLAP** — count each set, or the union once.

**A fact band is read at a GLANCE, and a glance is WIDE** — it fills the column it sits in rather
than huddling at one end. How much it may hold, and what folds instead, is
[hierarchy.md](./hierarchy.md).

## Numbers, money, dates

**An axis over whole things has no half** — a count axis ticks in integers.

**A column states its unit ONCE — only possible if it holds ONE unit**, or a value carries its own
per row.

**A RUN of figures ends on ONE right edge** — a register's column, a ledger, a list of only amounts:
the surface's own value slot, editable or not, and an absent figure draws its mark there. **A CELL
is not a run** — under a label-over-value fact sits a date, and a meter's count belongs to its own
bar — so there the figure starts where its reading starts.

**A per-unit price is not a total**; the currency's minor unit is the wrong floor. **Every number is
a door** — it presses through to the rows behind it, a `TotalsLine` figure by its `onPress` or
`href` — **except a dashboard's `MetricStrip`**, which informs: the drill is the content below.

## No dead rows — registers & tables

**The identifier leads.** A register's FIRST column is what the reader scans against a document, an
email, or someone on the phone. A name repeats; a status is shared by dozens.

**A set of records the reader COMPARES on a value is a register**, and a list of title-plus-prose
rows cannot be one: a value inside prose cannot be scanned down a column. The tell is a superlative
badge — "cheapest", "fastest" — appearing exactly where there is no column to read the answer off.

**How MUCH the row reveals decides expand-vs-drawer.** The threshold is not a row count but
whether the neighbouring rows survive the expansion.

**A leading MARK is on every row of the run, or on none** — one only some rows carry gives the run
two left edges.

**A register centres its cells, and centring resolves PER CELL** — so a trailing control centred
against the tallest cell drops below the field it acts on. `align="top"` is the residue, for an
extra line the DATA owns.

**A CELL LINES UP WITH ITS ROW BEFORE ITS COLUMN.** Reserve a second line only where EVERY row
draws one: held open for the few that do, every value stands in a box that fills from the top. The
fix is the column's `lines`, never a cross-axis knob.

**A washed row BLEEDS its wash and never insets its content**, so its content sits on the
container's own edge. A frame bounds the bleed by CLIPPING, never by padding — padding indents
every child for a gutter only the rows use.

**A fixed `width` is sized for the LONGEST locale**; a flexible column CLAIMS WHAT IT DRAWS — state
`content` and it neither holds back width it will not use nor takes the leftover ahead of a column
that would. Unstated, it claims the register's read width and the leftover by `flex`.

**A set whose size is KNOWN is not empty — it waits as the owed rows.**

**A bar of verbs is TWO groups, not a row of peers.** Separate the bar's own STATE (the count, the
escape) from the ACTS, with a region rung at the boundary and a group rung inside each: spaced
uniformly, four controls read as four equal things.

**A paginated register owes the reader a scroll reset, and it is not the pager's to give** — the
scroller owns its offset. **ONE pager, ABOVE the rows**, on the summary row rather than one of its
own, and its range IS the register's count.

## Row actions are always-visible siblings

**Row actions are always-visible siblings — never hover-revealed, never nested.** A row that
presses open AND carries its own controls is a role-less pressable row plus a sibling door holding
the tab stop, the name and the focus ring; never one pressable wrapping the lot.

**A trailing slot is not a PEER of the row's own affordance** — space them apart, or the reader
cannot tell which the press lands on.

**"Non-interactive by construction" excludes any SLOT.** A component taking children cannot promise
what is in them, so the door is a SIBLING inside the row, never a press around it.

**A row is a button wrapped around DATA, so its text stays SELECTABLE and a drag is not a press.**
Every gesture ending on a row owes the same guard.

## Master-detail

**A detail surface never contains a register of its own SIBLINGS** — the list they came from,
printed twice. **A record is ONE PAGE: a section is a place the attention band scrolls to, never a
route swapped to.**

**A column of always-open inputs makes a list look like a form** — editing in place is a mode the
reader enters, never a register's rest. **A screen is cut by the JOB, never by the subject**, and no
side pane stands beside a LIST: a third layout to learn, when the row's own door is the drawer.

## Commit & feedback surfaces

**A form's COMMIT sits with its fields on a PAGE** — directly beneath the last input it commits, on
the same left edge. **In a DIALOG it sits in the `DialogFooter`**, at the right edge, with its
`Reset` or `Cancel` immediately to its left. A `Reset` travels with the `Save` it undoes.

**The overlay is the exception because it has a second viewport.** A panel's body is its own scroll
port, so a commit inside it is a Save below the fold; the footer is the
one band the reader can always see. The ✕ the panel already draws is the dismissal, so a `Cancel`
earns its place only where ABANDONING is a decision worth putting in words — never a second
spelling of ✕.

**The footer also carries SURFACE verbs** — Close, Done, Back.

**Nested content or a sub-form goes in a `Well`, never a `Callout`** — a callout is a message, and
a message does not contain a form: a `CalloutTitle`, what it means as a muted `CalloutDescription`
under it, flush left.

**The action-gating law: disabled, plus the reason as a co-located `Callout`.** Self-evident
empties stay silent — an empty form's Save needs no sentence. **A press never moves its
neighbours**: what another press unlocks stands from the start, disabled.

**A DESTRUCTIVE act asks through `Alert.confirm`, and the only thing the caller decides is the
OBJECT**: the title names it ("Delete order SO-1043?"), never "Are you sure?"; the commit is VERB +
NOUN, never OK. A row's delete is its ⋯, a set's is the selection bar's own act over the rows it is COUNTING, a
record's is the `DangerSection`. **What is UNDOABLE asks nothing** and reports with
`useToast().undo`.

## Field annotations & linked records

**A verb about a VALUE goes ON the value** — `InlineButton` in the field's own `actions`, put there
by the app and drawn nowhere else. A VERB GOES WHERE ITS EFFECT LANDS.

**A REFERENCE to another record is a FIELD**: it wears the surface every field wears and peeks its
facts on press, with every verb in the peek's footer.

**A VARIABLE-LENGTH, ORDERED fact is a `ReorderList reorder="buttons"`, never a row of fields.**

**Only a control that ACTS ON ITS OWN shows a pointer cursor**, and its rest promises what pressing
it does: an action's `Button` stands on a ground, the dismiss (`muted`: Cancel, Close) on none, and a
destructive act turns solid when pointed at. **A datum that narrows the page is pressed
whole** — its name, mark and figure, a column with the words under it; held, its words wear the
applied filter's accent and the others stay as they are.

## Register rhythm and triage

**No rule between rows, one under the band** — a hairline divides CHROME from CONTENT, which
whitespace cannot say; between two rows it divides like from like, and the hover wash already did
that. A rule stands only between two groups present: over an empty list, a footer stands alone.
**Density is a number, never a mode.**

**Flat is the default; grouping is something the reader turns ON.**

**The status COLUMN is the floor** — and it asks for the FACT to be visible, not for a badge. A
status column earns its slot by carrying a REASON rather than a state, because the state is usually
derivable from a neighbour the row already shows. **A state the system DERIVES from an absence is
pending until a person confirms it.**

**ONE VALUE, ONE RENDERING.** A value's filter renders it the way its cell does, a record's fact
draws with the cell its register drew, and the chart beside the table draws its entities through
the same leading mark — a person's face, a row's picture — as each datum's `leading`, unsized: a
bar's name, a legend row after its swatch, a matrix's row and column heads. A category's legend row
is its swatch alone: the swatch is that identity.

**Group by what implies a different ACTION, never by a category the reader can already see.** Order
the bands by how much they need the reader NOW; an empty band renders nothing; at most one band
carries a dot. Grouping never costs the row press. The band aligns to the REGISTER, not to a
column. Ordinals restart inside a band. More headings than rows is an outline, not a register.

## The register's own craft

**A column header is a LABEL at the body size**, sentence case.

**THE FIRST COLUMN IS WHO OR WHAT THE ROW IS** — the name a person stated, else the party the row
is about; the lifecycle second or third; dates and money after them. **A MINTED CODE IS NOT A
COLUMN**: an autonumber or a formula nobody scans a register by is the record's title and search
key, or a supporting line on the subject.

**A `Table` does not scroll horizontally, and that is the feature.** Shed a neighbour before
crushing the flexible column; a shed meter rides under the name, never off the row. **Annotate every droppable column, or none**; `priority: "never"` is
the column that never sheds. **A conditional column and its cell are ONE
decision**: derive both from one value so they cannot be edited apart. **A MEASURED COLUMN IS AS
WIDE AS IT DRAWS, and the leftover is EVEN GAPS between columns**: grown, the name held blank
beside short names while the readings packed at the right. On two-line rows an age or `+N` takes
the second line (`beneath`). A column is shed only where the set that stays cannot seat it.

**A row's SUBJECT is `medium`, never semibold**, and it leads by WEIGHT and by its identity mark,
not by size. **A supporting line is ONE RUNG BELOW the line it supports** — never a fixed size —
**and it takes a REFERENCE, never a sentence** — a `{ reference }` statement, which never breaks
inside; two at most, a stage on one its dot and word (`Status size="xs"`). **A STACKED PAIR takes
tight leading** and belongs in a `CellStack`, which owns the rung, the tightening and the zero gap.
**How many lines a column may draw is the COLUMN's** — `TableColumn.lines`, and every cell in
it stands that band, so a row with nothing to say keeps the column's first line where its
neighbours put theirs.

**A CONTROL riding a text line must not resize that line.**

## The ladder

**A progress run is PHASED, TICKED, and coloured by WHERE the record is** — a neutral tick behind
it, its stage's own family where it stands, hollow ahead. Its rungs are the record's own states,
and pressing another moves it there — forward asks nothing, back asks and names the rung it lands
on, a door that ends the run badly asks. **The verb lives in the rung that owns it**, never a row
of moves under the run: the move already has a control. **An outcome is a door, not a rung** — the
doors stand at the rail's foot, the taken one filled in its colour, so the reader sees what endings
the record can have. A long run groups into PHASES: the record's own and the one AFTER it stand
open, earlier ones close to a heading with the tick and the day, one short enough to read whole
never closes. **A rung is a STEP, not a subject row** — its own
rhythm, and its stamp is the history's, reported beside it.

## Marks and character

**"Who can reach this" is drawn as MARKS, on every surface that asks it.** A visibility a reader
cannot see is one they will be surprised by.

**A mark's SHAPE varies only where the surface MIXES entity types**, and the ground
stays neutral: a hue MEANS a status or a category (`constitution.md` §8), and a person is neither.
`labelTone` keeps the hashed palette where hue does encode a category.

**Every register row should carry a LEADING MARK, best effort from the data.** A member a row is
ON — an owner, an assignee — is a face beside the name (`MemberChip chrome="cell"`): the face is
how a reader finds their rows.

**A GLYPH COLUMN BELONGS TO THE RUN, NOT THE ITEM.** Where an outline's stop has no glyph true of
it and not of the next, the rail keeps the column and draws its own quiet mark there, so every
label starts on one x; a borrowed glyph marks nothing.

**Character comes from the DATA — the answer when a screen reads "bland".** Ask what is inherently
visual in THIS data; what the screen DECIDES; what makes it belong to THIS product. Where the data
has a history, show the TRAJECTORY rather than the latest value alone; where the entity has ordered
stages, render the stage rather than naming it. Every visual has to make the screen CLEARER *and*
better-looking, so an image that identifies nothing is decoration and goes.

**SELECTION IS NEVER THE BRAND** — it is a neutral GROUND on a row and a dark EDGE on a control.

**A `Status` means STATE; everything else is text.** `tonal` is ONE status KIND per view — a
register's primary status column, or a multi-select FACT whole, whose chips all answer one field.
A list of CHOICES never wears it.

## Type and space at the call site

**A `size` rung scales the WHOLE component, not just its figure.**

**A LABEL sharing a row with a flexible VALUE must be told which one HOLDS.** The clamp brings its
own `min-width: 0`; what the call site still states is `flexShrink: 0` on the figure or control
that must stay whole. Same law on a categorical axis: the LABELS thin to fit, the bars never do.

## Lists that stay fast

**Keep IDENTITY, do not memoize.** Key a row by the record, never its position. Don't swap a list
for a placeholder on refetch — the reader loses their place to a spinner that says nothing.

## Accessibility at the call site

**State is an `aria-*` attribute**, verified in the DOM and never in the source: a framework can
drop a state prop on the way to the element. **Give every overlay a title** — `role="dialog"` is
name-required, and without one it announces as an unnamed dialog.

## Localization — a rendered string is WIRED

A string BUILT by concatenation hides inside caller data, so assert a wired string under the OTHER
language.

## Microcopy — content, not mechanics

A heading's `description` names the section's content or one domain rule. Widget behaviour ("edits
in place, saves on its own") and selling points never appear; instructions live where action is
needed — an empty state, a field hint.

**NEVER prose beside a CTA** — the label carries it. **Restatement is the second noise mode** — a sentence that repeats
the heading above it. **NEVER join text with a middot** — ` · ` is banned; use a column, a line, or a
comma. **STRUCTURE MUST ENCODE SOMETHING TRUE**: a two-column layout claims the two columns are
peers. **One job per element.** Plain language over jargon, effect over mechanism, and the reader's
own language rather than a translation of ours.

### Error messages — what happened → what it means → what to do

Three clauses, in that order, and the third is a verb the reader can perform.

### Empty states — confirm, orient, one action

First use, cleared by the user, and no search results are three different sentences: confirm what
is empty, orient, and offer nothing the heading row already renders.

**A child section with no rows is never a heading over a void.** It says what would be there in ONE
line under whatever already named it, with the act on that line — a poster with a glyph and a
centred verb is a second screen. An act over a SET is stood down while the set is empty.

**A STATEMENT'S HEAD IS A MONEY BAND**, the record's own: the figures hold with nothing booked
against them (a money section answering only once a row exists answers nobody on the day it
matters), and they are the band's CELLS, so a collection read against its invoice is a meter and a
due date is a countdown. Drawn as label-over-value text instead, the head and the band above it
print one story twice.

### Buttons and action labels — the outcome, verb-first

Name what happens, not what the control is. No trailing ellipsis, no `+` on a create, and Edit is
the word rather than an icon.
