Space & size
One geometric space scale that doubles every two steps and lands on round pixel
values, plus a small set of size ceilings and minimum targets. Nineteen tokens in total, and
every length in base.css comes from one of them.
The space scale
Ten steps, --space-0 through --space-9. The scale is geometric:
each step is double the one two places below it, so --space-1 is 4px,
--space-3 is 8px, --space-5 is 16px, --space-7 is 32px
and --space-9 is 64px. The even steps do the same thing offset by one, giving
6px, 12px, 24px and 48px. Every value is a whole number of pixels at a 16px root.
--space-0 is 0, unitless and deliberate. It exists so that
“no space here” is still written as a token rather than a bare zero, which keeps
the no-raw-values rule greppable.
| Token | Value | At 16px root | Scale |
|---|---|---|---|
--space-0 | | 0px | |
--space-1 | | 4px | |
--space-2 | | 6px | |
--space-3 | | 8px | |
--space-4 | | 12px | |
--space-5 | | 16px | |
--space-6 | | 24px | |
--space-7 | | 32px | |
--space-8 | | 48px | |
--space-9 | | 64px |
The brief asked for roughly eight steps. This ships ten, topping out at
--space-9 (4rem / 64px). The two extra steps sit at the top of the scale, not
scattered through it: 48px was tight for page-level gutters and section separation on wide
screens, so --space-8 and --space-9 were added rather than
reaching for a raw value at the one place it mattered.
If sign-off wants eight steps, --space-9 is the one to cut and
--space-8 is the fallback. Nothing below --space-8 is negotiable
— those steps are load-bearing in base.css.
Using it
Padding inside a box
One token on all four sides, or two for a block and inline split. Controls use the split, because an input needs more room at the sides than above and below.
Padded with --space-6 on every side.
CSS
.card {
padding: var(--space-6);
border-radius: var(--radius-lg);
}Gap in a row
gap is the only spacing mechanism that never collapses and never needs a
last-child exception, so prefer it over margins on the children wherever the parent is a flex
or grid container.
CSS
.actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
}That row wraps rather than shrinking. A Mongolian label runs 15–25% longer than its English equivalent, so a fixed-width action bar overflows on the first translation pass. More on that constraint →
Rhythm between blocks
Vertical rhythm is margin-block in a single direction.
base.css puts margin-block-end on paragraphs and lists and
margin-block-start on headings, so the two never fight over the same gap.
Section one.
Section two, separated by --space-8.
Section three.
CSS
section + section {
margin-block-start: var(--space-8);
}Which step for what
Guidance, not law. These are the steps base.css actually reaches for, written
down so a new component starts from the same instincts rather than from scratch. Deviate when
the design needs it. Just deviate to another step, not to a raw value.
| Job | Usual step | Where it shows up |
|---|---|---|
| Hairline gaps | --space-1, --space-2 |
Icon to label, badge padding, the gap inside a segmented control. |
| Inline control padding | --space-3 block, --space-4–--space-5 inline |
Inputs, selects and buttons in base.css. Buttons take the wider inline
value because a button is a target as well as a label. |
| Stack rhythm | --space-5 |
Paragraph, list and table margin-block-end. The default gap between
sibling blocks that belong to the same thought. |
| Container padding | --space-6 |
Card and dialog interiors, blockquote indent. |
| Heading separation | --space-7 |
margin-block-start on headings — enough to read as a break
without a rule line. |
| Section separation | --space-8, --space-9 |
Between top-level sections, and page gutters on wide viewports. |
Column minimums
Two floors for auto-reflowing grids, added after the components were built. The scale jumped
straight from --size-control to --size-container-sm with nothing
usable between, so .field-row and .card-grid were both deriving a
column floor by halving a container. A form column and a card column are not the same width,
so they get one token each.
| Token | Value | Used by |
|---|---|---|
--size-column-min | |
.field-row — below this a row of fields stacks. |
--size-card-min | |
.card-grid — the minmax() floor for a card column. |
--size-popover | |
.menu minimum width, capped against the viewport. |
--size-popover-max | |
.menu ceiling, which forces long labels to wrap. |
--size-icon | |
Any icon that should match the text beside it. em-relative, so it tracks whatever type step its component sits at. |
Size tokens
Nine tokens. These are not a scale and they are not interchangeable — each one answers a specific question. Six are ceilings (how wide may this get), two are floors (how small may this target be) and one is a starting height.
| Token | Value | Applied as | Job |
|---|---|---|---|
--size-measure | | max-inline-size | Reading measure. On p in base.css. |
--size-container-sm | | max-inline-size | Dialogs, forms, single-column panels. |
--size-container-md | | max-inline-size | Article and document width. |
--size-container-lg | | max-inline-size | Two-column content. |
--size-container-xl | | max-inline-size | Dashboards and wide tables. |
--size-container-2xl | | max-inline-size | The outermost shell. This docs layout uses it. |
--size-touch | | min-block-size | Minimum tappable target. Buttons. |
--size-control | | min-block-size | Minimum pointer-driven control height. Inputs, selects. |
--size-textarea | | min-block-size | Starting height for a free-text field. |
The reading measure
--size-measure is 66ch, and base.css applies it as
max-inline-size on p and nowhere else. Headings, list items and
table cells each get their own wrap treatment, because a heading that wraps at the paragraph
measure looks broken.
The unit matters more than the number. ch is the advance width of the
0 glyph in the element's own font at its own size, so the measure re-resolves per
element instead of being frozen at whatever the root font happened to be. That is exactly what
a bilingual system needs. Mongolian Cyrillic sets wider than Latin at the same nominal size,
and a rem-based measure would quietly hand Cyrillic a longer line than Latin at
identical markup. A ch-based one tracks the font it is actually measuring.
ch is not “characters”. Both scripts are proportional, so
66ch is roughly 66 zero-widths, which lands inside the 45–75 character
band that legibility research keeps pointing at. Close enough is the goal here; exact is not
available in a proportional face.
Container ceilings
Five ceilings, applied as max-inline-size with margin-inline: auto.
They are round rem values rather than a geometric progression, because a layout
ceiling is a judgement about content, not a step on a scale.
Each bar is min() of 100% and its own container token — for instance
inline-size: min(100%, var(--size-container-lg)) on the third one. The real
token, capped at the width of this column so the page never scrolls sideways. The cap is doing
visible work: the larger ceilings are wider than this content column, so on a narrow window
the last bars read identically. Widen the window and they separate.
Touch and control minimums
These two set min-block-size only. Never
block-size, never inline-size, never
max-inline-size. The control stays free to grow in both directions, and it
does.
Same rule, same padding, same
min-block-size: var(--size-touch). Different widths, because the label
decides.
CSS from base.css
button {
padding-block: var(--space-3);
padding-inline: var(--space-5);
/* A floor, not a height. */
min-block-size: var(--size-touch);
}
input:not([type="checkbox"], [type="radio"] /* … */) {
padding-block: var(--space-3);
padding-inline: var(--space-4);
min-block-size: var(--size-control);
}A fixed width clips a Mongolian label. min-block-size plus padding never
does.
“Create a new order” is 18 characters.
Шинэ захиалга үүсгэх is 20, and sets wider than the count suggests.
Any button given inline-size or max-inline-size tuned to the
English string either truncates the Cyrillic one or forces it onto a second line inside a
box that was sized for one. Because --size-touch is a floor rather than a
height, even the two-line case works: the button grows downward, the target stays at least
44px, and nothing is lost.
The two values differ on purpose. --size-touch is
(44px), the tappable minimum, and goes on anything a
finger hits — the kiosk app and the mobile app are the reason it exists.
--size-control is (40px), for text
fields driven by a pointer or a keyboard, which do not need the same target area.
The textarea height
A free-text field must not be the height of a single-line input. If it looks like one line it
invites one line, and the field that collects an address or a complaint comes back with a
fragment. --size-textarea is — a
real box that reads as “write something here”, with room for a Cyrillic string
that runs a quarter longer than the English draft the layout was designed against.
CSS from base.css
textarea:not([rows]) {
min-block-size: var(--size-textarea);
}The :not([rows]) guard is the whole point of that selector. An author who writes
<textarea rows="2"> has made a decision, and the system does not overrule
it — the token supplies a default only for the case where nobody decided. The rule is
restated in base.css rather than left in reset.css because the
shared control rule lives in a later layer and would otherwise shrink every textarea back down
to --size-control.