Mark Design

Sections

A section is a band — one horizontal stripe of a page, running edge to edge, carrying a background and the vertical rhythm. It is what every pattern on this site actually is.

Section, container, row

the skeleton

Three terms, outermost first, and each answers exactly one question:

<section class="py-6 surface-muted">   the band — edge to edge, its own ground
  <div class="container">               how wide the content may get
    <div class="mk-row mk-row--3up">    how that width is divided
      <div>the component</div>

They are separate because they vary independently. A full-bleed band can hold a narrow measure of prose; a plain-background band can hold a four-column row. Collapsing any two of them would force a decision that belongs to the other — which is why Containers and Rows are separate pages too.


The surface it wears

utils/surface.js

A band's background is a named tone, never a colour. Every tone resolves through Bootstrap's semantic variables, so all of them flip under [data-bs-theme] with no parallel dark rules to maintain.

surfaceclassWhat it is
(none) The page background. The default, and no class at all.
muted surface-muted A subtle grey band — the workhorse for separating one section from the next.
subtle surface-subtle One step deeper than muted.
primary surface-primary A faint brand tint.
dark surface-dark + data-bs-theme Always dark, whatever the page mode. Declares its own dark theme so nested components resolve against it.
no surface
surface: muted
surface: subtle
surface: primary
surface: dark

dark is the one that does something extra: it declares data-bs-theme="dark" on itself, so a button or a card placed inside resolves against the dark ground rather than the page's. That is why a dark band on a light page looks deliberate instead of broken.


Height

.mk-section--h-sm · --h-md · --h-lg · --h-fill

Four stops, and no class for the fifth. A band with no height stop is sized by its content — which is most bands, so naming it would only add a word meaning “unchanged”.

ClassHeightFor
--h-sm42vhA band that announces and gets out of the way.
--h-md60vhPresence without taking the screen.
--h-lg80vhSomething is visible beneath it — the point of not being fill.
--h-fill100dvh − --mk-section-fill-offsetThe rest of the first screen.

A sized band centres its content; --anchor-start and --anchor-end move it. end is what an overlaid navbar wants — it leaves the top of the frame to the chrome.

fill means the rest of the screen, not the whole screen. What remains depends on the bar above it: an overlay bar is out of flow and costs nothing, a solid one is in flow and costs its height. The site-header island measures that into --mk-section-fill-offset, which defaults to 0 — so a band with no bar above it, or one whose JS never ran, is simply full-height. The class itself is static, which is the fix for a real bug: this scale used to resolve through an attribute stamped on <html> by an inline script, so a fill band had no height at all until JS ran. See the scale ↗


Background

.mk-section--bg-img · --banner · --scrim-*

A photograph behind a band, with a scrim over it so the words survive. The photo arrives as a custom property rather than a plain background-image, and that is not a preference: the scrim and the picture are two layers of one property, and writing background-image at the callsite would replace the whole stack rather than adding to it. There is no way in CSS to set one layer.

<section class="mk-section mk-section--bg-img mk-section--scrim-neutral"
         style="--mk-section-bg-img:url('/img/street.jpg')">
ScrimWhat it does
(default)The brand tint, evenly over the picture.
--scrim-neutralDarkens top and bottom, leaves the middle of the photograph alone — shaped for overlaid chrome and a caption, not for the image.
--scrim-strongMaximum legibility, for a busy picture under a lot of copy.
--scrim-noneThe photograph as it is; the page takes responsibility for contrast.

--banner is the same treatment with an accent fill instead of a photograph. Both stamp data-bs-theme="dark" on the section, so nested chrome inverts without the caller saying it twice — and both are optional: a band that just wants a texture should write a plain background-image and skip --image entirely. Using both at once is the one combination to avoid; the inline declaration wins and the scrim silently disappears.


The edge it cuts

see Foundations → Shapes

A band's edge is square unless it says otherwise — shape_bottom and shape_top cut it into a curve, a tilt or a notch, painted in the band's own ground and hanging over the neighbour. It is documented as its own vocabulary rather than as another axis here, because a shape is a form a band can WEAR rather than a property of what a band IS, and the next thing that wears one will not be a section. Shapes has the set, the direction rule, and where the names came from.


The rhythm it keeps

mk-section

