In reviewMedium

Add semantic layout primitives (container, stack, cluster, grid, sidebar, split, center, cover) with docs and fixtures

Proposed by Jonathan Miller 39 minutes agoFunding opened 39 minutes agoFunded 39 minutes ago
Specification

Motivation

Give SynthCSS a small, intent-named structural vocabulary so that AI agents can build responsive layouts without composing low-level flex/grid utilities.

Scope

Implement eight layout primitives using only existing SynthCSS design tokens (spacing, sizes) from the :root token set added in #1. Use Flexbox/Grid and min()/max()/clamp()/minmax()/auto-fit. Media queries are allowed only where intrinsic techniques cannot do the job.

Class Required behavior
.container margin-inline: auto; max-width from a token (or a new --layout-max-width token if none exists); fluid horizontal padding via clamp(); full width below the max width.
.stack Flex column with gap from a default spacing token; children keep their natural width.
.cluster Flex row with flex-wrap: wrap, align-items: center and a token gap.
.grid display: grid; grid-template-columns: repeat(auto-fit, minmax(min(<token min>, 100%), 1fr)); never overflows narrow containers.
.sidebar Two children. The first child (sidebar) has a token-based basis width; the second (main) grows and has a min inline size of about 50%. It wraps to a single column on narrow widths via flex-wrap, without media queries. DOM order is preserved.
.split Flex row, justify-content: space-between, flex-wrap: wrap, token gap; wraps cleanly when space is tight.
.center box-sizing: content-box, margin-inline: auto, max-inline-size from a token. Centers the element horizontally (intrinsic centering).
.cover min-block-size: 100vh with a 100dvh progressive enhancement and no fixed height. Flex column. The direct child with class .cover-main (or the only child) is vertically centered via margin-block: auto.

Variants

Only -sm and -lg spacing variants, applied consistently to the gap-based primitives: .stack-sm/-lg, .cluster-sm/-lg, .grid-sm/-lg, .split-sm/-lg, .sidebar-sm/-lg. Each maps to an existing smaller/larger spacing token. No other variant matrix.

Constraints

  • No JavaScript.
  • No order, flex-direction: *-reverse, or grid placement that changes visual order relative to DOM order.
  • Classes work on any element (nav, main, ul, section, …) and do not depend on child tag types.
  • Nested primitives work. For example, .stack inside .grid inside .container must not leak margins or gaps between levels.

Documentation

Add a layout docs page (assumption: alongside the existing token reference). For each primitive include:

  • Purpose
  • HTML example
  • Variants
  • Responsive behavior
  • Recommended uses
  • Common misuses

Also include a compact "AI Layout Vocabulary" intent→class table (as in the proposal) that is short enough to paste into a model's context.

Verification

Assumption, pending a question: add an HTML fixture page that demonstrates every primitive, every variant, and a nested example. If the repo already has a test runner, add automated checks that these hold at about 375px and about 1280px widths:

  • grid column count drops
  • sidebar stacks
  • cluster/split wrap
  • no horizontal overflow

Otherwise, document how to view the fixtures at both widths and add a simple script check that every primitive class exists and references var(--…) tokens rather than hard-coded spacing values.

Acceptance Criteria

  • All eight classes exist in the shipped CSS and are included in the main bundle.
  • Spacing and size values reference existing tokens. Any newly required token (e.g. max width, sidebar width, grid min) is added to :root and documented.
  • -sm/-lg variants exist exactly for the gap-based primitives listed above.
  • No breakpoint utility classes are introduced, and no reading-order-altering properties are used.
  • The fixture page shows narrow and wide behavior for each primitive, plus nesting.
  • The docs cover every primitive with all six sections and include the AI vocabulary table.

Out of Scope

  • Breakpoint utility classes.
  • Component styles.
  • Alignment/justify variant matrices.
  • JavaScript.
  • Container-query-based variants.
Build #2
SUCCEEDED

Model claude-opus-5-5 · ceiling $5 · started 10 minutes ago · finished 3 minutes ago

25agent turns
1.2Mtokens
$1.38spent at list price

Builder summary
## Summary This adds eight layout classes: `.container`, `.stack`, `.cluster`, `.grid`, `.sidebar`, `.split`, `.center` and `.cover`. The gap-based ones (stack, cluster, grid, sidebar, split) get `-sm`/`-lg` variants. Every spacing and size value is a design token. The CSS uses no media queries or breakpoint classes, and nothing changes the visual order from the DOM order (no `order`, no `*-reverse`, no grid placement). There is no JavaScript. ## Files - **`src/layout.css`** (new): the primitives. - `.container`: `max-width: var(--container-width)` (an existing token), centred with `margin-inline: auto`. Side padding is `clamp(var(--space-4), 4vw, var(--space-6))` and uses `border-box`. - `.stack`: flex column with a gap. - `.cluster`: wrapping flex row, items centred vertically. - `.grid`: `repeat(auto-fit, minmax(min(var(--grid-min), 100%), 1fr))`. - `.sidebar`: wraps without media queries. The first child has `flex-basis: var(--sidebar-width)`. The second child has `flex-grow: 999` and `min-inline-size: 50%`. - `.split`: `space-between` with wrapping. - `.center`: `content-box`, `max-inline-size: var(--content-width)`, `margin-inline: auto`. - `.cover`: `min-block-size: 100vh` then `100dvh`, flex column. `.cover-main` (or the only child) gets `margin-block: auto`. - Gaps: `-sm` is `--space-2`, the default is `--space-4`, `-lg` is `--space-6`. A variant works alone (`stack-lg`) or with the base class (`stack stack-lg`). - **`src/synthcss.css`** (new): the main bundle. It `@import`s `tokens.css`, then `layout.css`. There was no bundle before, and the repo has no build step, so `@import` is how the classes get "into the main bundle". - **`src/tokens.css` and `docs/tokens.md`**: two new tokens, `--sidebar-width: 16rem` and `--grid-min: 16rem`, documented in the Sizing table. I used the existing `--container-width` instead of adding `--layout-max-width`. There are now 57 tokens (the limit is 70). Both new tokens are added to `REQUIRED_TOKENS` in `scripts/check-tokens.mjs`. - **`docs/layout.md`** (new): each primitive has the six sections (Purpose, HTML example, Variants, Responsive behavior, Recommended uses, Common misuses). It also has the compact "AI Layout Vocabulary" table, a nesting example, and how to verify. - **`examples/layout.html`** (new): shows every primitive and every variant in a 375px frame and a full-width frame, plus a nested sidebar › container › split/cluster › grid › stack example. The frames can be resized by dragging. Demo-only styles are kept separate. - **`scripts/check-layout.mjs`** and **`scripts/check-layout.test.mjs`** (new), run by `npm test`. No dependencies. They check that: - all 8 classes and 10 variants exist, and no other classes do (e.g. no `.container-sm`); - each variant's gap is a `--space-N` token, ordered sm < default < lg; - there are no at-rules and no order-changing properties or values; - there are no hard-coded `px`/`rem`/`em` values, and spacing uses `var(--space-N)`; - every `var()` points to a token defined in `tokens.css`; - the bundle imports both files in the right order; - the docs have every section and the vocabulary table, and the fixture uses every class. - **`scripts/check-layout-browser.mjs`** (new, optional; `npm run check:layout:browser`): runs the fixture in headless Chromium at 375px and 1280px. It checks that the grid drops columns, the sidebar stacks, cluster and split wrap, the cover's main child is centred, nested gaps and margins don't leak, and nothing overflows sideways. It needs Playwright, which I did **not** add as a dependency. You run `npm install --no-save playwright && npx playwright install chromium` first; without it the script prints that message and exits with code 2. - Also: `package.json` scripts, a README section, and a `.gitignore` for `node_modules/`. ## Verification - `npm test`: the token check and layout check pass, and all 16 unit tests pass. The new tests check that each kind of breakage is caught. -

Agent log
- **`src/layout.css`** (new): the primitives.
  - `.container`: `max-width: var(--container-width)` (an existing token), centred with `margin-inline: auto`. Side padding is `clamp(var(--space-4), 4vw, var(--space-6))` and uses `border-box`.
  - `.stack`: flex column with a gap.
  - `.cluster`: wrapping flex row, items centred vertically.
  - `.grid`: `repeat(auto-fit, minmax(min(var(--grid-min), 100%), 1fr))`.
  - `.sidebar`: wraps without media queries. The first child has `flex-basis: var(--sidebar-width)`. The second child has `flex-grow: 999` and `min-inline-size: 50%`.
  - `.split`: `space-between` with wrapping.
  - `.center`: `content-box`, `max-inline-size: var(--content-width)`, `margin-inline: auto`.
  - `.cover`: `min-block-size: 100vh` then `100dvh`, flex column. `.cover-main` (or the only child) gets `margin-block: auto`.
  - Gaps: `-sm` is `--space-2`, the default is `--space-4`, `-lg` is `--space-6`. A variant works alone (`stack-lg`) or with the base class (`stack stack-lg`).
