# Layout primitives

SLASHED ships breakpoint-free, container-query-driven layout primitives in the
`slashed.layout` layer (`core/layout.css`). Each is a single class with
per-instance tokens you can override inline (`style="--sf-stack-gap: …"`).
Tokens are declared in `core/tokens.layout.css`.

All primitives are exercised live in the [demo](/demo/).

## The primitives

| Class | What it does | Key tokens |
|---|---|---|
| `.sf-section` | vertical page rhythm; sizes `--xs/--s/--m/--l/--xl/--2xl` | `--sf-section-pad*` |
| `.sf-section-group` | collapses the gap between adjacent sections | — |
| `.sf-container` | centered max-width wrapper; declares the named `sf-layout` container; `--narrow` | `--sf-container-*`, `--sf-gutter` |
| `.sf-box` | isolated unit with padding and optional border outline | `--sf-box-padding`, `--sf-box-border-width`, `--sf-box-border-color` |
| `.sf-center` | intrinsic centering with max-width and gutters; `--intrinsic` | `--sf-center-max`, `--sf-center-gutter` |
| `.sf-place-center` | box-neutral centring: centres content on both axes (`grid` + `place-items:center`) in a plain element | — |
| `.sf-stack` | vertical flow with consistent gap (the "owl") | `--sf-stack-gap` |
| `.sf-cluster` | wrapping inline group; `--no-wrap` | `--sf-cluster-gap/-align/-justify` |
| `.sf-sidebar` | content + fixed-ish side panel that wraps when narrow | `--sf-sidebar-*` |
| `.sf-switcher` | N columns above a threshold, stacked below; `--no-wrap`, `--vertical` | `--sf-switcher-threshold/-gap` |
| `.sf-grid` | auto-fill responsive grid; `--fit`, `--xs … --2xl`, `--dense` | `--sf-grid-min`, `--sf-grid-gap` |
| `.sf-grid-cols-1 / -2 / -3 / -4 / -6` | fixed-column grids, container-responsive (no `-5`) | `--sf-grid-gap` |
| `.sf-grid-cols-1-2 / -2-1 / -1-3 / -3-1` | ratio two-column grids | `--sf-grid-gap` |
| `.sf-bento` | dense free-form grid; container modifiers `--2/--3/--6`, `--row-compact/--row-tall`; child span classes `.sf-bento-wide/-full/-tall/-featured` | `--sf-bento-*` |
| `.sf-alternate` | zigzag two-column layout, reverses every other row; CQ-responsive | `--sf-content-gap`, `--sf-gap` |
| `.sf-pancake` | sticky-footer grid: header / main(1fr) / footer | — |
| `.sf-content-grid` | breakout layout; children `.sf-breakout`, `.sf-full-bleed`; establishes an inline-size CQ scope (like `.sf-container`) so `.sf-grid-cols-*`/`.sf-bento` still respond when it replaces a container | `--sf-content-width`, `--sf-breakout-width` |
| `.sf-grid-flex` | flex-based grid alternative for uneven item counts; last-row leftovers stretch to fill (default) or stay fixed and centered (`--center`); `--xs … --2xl` | `--sf-grid-min`, `--sf-grid-gap` |
| `.sf-cover` | full-height region with a centered `.sf-cover__center`; `--min/--max/--padding-*` | `--sf-cover-*` |
| `.sf-frame` | aspect-ratio media box | `--sf-frame-ratio` |
| `.sf-bg-layer` | cover media layer behind the parent's content (parent auto-positions + isolates); composes under `.sf-scrim` | `--sf-bg-layer-inset/-fit/-position/-radius/-z` |
| `.sf-reel` | horizontal scroll strip | `--sf-reel-*`, `--sf-mask-scrim-*` |
| `.sf-imposter` | absolutely-centered overlay; `--fixed`, `--contain` | `--sf-imposter-margin` |
| `.sf-subgrid` / `.sf-subgrid-rows` | inherit parent grid tracks | — |
| `.sf-divider` | token-driven separator; `--vertical`, `--soft`, `--strong`, `--dashed`, `--dotted`, `--gradient` | `--sf-divider-*` |
| `.sf-gap` | injects gap into any existing flex/grid container without imposing display; `--xs … --2xl` | `--sf-gap` |
| `.sf-equal` | flowing multi-column layout (CSS `columns`, not grid) — content distributes like a newspaper column instead of sitting in fixed cells; `--2/--3/--4/--6` set a target `column-count` | `--sf-equal-min-col`, `--sf-equal-gap`, `--sf-equal-rule-*` |
| `.sf-section--collapse` | adjacent sections share padding 50/50 across the boundary | `--sf-section-pad` |
| `.sf-icon` | em-based inline icon sizing; `--xs … --2xl` | `--sf-icon-*` |
| `.sf-icon--boxed` | padded, bordered icon frame (content-box sizing) | `--sf-icon-box-pad`, `--sf-icon-box-radius`, `--sf-icon-box-bg`, `--sf-icon-box-border` |