Bands are spaced by their own padding rather than by margins between them, so two adjacent bands cannot collapse into an accidental gap. .mk-section is that rhythm as one class, and the space is 5.25rem, 8rem from lg — so each side takes 42px, rising to 64px. The 5.25rem is deliberately off the $spacers scale: 42 was arrived at by looking at the page, and no step lands there — py-6 gives 36 a side and reads tight, py-7 gives 48. What is being tuned is the distance between two bands, so that is the number to choose by eye.

Each section owns half the gap on each side. It pays half the space below itself and half above, so the distance between any two bands is half + half = the whole space. One number describes the page's rhythm.

PositionTopBottomGap to the next
First 5.25rem (84px) / 6rem (96px) 2.625rem (42px) / 4rem (64px) 5.25rem / 8rem
After another section 2.625rem (42px) / 4rem (64px) 2.625rem (42px) / 4rem (64px)

The first section is the only exception, and only on its top: nothing above paid the other half, so it takes a whole gap's worth there. In CSS that is stated backwards — every section gets that opening value, and a section following another halves the space instead — because a selector can say "preceded by" and cannot say "not preceded by".

The opening gap is its own token, --mk-section-lead, and at desktop it is 6rem where the space is 8. Every other gap on the page measures the distance between two bands; this one measures the distance between a band and the chrome above it, which is a different measurement. The navbar already occupies the eye up there, so matching the between-band gap reads as more air than the gaps further down rather than the same amount.

Two rules, no exceptions, which is the reason for halves rather than a bottom-only rhythm. Under bottom-only a section's box was asymmetric — the full space beneath its content and nothing above — so a band painting its own background had its content sitting on the top edge of the colour, and needed a surface-* special case to claw the padding back. With halves every band has padding on both sides, whatever it sits next to and whatever it is wearing, and that special case is gone.

The space is a token, so a band can be tightened or opened without a new class: style="--mk-section-space:1.5rem", or set it on a wrapper and everything inside inherits it. Each side still takes half of whatever it is set to.

Space BETWEEN blocks inside a section is a different question, and the answer is the same shape: it goes on the parent. .mk-stack is a single-column grid with one --mk-stack-space, so the gap applies only between its children — never before the first or after the last.

Containers do the same for themselves. Every container the system renders carries .mk-container beside its width rung — the rung says how wide, the marker says it takes part in this rhythm — and a container with another container after it leaves half the section space beneath itself. So a band holding a prose-width intro above a wider row of cards spaces the pair with no utility anywhere.

The selector asks :has(~ .mk-container) — "is another container after me" — rather than :not(:last-child), which asks whether anything is after me. Several patterns emit a <script> after their container, and the looser question put 42–64px under nine heroes that wanted none. Name the thing you are spacing away from.

<section class="mk-section">
  <div class="container">
    <div class="mk-stack">
      <div>a block</div>
      <div class="mk-row mk-row--3up">a row</div>

A margin on the bottom of a block is a claim about what comes after it, made by something that cannot see what comes after it — which is why the last child always ends up with a trailing margin somebody has to strip. That is the pb-0 the hero used to carry, one level down.

One exception, and it is principled: a block that introduces what follows owns the space beneath itself. .mk-title-block carries a margin-block-end for exactly that reason — the space belongs to the introduction rather than to the thing being introduced, because the introduction is the part that knows something is coming. A page composing a title block above a row needs no spacing utility at all.

That space is half the section space, so the gap between a title and what it introduces is the same measure as the gap between two bands — 4rem inside a section at desktop, 2.625rem below lg, following the section's ramp with no second number to keep in step. A title block composed outside an mk-section has no space to halve and falls back to 3rem, which is what it always had.

One name to read carefully: .mk-title-block is its own block, not a part of this one, and it does not need .mk-section to work. A hyphen separates words within a block name — an element would be __header — so the two are neighbours in the stylesheet rather than parent and child.

This replaces a rule features was keeping by hand — py-2 pb-6, because it so often follows a section header that supplied the space above it. An exception with a reason beside it was fine; an exception per pattern is the drift this page exists to prevent. See it stacked ↗


Why there is no "columns"

a deliberate omission

Section, container, row — and then it stops. There is no fourth term for columns, because with the division declared on the row there is no column class left to teach: a child of a row carries nothing at all.

Bootstrap's col-lg-* is still underneath for a proportion the row vocabulary does not name, and Rows says so. But it is the engine's escape hatch, not one of this system's terms — which is the difference between a language that stays small and one that names everything twice.