- **`src/synthcss.css`** (new): the main bundle. It `@import`s `tokens.css`, then `layout.css`. There was no bundle before, and the repo has no build step, so `@import` is how the classes get "into the main bundle".
- **`src/tokens.css` and `docs/tokens.md`**: two new tokens, `--sidebar-width: 16rem` and `--grid-min: 16rem`, documented in the Sizing table. I used the existing `--container-width` instead of adding `--layout-max-width`. There are now 57 tokens (the limit is 70). Both new tokens are added to `REQUIRED_TOKENS` in `scripts/check-tokens.mjs`.
- **`docs/layout.md`** (new): each primitive has the six sections (Purpose, HTML example, Variants, Responsive behavior, Recommended uses, Common misuses). It also has the compact "AI Layout Vocabulary" table, a nesting example, and how to verify.
- **`examples/layout.html`** (new): shows every primitive and every variant in a 375px frame and a full-width frame, plus a nested sidebar › container › split/cluster › grid › stack example. The frames can be resized by dragging. Demo-only styles are kept separate.
- **`scripts/check-layout.mjs`** and **`scripts/check-layout.test.mjs`** (new), run by `npm test`. No dependencies. They check that:
  - all 8 classes and 10 variants exist, and no other classes do (e.g. no `.container-sm`);
  - each variant's gap is a `--space-N` token, ordered sm < default < lg;
  - there are no at-rules and no order-changing properties or values;
  - there are no hard-coded `px`/`rem`/`em` values, and spacing uses `var(--space-N)`;
  - every `var()` points to a token defined in `tokens.css`;
  - the bundle imports both files in the right order;
  - the docs have every section and the vocabulary table, and the fixture uses every class.
- **`scripts/check-layout-browser.mjs`** (new, optional; `npm run check:layout:browser`): runs the fixture in headless Chromium at 375px and 1280px. It checks that the grid drops columns, the sidebar stacks, cluster and split wrap, the cover's main child is centred, nested gaps and margins don't leak, and nothing overflows sideways. It needs Playwright, which I did **not** add as a dependency. You run `npm install --no-save playwright && npx playwright install chromium` first; without it the script prints that message and exits with code 2.
- Also: `package.json` scripts, a README section, and a `.gitignore` for `node_modules/`.

## Verification

- `npm test`: the token check and layout check pass, and all 16 unit tests pass. The new tests check that each kind of breakage is caught.
- The browser check passed in the sandbox (Playwright with Chromium). I also tried two deliberate breakages, and the check caught both:
  - removing `flex-wrap` from the sidebar;
  - removing `min()` from the grid.
- I looked at full-page screenshots at 375px and 1280px.

## Decisions to review

1. **Stack width:** `.stack` keeps the default flex `align-items: stretch`. "Children keep their natural width" is read as "the stack doesn't grow, shrink or set widths; blocks fill it like normal flow". Using `flex-start` instead would make nested `.grid` and `.container` shrink to their content and break nesting. The downside is that a button in a stack stretches; the docs list this as a misuse and suggest wrapping it in `.cluster`.
2. **Two small resets I added, both at zero specificity with `:where()`:**
   - Direct children of gap-based primitives and `.cover` lose their block margins, so only `gap` sets spacing. This is what stops margins leaking between nested levels.
   - `ul`/`ol` with a gap-based class lose bullets and indent. The docs recommend `role="list"`.
3. **Extra properties not in the spec:** `.split` also sets `align-items: center`, and `.cover` has a `--space-4` gap between its children.
4. **Fluid padding:** the `4vw` inside the container's `clamp()` is the only non-token length. It is allowed because both its limits are tokens.
5. **Fixture cover height:** inside the fixture's demo frames, `.cover` is capped at 16rem tall so the page stays readable. The page notes this.
2026-10-02T23:58:05Z  [claude] I've built all eight layout primitives with their docs and fixture page, and pushed them to `nabled/feature-2-job-2`. `npm test` passes, as does a check in a headless browser at 375px and 1280px.

**What's in the change:**
- **`src/layout.css`:** the eight classes (`.container`, `.stack`, `.cluster`, `.grid`, `.sidebar`, `.split`, `.center`, `.cover`). The five gap-based ones (stack, cluster, grid, sidebar, split) each have a `-sm` and `-lg` version, and nothing else does. All spacing and sizes come from tokens. There are no media queries and no JavaScript, and nothing changes the on-screen order from the HTML order.
- **`src/synthcss.css` (new):** the repo had no main bundle, so I added one that imports the tokens file, then the layout file.
- **New tokens:** `--sidebar-width` and `--grid-min` (both 16rem), added to `:root` and to the token docs. I used the existing `--container-width` instead of adding `--layout-max-width`.
- **`docs/layout.md`:** every primitive has all six sections, plus the short "AI Layout Vocabulary" table and a nesting example.
- **`examples/layout.html`:** every primitive and variant shown in a 375px frame and a full-width frame, plus a nested example.

**How it was checked:**
- **`npm test`:** a new no-dependency script checks that every class and variant exists, that values use tokens, and that there are no media queries or order changes. It also checks the bundle, the docs sections and the fixture. New unit tests confirm it fails when any of these is broken.
- **Browser check (`npm run check:layout:browser`):** loads the fixture at both widths. It checks that the grid drops columns, the sidebar stacks, cluster and split wrap, and nothing overflows sideways. Two deliberate breakages (sidebar wrapping, grid overflow protection) were both caught. It needs Playwright, which I didn't add as a dependency: you install it once with `npm install --no-save playwright && npx playwright install chromium`.

**Decisions for you to review:**
- **Stack width:** I read "children keep their natural width" as "children fill the stack like normal page flow". The alternative makes a grid or container nested inside a stack shrink to its content. The downside is that a button inside a `.stack` stretches to full width; the docs suggest wrapping it in `.cluster`.
- **Two resets I added:** direct children of the gap-based primitives lose their top and bottom margins, so only the gap sets spacing and nested layouts don't leak margins. Lists using these classes also lose their bullets and indent. Any style you write overrides both.
- **Small additions beyond the spec:** `.split` centres its items vertically, and `.cover` has a gap between its children. The fixture caps `.cover` at 16rem tall inside its demo frames so the page stays readable.
- **One raw length:** the container's fluid side padding uses `4vw` between two spacing tokens. It's the only length that isn't a token, and the check allows it.

The CSS, docs, fixture and screenshots at both widths are in `/mnt/session/outputs`.
2026-10-02T23:58:07Z  Changed files:
added  .gitignore  +1 -0
modified  README.md  +10 -0
added  docs/layout.md  +423 -0
modified  docs/tokens.md  +4 -2
added  examples/layout.html  +240 -0
modified  package.json  +3 -1
added  scripts/check-layout-browser.mjs  +128 -0
added  scripts/check-layout.mjs  +208 -0
added  scripts/check-layout.test.mjs  +57 -0
modified  scripts/check-tokens.mjs  +2 -0
added  src/layout.css  +152 -0
added  src/synthcss.css  +11 -0
modified  src/tokens.css  +2 -0
2026-10-02T23:58:08Z  Opened pull request https://github.com/nabledhq/synthcss/pull/2
2026-10-02T23:58:08Z  Finished: success=true turns=25 tokens(in/out)=1149395/40919 list cost=$1.38