`.sf-bento` has two distinct, non-interchangeable modifier families — a `--`
(double-dash) class always modifies the **container** (`.sf-bento` itself), a
`-` (single-dash) class targets a **child** placed inside it:

| Class | Applies to | Effect |
|---|---|---|
| `.sf-bento--2` / `--3` / `--6` | container | sets the column count |
| `.sf-bento--row-compact` / `--row-tall` | container | sets the row-height tier for **every** auto row in the grid |
| `.sf-bento-wide` | a child | that item spans 2 columns |
| `.sf-bento-full` | a child | that item spans the full row |
| `.sf-bento-tall` | a child | that **one item** spans 2 rows |
| `.sf-bento-featured` | a child | that item spans 2 columns and 2 rows |

`.sf-bento--row-tall` and `.sf-bento-tall` are not aliases: the first
stretches every row in the grid, the second stretches one grid item. Put the
container modifier on `.sf-bento`, the child modifiers on the items inside
it — never on the same element.

## Size-modifier scale -- `xs..2xl` everywhere

Every size-aware primitive supports the canonical range
`--xs --s --m --l --xl --2xl`.

| primitive | what the suffix scales | `--xs` | `--s` | `--m` *(default)* | `--l` | `--xl` | `--2xl` |
|---|---|:-:|:-:|:-:|:-:|:-:|:-:|
| `.sf-stack`   | block-axis gap         | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-cluster` | inline-axis gap        | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-gap`     | layout-agnostic gap    | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-grid` | min column width       | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-grid-flex` | min item width    | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-section` | block padding          | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `.sf-icon`    | font-size              | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

Three suffixes scale different physical dimensions. `.sf-grid--xs` means "narrow
column tier" (denser layouts), not "small gap" — gap on `.sf-grid` is controlled
separately by `--sf-grid-gap`, independent of the column-min modifier. To go beyond
the built-in scale, override the scoped token directly:
`style="--sf-stack-gap: var(--sf-space-3xl)"`. The underlying space tokens
(`--sf-space-2xs`/`-3xl`/`-4xl`) are part of the public token API.

## Why no breakpoints

Primitives respond to **their own container width** via `@container`, not the
viewport. Drop a `.sf-grid-cols-3` inside any width context and it adapts — no media
queries, no breakpoint tokens. See the
[container query contract](architecture.md#container-query-contract) for the
two named containers (`sf-layout`, `sf-alternate`) and when to use them.

## Composing

Primitives nest freely:

```html
<section class="sf-section">
  <div class="sf-container">
    <div class="sf-stack">
      <h1>Title</h1>
      <div class="sf-grid sf-grid--m">
        <article class="sf-stack sf-stack--s">…</article>
        <article class="sf-stack sf-stack--s">…</article>
      </div>
    </div>
  </div>
</section>
```

Override a single instance without new CSS:

```html
<div class="sf-cluster" style="--sf-cluster-gap: var(--sf-space-l)">…</div>
```

## Responsive gaps

Gaps are already fluid: `--sf-grid-gap` defaults to `--sf-gap`, which interpolates
between its mobile and desktop ends via the shared space scale. Retune the whole
rhythm at once with the space-scale endpoints (`--sf-space-base-min` /
`--sf-space-base-max`) rather than per-token knobs.

When one primitive needs a **different** gap on small vs large screens, override
its scoped token inside a `@container` query — the same container-driven model the
primitives themselves use, so the gap reacts to the same width that collapses the
columns. Endpoints stay live tokens, so they still follow any scale retuning:

```css
.product-grid { --sf-grid-gap: var(--sf-space-l); }

