@arag 0.1.0

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.

The full scale. Bar widths are the token itself, at 1:1.
Token Value At 16px root Scale
--space-00px
--space-14px
--space-26px
--space-38px
--space-412px
--space-516px
--space-624px
--space-732px
--space-848px
--space-964px
Open question — trimmable

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.

TokenValueUsed 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-measuremax-inline-sizeReading measure. On p in base.css.
--size-container-smmax-inline-sizeDialogs, forms, single-column panels.
--size-container-mdmax-inline-sizeArticle and document width.
--size-container-lgmax-inline-sizeTwo-column content.
--size-container-xlmax-inline-sizeDashboards and wide tables.
--size-container-2xlmax-inline-sizeThe outermost shell. This docs layout uses it.
--size-touchmin-block-sizeMinimum tappable target. Buttons.
--size-controlmin-block-sizeMinimum pointer-driven control height. Inputs, selects.
--size-textareamin-block-sizeStarting 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.

Reminder

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.

--size-container-sm ·
--size-container-md ·
--size-container-lg ·
--size-container-xl ·
--size-container-2xl ·

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);
}
The rule

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.