# 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 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 flex child shrinks to its parent by default, and 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, invisible until the first list long enough to scroll — the same list that made
the swap worth having.

**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 takes its
slot on the first paint and is covered by anything opened later — rendering and announcing
perfectly, 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 row beat belongs to the PAGE, not to the row.** The kit's row defaults do not agree with each
other, each picked for its own surface, so collect every row primitive's height on one screen
before calling any right, then pick one beat and hand it to them.

**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 and the one that asked for more carries a gap.
Compute the share from the same condition that picks the direction. And check WHICH condition:
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
restyle a heading level per page. **A heading is WORDS and, where the section is operable, 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` / `RecordSummary` |
| `##` | `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 |

**Which rung a section title takes is the SURFACE's answer, never the call site's.** The CONTAINER
publishes it and one `SectionHeadingTitle` answers to it, so write the same heading either way; a
whole PAGE surface docked in an overlay keeps the page rung, in the container's BARE slot, and 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 the section holding it.

### The add-placement law

**A section's ADD rides its heading row, right edge** — Add files, Add fee, New line. Two verbs for
one act is the defect it prevents, so where the region is TRULY EMPTY the toolbar stands down and
that act moves into the empty state. Out of width the VERB wraps; the title never gives way. A
header CHECKBOX is legible only heading a COLUMN of checkboxes.

## Bands, tabs and lenses

**A partitioning control states its band's size as a PROP**, never by counting rows itself — the
band knows its own size and the control naming it cannot be the one to guess. A grouping control
CLEARS; it never offers "no grouping" as a choice.

**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 strip over a category hides the
whole list behind a click and says nothing of what to do next.

**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, charging a click to reach a sibling. 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 40px control band affords 28** (`CONTROL_CONTENT_HEIGHT`), and a surface pads by
`CONTROL_PADDING_V` rather than a number of its own. The height holds for free only while the
content is TEXT; the moment something with a FIXED height goes in — an avatar, a swatch, a
thumbnail — that inset decides the control's whole height. A fixed-height child is sized against
the band's budget, and a surface that needs more room than the budget 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.** A weekly
PATTERN with no dates is a `Timetable`; a subject-by-date sheet of STATES is a `StateMatrix`, where
a glyph per cell carries the meaning and a blank does not mean zero — the same entry
with no axis is the wallboard, and with `mark="intensity"` it is density. 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.

**Rows of days, or rows of resources — pick by what the reader is comparing.** Days on the axis is
a calendar; a channel, an owner or a vehicle on the axis is a Gantt.

## 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.** The ring is `:focus-visible` on `--ring`; a page
that draws one for everything draws it on things that never take focus.

## 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 themeable control derives EVERY state from its token** — rest, hover, press, disabled — so a
theme moves all four together.

**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**: the acts go inside it, or the card
is the act. **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.** A column of mixed units is
two, or a value carrying its unit 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**, and the currency's minor unit is the wrong floor for it — a
rate quoted to four places rounds to nothing at two.

**Every number is a door** — it presses through to the rows behind it.

## 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 when there is no column to read the answer off,
gone the moment the value gets one.

**Sibling surfaces must be the same form.** Two tabs of one dialog showing one KIND of subject in
two forms reads as an accident: a reader parses shape first.

**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 COLUMN holds the gutter open with an empty box of the mark's width, the
header still on the column edge; a stack of rows has no column to do that, so it marks them all.

**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 (`lines`, below). A line only some rows draw — the countdown under the dates that are
late — held open everywhere stands each value in a box that fills from the top, a rung clear of the
party and the amount on its own line. 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 and the emphasis ladder stops reading as one —
the colours are not the defect, the spacing is. The grouping pays again at phone width, where the
only break available is that boundary.

**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 surface answers *which record is this* first, and the block that does it is SHORT.** It
is then ONE READING COLUMN of sections, in the order the job runs — its facts, its files, the talk
and the registers that merely name it all among them. Beside the column sits a rail in a reserved
gutter, folding to a pinned bar where the gutter and the column's floor stop fitting. **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 resting state.

**A screen is cut by the JOB, never by the subject.** Two work shapes — a queue you work through
and one you inspect — and no side pane beside a LIST: neither overlay nor region, a third layout
to learn, because 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 on a dialog that looks whole; the footer is the
one band the reader can always see. On a page the fields ARE the document and the commit ends it.
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 as a second spelling of ✕.

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

**A full-page editor is not a dialog.** **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.

**The action-gating law: disabled, plus the reason as a co-located `Callout`.** A control the
reader cannot press and cannot explain is a dead end. Self-evident empties stay silent — a Save
with nothing typed needs no sentence.

**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 resting 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"`, not a row of fields.**

**Only a control that ACTS ON ITS OWN shows a pointer cursor**, and its resting treatment promises
what pressing it does. A field that opens an editor is one; a row whose press is its own door is
one; a label is not.

## 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 work. **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.**

**A strip whose every option shows the same register with fewer rows is a FILTER**, not a set of
tabs.

**Whose move it is and by when is ONE line** (`Awaiting`): a party column and a date column leave
the join to the reader, and the join is the reading.

**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 same rule reaches the CHART beside the table — the chart draws the entity its table draws, through the same
leading mark. A LEGEND row is the exception: its subject is a category, and 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. Air separates bands, a hairline separates rows. 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, in the header too.

**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** — and this is where "cramped" usually comes from: 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. **Annotate every droppable column, or none** — a partial priority is
the trap, and `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 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**: an explanation truncates to the clause the reader
already knew. **A STACKED PAIR takes tight leading** — it is not prose — and belongs in a
`CellStack`, which owns the rung, the tightening and the zero gap. A hand-rolled pair gets one of
the three wrong silently, because the excess comes from the LINE BOX and no spacing probe can see
it. **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 cell cannot declare it: the rows that break a column have no caption.

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

## 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.

**An identity mark's GLYPH comes from the name's distinguishing part.** An organisation's first
word is its legal form, so untreated every row marks one letter.

**Every register row should carry a LEADING MARK, best effort from the data.** But a member as ONE
FIELD AMONG COLUMNS is TEXT, not a face: it competes with the mark the row already carries for its
own subject, and on a real book a few people own hundreds of rows.

**A mark derived from POSITION is not identity** — the list already draws position, and the glyph
comes from the NAME.

**A column of faces must not draw an UNBOUNDED audience as faces.** An overlapping stack separates
with translucent INK, never with the surface colour, so it survives every ground.

**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 — and the same test cuts the other way, 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 for a register's ONE primary
status column.

## 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 by its position. Don't define a
component inside another component's render — hoist it. Don't swap a list for a placeholder on refetch — the
reader loses their place to a spinner that says nothing. Reach for `memo` last, and only with a
measurement; it does nothing against a freshly-built prop.

## Accessibility at the call site

**State is an `aria-*` attribute.** Verify in the DOM, never in the source: a framework can drop a
state prop on the way to the element, and the source will still read correctly.

**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

**Write the check as an ALLOWLIST, not a hunt.** A string BUILT by concatenation hides inside
caller data, so assert a wired string under the OTHER language rather than grepping for quotes.

## 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.

### 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.