@container (min-width: 48rem) {
  .product-grid { --sf-grid-gap: var(--sf-space-xl); }
}
```

```html
<div class="sf-container">
  <div class="sf-grid product-grid">…</div>
</div>
```

The override needs a container ancestor (`.sf-container` or `.sf-cq`) — the same
requirement as `.sf-grid-cols-*`. The gap steps at the breakpoint rather than
interpolating across it; for a gap that single step is imperceptible in normal use.
The same pattern works for any scoped gap token (`--sf-gap`, `--sf-content-gap`,
`--sf-gutter`, `--sf-cluster-gap`, …).

## Container-query scope

The container-responsive primitives (`.sf-grid-cols-*`, `.sf-bento`, and any
`@container`-scoped token override like the one above) resolve against the
nearest ancestor that establishes an inline-size query container. Two wrappers
establish one for you: `.sf-container` and `.sf-content-grid` — so either can
host those primitives directly.

`.sf-center` deliberately does **not**. It's a minimal centring primitive with
no side effects (it doesn't become a containing block for `position: fixed`
descendants or a new stacking context), so it stays composable. When you need a
container-responsive child inside a centred wrapper, add the query scope
explicitly by composing `.sf-cq`:

```html
<div class="sf-center sf-cq">
  <div class="sf-grid-cols-3">…</div>
</div>
```

## Centring: pick the primitive, not a `.center` utility

Coming from a utility framework, `.sf-center` is a false friend. There, a
`.center` class usually *centres the content inside it* (flex `place-items`,
`margin: auto`, `text-align`). In SLASHED that job belongs to
**`.sf-place-center`**; `.sf-center` is the Every-Layout **Center primitive**: a
`max-width`, guttered wrapper that centres **itself** in its parent — it does not
centre its own children (unless you add `--intrinsic`). The two are a pair:
`.sf-center` centres *itself*, `.sf-place-center` centres *its content*.

`.sf-place-center` is the box-neutral centring primitive — `display: grid;
place-items: center` — the named form of the community
`display:grid; place-items:center` idiom. Reach for it when you have a plain
element and just want its content centred on both axes, without adopting a
row/column/wrap primitive:

```html
<div class="sf-place-center" style="min-block-size: 12rem">
  <p>centred on both axes</p>
</div>
```

Otherwise centring is expressed as intent on the right primitive:

| To centre… | Use | Mechanism |
|---|---|---|
| content in a plain box, both axes | `.sf-place-center` | `grid` + `place-items` |
| a block wrapper horizontally | `.sf-center` (prose measure) or `.sf-container` (app wrapper, adds CQ) | `margin-inline: auto` |
| children in a row | `.sf-cluster--center` | `justify-content` |
| children in a column | `.sf-stack--center` | `align-items` |
| leftover grid items | `.sf-grid-flex--center` | `justify-content` |
| an element on both axes (overlay) | `.sf-imposter` | absolute + translate |
| content in a full-height region | `.sf-cover__center` | `margin-block: auto` |
| a wrapper's children by their intrinsic width | `.sf-center--intrinsic` | flex column + `align-items` |

Two things to remember about `.sf-center` specifically:

- **It centres itself, not its content.** For content-centring reach for the
  modifiers above.
- **It needs a block-level element in normal flow.** `margin-inline: auto`
  doesn't centre an inline element, and inside a flex or grid container the
  parent's layout takes over the item's sizing and alignment, so the primitive's
  own `width`/`margin` model no longer drives the result. `.sf-center` is
  `content-box`, so `--sf-center-max` bounds the
  *content* and the gutter sits outside it — pick it over `.sf-container` when
  you want a typographic measure; pick `.sf-container` when you want an
  app-level wrapper that also establishes a container-query scope.