Show patch
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/README.md b/README.md
index b92f5cb..3d1a1bc 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,13 @@ All visual decisions (color, spacing, typography, radius, borders, shadows, sizi
 ```
 
 See [docs/tokens.md](docs/tokens.md) for the full token reference, override examples, guidance for AI agents and reduced-motion behavior. Run `npm test` to check that the tokens, the docs and the contrast requirements are in sync. It needs only Node.js 18 or later.
+
+## Layout primitives
+
+Eight intent-named layout classes in [`src/layout.css`](src/layout.css): `.container`, `.stack`, `.cluster`, `.grid`, `.sidebar`, `.split`, `.center` and `.cover`, plus `-sm`/`-lg` gap variants for `stack`, `cluster`, `grid`, `sidebar` and `split`. They respond to the space they are given, with no media queries or breakpoint classes. Load everything with the main bundle:
+
+```html
+<link rel="stylesheet" href="synthcss/src/synthcss.css">
+```
+
+See [docs/layout.md](docs/layout.md) for each primitive and a compact "AI Layout Vocabulary" table to give to a model, and [examples/layout.html](examples/layout.html) for a fixture page showing every primitive at narrow and wide widths.
diff --git a/docs/layout.md b/docs/layout.md
new file mode 100644
index 0000000..20f5fc3
--- /dev/null
+++ b/docs/layout.md
@@ -0,0 +1,423 @@
+# Layout primitives
+
+SynthCSS ships eight layout classes in [`src/layout.css`](../src/layout.css). Each one
+names a layout intent: "stack these", "put these in a grid", "sidebar next to content".
+Use them instead of writing flexbox or grid rules yourself.
+
+Load the main bundle, which includes the [design tokens](tokens.md) and the layout primitives:
+
+```html
+<link rel="stylesheet" href="synthcss/src/synthcss.css">
+```
+
+How the primitives behave:
+
+- **Intrinsic.** They respond to the width of their parent, not to the viewport. They
+  use flexbox, grid, `min()`, `clamp()`, `minmax()` and `auto-fit`. There are no media
+  queries and no breakpoint classes such as `md:grid-3`.
+- **Token-based.** Every gap, padding and width is a token from `tokens.css`. Change
+  `--space-4` and every default gap changes with it.
+- **DOM order is visual order.** Nothing uses `order`, `*-reverse` or grid placement,
+  so keyboard and screen reader order always match what is on screen.
+- **Any element.** Put the classes on `div`, `nav`, `main`, `section`, `ul` and so on.
+  They never look at the tag names of the children. On `ul` and `ol`, the gap-based
+  primitives remove bullets and indent; add `role="list"` to keep list semantics in
+  Safari/VoiceOver.
+- **Gap, not margins.** The gap-based primitives set the spacing between children
+  with `gap` and remove the block margins of direct children (for example the default
+  margins of `<p>` and `<h2>`). This reset has zero specificity, so any rule you write
+  wins. Because spacing belongs to the parent, primitives nest without leaking margins
+  or gaps into each other.
+- **Variants.** Only the gap-based primitives have variants: `-sm` uses `--space-2`,
+  the default uses `--space-4` and `-lg` uses `--space-6`. A variant works on its own
+  (`class="stack-lg"`) or next to the base class (`class="stack stack-lg"`).
+
+Tokens used by the layout primitives:
+
+| Token | Default | Used by |
+| --- | --- | --- |
+| `--container-width` | `72rem` | `.container` maximum width |
+| `--content-width` | `42rem` | `.center` maximum inline size |
+| `--sidebar-width` | `16rem` | `.sidebar` first child's preferred width |
+| `--grid-min` | `16rem` | `.grid` minimum column width |
+| `--space-2`, `--space-4`, `--space-6` | `0.5rem`, `1rem`, `2rem` | `-sm`, default and `-lg` gaps; `.container` padding (`--space-4` to `--space-6`) |
+
+They are documented with the other tokens in [tokens.md](tokens.md#sizing). Override them
+on `:root`, or on one element to change only that layout:
+
+```html
+<div class="grid" style="--grid-min: 12rem">…</div>
+```
+
+## AI Layout Vocabulary
+
+Paste this table into a model's context.
+
+| Intent | Class |
+| --- | --- |
+| Page-width wrapper, centered, with side padding | `.container` |
+| Vertical list of blocks with even spacing | `.stack` |
+| Row of small items that wraps (tags, buttons, links) | `.cluster` |
+| Responsive cards or tiles, as many columns as fit | `.grid` |
+| Narrow side panel next to main content, stacks when narrow | `.sidebar` (sidebar is the 1st child, main the 2nd) |
+| Two groups pushed to opposite ends (header bar, toolbar) | `.split` |
+| Readable, horizontally centered column of text | `.center` |
+| Full-viewport-height section with vertically centered content | `.cover` (+ `.cover-main` on the centered child) |
+| Tighter / looser spacing | add `-sm` / `-lg`: `stack`, `cluster`, `grid`, `sidebar`, `split` only |
+
+Rules: no breakpoints, no `order`, no margins between children; nest primitives instead.
+
+## `.container`
+
+### Purpose
+
+The outer wrapper of a page or a page section. It is centered, never wider than
+`--container-width`, and keeps content away from the screen edges with padding.
+
+### HTML example
+
+```html
+<main class="container">
+  <h1>Settings</h1>
+  …
+</main>
+```
+
+### Variants
+
+None. To change the width, override `--container-width`.
+
+### Responsive behavior
+
+Below `--container-width` it is the full width of its parent. Above that, it stops
+growing and is centered with `margin-inline: auto`. Side padding is fluid:
+`clamp(var(--space-4), 4vw, var(--space-6))`, so it grows from 1rem on phones to 2rem
+on large screens. The padding is included in the maximum width (`box-sizing: border-box`).
+
+### Recommended uses
+
+- The `main` element, or each full-width band of a landing page (put the background on
+  an outer element and `.container` inside it).
+- Header and footer content that must line up with the page content.
+
+### Common misuses
+
+- Nesting a `.container` inside another `.container`. The padding is applied twice.
+- Using it to narrow text. Use `.center` for readable text columns.
+- Adding your own `max-width` or side margins. Override `--container-width` instead.
+
+## `.stack`
+
+### Purpose
+
+Children placed one below the other with an even gap. This is the most common layout.
+
+### HTML example
+
+```html
+<form class="stack">
+  <label>Email <input type="email"></label>
+  <label>Password <input type="password"></label>
+  <button type="submit">Sign in</button>
+</form>
+```
+
+### Variants
+
+`.stack-sm` (`--space-2`), `.stack` (`--space-4`), `.stack-lg` (`--space-6`).
+
+### Responsive behavior
+
+A flex column; the same at every width. Children keep their normal width: block
+elements fill the stack's width, as they would in normal flow, and the stack never
+grows, shrinks or reorders them.
+
+### Recommended uses
+
+- Form fields, article content, card content, sidebar navigation links.
+- Page sections: `<main class="container stack-lg">`.
+
+### Common misuses
+
+- Adding margins to children to space them. The gap already does this.
+- Using a stack for items that should sit in a row. Use `.cluster` or `.split`.
+- Expecting a button in a stack to keep its content width. Flex items stretch across
+  the stack; wrap the button in a `.cluster` to keep it narrow.
+
+## `.cluster`
+
+### Purpose
+
+A row of small items that wraps onto more lines when it runs out of space. Items are
+vertically centered against each other.
+
+### HTML example
+
+```html
+<ul class="cluster" role="list">
+  <li><a href="/docs">Docs</a></li>
+  <li><a href="/blog">Blog</a></li>
+  <li><a href="/pricing">Pricing</a></li>
+</ul>
+```
+
+### Variants
+
+`.cluster-sm` (`--space-2`), `.cluster` (`--space-4`), `.cluster-lg` (`--space-6`).
+The gap is the same between items and between wrapped lines.
+
+### Responsive behavior
+
+Items stay in one row while they fit, then wrap (`flex-wrap: wrap`). Items keep their
+own width; they do not stretch.
+
+### Recommended uses
+
+- Tags, badges, chips, button groups, inline navigation, form actions, avatars.
+
+### Common misuses
+
+- Card grids. Use `.grid`, which gives equal-width columns.
+- Pushing items to opposite ends. Use `.split`.
+- Putting long text paragraphs in it. Each item is only as wide as its content.
+
+## `.grid`
+
+### Purpose
+
+Equal-width columns. It fits as many columns as possible, each at least `--grid-min`
+wide, and drops columns as space gets smaller.
+
+### HTML example
+
+```html
+<ul class="grid" role="list">
+  <li class="card">…</li>
+  <li class="card">…</li>
+  <li class="card">…</li>
+</ul>
+```
+
+### Variants
+
+`.grid-sm` (`--space-2`), `.grid` (`--space-4`), `.grid-lg` (`--space-6`).
+
+### Responsive behavior
+
+`grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min), 100%), 1fr))`.
+With the default `--grid-min` of 16rem it shows one column on a 375px phone and three or
+four on a 1280px desktop, depending on the space around the grid. `min(…, 100%)` means a column is never wider than the grid, so
+it never overflows a narrow parent. Columns stretch to fill the row; with fewer items
+than columns, the items share the full width.
+
+### Recommended uses
+
+- Card lists, product tiles, feature lists, dashboards of stat panels, image galleries.
+- Change the column size for one grid with `style="--grid-min: 12rem"`.
+
+### Common misuses
+
+- Asking for an exact number of columns. The grid chooses the count from the space;
+  change `--grid-min` instead.
+- Placing items with `grid-column` or `grid-row`. That breaks DOM order.
+- Using it for a sidebar layout with unequal columns. Use `.sidebar`.
+
+## `.sidebar`
+
+### Purpose
+
+Two children side by side: a narrow sidebar (first child) and a main area (second
+child) that takes the rest of the space. When there is not enough room, they stack.
+
+### HTML example
+
+```html
+<div class="sidebar">
+  <nav class="stack">…</nav>
+  <main class="stack">…</main>
+</div>
+```
+
+### Variants
+
+`.sidebar-sm` (`--space-2`), `.sidebar` (`--space-4`), `.sidebar-lg` (`--space-6`).
+
+### Responsive behavior
+
+The first child has a basis of `--sidebar-width`. The second child grows to fill the
+row and must be at least 50% of the layout wide. When both no longer fit on one row,
+`flex-wrap` puts the main area under the sidebar and both become full width. This
+happens from the layout's own width, so a `.sidebar` inside a narrow column stacks
+even on a wide screen. There are no media queries. The sidebar always comes first, in
+the DOM and on screen.
+
+### Recommended uses
+
+- Settings pages and docs with a navigation list next to the content.
+- An image or avatar next to text (media object).
+- A filter panel next to search results.
+
+### Common misuses
+
+- Putting more than two children in it. Wrap extra content in one of the two children.
+- Putting the sidebar second in the DOM to show it on the right. Order is never
+  changed; the first child is always the sidebar.
+- Setting a fixed `width` on the main area. It sizes itself.
+
+## `.split`
+
+### Purpose
+
+Two groups pushed to opposite ends of a row, for example a title on the left and
+actions on the right.
+
+### HTML example
+
+```html
+<header class="split">
+  <a href="/">Brand</a>
+  <nav class="cluster">…</nav>
+</header>
+```
+
+### Variants
+
+`.split-sm` (`--space-2`), `.split` (`--space-4`), `.split-lg` (`--space-6`).
+The gap is the minimum space between the groups.
+
+### Responsive behavior
+
+`justify-content: space-between` places the first child at the start and the last at
+the end; items are vertically centered. When the groups do not fit on one row, the
+second wraps under the first and starts at the left edge. It never overflows.
+
+### Recommended uses
+
+- Site headers, card headers (title and menu), table toolbars, footers (copyright and
+  links), "label ... value" rows.
+
+### Common misuses
+
+- Using it for more than two or three children. Space is shared between every child;
+  group items into a `.cluster` first.
+- Using it for equal columns. Use `.grid`.
+
+## `.center`
+
+### Purpose
+
+A readable column of content that is never wider than `--content-width` and is centered
+horizontally in its parent.
+
+### HTML example
+
+```html
+<article class="center stack">
+  <h1>Release notes</h1>
+  <p>…</p>
+</article>
+```
+
+### Variants
+
+None. Override `--content-width` to change the width.
+
+### Responsive behavior
+
+Narrower than `--content-width`, it fills its parent. Wider, it stops at
+`--content-width` and is centered with `margin-inline: auto`. It uses
+`box-sizing: content-box`, so padding you add is outside the measure and the text
+column keeps its width. It centers the box, not the text inside it.
+
+### Recommended uses
+
+- Articles, blog posts, documentation pages, sign-in forms, empty states.
+- Inside a `.container`, to give long text a comfortable line length.
+
+### Common misuses
+
+- Expecting it to center text. Add `text-align: center` yourself if you want that.
+- Expecting it to center vertically. Use `.cover`.
+- Adding side padding without accounting for it; the box gets wider by the padding.
+
+## `.cover`
+
+### Purpose
+
+A section at least as tall as the viewport with its main content vertically centered,
+and optional content at the top and bottom.
+
+### HTML example
+
+```html
+<section class="cover">
+  <header>…</header>
+  <div class="cover-main center stack">
+    <h1>Welcome</h1>
+    <a href="/start">Get started</a>
+  </div>
+  <footer>…</footer>
+</section>
+```
+
+### Variants
+
+None. Mark the centered child with `.cover-main`. If the cover has only one child, that
+child is centered without the class.
+
+### Responsive behavior
+
+`min-block-size: 100vh`, and `100dvh` in browsers that support it, so mobile browser
+toolbars do not cut it off. It has no fixed height, so it grows if the content is
+taller than the screen. `.cover-main` gets `margin-block: auto`, which centers it
+vertically; other children sit at the top and bottom, separated by `--space-4`.
+
+### Recommended uses
+
+- Landing page heroes, sign-in pages, error and empty-state pages, splash screens.
+
+### Common misuses
+
+- Setting a fixed `height`. Content then overflows on small screens.
+- Putting more than one `.cover-main` in it.
+- Using it for horizontal centering. Combine it with `.center` for that.
+
+## Nesting
+
+Primitives compose. Each controls only the space between its own direct children, so
+nested primitives do not leak gaps or margins into each other:
+
+```html
+<div class="sidebar-lg">
+  <nav class="stack-sm">…</nav>
+  <main class="container stack">
+    <header class="split">
+      <h1>Reports</h1>
+      <div class="cluster-sm"><button>Export</button><button>New</button></div>
+    </header>
+    <ul class="grid" role="list">
+      <li class="stack-sm">…</li>
+      <li class="stack-sm">…</li>
+    </ul>
+  </main>
+</div>
+```
+
+## Verification
+
+- `npm test` runs [`scripts/check-layout.mjs`](../scripts/check-layout.mjs). It needs only
+  Node.js and checks that every primitive and every `-sm`/`-lg` variant is defined, that
+  no other classes, media queries or order-changing properties exist, that spacing and
+  sizes use `var(--token)` values defined in `tokens.css`, that `src/synthcss.css`
+  bundles the file, that this page covers every primitive with all six sections and the
+  vocabulary table, and that the fixture uses every class.
+- [`examples/layout.html`](../examples/layout.html) shows every primitive and variant in
+  a 375px frame and in a full-width frame, plus a nested example. Open it in a browser
+  and also resize the window to about 375px and about 1280px wide (or use device
+  emulation in the browser developer tools). Frames can be resized by dragging their
+  bottom-right corner.
+- `npm run check:layout:browser` loads the fixture in headless Chromium at 375px and
+  1280px and checks that the grid drops columns, the sidebar stacks, the cluster and
+  split wrap and nothing overflows horizontally. It needs Playwright, which is not a
+  dependency of this repository: run `npm install --no-save playwright` and
+  `npx playwright install chromium` first.
diff --git a/docs/tokens.md b/docs/tokens.md
index 95e1577..9a53fcb 100644
--- a/docs/tokens.md
+++ b/docs/tokens.md
@@ -102,8 +102,10 @@ Subtle and low-opacity. Use them for elevation only, not for decoration.
 | --- | --- | --- |
 | `--control-height` | `2.5rem` | Height of buttons and other single-line controls. |
 | `--input-height` | `2.5rem` | Height of text inputs and selects. Keep it equal to `--control-height` so they line up. |
-| `--container-width` | `72rem` | Maximum width of the main page container. |
-| `--content-width` | `42rem` | Maximum width of readable text blocks, about 65–75 characters per line. |
+| `--container-width` | `72rem` | Maximum width of the main page container (`.container`). |
+| `--content-width` | `42rem` | Maximum width of readable text blocks, about 65–75 characters per line. Used by `.center`. |
+| `--sidebar-width` | `16rem` | Preferred width of the first child of a `.sidebar` layout before it wraps. |
+| `--grid-min` | `16rem` | Minimum column width of a `.grid` before it drops a column. |
 
 ### Focus
 
diff --git a/examples/layout.html b/examples/layout.html
new file mode 100644
index 0000000..5338634
--- /dev/null
+++ b/examples/layout.html
@@ -0,0 +1,240 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>SynthCSS layout primitives fixture</title>
+  <link rel="stylesheet" href="../src/synthcss.css">
+  <style>
+    /* Demo-only styles: they make boxes visible and never set layout.
+       The fixed frame widths below simulate a phone-sized parent. */
+    body {
+      margin: 0;
+      background: var(--color-background);
+      color: var(--color-text);
+      font-family: var(--font-sans);
+      font-size: var(--text-base);
+      line-height: var(--leading-normal);
+    }
+    h1, h2, h3 { line-height: var(--leading-tight); }
+    h2 { font-size: var(--text-2xl); }
+    h3 { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--color-text-muted); }
+    code { font-family: var(--font-mono); font-size: var(--text-sm); }
+    .demo-box {
+      padding: var(--space-3);
+      background: var(--color-surface);
+      border: var(--border-width) solid var(--border-color);
+      border-radius: var(--radius-md);
+    }
+    .demo-tag {
+      padding: var(--space-1) var(--space-3);
+      background: var(--color-surface);
+      border: var(--border-width) solid var(--border-color);
+      border-radius: var(--radius-full);
+      font-size: var(--text-sm);
+    }
+    .demo-button {
+      height: var(--control-height);
+      padding: 0 var(--space-4);
+      border: 0;
+      border-radius: var(--radius-md);
+      background: var(--color-primary);
+      color: var(--color-on-primary);
+      font: inherit;
+    }
+    /* Frames: every primitive is shown in a narrow (375px) and a wide parent.
+       Frames can be resized horizontally to watch the layout respond. */
+    .demo-frame {
+      box-sizing: border-box;
+      padding: var(--space-2);
+      border: var(--border-width) dashed var(--color-text-muted);
+      border-radius: var(--radius-sm);
+      overflow: auto;
+      resize: horizontal;
+    }
+    .demo-frame-narrow { inline-size: 375px; max-inline-size: 100%; }
+    .demo-frame-wide { inline-size: 100%; }
+    .demo-outline { outline: var(--border-width) dashed var(--color-primary); }
+    /* The real .cover is at least one viewport tall. Inside the small demo
+       frames it is limited so the page stays readable. */
+    .demo-frame .cover { min-block-size: 16rem; }
+  </style>
+</head>
+<body>
+  <main class="container stack-lg">
+    <header class="stack-sm">
+      <h1>SynthCSS layout primitives</h1>
+      <p>Every primitive and variant, in a narrow frame (375px) and in a wide frame.
+        Open this page at about 375px and about 1280px viewport width as well.
+        Reference: <a href="../docs/layout.md">docs/layout.md</a>.</p>
+    </header>
+
+    <!-- ===================================================== container -->
+    <section class="stack" id="container">
+      <h2><code>.container</code></h2>
+      <p>The page you are reading is inside a <code>.container</code>. Below, the outline is the container and the box is its content.</p>
+      <h3>Narrow</h3>
+      <div class="demo-frame demo-frame-narrow" data-fixture="container-narrow">
+        <div class="container demo-outline"><div class="demo-box">Full width with fluid side padding.</div></div>
+      </div>
+      <h3>Wide</h3>
+      <div class="demo-frame demo-frame-wide" data-fixture="container-wide">
+        <div class="container demo-outline"><div class="demo-box">Capped at <code>--container-width</code> and centered.</div></div>
+      </div>
+    </section>
+
+    <!-- ========================================================= stack -->
+    <section class="stack" id="stack">
+      <h2><code>.stack</code>, <code>.stack-sm</code>, <code>.stack-lg</code></h2>
+      <h3>Narrow</h3>
+      <div class="demo-frame demo-frame-narrow cluster" data-fixture="stack-narrow">
+        <div class="stack-sm"><div class="demo-box">sm</div><div class="demo-box">sm</div><div class="demo-box">sm</div></div>
+        <div class="stack"><div class="demo-box">default</div><div class="demo-box">default</div><div class="demo-box">default</div></div>
+        <div class="stack-lg"><div class="demo-box">lg</div><div class="demo-box">lg</div><div class="demo-box">lg</div></div>
+      </div>
+      <h3>Wide</h3>
+      <div class="demo-frame demo-frame-wide" data-fixture="stack-wide">
+        <article class="stack">
+          <h4>Article title</h4>
+          <p>Paragraph margins are removed; only the stack gap separates children.</p>
+          <p>Second paragraph.</p>
+        </article>
+      </div>
+    </section>
+
+    <!-- ======================================================= cluster -->
+    <section class="stack" id="cluster">
+      <h2><code>.cluster</code>, <code>.cluster-sm</code>, <code>.cluster-lg</code></h2>
+      <h3>Narrow (items wrap)</h3>
+      <div class="demo-frame demo-frame-narrow stack" data-fixture="cluster-narrow">
+        <ul class="cluster-sm" role="list" data-check="cluster"><li class="demo-tag">design</li><li class="demo-tag">tokens</li><li class="demo-tag">layout</li><li class="demo-tag">accessibility</li><li class="demo-tag">responsive</li><li class="demo-tag">ai</li></ul>
+        <div class="cluster"><button class="demo-button" type="button">Save</button><button class="demo-button" type="button">Save as draft</button><button class="demo-button" type="button">Cancel</button></div>
+        <div class="cluster-lg"><span class="demo-tag">one</span><span class="demo-tag">two</span><span class="demo-tag">three</span><span class="demo-tag">four</span></div>
+      </div>
+      <h3>Wide (one row)</h3>
+      <div class="demo-frame demo-frame-wide stack" data-fixture="cluster-wide">
+        <ul class="cluster-sm" role="list" data-check="cluster"><li class="demo-tag">design</li><li class="demo-tag">tokens</li><li class="demo-tag">layout</li><li class="demo-tag">accessibility</li><li class="demo-tag">responsive</li><li class="demo-tag">ai</li></ul>
+        <div class="cluster"><button class="demo-button" type="button">Save</button><button class="demo-button" type="button">Save as draft</button><button class="demo-button" type="button">Cancel</button></div>
+        <div class="cluster-lg"><span class="demo-tag">one</span><span class="demo-tag">two</span><span class="demo-tag">three</span><span class="demo-tag">four</span></div>
+      </div>
+    </section>
+
+    <!-- ========================================================== grid -->
+    <section class="stack" id="grid">
+      <h2><code>.grid</code>, <code>.grid-sm</code>, <code>.grid-lg</code></h2>
+      <h3>Narrow (one column)</h3>
+      <div class="demo-frame demo-frame-narrow stack" data-fixture="grid-narrow">
+        <div class="grid" data-check="grid"><div class="demo-box">1</div><div class="demo-box">2</div><div class="demo-box">3</div><div class="demo-box">4</div></div>
+        <div class="grid-sm"><div class="demo-box">sm</div><div class="demo-box">sm</div></div>
+        <div class="grid-lg"><div class="demo-box">lg</div><div class="demo-box">lg</div></div>
+      </div>
+      <h3>Wide (as many columns as fit)</h3>
+      <div class="demo-frame demo-frame-wide stack" data-fixture="grid-wide">
+        <div class="grid" data-check="grid"><div class="demo-box">1</div><div class="demo-box">2</div><div class="demo-box">3</div><div class="demo-box">4</div></div>
+        <div class="grid-sm"><div class="demo-box">sm</div><div class="demo-box">sm</div><div class="demo-box">sm</div><div class="demo-box">sm</div></div>
+        <div class="grid-lg"><div class="demo-box">lg</div><div class="demo-box">lg</div><div class="demo-box">lg</div><div class="demo-box">lg</div></div>
+      </div>
+    </section>
+
+    <!-- ======================================================= sidebar -->
+    <section class="stack" id="sidebar">
+      <h2><code>.sidebar</code>, <code>.sidebar-sm</code>, <code>.sidebar-lg</code></h2>
+      <h3>Narrow (stacks, sidebar first)</h3>
+      <div class="demo-frame demo-frame-narrow stack" data-fixture="sidebar-narrow">
+        <div class="sidebar" data-check="sidebar"><nav class="demo-box">Sidebar</nav><div class="demo-box">Main content grows to fill the rest of the row.</div></div>
+        <div class="sidebar-sm"><aside class="demo-box">sm</aside><div class="demo-box">Main</div></div>
+        <div class="sidebar-lg"><aside class="demo-box">lg</aside><div class="demo-box">Main</div></div>
+      </div>
+      <h3>Wide (side by side)</h3>
+      <div class="demo-frame demo-frame-wide stack" data-fixture="sidebar-wide">
+        <div class="sidebar" data-check="sidebar"><nav class="demo-box">Sidebar</nav><div class="demo-box">Main content grows to fill the rest of the row.</div></div>
+        <div class="sidebar-sm"><aside class="demo-box">sm</aside><div class="demo-box">Main</div></div>
+        <div class="sidebar-lg"><aside class="demo-box">lg</aside><div class="demo-box">Main</div></div>
+      </div>
+    </section>
+
+    <!-- ========================================================= split -->
+    <section class="stack" id="split">
+      <h2><code>.split</code>, <code>.split-sm</code>, <code>.split-lg</code></h2>
+      <h3>Narrow (wraps)</h3>
+      <div class="demo-frame demo-frame-narrow stack" data-fixture="split-narrow">
+        <header class="split" data-check="split"><strong class="demo-box">SynthCSS Dashboard</strong><nav class="cluster-sm"><a href="#split">Docs</a><a href="#split">Pricing</a><a href="#split">Sign in</a></nav></header>
+        <div class="split-sm"><span class="demo-box">Left</span><span class="demo-box">Right</span></div>
+        <div class="split-lg"><span class="demo-box">Left</span><span class="demo-box">Right</span></div>
+      </div>
+      <h3>Wide (ends of one row)</h3>
+      <div class="demo-frame demo-frame-wide stack" data-fixture="split-wide">
+        <header class="split" data-check="split"><strong class="demo-box">SynthCSS Dashboard</strong><nav class="cluster-sm"><a href="#split">Docs</a><a href="#split">Pricing</a><a href="#split">Sign in</a></nav></header>
+        <div class="split-sm"><span class="demo-box">Left</span><span class="demo-box">Right</span></div>
+        <div class="split-lg"><span class="demo-box">Left</span><span class="demo-box">Right</span></div>
+      </div>
+    </section>
+
+    <!-- ======================================================== center -->
+    <section class="stack" id="center">
+      <h2><code>.center</code></h2>
+      <h3>Narrow (fills the parent)</h3>
+      <div class="demo-frame demo-frame-narrow" data-fixture="center-narrow">
+        <div class="center demo-box">A readable column. It is never wider than <code>--content-width</code> and sits in the middle of its parent.</div>
+      </div>
+      <h3>Wide (capped and centered)</h3>
+      <div class="demo-frame demo-frame-wide" data-fixture="center-wide">
+        <div class="center demo-box">A readable column. It is never wider than <code>--content-width</code> and sits in the middle of its parent.</div>
+      </div>
+    </section>
+
+    <!-- ========================================================= cover -->
+    <section class="stack" id="cover">
+      <h2><code>.cover</code></h2>
+      <p>In these frames the cover is limited to 16rem tall; on a real page it is at least one viewport tall.</p>
+      <h3>Narrow</h3>
+      <div class="demo-frame demo-frame-narrow" data-fixture="cover-narrow">
+        <section class="cover demo-outline">
+          <header class="demo-box">Header</header>
+          <div class="cover-main demo-box">Centered with <code>.cover-main</code></div>
+          <footer class="demo-box">Footer</footer>
+        </section>
+      </div>
+      <h3>Wide (only child is centered)</h3>
+      <div class="demo-frame demo-frame-wide" data-fixture="cover-wide">
+        <section class="cover demo-outline">
+          <div class="center demo-box">Only child: centered vertically, and horizontally by <code>.center</code>.</div>
+        </section>
+      </div>
+    </section>
+
+    <!-- ======================================================== nested -->
+    <section class="stack" id="nested">
+      <h2>Nested: <code>.stack</code> in <code>.grid</code> in <code>.container</code> in <code>.sidebar</code></h2>
+      <h3>Narrow</h3>
+      <div class="demo-frame demo-frame-narrow" data-fixture="nested-narrow">
+        <div class="sidebar-lg">
+          <nav class="stack-sm"><a href="#nested">Overview</a><a href="#nested">Reports</a><a href="#nested">Settings</a></nav>
+          <div class="container stack demo-outline">
+            <div class="split"><h4>Reports</h4><div class="cluster-sm"><button class="demo-button" type="button">Export</button><button class="demo-button" type="button">New</button></div></div>
+            <ul class="grid-sm" role="list">
+              <li class="stack-sm demo-box"><h5>Revenue</h5><p>Up 12% this month.</p></li>
+              <li class="stack-sm demo-box"><h5>Users</h5><p>4,210 active.</p></li>
+              <li class="stack-sm demo-box"><h5>Churn</h5><p>Down to 1.8%.</p></li>
+            </ul>
+          </div>
+        </div>
+      </div>
+      <h3>Wide</h3>
+      <div class="demo-frame demo-frame-wide" data-fixture="nested-wide">
+        <div class="sidebar-lg">
+          <nav class="stack-sm"><a href="#nested">Overview</a><a href="#nested">Reports</a><a href="#nested">Settings</a></nav>
+          <div class="container stack demo-outline">
+            <div class="split"><h4>Reports</h4><div class="cluster-sm"><button class="demo-button" type="button">Export</button><button class="demo-button" type="button">New</button></div></div>
+            <ul class="grid-sm" role="list">
+              <li class="stack-sm demo-box"><h5>Revenue</h5><p>Up 12% this month.</p></li>
+              <li class="stack-sm demo-box"><h5>Users</h5><p>4,210 active.</p></li>
+              <li class="stack-sm demo-box"><h5>Churn</h5><p>Down to 1.8%.</p></li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </section>
+  </main>
+</body>
+</html>
diff --git a/package.json b/package.json
index 7ca26cd..e97a08e 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
   },
   "scripts": {
     "check:tokens": "node scripts/check-tokens.mjs",
-    "test": "node scripts/check-tokens.mjs && node --test scripts/check-tokens.test.mjs"
+    "check:layout": "node scripts/check-layout.mjs",
+    "check:layout:browser": "node scripts/check-layout-browser.mjs",
+    "test": "node scripts/check-tokens.mjs && node scripts/check-layout.mjs && node --test scripts/check-tokens.test.mjs scripts/check-layout.test.mjs"
   }
 }
diff --git a/scripts/check-layout-browser.mjs b/scripts/check-layout-browser.mjs
new file mode 100644
index 0000000..1bb83c8
--- /dev/null
+++ b/scripts/check-layout-browser.mjs
@@ -0,0 +1,128 @@
+#!/usr/bin/env node
+// Optional browser check of examples/layout.html at 375px and 1280px viewport width.
+// Needs Playwright, which is not a dependency of this repository:
+//   npm install --no-save playwright && npx playwright install chromium
+// Usage: node scripts/check-layout-browser.mjs
+
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { dirname, resolve } from "node:path";
+
+let chromium;
+try {
+  ({ chromium } = await import("playwright"));
+} catch {
+  console.error(
+    "check-layout-browser: Playwright is not installed.\n" +
+      "Run `npm install --no-save playwright && npx playwright install chromium`, then try again.",
+  );
+  process.exit(2);
+}
+
+const repo = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const fixtureUrl = pathToFileURL(resolve(repo, "examples/layout.html")).href;
+
+// Runs in the page. Returns measurements for one frame.
+function measure(name) {
+  const frame = document.querySelector(`[data-fixture="${name}"]`);
+  const rect = (el) => el.getBoundingClientRect();
+  const tops = (el) => [...el.children].map((c) => Math.round(rect(c).top));
+  const one = (sel) => frame.querySelector(`[data-check="${sel}"]`);
+  const out = { overflow: frame.scrollWidth > frame.clientWidth };
+  const grid = one("grid");
+  if (grid) out.gridColumns = getComputedStyle(grid).gridTemplateColumns.split(" ").length;
+  const sidebar = one("sidebar");
+  if (sidebar) {
+    const [a, b] = [...sidebar.children].map(rect);
+    out.sidebarStacked = b.top >= a.bottom;
+    out.sidebarFirstIsSidebar = sidebar.children[0].tagName === "NAV" && (b.top >= a.bottom || a.left < b.left);
+  }
+  const cluster = one("cluster");
+  if (cluster) out.clusterRows = new Set(tops(cluster)).size;
+  const split = one("split");
+  if (split) {
+    const [a, b] = [...split.children].map(rect);
+    out.splitWrapped = b.top >= a.bottom;
+    out.splitEnds = Math.abs(rect(split).right - b.right) < 1 && Math.abs(rect(split).left - a.left) < 1;
+  }
+  const cover = frame.querySelector(".cover");
+  if (cover) {
+    const main = cover.querySelector(":scope > .cover-main") ?? cover.firstElementChild;
+    const c = rect(cover);
+    const m = rect(main);
+    out.coverCentered = Math.abs((c.top + c.bottom) / 2 - (m.top + m.bottom) / 2) < 2;
+  }
+  return out;
+}
+
+function nestedMeasure(name) {
+  const frame = document.querySelector(`[data-fixture="${name}"]`);
+  const ps = [...frame.querySelectorAll(".grid-sm > .stack-sm > *")];
+  const grid = frame.querySelector(".grid-sm");
+  return {
+    overflow: frame.scrollWidth > frame.clientWidth,
+    childMargins: ps.every((p) => {
+      const s = getComputedStyle(p);
+      return s.marginTop === "0px" && s.marginBottom === "0px";
+    }),
+    gridGap: getComputedStyle(grid).rowGap,
+    stackGap: getComputedStyle(ps[0].parentElement).rowGap,
+    gridColumns: getComputedStyle(grid).gridTemplateColumns.split(" ").length,
+  };
+}
+
+const errors = [];
+const expect = (ok, msg) => {
+  if (!ok) errors.push(msg);
+};
+
+const browser = await chromium.launch();
+try {
+  for (const width of [375, 1280]) {
+    const page = await browser.newPage({ viewport: { width, height: 800 } });
+    await page.goto(fixtureUrl);
+    const docOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth);
+    expect(!docOverflow, `${width}px: page overflows horizontally`);
+
+    for (const size of ["narrow", "wide"]) {
+      const narrow = size === "narrow" || width < 600;
+      const at = `${width}px ${size} frame`;
+      for (const p of ["container", "stack", "cluster", "grid", "sidebar", "split", "center", "cover"]) {
+        const m = await page.evaluate(measure, `${p}-${size}`);
+        expect(!m.overflow, `${at}: .${p} overflows horizontally`);
+        if (p === "grid") {
+          if (narrow) expect(m.gridColumns === 1, `${at}: .grid has ${m.gridColumns} columns, expected 1`);
+          else expect(m.gridColumns >= 3, `${at}: .grid has ${m.gridColumns} columns, expected 3 or more`);
+        }
+        if (p === "sidebar") {
+          expect(m.sidebarStacked === narrow, `${at}: .sidebar ${narrow ? "should" : "should not"} stack`);
+          expect(m.sidebarFirstIsSidebar, `${at}: .sidebar changed visual order`);
+        }
+        if (p === "cluster") {
+          if (narrow) expect(m.clusterRows > 1, `${at}: .cluster did not wrap`);
+          else expect(m.clusterRows === 1, `${at}: .cluster wrapped (${m.clusterRows} rows)`);
+        }
+        if (p === "split") {
+          expect(m.splitWrapped === narrow, `${at}: .split ${narrow ? "should" : "should not"} wrap`);
+          if (!narrow) expect(m.splitEnds, `${at}: .split items are not at opposite ends`);
+        }
+        if (p === "cover") expect(m.coverCentered, `${at}: .cover main child is not vertically centered`);
+      }
+      const n = await page.evaluate(nestedMeasure, `nested-${size}`);
+      expect(!n.overflow, `${at}: nested example overflows horizontally`);
+      expect(n.childMargins, `${at}: nested children have block margins`);
+      expect(n.gridGap === "8px" && n.stackGap === "8px", `${at}: nested gaps leaked (grid ${n.gridGap}, stack ${n.stackGap})`);
+      if (narrow) expect(n.gridColumns === 1, `${at}: nested grid has ${n.gridColumns} columns, expected 1`);
+      else expect(n.gridColumns >= 2, `${at}: nested grid has ${n.gridColumns} columns, expected 2 or more`);
+    }
+    await page.close();
+  }
+} finally {
+  await browser.close();
+}
+
+if (errors.length) {
+  for (const e of errors) console.error(`  FAIL ${e}`);
+  console.error(`\ncheck-layout-browser: ${errors.length} problem(s) found.`);
+  process.exit(1);
+}
+console.log("check-layout-browser: layouts respond correctly at 375px and 1280px with no horizontal overflow.");
diff --git a/scripts/check-layout.mjs b/scripts/check-layout.mjs
new file mode 100644
index 0000000..bc6eae3
--- /dev/null
+++ b/scripts/check-layout.mjs
@@ -0,0 +1,208 @@
+#!/usr/bin/env node
+// Dependency-free verification of the layout primitives in src/layout.css,
+// the main bundle, docs/layout.md and the examples/layout.html fixture.
+// Usage: node scripts/check-layout.mjs
+
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, resolve } from "node:path";
+import { parseBlocks, parseDeclarations, parseTokens } from "./check-tokens.mjs";
+
+export const PRIMITIVES = ["container", "stack", "cluster", "grid", "sidebar", "split", "center", "cover"];
+export const GAP_PRIMITIVES = ["stack", "cluster", "grid", "sidebar", "split"];
+export const VARIANTS = GAP_PRIMITIVES.flatMap((p) => [`${p}-sm`, `${p}-lg`]);
+export const HELPER_CLASSES = ["cover-main"];
+export const DOC_SECTIONS = [
+  "Purpose",
+  "HTML example",
+  "Variants",
+  "Responsive behavior",
+  "Recommended uses",
+  "Common misuses",
+];
+
+const SPACING_PROPS = /^(gap|row-gap|column-gap|padding|padding-[\w-]+|margin|margin-[\w-]+)$/;
+// Units that would mean a hard-coded size. %, vw, vh and dvh are relative and allowed.
+const HARD_CODED_LENGTH = /(?:^|[^\w-])(-?\d*\.?\d+)(px|rem|em|ch|ex|pt|pc|cm|mm|in|q|vmin|vmax|svh|lvh|svw|lvw|dvw)\b/i;
+const REORDERING_PROPS = /^(order|grid-area|grid-row|grid-row-start|grid-row-end|grid-column|grid-column-start|grid-column-end|direction)$/;
+
+const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, "");
+
+export function parseRules(rawCss) {
+  const rules = [];
+  const atRules = [];
+  for (const { prelude, body } of parseBlocks(stripComments(rawCss))) {
+    if (prelude.startsWith("@")) {
+      atRules.push(prelude);
+      continue;
+    }
+    const classes = new Set([...prelude.matchAll(/\.([a-zA-Z][\w-]*)/g)].map((m) => m[1]));
+    // Selectors that are just ".name", for the "class exists" check.
+    const plain = new Set(
+      prelude
+        .split(",")
+        .map((s) => s.trim())
+        .filter((s) => /^\.[a-zA-Z][\w-]*$/.test(s))
+        .map((s) => s.slice(1)),
+    );
+    const decls = parseDeclarations(body).filter((d) => d.prop);
+    rules.push({ selector: prelude.replace(/\s+/g, " "), classes, plain, decls });
+  }
+  return { rules, atRules };
+}
+
+export function checkLayoutCss(layoutCss, tokensCss) {
+  const errors = [];
+  let parsed;
+  try {
+    parsed = parseRules(layoutCss);
+  } catch (err) {
+    return [`layout.css: ${err.message}`];
+  }
+  const { rules, atRules } = parsed;
+  const tokens = parseTokens(tokensCss).root;
+
+  for (const at of atRules) {
+    errors.push(`layout.css must not use at-rules such as media queries or breakpoints, found: ${at}`);
+  }
+
+  const allowed = new Set([...PRIMITIVES, ...VARIANTS, ...HELPER_CLASSES]);
+  const used = new Set(rules.flatMap((r) => [...r.classes]));
+  for (const cls of [...PRIMITIVES, ...VARIANTS]) {
+    if (!rules.some((r) => r.plain.has(cls))) errors.push(`class .${cls} is not defined in layout.css`);
+  }
+  for (const cls of used) {
+    if (!allowed.has(cls)) errors.push(`unexpected class .${cls}; only the primitives and their -sm/-lg gap variants are allowed`);
+  }
+
+  // Each variant must set its own gap from a spacing token, different from the base.
+  const gapOf = (cls) => {
+    let value;
+    for (const r of rules) if (r.plain.has(cls)) for (const d of r.decls) if (d.prop === "gap") value = d.value;
+    return value;
+  };
+  for (const p of GAP_PRIMITIVES) {
+    const base = gapOf(p);
+    if (!base || !/^var\(--space-\d+\)$/.test(base)) errors.push(`.${p} must set gap to a var(--space-N) token`);
+    for (const v of [`${p}-sm`, `${p}-lg`]) {
+      const gap = gapOf(v);
+      if (!gap || !/^var\(--space-\d+\)$/.test(gap)) errors.push(`.${v} must set gap to a var(--space-N) token`);
+      else if (gap === base) errors.push(`.${v} uses the same gap as .${p}`);
+    }
+    const n = (v) => Number(/--space-(\d+)/.exec(gapOf(v) ?? "")?.[1]);
+    if (!(n(`${p}-sm`) < n(p) && n(p) < n(`${p}-lg`))) errors.push(`.${p}-sm < .${p} < .${p}-lg gap order is wrong`);
+  }
+
+  for (const { selector, decls } of rules) {
+    for (const { prop, value } of decls) {
+      const where = `${selector} { ${prop}: ${value} }`;
+      if (REORDERING_PROPS.test(prop)) errors.push(`reading-order-changing property in ${where}`);
+      if (/-reverse\b/.test(value) || /\bdense\b/.test(value)) errors.push(`reading-order-changing value in ${where}`);
+      if (HARD_CODED_LENGTH.test(value)) errors.push(`hard-coded length in ${where}; use a var(--token)`);
+      if (SPACING_PROPS.test(prop) && !/^(0|auto)$/.test(value) && !/var\(--space-\d+\)/.test(value)) {
+        errors.push(`spacing in ${where} must use a var(--space-N) token`);
+      }
+      for (const m of value.matchAll(/var\(\s*(--[\w-]+)/g)) {
+        if (!tokens.has(m[1])) errors.push(`${where} references ${m[1]}, which is not defined in tokens.css`);
+      }
+    }
+  }
+  return errors;
+}
+
+export function checkBundle(bundleCss) {
+  const imports = [...stripComments(bundleCss).matchAll(/@import\s+(?:url\()?\s*["']?([^"')\s]+)/g)].map((m) => m[1]);
+  const errors = [];
+  const t = imports.indexOf("tokens.css");
+  const l = imports.indexOf("layout.css");
+  if (t === -1) errors.push("src/synthcss.css does not import tokens.css");
+  if (l === -1) errors.push("src/synthcss.css does not import layout.css");
+  if (t !== -1 && l !== -1 && t > l) errors.push("src/synthcss.css must import tokens.css before layout.css");
+  return errors;
+}
+
+export function checkDocs(markdown) {
+  const errors = [];
+  const lines = markdown.split("\n");
+  const sectionOf = (cls) => {
+    const start = lines.findIndex((l) => new RegExp(`^##\\s+\`\\.${cls}\`\\s*$`).test(l));
+    if (start === -1) return null;
+    let end = lines.findIndex((l, i) => i > start && /^##\s/.test(l));
+    if (end === -1) end = lines.length;
+    return lines.slice(start + 1, end);
+  };
+  for (const cls of PRIMITIVES) {
+    const section = sectionOf(cls);
+    if (!section) {
+      errors.push(`docs/layout.md has no "## \`.${cls}\`" section`);
+      continue;
+    }
+    for (const name of DOC_SECTIONS) {
+      if (!section.some((l) => new RegExp(`^###\\s+${name}\\s*$`, "i").test(l))) {
+        errors.push(`docs/layout.md section .${cls} is missing "### ${name}"`);
+      }
+    }
+    if (!section.join("\n").includes("```html")) errors.push(`docs/layout.md section .${cls} has no html code block`);
+  }
+  const vocabStart = lines.findIndex((l) => /^##\s+AI Layout Vocabulary\s*$/i.test(l));
+  if (vocabStart === -1) {
+    errors.push('docs/layout.md has no "## AI Layout Vocabulary" section');
+  } else {
+    const rows = [];
+    for (let i = vocabStart + 1; i < lines.length && !/^##\s/.test(lines[i]); i++) {
+      if (/^\s*\|/.test(lines[i])) rows.push(lines[i]);
+    }
+    for (const cls of PRIMITIVES) {
+      if (!rows.some((r) => r.includes(`\`.${cls}\``) || r.includes(`\`${cls}\``))) {
+        errors.push(`AI Layout Vocabulary table does not mention .${cls}`);
+      }
+    }
+  }
+  return errors;
+}
+
+export function checkFixture(html) {
+  const errors = [];
+  if (!/href="[^"]*synthcss\.css"/.test(html)) errors.push("examples/layout.html must load src/synthcss.css");
+  const classes = new Set([...html.matchAll(/class="([^"]*)"/g)].flatMap((m) => m[1].split(/\s+/)));
+  for (const cls of [...PRIMITIVES, ...VARIANTS, ...HELPER_CLASSES]) {
+    if (!classes.has(cls)) errors.push(`examples/layout.html does not use .${cls}`);
+  }
+  if (/<script\b/i.test(html)) errors.push("examples/layout.html must not use JavaScript");
+  return errors;
+}
+
+export function checkLayout({ layoutCss, tokensCss, bundleCss, docs, fixture }) {
+  return [
+    ...checkLayoutCss(layoutCss, tokensCss),
+    ...checkBundle(bundleCss),
+    ...checkDocs(docs),
+    ...checkFixture(fixture),
+  ];
+}
+
+export function readRepoFiles(repo) {
+  const read = (p) => readFileSync(resolve(repo, p), "utf8");
+  return {
+    layoutCss: read("src/layout.css"),
+    tokensCss: read("src/tokens.css"),
+    bundleCss: read("src/synthcss.css"),
+    docs: read("docs/layout.md"),
+    fixture: read("examples/layout.html"),
+  };
+}
+
+const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
+
+if (isMain) {
+  const repo = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+  const errors = checkLayout(readRepoFiles(repo));
+  if (errors.length) {
+    for (const e of errors) console.error(`  FAIL ${e}`);
+    console.error(`\ncheck-layout: ${errors.length} problem(s) found.`);
+    process.exit(1);
+  }
+  console.log(
+    `check-layout: ${PRIMITIVES.length} primitives and ${VARIANTS.length} variants defined with tokens, bundled, documented and in the fixture.`,
+  );
+}
diff --git a/scripts/check-layout.test.mjs b/scripts/check-layout.test.mjs
new file mode 100644
index 0000000..73b344e
--- /dev/null
+++ b/scripts/check-layout.test.mjs
@@ -0,0 +1,57 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { checkLayout, readRepoFiles } from "./check-layout.mjs";
+
+const repo = new URL("..", import.meta.url).pathname;
+const files = readRepoFiles(repo);
+
+const errorsWith = (changes) => checkLayout({ ...files, ...changes });
+const assertError = (errors, text) => assert.ok(errors.some((e) => e.includes(text)), errors.join("\n"));
+
+test("repository layout files pass", () => {
+  assert.deepEqual(errorsWith({}), []);
+});
+
+test("fails when a primitive is missing", () => {
+  const layoutCss = files.layoutCss.replace(/\.center \{[\s\S]*?\}/, "");
+  assertError(errorsWith({ layoutCss }), "class .center is not defined");
+});
+
+test("fails when a variant is missing or extra variants are added", () => {
+  const missing = files.layoutCss.replace(".split-lg { gap: var(--space-6); }", "");
+  assertError(errorsWith({ layoutCss: missing }), ".split-lg uses the same gap as .split");
+  const extra = files.layoutCss + "\n.container-sm { padding-inline: var(--space-2); }\n";
+  assertError(errorsWith({ layoutCss: extra }), "unexpected class .container-sm");
+});
+
+test("fails on hard-coded spacing and undefined tokens", () => {
+  const hardCoded = files.layoutCss.replace(".stack-sm { gap: var(--space-2); }", ".stack-sm { gap: 8px; }");
+  assertError(errorsWith({ layoutCss: hardCoded }), "hard-coded length");
+  const undefinedToken = files.layoutCss.replace("var(--grid-min)", "var(--grid-minimum)");
+  assertError(errorsWith({ layoutCss: undefinedToken }), "--grid-minimum, which is not defined");
+});
+
+test("fails on media queries and reading-order changes", () => {
+  const media = files.layoutCss + "\n@media (min-width: 40rem) { .grid { gap: var(--space-5); } }\n";
+  assertError(errorsWith({ layoutCss: media }), "must not use at-rules");
+  const order = files.layoutCss.replace("flex-direction: column;", "flex-direction: column-reverse;");
+  assertError(errorsWith({ layoutCss: order }), "reading-order-changing value");
+  const orderProp = files.layoutCss + "\n.sidebar > :first-child { order: 2; }\n";
+  assertError(errorsWith({ layoutCss: orderProp }), "reading-order-changing property");
+});
+
+test("fails when the bundle does not include layout.css", () => {
+  assertError(errorsWith({ bundleCss: '@import url("tokens.css");' }), "does not import layout.css");
+});
+
+test("fails when docs miss a section or the vocabulary table", () => {
+  const noMisuses = files.docs.replace(/(## `\.cover`[\s\S]*?)### Common misuses/, "$1### Pitfalls");
+  assertError(errorsWith({ docs: noMisuses }), 'section .cover is missing "### Common misuses"');
+  const noVocab = files.docs.replace("## AI Layout Vocabulary", "## Vocabulary");
+  assertError(errorsWith({ docs: noVocab }), "AI Layout Vocabulary");
+});
+
+test("fails when the fixture does not use a variant", () => {
+  const fixture = files.fixture.replaceAll("grid-lg", "grid");
+  assertError(errorsWith({ fixture }), "does not use .grid-lg");
+});
diff --git a/scripts/check-tokens.mjs b/scripts/check-tokens.mjs
index ed9c707..ee0c6ed 100644
--- a/scripts/check-tokens.mjs
+++ b/scripts/check-tokens.mjs
@@ -55,6 +55,8 @@ export const REQUIRED_TOKENS = [
   "--input-height",
   "--container-width",
   "--content-width",
+  "--sidebar-width",
+  "--grid-min",
   "--focus-color",
   "--focus-width",
   "--focus-offset",
diff --git a/src/layout.css b/src/layout.css
new file mode 100644
index 0000000..bf5bcbf
--- /dev/null
+++ b/src/layout.css
@@ -0,0 +1,152 @@
+/*
+ * SynthCSS layout primitives
+ *
+ * Eight intent-named layout classes built on Flexbox and Grid. They respond to
+ * the space they are given (intrinsic layout), so they need no media queries
+ * and no breakpoint classes. Every spacing and size value is a design token
+ * from tokens.css.
+ *
+ * Rules:
+ * - Visual order always follows DOM order (no `order`, no `*-reverse`,
+ *   no grid placement).
+ * - Classes work on any element and never select children by tag name.
+ * - Spacing between children comes from `gap` only, so nested primitives do
+ *   not leak margins or gaps into each other.
+ * - The -sm / -lg variants work alone (`stack-sm`) or together with the
+ *   base class (`stack stack-sm`).
+ *
+ * Reference: docs/layout.md
+ */
+
+/* Direct children of gap-based primitives drop their block margins (for
+   example the default margins of <p> and <h2>), so only `gap` sets spacing.
+   :where() keeps this at zero specificity, so any author rule wins. */
+:where(
+    .stack, .stack-sm, .stack-lg,
+    .cluster, .cluster-sm, .cluster-lg,
+    .grid, .grid-sm, .grid-lg,
+    .sidebar, .sidebar-sm, .sidebar-lg,
+    .split, .split-sm, .split-lg,
+    .cover
+  ) > * {
+  margin-block: 0;
+}
+
+/* Lists used as layouts (<ul class="cluster">) lose their bullets and indent.
+   Add role="list" to keep list semantics in Safari/VoiceOver. */
+:where(
+    .stack, .stack-sm, .stack-lg,
+    .cluster, .cluster-sm, .cluster-lg,
+    .grid, .grid-sm, .grid-lg,
+    .split, .split-sm, .split-lg
+  ):where(ul, ol) {
+  list-style: none;
+  padding-inline-start: 0;
+}
+
+/* Container: centered page-width wrapper with fluid side padding. */
+.container {
+  box-sizing: border-box;
+  max-width: var(--container-width);
+  margin-inline: auto;
+  padding-inline: clamp(var(--space-4), 4vw, var(--space-6));
+}
+
+/* Stack: vertical flow of children separated by a gap. */
+.stack,
+.stack-sm,
+.stack-lg {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  gap: var(--space-4);
+}
+
+.stack-sm { gap: var(--space-2); }
+.stack-lg { gap: var(--space-6); }
+
+/* Cluster: a wrapping row of items (tags, buttons, nav links). */
+.cluster,
+.cluster-sm,
+.cluster-lg {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: var(--space-4);
+}
+
+.cluster-sm { gap: var(--space-2); }
+.cluster-lg { gap: var(--space-6); }
+
+/* Grid: as many equal columns as fit, never narrower than --grid-min. */
+.grid,
+.grid-sm,
+.grid-lg {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min), 100%), 1fr));
+  gap: var(--space-4);
+}
+
+.grid-sm { gap: var(--space-2); }
+.grid-lg { gap: var(--space-6); }
+
+/* Sidebar: first child is a fixed-basis sidebar, second child is the main
+   area. They sit side by side while the main area can be at least 50% wide,
+   and wrap into one column (sidebar first) when it cannot. */
+.sidebar,
+.sidebar-sm,
+.sidebar-lg {
+  display: flex;
+  flex-wrap: wrap;
+  gap: var(--space-4);
+}
+
+.sidebar-sm { gap: var(--space-2); }
+.sidebar-lg { gap: var(--space-6); }
+
+:is(.sidebar, .sidebar-sm, .sidebar-lg) > :first-child {
+  flex-basis: var(--sidebar-width);
+  flex-grow: 1;
+}
+
+:is(.sidebar, .sidebar-sm, .sidebar-lg) > :last-child:not(:first-child) {
+  flex-basis: 0;
+  flex-grow: 999;
+  min-inline-size: 50%;
+}
+
+/* Split: items pushed to opposite ends of a row; wraps when tight. */
+.split,
+.split-sm,
+.split-lg {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  align-items: center;
+  gap: var(--space-4);
+}
+
+.split-sm { gap: var(--space-2); }
+.split-lg { gap: var(--space-6); }
+
+/* Center: horizontally centered column with a readable maximum width. */
+.center {
+  box-sizing: content-box;
+  max-inline-size: var(--content-width);
+  margin-inline: auto;
+}
+
+/* Cover: at least one viewport tall; .cover-main (or an only child) is
+   vertically centered, other children sit at the top and bottom. */
+.cover {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-4);
+  min-block-size: 100vh;
+  min-block-size: 100dvh;
+}
+
+.cover > .cover-main,
+.cover > :only-child {
+  margin-block: auto;
+}
diff --git a/src/synthcss.css b/src/synthcss.css
new file mode 100644
index 0000000..20e1cc0
--- /dev/null
+++ b/src/synthcss.css
@@ -0,0 +1,11 @@
+/*
+ * SynthCSS main bundle
+ *
+ * Link this one file to get everything SynthCSS ships:
+ *   <link rel="stylesheet" href="synthcss/src/synthcss.css">
+ *
+ * Tokens must load first: every other file reads them through var(--token).
+ */
+
+@import url("tokens.css");
+@import url("layout.css");
diff --git a/src/tokens.css b/src/tokens.css
index cea0d60..c782488 100644
--- a/src/tokens.css
+++ b/src/tokens.css
@@ -78,6 +78,8 @@
   --input-height: 2.5rem;
   --container-width: 72rem;
   --content-width: 42rem;
+  --sidebar-width: 16rem;
+  --grid-min: 16rem;
 
   /* Focus */
   --focus-color: var(--color-primary);
Acceptance · round 1
Checking
CINo checks
Automated reviewNot run

The vote opens once CI finishes and the automated review has posted its verdict.

CI details
No CI checks ran on this commit.
Discussion · 0

No comments yet.