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.
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.
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.
| surface | class | What 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.
.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”.
| Class | Height | For |
|---|---|---|
--h-sm | 42vh | A band that announces and gets out of the way. |
--h-md | 60vh | Presence without taking the screen. |
--h-lg | 80vh | Something is visible beneath it — the point of not being fill. |
--h-fill | 100dvh − --mk-section-fill-offset | The 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 ↗
.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')"> | Scrim | What it does |
|---|---|
| (default) | The brand tint, evenly over the picture. |
--scrim-neutral | Darkens top and bottom, leaves the middle of the photograph alone — shaped for overlaid chrome and a caption, not for the image. |
--scrim-strong | Maximum legibility, for a busy picture under a lot of copy. |
--scrim-none | The 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.
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.
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.
| Position | Top | Bottom | Gap 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 ↗
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.