/* ==========================================================================
   Zuidema Metselwerken — site stylesheet
   Loaded after the platform's `fse-blocks` handle (declared as a dependency in
   functions.php) and registered a second time with add_editor_style(), the same
   mechanism sites/dewisseldokkum, sites/elderingict and
   sites/vakantieparkinstyle already use. It EXTENDS the platform; it replaces
   nothing.

   TOKEN RULE. Every colour, size and space below is a `var(--wp--…)` declared
   in a theme.json. There is not one raw colour literal in this file, and there
   must never be: a colour written here is a second source of truth beside the
   palette, which is the drift `bin/check-theme-json.js` exists to prevent.

   WHAT IS DELIBERATELY NOT HERE. No card system, no shadow, no radius, no
   gradient, no icon set, no animation. This site's character comes from
   photography, alignment and type, and nothing in this file is decoration —
   each block below exists because a measured composition problem could not be
   solved with theme.json or a block attribute alone.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. NAVIGATION BREAKPOINT — the inline menu needs more room than core gives it
   --------------------------------------------------------------------------
   Core switches `overlayMenu: "mobile"` at exactly 600px: below it the burger,
   from 600px up the inline menu. Measured 2026-09-21 on this site's header:
   the five items are 447px wide and the wordmark 138px = 585px, against a
   584px content column at a 600px viewport. One pixel short, so the navigation
   wrapped and the header went 89px -> 131px at that single width — and stayed
   visually cramped for the whole 600-781px band above it.

   Rather than shrink the wordmark or the labels, the overlay is used until the
   inline menu genuinely fits. 782px is core's own column-stacking breakpoint,
   which this site already changes shape at, so the whole page now reflows at
   ONE width instead of two.

   Two declarations, each the exact counterpart of a core rule in its own
   `@media (min-width: 600px)` block; same specificity, and this file loads
   after core's block styles. */
@media (min-width: 600px) and (max-width: 781.98px) {
	.wp-block-navigation__responsive-container:not(.hidden-by-default):not(.is-menu-open) {
		display: none;
	}

	.wp-block-navigation__responsive-container-open:not(.always-shown) {
		display: flex;
	}
}

/* --------------------------------------------------------------------------
   2. MOBILE NAVIGATION OVERLAY
   --------------------------------------------------------------------------
   The overlay was core's default: white, right-aligned, five links and ~1100px
   of empty space below them. It is the only navigation this site has below
   782px, and on a site whose likeliest mobile action is a telephone call it
   offered no way to call.

   The wordmark and the telephone row are ordinary inner blocks of
   `core/navigation`, so they are part of the menu for assistive technology and
   for the focus trap core already manages — no drawer library, no second
   navigation, no change to the block's semantics. They carry `zm-overlay-only`
   because inner blocks render in BOTH presentations, and neither belongs in the
   inline desktop bar. `:not(.is-menu-open)` is core's own state class, so the
   rule follows the component rather than a breakpoint of its own. */
.wp-block-navigation__responsive-container:not(.is-menu-open) .zm-overlay-only {
	display: none;
}

/* The overlay's content column is a flex column; these give it a left-aligned
   editorial rhythm instead of core's right-aligned list. */
.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
	width: 100%;
	gap: var(--wp--preset--spacing--60);
	/* The nav block is `justifyContent: right` so the DESKTOP bar sits against
	   the right edge, and core reuses that setting as `align-items` inside the
	   overlay — which right-aligned the wordmark and the telephone block while
	   the menu items ran left. One column, one edge. */
	align-items: flex-start;
}

.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
	gap: var(--wp--preset--spacing--40);
	width: 100%;
}

/* Menu items read as destinations, not as a list of links: one step up in size,
   each separated by the platform hairline. */
.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item {
	width: 100%;
	border-bottom: 1px solid var(--wp--custom--border--subtle);
}

.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
	display: block;
	width: 100%;
	padding-block: var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--subsection);
	font-weight: 700;
	letter-spacing: -0.01em;
	color: var(--wp--preset--color--foreground);
}

/* A constrained group centres itself inside the overlay column; these two keep
   the brand and the contact block on the same left edge as the menu items. */
.wp-block-navigation__responsive-container.is-menu-open .zm-overlay-only {
	width: 100%;
	margin-inline: 0;
}

.zm-overlay-brand img {
	width: 168px;
	height: auto;
}

/* The close control is core's own button; it only needed a real target size. */
.wp-block-navigation__responsive-container-close,
.wp-block-navigation__responsive-container-open {
	padding: var(--wp--preset--spacing--20);
}

/* --------------------------------------------------------------------------
   3. (REMOVED IN P3-2, 2026-10-02)
   --------------------------------------------------------------------------
   This section styled `.zm-cta-on-dark`, an outline button drawn in
   `background` for dark grounds. No pattern, part, template, script or stored
   page carried the class, so its two rules were removed. The number stays so
   the section references elsewhere in this file still point where they did. */

/* --------------------------------------------------------------------------
   4. LINK AFFORDANCE ON NEUTRAL HEADINGS
   --------------------------------------------------------------------------
   `theme.json` paints a linked heading `foreground` so the page stops reading
   as a wall of red. Hover and focus return it to `primary` (declared there
   too); the underline below is what carries the affordance for a visitor who
   cannot see the colour change, and what keeps the neutral colour from making a
   link indistinguishable from a heading on keyboard focus.
   WCAG 2.1 SC 1.4.1. */
:is(h1, h2, h3, h4) a:hover,
:is(h1, h2, h3, h4) a:focus-visible {
	text-decoration: underline;
	text-underline-offset: 0.18em;
}

/* --------------------------------------------------------------------------
   4b. LINKS ON A DARK BAND
   --------------------------------------------------------------------------
   `styles.elements.link` paints every <a> on this site in `primary`, which is
   the right answer on every light ground it was designed for: 5.07:1 on white,
   4.69:1 on `secondary`. On the `foreground` band it measures 3.22:1 — below
   the 4.5 floor for the normal-size links there and only just over the 3.0
   large-text floor for the display-size ones.

   No new colour is available or wanted: on that ground the legible token is
   `background`, which is what the band already sets for its body copy, at
   16.30:1. So a link there keeps the band's own colour and is distinguished by
   the underline instead — which is also the more robust signal, since colour
   was carrying the affordance alone (WCAG 2.1 SC 1.4.1).

   Scoped to the band class core emits for a `foreground` background, so it
   follows the composition rather than a page or a pattern. Buttons are excluded:
   they carry their own variant colours and are already measured. */
.has-foreground-background-color a:not(.fse-blocks-button-link) {
	color: var(--wp--preset--color--background);
	text-decoration: underline;
	text-underline-offset: 0.15em;
	text-decoration-thickness: 1px;
}

.has-foreground-background-color a:not(.fse-blocks-button-link):hover,
.has-foreground-background-color a:not(.fse-blocks-button-link):focus-visible {
	text-decoration-thickness: 3px;
}

/* --------------------------------------------------------------------------
   5. PROJECT PREVIOUS / NEXT
   --------------------------------------------------------------------------
   Two destinations at the foot of a project page, set as a rule-separated pair
   rather than as a third and fourth button — the page already ends in a CTA
   row, and this is navigation, not a call to action. */
.zm-projectnav {
	border-top: 1px solid var(--wp--custom--border--subtle);
	padding-top: var(--wp--preset--spacing--50);
}

.zm-projectnav a {
	color: var(--wp--preset--color--foreground);
	text-decoration: none;
}

.zm-projectnav a:hover,
.zm-projectnav a:focus-visible {
	color: var(--wp--preset--color--primary);
	text-decoration: underline;
	text-underline-offset: 0.18em;
}

/* ==========================================================================
   Z-9 — VISUAL DESIGN & INTERACTION
   Added 2026-09-21. Everything below is composition and interaction; no new
   colour, no new size that is not derived from a token, no library.
   ========================================================================== */

/* --------------------------------------------------------------------------
   6. STICKY HEADER — two states
   --------------------------------------------------------------------------
   MEASURED BEFORE: the header was 89px, static, and scrolled away, so on a
   4,901px home page the navigation was out of reach for 98% of the scroll.

   Two states, and the compaction is real rather than cosmetic:
     TOP       spacing|40 padding  -> 105px, deliberately airier than before
     SCROLLED  spacing|20 padding  -> 81px, LESS than the 89px it replaced

   So the persistent cost of a permanently available header is lower than the
   cost of the old one that disappeared.

   `--zm-header-h` carries the current height to `scroll-margin-top`, which is
   what stops a sticky header covering an anchor. The home page's four service
   cards link to `/diensten/#metselwerk` and five siblings; without this the
   heading they target lands underneath the bar.

   HOW THE STATE IS SET: by the platform (fse-base-theme `ewo-header-state.js`,
   promoted 2026-10-05, offset 0 in functions.php), which sets
   `ewo-header-scrolled` on <html> from an IntersectionObserver, never a scroll
   listener. This site owns no JavaScript. With no JavaScript the class never
   appears, the header stays in its TOP state and remains sticky — nothing is
   hidden and nothing breaks.
   -------------------------------------------------------------------------- */
:root {
	--zm-header-h: 105px;
	/* One editorial measure, used wherever prose runs under a heading. */
	--zm-measure: 62ch;
	/* The distance from the content column to the viewport edge. `max(0px, …)`
	   keeps it at zero below the content width, where there is no gutter to
	   bleed into. */
	--zm-gutter: max(0px, calc((100vw - var(--wp--style--global--content-size)) / 2));
}

.wp-site-blocks > header {
	position: sticky;
	top: 0;
	z-index: 100;
	background-color: var(--wp--preset--color--background);
	transition: background-color var(--zm-duration) ease, box-shadow var(--zm-duration) ease;
}

/* THE PADDING AND THE HAIRLINE LIVE HERE, NOT IN THE BLOCK. Both used to be
   serialised as an inline `style` on the header group, and an inline style
   beats every selector — so the two states could not differ. `parts/header.html`
   now declares neither, and this file owns both. At the top the hairline is
   transparent; it fades in with the compaction, which is what tells the reader
   the bar has detached from the page. */
.zm-header {
	border-bottom: 1px solid transparent;
	padding-block: var(--wp--preset--spacing--40);
	transition: padding-block var(--zm-duration) ease, border-color var(--zm-duration) ease;
}

.ewo-header-scrolled .zm-header {
	border-bottom-color: var(--wp--custom--border--subtle);
	padding-block: var(--wp--preset--spacing--20);
}

.ewo-header-scrolled .wp-site-blocks > header {
	/* Deliberately not a drop shadow: a 1px hairline plus the solid ground is
	   enough separation on a site with this much white, and a shadow here would
	   be the first soft edge anywhere in the design. */
	box-shadow: 0 1px 0 0 var(--wp--custom--border--subtle);
}

/* The wordmark loses about a fifth of its size on scroll. `transform` rather
   than width/height because the block serialises its dimensions inline, which
   no stylesheet can override without `!important` — and because a transform
   costs the compositor nothing. */
.wp-site-blocks > header .wp-block-image {
	transform-origin: left center;
	transition: transform var(--zm-duration) ease;
}

.ewo-header-scrolled .wp-site-blocks > header .wp-block-image {
	transform: scale(0.82);
}

.ewo-header-scrolled {
	--zm-header-h: 81px;
}

/* Anchor targets clear the bar. `:target` alone is not enough — the offset has
   to apply to the element the browser scrolls to, whether by fragment or by
   script. */
[id] {
	scroll-margin-top: calc(var(--zm-header-h) + var(--wp--preset--spacing--40));
}

@media (prefers-reduced-motion: reduce) {
	.wp-site-blocks > header,
	.zm-header,
	.wp-site-blocks > header .wp-block-image {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
   7. MEASURE — prose stops running the full width of the page
   --------------------------------------------------------------------------
   MEASURED BEFORE: the /diensten/ lead was 25px type across 1200px — 278
   characters over roughly 110 per line, against the 45-75 that is readable.
   /projecten/ and the home page's section intros had the same shape.

   The cap is in `ch`, so it follows the type size rather than a pixel guess,
   and it is deliberately applied to TEXT only: images and bands keep the full
   column. Text measure narrower than image width is the whole point.
   -------------------------------------------------------------------------- */
/* ONE OF THE TWO `!important` RULES IN THIS FILE, AND BOTH ANSWER CORE'S OWN.
   (The other is the contact tile's e-mail step-down in section 17, which
   answers core's `!important` on the preset font-size classes.) A constrained
   layout emits
     .wp-container-…> :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
         margin-left: auto !important; margin-right: auto !important; }
   so ANY child narrower than the content column is centred, and no amount of
   specificity beats it. Measured before this rule: the section intro sat at
   x=388 with `margin-left: 380px` while its own heading started at x=120 — the
   left edge the whole page is built on, broken by a width cap.
   `!important` is the only thing that overrides `!important`. It is scoped to
   two classes that exist for exactly this purpose and nothing else. */
.zm-measure,
.zm-measure-tight {
	/* Not `0 auto`: inside an `alignfull` section that puts the paragraph at the
	   VIEWPORT edge (x=8), while its own heading is centred onto the content
	   column (x=120). The start margin reproduces exactly the offset core's
	   auto-centring would have given a full-width child, so a capped paragraph
	   shares the left edge of the heading above it. `max(0px, …)` collapses to
	   zero inside a container narrower than the content column — a grid column,
	   say — where there is no offset to reproduce. */
	margin-inline-start: max(0px, calc((100% - var(--wp--style--global--content-size)) / 2)) !important;
	margin-inline-end: auto !important;
}

.zm-measure {
	max-width: var(--zm-measure);
}

.zm-measure-tight {
	max-width: 46ch;
}

/* --------------------------------------------------------------------------
   8. EDITORIAL ROWS — the services composition
   --------------------------------------------------------------------------
   Four equal cards in a row is the most template-like thing a site can do, and
   at 282px wide the photographs were too small to show brickwork at all — the
   one thing this company sells.

   Each service is now a full-width row: a large photograph running to one
   viewport edge, and the text held to a readable measure on the other side.
   The rows alternate, so the page develops a left-right rhythm instead of
   marching down a single column.

   `core/columns` stays the markup — this is a restyle of an existing primitive,
   not a new component. The grid below overrides the flex columns on wide
   screens and returns to the block's own stacking below.
   -------------------------------------------------------------------------- */
.zm-editorial > .wp-block-columns {
	align-items: center;
	gap: clamp(var(--wp--preset--spacing--50), 5vw, var(--wp--preset--spacing--80));
}

@media (min-width: 880px) {
	.zm-editorial > .wp-block-columns {
		display: grid;
		grid-template-columns: 7fr 5fr;
	}

	/* The image column reaches the viewport edge on its own side. `overflow-x:
	   clip` on the section (below) is what keeps that from creating a scrollbar;
	   `clip` rather than `hidden` because `hidden` would make the section a
	   scroll container and break the sticky header above it. */
	.zm-editorial .zm-bleed-start {
		margin-inline-start: calc(var(--zm-gutter) * -1);
		width: calc(100% + var(--zm-gutter));
		max-width: none;
	}

	.zm-editorial .zm-bleed-end {
		margin-inline-end: calc(var(--zm-gutter) * -1);
		width: calc(100% + var(--zm-gutter));
		max-width: none;
	}

	/* Reversed row: text first in the DOM stays first for a screen reader, and
	   the grid swaps only the visual order. Safe here because each row is a
	   self-contained pair — there is no reading order to scramble. */
	.zm-editorial--reverse > .wp-block-columns {
		grid-template-columns: 5fr 7fr;
	}

	.zm-editorial--reverse .zm-editorial__media {
		order: 2;
	}

	.zm-editorial--reverse .zm-editorial__text {
		order: 1;
	}
}

/* BELOW THE SPLIT, THE PHOTOGRAPH TAKES THE WHOLE SCREEN. Stacked at 375 the
   image sat inside the 8px root gutter, which reads as a picture pasted onto a
   page rather than as the page. Mobile is a design here, not a collapsed
   desktop: the editorial media runs edge to edge and the text keeps the gutter,
   which is the same relationship the split has on desktop.

   UPPER BOUND 899.98 -> 639.98, 2026-09-23 (Phase 1). Section 26 gives 640px
   and up a two-column editorial row, and a photograph that bleeds 8px past its
   own column overlaps the text beside it. The edge-to-edge treatment now ends
   exactly where the row stops being one column, which is what it always meant. */
@media (max-width: 639.98px) {
	.zm-editorial .zm-editorial__media {
		margin-inline: calc(var(--wp--preset--spacing--30) * -1);
		width: auto;
	}
}

.zm-clip {
	overflow-x: clip;
}

/* --------------------------------------------------------------------------
   9. IMAGE FRAMES AND HOVER
   --------------------------------------------------------------------------
   P2, 2026-10-02: THE PHOTOGRAPH NO LONGER MOVES. Until P2 a card's image
   scaled to 1.03 on hover. A portfolio entry is a photograph of finished work,
   and a moving photograph reads as an interface element; the title's colour
   change below is the one hover state a card keeps. The frame still clips, so
   a later cropped frame cannot spill.
   -------------------------------------------------------------------------- */
.zm-frame {
	overflow: clip;
}

/* The card's title follows the image. Colour, not movement: the heading is
   already `foreground` and turns `primary` the way every other link does. */
.zm-card:hover h2 a,
.zm-card:hover h3 a,
.zm-card:focus-within h2 a,
.zm-card:focus-within h3 a {
	color: var(--wp--preset--color--primary);
}

/* --------------------------------------------------------------------------
   10. NAVIGATION — hover and current page
   --------------------------------------------------------------------------
   The desktop bar had no hover state and no indication of where you are. A
   rule grows from the left under the label; the current page keeps it. Drawn
   with a pseudo-element rather than `text-decoration` so it can animate and so
   it sits clear of the descenders.
   -------------------------------------------------------------------------- */
@media (min-width: 782px) {
	.wp-block-navigation .wp-block-navigation-item__content {
		position: relative;
		padding-block: var(--wp--preset--spacing--20);
	}

	.wp-block-navigation .wp-block-navigation-item__content::after {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		bottom: 0;
		height: 2px;
		background-color: var(--wp--preset--color--primary);
		transform: scaleX(0);
		transform-origin: left center;
		transition: transform var(--zm-duration) var(--zm-ease-out);
	}

	/* `current-menu-ancestor` is the project pages: a visitor inside
	   /project-heerenveen/ sees the rule under "Projecten", because that is
	   the section they are in. Both classes are supplied by
	   the platform's current-item marking (fse-base-theme) — WordPress adds neither
	   to a custom navigation link, which is why this rule had never once
	   fired before 2026-09-22. */
	.wp-block-navigation .wp-block-navigation-item__content:hover::after,
	.wp-block-navigation .wp-block-navigation-item__content:focus-visible::after,
	.wp-block-navigation .current-menu-item > .wp-block-navigation-item__content::after,
	.wp-block-navigation .current-menu-ancestor > .wp-block-navigation-item__content::after {
		transform: scaleX(1);
	}

	@media (prefers-reduced-motion: reduce) {
		.wp-block-navigation .wp-block-navigation-item__content::after {
			transition: none;
		}
	}
}

/* --------------------------------------------------------------------------
   11. BUTTON INTERACTION
   --------------------------------------------------------------------------
   The platform base already lifts a button 1px on hover. This adds the one
   thing missing from a CTA that says "go": a mark that moves in the direction
   of travel. It is a pseudo-element, so no markup, no icon font, no SVG, and
   it is suppressed under reduced motion.
   -------------------------------------------------------------------------- */
.zm-cta .fse-blocks-button-link::after {
	content: "\2192"; /* literal: rightwards arrow, the only glyph this design uses as an icon */
	margin-inline-start: var(--wp--preset--spacing--30);
	transition: transform var(--zm-duration) var(--zm-ease-out);
	display: inline-block;
}

.zm-cta .fse-blocks-button-link:hover::after,
.zm-cta .fse-blocks-button-link:focus-visible::after {
	transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
	.zm-cta .fse-blocks-button-link::after {
		transition: none;
	}

	.zm-cta .fse-blocks-button-link:hover::after,
	.zm-cta .fse-blocks-button-link:focus-visible::after {
		transform: none;
	}
}

/* --------------------------------------------------------------------------
   12. STATISTICS — evidence, not headline
   --------------------------------------------------------------------------
   The band was 266px tall for three numbers and the red mass competed with the
   hero directly above it. It is now a rule-divided row: the figures keep the
   `accent` they have always had, at a smaller step, and the hairlines do the
   structural work.
   -------------------------------------------------------------------------- */
@media (min-width: 782px) {
	.zm-stats .wp-block-column + .wp-block-column {
		border-inline-start: 1px solid var(--wp--custom--border--subtle);
		padding-inline-start: var(--wp--preset--spacing--60);
	}
}

/* ==========================================================================
   Z-10 — PAGE ART DIRECTION & LIVING UI
   Added 2026-09-21. Composition and interaction only. No new colour, no new
   size that is not derived from a token, no library, no change to the
   platform's spacing scale.
   ========================================================================== */

/* --------------------------------------------------------------------------
   13. VISUALLY HIDDEN TEXT
   --------------------------------------------------------------------------
   WAS "13. COUNTERS" UNTIL 2026-09-23. The `.zm-count` rule that lived here
   asked for `tabular-nums` so the digits would not shuffle while a figure
   counted up. Phase 1 removed the counters — see the JavaScript's own header
   for why — and with them the reason for that rule. Tabular numerals are still
   wanted, but for a better reason and site-wide: see section 22.

   `.zm-sr-only` stays, and is used by the project cards rather than by any
   figure: it carries the comma that keeps a split card title announcing as
   "48 appartementen, IJsselmuiden". Present in the accessibility tree and
   never painted. Clip-based rather than `display: none` (which would remove it
   from the tree) and never `width: 0`, which some screen readers skip.
   -------------------------------------------------------------------------- */
.zm-sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* --------------------------------------------------------------------------
   14. PROJECT CARDS — name, place, and a target worth the photograph
   --------------------------------------------------------------------------
   The transcribed title is split in two VISUALLY only: both halves are inside
   the same `<a>`, so the link still announces "48 appartementen, IJsselmuiden".
   See the pattern's docblock.

   THE WHOLE CARD IS THE TARGET, and it is the existing link that grows to
   cover it — not a second link, not a wrapper, not a click handler. So the
   card has exactly ONE tab stop, the focus ring still draws around the words
   (where a keyboard user is looking), and the accessible name is unchanged.
   -------------------------------------------------------------------------- */
.zm-card__title a {
	display: block;
}

.zm-card__name {
	display: block;
}

.zm-card__place {
	display: block;
	margin-top: 0.15em;
	font-weight: 600;
	font-size: 0.7em;
	letter-spacing: 0.01em;
	color: var(--wp--preset--color--muted);
	transition: color var(--zm-duration) ease;
}

/* THE ARROW IS THE CARD'S ONLY ORNAMENT. Same glyph, same easing and same 4px
   travel as `.zm-cta` and the contact actions, so "this is a link you follow"
   is one gesture everywhere on the site. Drawn with `content` rather than an
   icon file: no sprite, no SVG, no icon font. It sits on the NAME line so it
   tracks the title rather than floating in the card's corner. */
.zm-card__name::after {
	content: "\2192";
	display: inline-block;
	margin-inline-start: 0.4em;
	color: var(--wp--preset--color--primary);
	transition: transform var(--zm-duration) var(--zm-ease-out);
}

.zm-card:hover .zm-card__name::after,
.zm-card:focus-within .zm-card__name::after {
	transform: translateX(4px);
}

/* THE FEATURED PROJECT DOES NOT TAKE THE ARROW. It is a `zm-card` so it
   inherits the image and title behaviour, but it ends in an explicit
   "Bekijk dit project" button — two arrows two lines apart is the affordance
   stated twice. The cards below have no button, which is exactly why they
   need it. */
.zm-feature .zm-card__name::after {
	content: none;
}

/* The scope line is the card's only metadata. Held to a tight measure so a
   four-item scope wraps to two readable lines instead of running the column. */
.zm-card__scope {
	max-width: 38ch;
}

.zm-card:hover .zm-card__place,
.zm-card:focus-within .zm-card__place {
	color: var(--wp--preset--color--primary);
}

/* Scoped to the portfolio rows DELIBERATELY. The featured project is also a
   `.zm-card` — it wants the same image and title behaviour — but it contains a
   CTA button, and an overlay stretched across that row would sit on top of the
   button and swallow its clicks. */
.zm-portfolio__row .zm-card {
	position: relative;
}

.zm-portfolio__row .zm-card__title a::after {
	content: "";
	position: absolute;
	inset: 0;
}

@media (prefers-reduced-motion: reduce) {
	.zm-card__place,
	.zm-card__name::after {
		transition: none;
	}

	.zm-card:hover .zm-card__name::after,
	.zm-card:focus-within .zm-card__name::after {
		transform: none;
	}
}

/* --------------------------------------------------------------------------
   15. PORTFOLIO ROWS — one raster, not six compositions
   --------------------------------------------------------------------------
   REPLACES the Z-10 alternation. That pass gave the six projects alternating
   7fr/5fr widths and dropped the narrow one by a section step, on the theory
   that asymmetry reads as editorial. Reviewed in the browser it does not: with
   each photograph ALSO at its own natural ratio, no two projects were
   presented alike, the one portrait towered over its neighbour, and the page
   read as a pile of unrelated tiles rather than as one portfolio.

   A portfolio index is a RASTER. Two equal columns, one 4:3 frame (fixed in
   `zuidemametselwerken_project_card()`), one title step, one metadata line.
   The hierarchy on this page is carried entirely by the featured project
   above, which is bled to the viewport edge and about twice the area — so the
   six below can afford to be, and should be, identical in treatment.

   The double class answers core's own `.wp-block-columns { display: flex }`,
   which ties a one-class selector; measured with a single class the rows
   computed the right `grid-template-columns` and kept `display: flex`.

   LOWER BOUND 782 -> 560, 2026-09-23 (Phase 1). 782 is core's stacking
   breakpoint, and taking the raster down to one column there is most of why
   this site's home page was 12.002px tall at 768 against the professionality
   benchmark's 8.867px. A raster is a raster at 560 too: two 4:3 cards about
   270px wide, which is larger than the card was at 1024 before Z-11. Below 560
   it still stacks, and that is the mobile design rather than a fallback.

   KEPT AT 560 IN PHASE 2, DELIBERATELY, when everything else moved to the
   ladder's 640. Measured both ways: with the raster also switching at 640, the
   home page jumped +67.4% there and /projecten/ +83.6%, because the editorial
   rows stack at the same width — one boundary carrying two transitions. Split
   back apart they are +18.0% and +11.1% at 640 and the raster's own change
   lands at 560. A card raster's column count is content-driven, not a page
   state, and it is the one component that earns a width of its own.
   -------------------------------------------------------------------------- */
@media (min-width: 560px) {
	.wp-block-columns.zm-portfolio__row {
		display: grid;
		grid-template-columns: 1fr 1fr;
		align-items: start;
		gap: clamp(var(--wp--preset--spacing--60), 4vw, var(--wp--preset--spacing--70));
	}
}

/* --------------------------------------------------------------------------
   15b. THE FEATURED PROJECT — the one image on the index that is not a card
   --------------------------------------------------------------------------
   `zm-editorial` splits 7fr/5fr. The featured case study takes 8fr/4fr, which
   at 1440 measures 888px of photograph against a card's 580 — a little over
   twice the area — and it is the only image on the page that reaches the
   viewport edge. The ratio stays 4:3: size and bleed carry the hierarchy so
   the index keeps ONE frame throughout.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.zm-editorial--feature > .wp-block-columns {
		grid-template-columns: 8fr 4fr;
	}
}

/* --------------------------------------------------------------------------
   15c. THE SPLIT HERO — /zuidema-contact/
   --------------------------------------------------------------------------
   The other Level A pages open with a cover: photograph behind, type on top.
   Contact opens BESIDE its photograph instead, so the telephone number and the
   e-mail address are real text in the page's first screen rather than an
   overlay — larger, selectable, and never competing with an image for
   contrast. It is the same `zm-editorial` bleed primitive, balanced 6fr/6fr
   rather than 7fr/5fr because here the content side carries the two actions
   and deserves the same weight as the photograph.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.zm-hero-split > .wp-block-columns {
		grid-template-columns: 6fr 6fr;
	}
}

/* --------------------------------------------------------------------------
   16. FACT LIST — the lead project's documented quantities
   --------------------------------------------------------------------------
   The same figures the home page publishes, at the size of supporting evidence
   rather than of a headline. A wrapping flex row, so three facts sit side by
   side on a wide column and wrap to two and one as the column narrows, with no
   breakpoint of their own.
   -------------------------------------------------------------------------- */
.zm-facts {
	font-variant-numeric: tabular-nums;
}

.zm-fact {
	flex: 0 1 auto;
}

.zm-fact__label {
	display: block;
	margin-top: 0.2em;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted);
}

/* --------------------------------------------------------------------------
   17. CONTACT — the two actions, as typography
   --------------------------------------------------------------------------
   REPLACES the Z-10 tiles. Making telephone and e-mail into actions rather
   than metadata was right; making them two bordered rectangles was not. An
   outlined box is the most generic component there is, and on a site built
   from flat type, hairlines and photography it reads as something imported
   from another design. The rectangles are gone. What is left is what this
   design language already uses everywhere else: a small uppercase label, a
   large value, a hairline between rows, and the arrow.

   THE WHOLE ROW IS STILL THE TARGET, and it is still the EXISTING link that
   grows to cover it — one tab stop per action, `tel:`/`mailto:` untouched,
   accessible name unchanged. `position: relative` on the row is what the
   stretched link resolves against.
   -------------------------------------------------------------------------- */
.zm-actions {
	border-top: 1px solid var(--wp--custom--border--subtle);
}

.zm-action {
	position: relative;
	border-bottom: 1px solid var(--wp--custom--border--subtle);
}

.zm-action a {
	display: block;
	padding-block: var(--wp--preset--spacing--50);
	text-decoration: none;
	color: inherit;
}

.zm-action a::after {
	content: "";
	position: absolute;
	inset: 0;
}

.zm-action__label {
	display: block;
	margin-bottom: var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

.zm-action__value {
	display: block;
	font-size: var(--wp--preset--font-size--field);
	font-weight: 800;
	line-height: 1.15;
	letter-spacing: -0.01em;
	transition: color var(--zm-duration) ease;
}

.zm-action__value::after {
	content: "\2192";
	display: inline-block;
	margin-inline-start: 0.35em;
	color: var(--wp--preset--color--primary);
	transition: transform var(--zm-duration) var(--zm-ease-out);
}

.zm-action:hover .zm-action__value,
.zm-action:focus-within .zm-action__value {
	color: var(--wp--preset--color--primary);
}

.zm-action:hover .zm-action__value::after,
.zm-action:focus-within .zm-action__value::after {
	transform: translateX(4px);
}

/* Address and KvK: supporting metadata on one line, label set apart from value
   by weight and colour rather than by a heading each. */
.zm-contact-meta .zm-meta-label {
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.12em;
	font-size: 0.85em;
	margin-inline-end: var(--wp--preset--spacing--30);
	color: var(--wp--preset--color--foreground);
}

/* THE E-MAIL ADDRESS MUST NOT BREAK MID-WORD. It is one unbreakable
   27-character token; at the `field` step the telephone uses it does not fit a
   narrow column, and it split as "…metselwerken.n / l". It therefore takes the
   `medium` step instead of `field` — the deliberate size difference Z-9
   recorded, for the same reason — and steps down once more on a narrow phone.

   The `!important` is one of the two in this file and answers core's own:
   WordPress emits every preset font-size utility as
   `.has-medium-font-size { font-size: … !important }`, so no selector of ours
   wins on specificity. See section 7. */
.zm-action__value.has-medium-font-size {
	font-size: var(--wp--preset--font-size--medium) !important;
}

@media (max-width: 420px) {
	.zm-action__value.has-medium-font-size {
		font-size: var(--wp--preset--font-size--normal) !important;
	}
}

@media (prefers-reduced-motion: reduce) {
	.zm-action__value,
	.zm-action__value::after {
		transition: none;
	}

	.zm-action:hover .zm-action__value::after,
	.zm-action:focus-within .zm-action__value::after {
		transform: none;
	}
}

/* --------------------------------------------------------------------------
   18. THE ENQUIRY FORM — a measure a form can be filled in
   --------------------------------------------------------------------------
   MEASURED BEFORE: every input ran the full 1200px content column. A name is
   about 20 characters and a "Naam" field a metre wide is the clearest possible
   signal that nobody laid this page out.

   Nothing here touches the form's markup, its fields, its labels, its required
   marks, its validation or its error rendering — all of that is the platform
   block's and stays exactly as it is. This is width, rhythm and the weight of
   the submit control.
   -------------------------------------------------------------------------- */
.zm-intake .fse-blocks-form {
	max-width: 34rem;
}

.zm-intake .fse-blocks-form-field + .fse-blocks-form-field {
	margin-top: var(--wp--preset--spacing--50);
}

.zm-intake .fse-blocks-form legend {
	/* The form's own name repeats the heading beside it. Kept in the
	   accessibility tree, where it names the fieldset, and out of the picture,
	   where it was a second smaller heading saying the same thing. */
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.zm-intake .fse-blocks-form button[type="submit"],
.zm-intake .fse-blocks-form input[type="submit"] {
	margin-top: var(--wp--preset--spacing--50);
	padding: var(--wp--preset--spacing--40) var(--wp--preset--spacing--60);
	font-size: var(--wp--preset--font-size--normal);
	font-weight: 700;
}

/* --------------------------------------------------------------------------
   19. COLOPHON
   --------------------------------------------------------------------------
   The footer's last line carried the owner and the KvK number as two separate
   paragraphs pushed to opposite ends, which gave a registration number the
   same standing as the company. They are now one statement — owner first, KvK
   as its subordinate — and the privacy link takes the end of the row, where a
   legal link belongs and where it no longer sits inside "Snel naar" among the
   navigational ones.
   -------------------------------------------------------------------------- */
.zm-colophon {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--30) var(--wp--preset--spacing--50);
	align-items: baseline;
}

.zm-colophon-owner {
	color: var(--wp--preset--color--foreground);
	font-weight: 600;
}

/* --------------------------------------------------------------------------
   20. /diensten/ — the service rows alternate
   --------------------------------------------------------------------------
   Seven rows of image-left/text-right is a catalogue. The image side swaps on
   every second row so the page develops a rhythm instead of marching. Order in
   the DOM never changes — image first, then heading and text — so a screen
   reader and a keyboard user always meet the service in the same sequence;
   only the visual order flips. Below core's stacking breakpoint the flip is
   removed, because a stacked row has no sides.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   20. /diensten/ — alternating editorial rows inside ONE section
   --------------------------------------------------------------------------
   Section 8 flips a row by putting `zm-editorial--reverse` on the SECTION,
   which works when the section holds a single row. /diensten/ stacks three
   rows in one section and has to flip them individually, so the modifier goes
   on the row. The order rules in section 8 are already descendant selectors
   and match either way; only the track sizing had to learn the second form.
   Measured before this rule: a reversed row put the photograph in the 5fr
   track, so it rendered 593px wide against 783px on the rows either side of it
   — the alternation made the image shrink every other row.

   THE TALL MODIFIER. A 9:16 photograph in a 7fr media track renders about
   1024px tall against 587px for the landscape rows, which is the "let a 9:16
   dominate the row because it was left uncropped" failure. `--tall` narrows
   the media track instead of cropping the photograph: the image stays whole
   and the row comes back within reach of its neighbours.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.wp-block-columns.zm-editorial--reverse {
		grid-template-columns: 5fr 7fr;
	}

	/* WHICH TRACK IS NARROW DEPENDS ON WHICH SIDE THE MEDIA IS ON. `--tall`
	   narrows the MEDIA track; the media sits in track one on a normal row and
	   in track two on a reversed one, so the two cases are not the same rule.
	   Written as one declaration for both until Z-14, when metselwerk moved to
	   the first (non-reversed) position and its 3:4 photograph took the 8fr
	   track instead of the 4fr one — rendering 877x1024 against 587px rows
	   either side of it, the exact domination `--tall` exists to prevent. */
	.zm-editorial--tall:not(.zm-editorial--reverse) > .wp-block-columns,
	.wp-block-columns.zm-editorial--tall:not(.zm-editorial--reverse) {
		grid-template-columns: 4fr 8fr;
	}

	.zm-editorial--tall.zm-editorial--reverse > .wp-block-columns,
	.wp-block-columns.zm-editorial--tall.zm-editorial--reverse {
		grid-template-columns: 8fr 4fr;
	}
}

/* ==========================================================================
   PHASE 1 — PROFESSIONAL B2B DESIGN FOUNDATION
   Added 2026-09-23. Typeface support, the measurement system, the hairline,
   the structural rhythm and the designed tablet state. No new colour value,
   no new font size, no library, no page composition changed.
   ========================================================================== */

/* --------------------------------------------------------------------------
   21. THE MODULE — a rhythm taken from the brick, not drawn as one
   --------------------------------------------------------------------------
   Until this pass every band on every page used `spacing|80`, the platform's
   largest step, which gave a site whose whole subject is course and rhythm
   exactly one rhythm.

   The module below is the half-brick. A standard brick with a 10mm joint is
   2:1 stretcher to header, and courses offset by half a stretcher; expressed
   as spacing that gives one unit and multiples of it, which is all a rhythm
   needs to be. The three band steps are 2, 4 and 6 modules.

   SPACING IS PLATFORM POLICY AND STAYS THERE. `bin/check-theme-json.js` fails
   any site theme that declares `settings.spacing` or `styles.spacing`, and
   correctly: the platform owns the scale. So these are CSS custom properties
   on this site's own stylesheet, not theme.json presets. `--zm-band-md`
   matches `spacing|80` at both ends of the scale (40px and 64px), but its
   slope is 6.4vw against the preset's 6vw, so between about 625 and 1067px
   it renders up to 4px larger (57.6 against 54px at 900). Adopting it is
   therefore not pixel-neutral in that range; corrected in T5, 2026-10-03.

   NOTHING IS RE-SPACED IN THIS PHASE. The steps are consumed by the
   primitives below; applying them band by band is Phase 2's work, where a
   changed rhythm can be seen against a changed composition.

   NO BRICK IS DRAWN. No texture, no pattern, no material overlay — the
   metaphor lives in the proportion and nowhere else.
   -------------------------------------------------------------------------- */
:root {
	--zm-module: clamp(0.625rem, 1.6vw, 1rem); /* 10-16px: the half-brick */
	--zm-band-sm: calc(var(--zm-module) * 2);
	--zm-band-md: calc(var(--zm-module) * 4); /* == spacing|80 at both ends */
	--zm-band-lg: calc(var(--zm-module) * 6);
	--zm-hairline: 1px;
	/* THE JOINT. An alias, not a new colour: the platform already ships this
	   hairline as `--wp--custom--border--subtle` and the mobile overlay already
	   draws with it. Naming it here says what it means on this site without
	   adding a second source of truth for its value. */
	--zm-rule: var(--wp--custom--border--subtle);
}

/* --------------------------------------------------------------------------
   22. NUMERALS — the site's second voice
   --------------------------------------------------------------------------
   This site publishes 1.626.000, 230.000, 17.000 m² and 1.400, often in a
   column, and the figures are the one thing no competitor in this market
   publishes at all. They have to read as measurements.

   `tabular-nums` makes every digit the same advance width, so a column of
   figures aligns on its thousand separators instead of drifting; `lining-nums`
   pins them to cap height, so they sit as data rather than as running text.
   Archivo carries both features — verified by measuring all ten digit advances
   at 100px: 0.00px spread with the property, 5.23px without it.

   `font-variant-numeric` is the modern property and `font-feature-settings` is
   the fallback for engines that predate it; both are declared because a figure
   that silently loses its alignment is the exact failure this rule exists to
   prevent.
   -------------------------------------------------------------------------- */
.zm-num,
.zm-record__value,
.zm-facts,
.zm-quantities,
.zm-card__name {
	font-variant-numeric: tabular-nums lining-nums;
	font-feature-settings: "tnum" 1, "lnum" 1;
}

/* --------------------------------------------------------------------------
   23. THE RECORD — labelled values, divided by the joint
   --------------------------------------------------------------------------
   `zuidemametselwerken_record_block()` emits a `<dl>`: terms and their values,
   which is what this content actually is and the only markup of the three
   candidates (table, grid of cards, description list) that says so to a screen
   reader.

   THE HAIRLINE IS THE JOINT AND IT IS THE SITE'S ONLY DIVIDER. No box, no
   border, no card, no fill, no radius. A 1px rule between rows is the whole
   ornament, which is why the record can sit directly on the page without a
   container: the alignment carries it.

   The label column is fixed so the values align down the record; it collapses
   to a stacked pair below 560px, where a 190px label column would leave a
   value four characters wide.
   -------------------------------------------------------------------------- */
.zm-record {
	margin: 0;
	max-width: 46rem;
	border-top: var(--zm-hairline) solid var(--zm-rule);
}

/* THE RECORD STARTS AT THE CONTENT EDGE, AND THAT IS FIXED IN THE MARKUP
   RATHER THAN HERE. A constrained group centres every child narrower than the
   content size with `margin-left: auto !important`, which no ordinary rule in
   this file can answer. `zuidemametselwerken_record_block()` therefore wraps
   the list in a FLOW group, where core never applies the centring rule at all
   and `max-width` left-aligns on its own — see the comment on that function
   for the measurement. Nothing is needed here. */

.zm-record__row {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0 var(--wp--preset--spacing--50);
	padding-block: var(--zm-band-sm);
	border-bottom: var(--zm-hairline) solid var(--zm-rule);
}

.zm-record__label {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

.zm-record__value {
	margin: 0;
	font-size: var(--wp--preset--font-size--medium);
	line-height: 1.45;
	color: var(--wp--preset--color--foreground);
}

@media (min-width: 560px) {
	.zm-record__row {
		grid-template-columns: 13rem 1fr;
		align-items: baseline;
	}

	.zm-record__label {
		/* Optically aligned with the value's cap height rather than its
		   baseline: the label is two steps smaller, so a shared baseline
		   makes it look like it has fallen. */
		transform: translateY(0.1em);
	}
}

/* --------------------------------------------------------------------------
   24. QUANTITIES — a measurement, not a campaign figure
   --------------------------------------------------------------------------
   The per-project figures moved from `accent` to `foreground` and from weight
   800 to 700 in this pass (see zuidemametselwerken_project()). The rule below
   is the rest of that change: the unit sits with its figure, the label sits
   under it, and the whole row is held on the same hairline the record uses so
   the two blocks read as one instrument.

   `spacing|60` between the label and the next figure keeps the row legible
   when it wraps on a narrow column; nothing here sets a width, so the columns
   are still core's.
   -------------------------------------------------------------------------- */
.zm-quantities {
	padding-top: var(--zm-band-sm);
	border-top: var(--zm-hairline) solid var(--zm-rule);
}

/* --------------------------------------------------------------------------
   25. CAPTIONS — documentation, not gallery copy
   --------------------------------------------------------------------------
   Phase 3 writes the captions; this is the treatment waiting for them. A
   caption on this site names what the photograph shows and, where it is known,
   where it was taken — the register of a site record, not of a photography
   portfolio. So: small, muted, held to a short measure, set immediately under
   the frame with no rule and no italic.

   Both spellings are covered because this site has captions written as
   paragraphs (the steenzagerij sequence, published before core's image caption
   was used anywhere here) and will have `core/image` captions later. They must
   not look like two different things.
   -------------------------------------------------------------------------- */
.zm-caption,
.zm-frame figcaption,
.wp-block-image figcaption {
	margin-top: var(--zm-module);
	margin-bottom: 0;
	max-width: 52ch;
	font-size: var(--wp--preset--font-size--small);
	font-style: normal;
	line-height: 1.55;
	color: var(--wp--preset--color--muted);
	text-align: start;
}

/* Project context under a caption — place, scope, or the record's own words.
   One step quieter again, and never on its own line above the caption. */
.zm-caption__meta {
	display: block;
	margin-top: 0.35em;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	font-weight: 600;
	font-size: 0.8em;
}

/* --------------------------------------------------------------------------
   26. THE RESPONSIVE LADDER — five states, and no cliff between them
   --------------------------------------------------------------------------
   MEASURED BEFORE (home page, document height by viewport width):

       1440  5.694    900  4.714    781  12.133   <- one pixel
       1280  5.494    800  4.452    768  12.002
       1024  4.985    782  4.441    600  10.473   390  8.885

   At 782px the page was 4.441px tall and at 781px it was 12.133px: core stacks
   every `wp-block-columns` at once below 782, so a tablet in portrait received
   every photograph at full viewport width, one per screen, for 12.000px. The
   professionality benchmark measured 8.867px at the same width with more
   content on the page. It is the one defect in this phase that needs no owner
   decision and no taste: a serious B2B site cannot be least engineered at the
   width its buyers hold in their hands.

   THE LADDER, as implemented:

       >= 1200   full desktop          editorial 7fr/5fr, raster 3-up
       1024-1199 reduced desktop       same structure, platform gutters
        900-1023 compact desktop       editorial 7fr/5fr holds, raster 3-up
        640-899  DESIGNED TABLET       editorial 55/45, raster 2-up  <- new
        560-639  compact tablet        stacked editorial, raster 2-up
         < 560   mobile                single column, media edge to edge

   `display: grid` IS THE MECHANISM, AND IT NEEDS NO `!important`. Core stacks
   with `flex-basis: 100% !important` on the COLUMN and `flex-wrap: wrap` on
   the container. Both properties are inert inside a grid container, so
   declaring `display: grid` on the row neutralises the stack outright. This is
   the same device section 8 already uses at 900px — and the same trap it
   documents: `grid-template-columns` without `display: grid` silently does
   nothing, because core's `.wp-block-columns { display: flex }` ties a
   one-class selector.

   `:has()` SELECTS THE CARD RASTERS. The home project teaser and the services
   3-up are plain `wp:columns` with no class of their own; their children carry
   `.zm-card`. Matching on the child avoids editing two patterns for a layout
   fact that is already in the markup. An engine without `:has()` falls back to
   core's stacking, which is exactly today's behaviour — the degradation is the
   status quo, not a broken page.
   -------------------------------------------------------------------------- */

/* 560-899: the unclassed card rasters are two across. `.zm-portfolio__row` has
   its own rule in section 15 and is two across from 560px up, because the index
   was designed as a pair.

   WHERE THREE BECOMES TWO WAS MEASURED, BOTH WAYS, AND DECIDED ON THE CARD
   RATHER THAN ON THE GRAPH. Ending this rule at 781.98 instead — leaving core's
   three-up flex row in place from 782 to 899 — moves the transition to the
   site's single reflow width and produces a smoother curve at 880 (-2.1%
   against +31.9%). It was rejected: at 800px a three-up row renders cards about
   250px wide and at 700px about 220px, and section 8 of this file abandoned
   four-up cards for exactly that reason — below roughly 230px the photograph
   stops showing brickwork, which is the one thing this company sells. Two cards
   at 300-430px do show it.

   So the step at 880/879 is deliberate: it is the desktop-to-tablet boundary of
   the ladder (section 28), the same width at which the editorial rows change
   proportion, and it is a column-count change rather than a layout failure.

   MEASURED AT THAT BOUNDARY, 2026-09-23: home 4.685px -> 6.185px (+32.0%), of
   which +1.067px is the home PROJECT TEASER alone. That teaser chunks its seven
   cards into `array_chunk(…, 3)` rows and pads the last row with empty columns,
   so at two-up each row of three costs two lines instead of one and the block
   spends five lines where four would do. /projecten/, whose index is one raster
   of six, moves +1.4% across the same boundary. Consolidating the teaser into a
   single grid is a home-page change and belongs with the home-page pass, not
   here. */
@media (min-width: 560px) and (max-width: 879.98px) {
	.wp-block-columns:has(> .wp-block-column.zm-card) {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		align-items: start;
	}

	/* The home teaser pads its last row to three with empty columns so the
	   flex row keeps its widths (see zuidema-projecten-grid.php). In a grid
	   they would reserve cells and open a hole, so they are removed here and
	   only here. */
	.wp-block-columns:has(> .wp-block-column.zm-card) > .wp-block-column:empty {
		display: none;
	}
}

/* 640-899: the designed tablet state for editorial rows. 55/45 rather than the
   desktop 7fr/5fr (58/42) because the text column needs a little more room
   once the photograph is no longer bleeding to the viewport edge. */
@media (min-width: 640px) and (max-width: 879.98px) {
	.zm-editorial > .wp-block-columns {
		display: grid;
		grid-template-columns: 55fr 45fr;
		align-items: center;
	}

	.zm-editorial--reverse > .wp-block-columns {
		grid-template-columns: 45fr 55fr;
	}

	/* Same order swap as section 8: the DOM order is media then text on every
	   row, so a screen reader and a keyboard user meet each service in one
	   sequence, and only the visual order alternates. */
	.zm-editorial--reverse .zm-editorial__media {
		order: 2;
	}

	.zm-editorial--reverse .zm-editorial__text {
		order: 1;
	}

	/* A portrait photograph in a 55% column at 640px is 457px tall against a
	   three-line text column. `--tall` already exists for exactly this and is
	   applied here at the tablet's own proportion. */
	.zm-editorial--tall:not(.zm-editorial--reverse) > .wp-block-columns,
	.wp-block-columns.zm-editorial--tall:not(.zm-editorial--reverse) {
		grid-template-columns: 40fr 60fr;
	}

	.zm-editorial--tall.zm-editorial--reverse > .wp-block-columns,
	.wp-block-columns.zm-editorial--tall.zm-editorial--reverse {
		grid-template-columns: 60fr 40fr;
	}
}

/* --------------------------------------------------------------------------
   27. THE TELEPHONE NUMBER IN THE COMPACT HEADER
   --------------------------------------------------------------------------
   Below 782px the inline menu is replaced by the overlay, and until this pass
   the only way to reach the telephone number was to open that overlay. For a
   specialist subcontractor the telephone IS the conversion, so it belongs in
   the bar itself.

   A TYPOGRAPHIC ACTION, NOT A SECOND BUTTON. The header already holds a
   wordmark and a burger; a third filled control would make a three-object bar
   out of a two-object one. This is the number, in the interaction red the rest
   of the site uses for actions, at the size the navigation uses.

   Hidden at 782px and above, where the inline menu is present and the header
   has its own shape. The element is outside `wp:navigation`, so the overlay's
   focus trap, `role="dialog"` and Escape behaviour are untouched; in the tab
   order it sits between the wordmark and the burger, which is where a visitor
   scanning left to right would expect it.
   -------------------------------------------------------------------------- */
.zm-header-tel {
	display: none;
	/* The header row is `space-between`, so with three objects the number sits
	   between the wordmark and the burger on its own. This margin is a floor
	   under that gap for the widths where the row has almost no free space
	   left to distribute — at 340px it is the difference between a gap and a
	   collision. */
	margin-inline-end: var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--normal);
	font-weight: 700;
	letter-spacing: -0.01em;
	white-space: nowrap;
	font-variant-numeric: tabular-nums lining-nums;
}

.zm-header-tel a {
	text-decoration: none;
}

.zm-header-tel a:hover,
.zm-header-tel a:focus-visible {
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

@media (max-width: 781.98px) {
	.zm-header-tel {
		display: block;
	}
}

/* THE FLOOR WAS MEASURED, NOT GUESSED. The first cut hid the number below
   380px, which would have removed it from a 375px iPhone — one of the most
   common viewports there is. Measured header height with the number present:

       320px -> 153px (wraps to two rows)      375px -> 105px
       340px -> 105px                          390px -> 105px
       360px -> 105px                          414px -> 105px

   So the header carries all three objects down to 340px and only breaks below
   it. The rule now hides the number exactly where it stops fitting. Under
   340px the overlay still carries it as a full-width primary action, which is
   where it has always been. */
@media (max-width: 339.98px) {
	.zm-header-tel {
		display: none;
	}
}

/* ==========================================================================
   PHASE 2 — RESPONSIVE LADDER & THE PROJECT DOSSIER
   Added 2026-09-23. Geometry and semantics only: no new colour value, no new
   font size, no library, no page composition redesigned.
   ========================================================================== */

/* --------------------------------------------------------------------------
   28. THE LADDER, AS FIVE STATES
   --------------------------------------------------------------------------
   Phase 1 removed the 782/781 cliff on the HOME page and the project index.
   Measured again on 2026-09-23 before this pass, it was still there on every
   PROJECT DETAIL page and it was the largest one left:

       /project-heerenveen/   782px -> 2.591px      781px -> 4.841px   (+86.8%)

   Same single cause as before, in a place Phase 1 did not reach: core stacks
   EVERY `.wp-block-columns` below 782px with `flex-basis: 100% !important`, and
   a project page has three of them — the quantities row, the photography, and
   the previous/next nav. The photography alone went 527px -> 2.136px, because
   each photograph stopped being ~400px wide and became the full content column.

   So this section finishes the job rather than patching a width. The five
   states below are the whole system; every component transition lands on one
   of these boundaries and nowhere else:

       >= 1200   full desktop        editorial 7fr/5fr bled, raster 3-up
       1024-1199 reduced desktop     same geometry, platform gutters narrow it
        880-1023 compact desktop     same geometry — the last width it still fits
        640-879  designed tablet     editorial 55/45 unbled, raster 2-up
         < 640   mobile              single column, media edge to edge

   WHAT MOVED, AND WHY 880 RATHER THAN 900. Phase 1 put the desktop/tablet
   boundary at 900 and the raster's second transition at 560, which produced a
   +31.9% step at 880 and a +37.4% step at 559 — two reflow widths that were
   ladder boundaries in nothing but this stylesheet. Sections 8, 15, 15b, 15c,
   20 and 26 now all change at 880 and 640, so the page reflows at the SAME two
   widths everywhere instead of at four.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   29. LABELLED FIGURES THAT FLOW
   --------------------------------------------------------------------------
   Both figure rows on this site — the home page's three aggregates and a
   project's documented quantities — are short label/value pairs, and both used
   to be `core/columns`, so both fell to one column at 782px whether or not the
   content needed it. Three figures 200px wide do not need a breakpoint; they
   need a track size.

   `auto-fit` + `minmax()` is therefore the mechanism rather than a media query:
   the column count follows the space actually available, so there is no width
   at which this component jumps. The minimum is set from the LONGEST STRING
   each row has to hold — `1.626.000` at the `stat` step, `17.000 m²` at
   `x-large` — measured, not guessed.

   `display: grid` also neutralises core's `flex-basis: 100% !important`, which
   is why no `!important` is needed here. Same device as section 26.
   -------------------------------------------------------------------------- */
.zm-quantities {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
	gap: var(--zm-band-sm) var(--wp--preset--spacing--60);
	margin: 0;
	/* THE SAME MEASURE AS THE RECORD ABOVE IT. Both halves of the dossier share
	   one left edge and one right edge, so they read as one instrument rather
	   than as a list followed by an unrelated band. Measured at 1440 before
	   this line: the record ended at 856px and the quantities' rule ran on to
	   1320. */
	max-width: 46rem;
	padding-top: var(--zm-band-sm);
	border-top: var(--zm-hairline) solid var(--zm-rule);
}

/* COLUMN-REVERSE IS PRESENTATION ONLY. The DOM is `<dt>` then `<dd>` — "steen:
   1.000.000" to a screen reader, in that order. The eye wants the figure
   first, so the visual order is flipped and the reading order is not. */
.zm-quantity {
	display: flex;
	flex-direction: column-reverse;
	align-items: start;
}

.zm-quantity__value {
	margin: 0;
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 700;
	line-height: 1.1;
	color: var(--wp--preset--color--foreground);
}

.zm-quantity__label {
	margin: 0.2em 0 0;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

/* The home page's aggregates, given the same behaviour from the same reasoning.
   A wider minimum because `1.626.000` is set at the `stat` step (41.6-57.6px),
   not at `x-large`. The band's own composition, colour and type are untouched. */
.wp-block-columns.zm-stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
	align-items: start;
}

/* The featured project's three figures, same device. As core flex columns they
   shrank to 93-99px between 782 and 890, and `1.000.000` (100px at `medium`,
   measured) broke inside the number. 7rem holds it with room to spare. */
.wp-block-columns.zm-figures {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
	align-items: start;
}

/* --------------------------------------------------------------------------
   30. THE PROJECT GALLERY KEEPS ITS TWO COLUMNS INTO THE TABLET
   --------------------------------------------------------------------------
   A project page's photography is composed by `zuidemametselwerken_project()`,
   which derives each column's width from the photographs' own aspect ratios so
   that every image in a row finishes at the same height and nothing is cropped
   (see `zuidemametselwerken_row_widths()`). That works down to 782px, where
   core takes over and stacks it.

   P3-2, 2026-10-02: the 640-781 two-column sheet that stood here only ever
   applied to pre-P2 gallery markup. P2 rows carry `is-not-stacked-on-mobile`
   and stay ratio rows down to 640 (section 50), and every project page has
   been re-seeded with that markup, so the rule matched nothing and was removed.

   Below 640 the photography stacks, which on a page whose subject IS the
   photograph is the right answer rather than a fallback — with one guard: a
   tall portrait in a 374px column is 500px of scroll, and two of them is a
   screen and a half. `max-height` caps it WITHOUT cropping, because the width
   follows.
   -------------------------------------------------------------------------- */
@media (max-width: 639.98px) {
	.zm-gallery figure.wp-block-image img {
		max-height: 72vh;
		width: auto;
		margin-inline: auto;
	}
}

/* --------------------------------------------------------------------------
   31. PREVIOUS / NEXT KEEPS ITS TWO SIDES
   --------------------------------------------------------------------------
   Two cells, one left-aligned and one right-aligned: stacked, the right-aligned
   one reads as a mistake rather than as the other end of a pair. They hold a
   label and a project title, so they fit side by side well below core's
   breakpoint; the tablet state keeps them there and mobile stacks them with the
   alignment normalised.
   -------------------------------------------------------------------------- */
@media (min-width: 640px) and (max-width: 781.98px) {
	.zm-projectnav .wp-block-columns {
		display: grid;
		grid-template-columns: 1fr 1fr;
		align-items: start;
	}
}

@media (max-width: 639.98px) {
	.zm-projectnav .wp-block-column p {
		text-align: start !important; /* answers the inline text-align the cell carries for its desktop side */
	}
}

/* ==========================================================================
   PHASE 3 — HOMEPAGE ARCHITECTURE
   Added 2026-09-23. Three primitives, all built on the ladder (section 28) and
   the editorial grid (section 8). No new colour value, no new font size, no
   library, no brick drawn.
   ========================================================================== */

/* --------------------------------------------------------------------------
   32. THE HOME HERO — a split, not a cover
   --------------------------------------------------------------------------
   `zm-editorial` splits 7fr/5fr with the TEXT dominant, which is right for a
   service row and wrong for a hero: here the photograph is the proof and the
   type is the caption. `zm-hero-home` inverts the tracks to 5fr/7fr and does
   nothing else — the bleed, the stacking and the tablet proportion all come
   from sections 8 and 26 unchanged.

   WHY 3:2 AND NOT THE PHOTOGRAPH'S OWN 2.25:1. `metselaar-aan-het-werk` is
   2500x1113. Bled to the viewport edge at 1440 the media track is ~860px wide,
   and at its native ratio that is 382px tall against a type column measuring
   about 470 — the photograph would sit in the middle of its own column with
   white above and below it. 3:2 gives 573px, which balances the pair, and the
   crop is taken from a panorama's ends rather than from its subject.

   P3-2, 2026-10-02: a 4:3 rule for this image below 640px stood here. It never
   applied — the image carries its 3:2 as an inline style, which a stylesheet
   rule cannot beat — so the hero has always been 3:2 on a phone (measured
   390x260 at 390px). The rule was removed; nothing rendered changes.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.zm-hero-home > .wp-block-columns {
		/* 6fr/6fr, not the 5fr/7fr this started at. Measured at 1440 with the
		   narrower type column: the H1 broke to THREE lines — "Metselwerk / op /
		   projectschaal" — because `display` is 67px and 455px cannot hold
		   "Metselwerk op". An equal split gives the type column ~570px, the
		   authored break lands where it was written to, and the photograph is
		   still the larger object once it bleeds to the viewport edge. */
		grid-template-columns: 6fr 6fr;
	}
}

@media (min-width: 640px) and (max-width: 879.98px) {
	.zm-hero-home > .wp-block-columns {
		grid-template-columns: 50fr 50fr;
	}
}

/* The two actions sit on one baseline: a solid button and a telephone number
   that is type rather than a second button. `wrap` so the number drops under
   the button on a narrow column instead of shrinking it. */
.zm-hero-actions {
	align-items: center;
}

.zm-hero-tel {
	font-variant-numeric: tabular-nums lining-nums;
	white-space: nowrap;
}

.zm-hero-tel a {
	text-decoration: none;
}

.zm-hero-tel a:hover,
.zm-hero-tel a:focus-visible {
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

/* Below 640 the compact header already carries this number (section 27), about
   40px above the hero's copy of it — the same telephone number twice in one
   screen. The header's is the persistent one, so the hero's steps aside and
   the mobile hero keeps a single action. */
@media (max-width: 639.98px) {
	.zm-hero-tel {
		display: none;
	}
}

/* --------------------------------------------------------------------------
   33. THE CAPABILITY SEQUENCE
   --------------------------------------------------------------------------
   Seven operations in the order the work is done, on the hairline that divides
   every other record on this site. It is an `<ol>` because the order is the
   claim; the visible numbers are CSS counters, so a screen reader hears the
   list semantics and never reads "01" as content.

   No card, no icon, no box. The only ornament is the rule between steps, at
   the same weight as the project record's — which is what makes the home page
   and the dossier read as one document.
   -------------------------------------------------------------------------- */
.zm-capability {
	counter-reset: zm-step;
	list-style: none;
	margin: 0;
	padding: 0;
	border-top: var(--zm-hairline) solid var(--zm-rule);
}

.zm-capability__step {
	counter-increment: zm-step;
	border-bottom: var(--zm-hairline) solid var(--zm-rule);
}

.zm-capability__step a {
	display: grid;
	grid-template-columns: 2.75rem 1fr;
	align-items: baseline;
	gap: var(--wp--preset--spacing--40);
	/* 1.5 modules rather than the 2 of a band step: seven rows at the band
	   step measured 630px against a 517px photograph beside them, which left
	   the image floating in its own column. One and a half brings the list to
	   ~518 and the pair finishes together. It is still the module, not a
	   number chosen to fit one photograph. */
	padding-block: calc(var(--zm-module) * 1.5);
	text-decoration: none;
	color: var(--wp--preset--color--foreground);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 600;
	line-height: 1.3;
}

/* The step number. `content` rather than markup, and `aria-hidden` is not
   needed because generated content on a ::before is not in the accessibility
   tree as text a reader announces in place of the label. */
.zm-capability__step a::before {
	content: counter(zm-step, decimal-leading-zero);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	font-variant-numeric: tabular-nums lining-nums;
	letter-spacing: 0.08em;
	color: var(--wp--preset--color--muted);
	transition: color var(--zm-duration) ease;
}

.zm-capability__step a:hover,
.zm-capability__step a:focus-visible {
	color: var(--wp--preset--color--primary);
}

.zm-capability__step a:hover::before,
.zm-capability__step a:focus-visible::before {
	color: var(--wp--preset--color--primary);
}

@media (prefers-reduced-motion: reduce) {
	.zm-capability__step a::before {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
   34. THE HOME DOSSIER GRID
   --------------------------------------------------------------------------
   One `wp:columns` holding all six cards, laid out by the ladder rather than
   by chunking them into rows in PHP. Three across from 880, two from 560, one
   below — the same three states the index raster uses (section 15), so the
   home teaser and /projecten/ reflow at the same widths.

   `minmax(0, 1fr)` rather than `1fr`: a grid track's default minimum is
   `auto`, which a long scope line can push past its share and make the row
   uneven.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.wp-block-columns.zm-dossier-grid {
		display: grid;
		grid-template-columns: repeat(3, minmax(0, 1fr));
		align-items: start;
	}
}

@media (min-width: 560px) and (max-width: 879.98px) {
	.wp-block-columns.zm-dossier-grid {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		align-items: start;
	}
}

/* ==========================================================================
   PHASE 4 — THE CAPABILITY PAGE
   Added 2026-09-24. /diensten/ only: the in-page index, the seven operation
   rows and the width at which they stop being rows. No new colour, no new font
   size, no new token, and nothing here is reachable from another page.
   ========================================================================== */

/* --------------------------------------------------------------------------
   35. THE IN-PAGE INDEX
   --------------------------------------------------------------------------
   The same `zm-capability` component the home page states the sequence with,
   compacted into a two-column block. Reused rather than rewritten so the two
   pages say the seven operations in one visual language — same hairline, same
   counter, same hover — and so a change to the component is a change to both.

   TWO COLUMNS FILLED DOWNWARDS, not across: `grid-auto-flow: column` over four
   explicit rows gives 01-04 and 05-07, which is the order the eye reads a
   numbered list in. Filling across would print 01 02 / 03 04 / … and make the
   numbering look shuffled.

   THE RULES MOVE FROM THE CONTAINER TO THE ITEM. `zm-capability` draws a rule
   above the list and one below every step, which is correct for a single
   column and wrong for two: the first item of the second column would start
   without a rule. Here every step draws its own top rule and the list draws one
   bottom rule under both columns.

   HALF THE STEP HEIGHT. The list on the home page is a destination and gets
   1.5 modules; this one is a table of contents above the thing it indexes and
   gets 0.75, so it costs about 190px instead of 380 before the first operation.
   -------------------------------------------------------------------------- */
.zm-ops-index {
	border-top: 0;
	border-bottom: var(--zm-hairline) solid var(--zm-rule);
}

.zm-ops-index .zm-capability__step {
	border-bottom: 0;
	border-top: var(--zm-hairline) solid var(--zm-rule);
}

.zm-ops-index .zm-capability__step a {
	padding-block: calc(var(--zm-module) * 0.75);
	font-size: var(--wp--preset--font-size--small);
	grid-template-columns: 2.25rem 1fr;
	gap: var(--wp--preset--spacing--30);
	/* THE TARGET HAS A FLOOR THE MODULE CANNOT GIVE IT. `--zm-module` is a
	   viewport clamp — 16px at 1440, 10px on a phone — so a padding multiplier
	   that makes a 44px row on the desktop makes a 34px one where it matters,
	   which is the wrong way round for seven jump links a thumb has to hit.
	   Measured at 390 before this floor: 34px. WCAG 2.5.5 asks for 44. The
	   module still sets the rhythm everywhere the rhythm is already enough.

	   `box-sizing` IS PART OF THE FLOOR, NOT HOUSEKEEPING. This anchor inherits
	   `content-box`, so a bare `min-height: 44px` measured 68 on the desktop —
	   44 plus the padding — and made the index taller than the compact block it
	   is meant to be. With `border-box` the padding counts towards the floor and
	   the rule does what it says: at least 44, and the module above that. */
	box-sizing: border-box;
	min-height: 44px;
	align-content: center;
}

@media (min-width: 560px) {
	.zm-ops-index {
		display: grid;
		grid-auto-flow: column;
		grid-template-rows: repeat(4, auto);
		column-gap: var(--wp--preset--spacing--70);
	}
}

/* --------------------------------------------------------------------------
   36. THE OPERATION ROW
   --------------------------------------------------------------------------
   Seven of one component. The layout is `zm-editorial` (section 8) with the
   per-row modifiers section 20 already defines, so nothing here re-implements
   the split, the bleed or the tall track — this section adds only what makes a
   row an OPERATION: its number, the rule that separates it from the next one,
   and the evidence block at its foot.

   THE RULE IS THE SEPARATOR, NOT A BOX. Seven cards on a capability page is the
   services-menu layout this site left behind in Phase 3. A hairline at the same
   weight as the project record's is enough to say "next operation" and keeps
   the page reading as one document.
   -------------------------------------------------------------------------- */
.zm-ops > .wp-block-columns + .wp-block-columns {
	border-top: var(--zm-hairline) solid var(--zm-rule);
	padding-top: var(--wp--preset--spacing--70);
}

/* The cross-reference number. Tabular figures because seven of them sit in a
   vertical line down the page and proportional digits would make that line
   ragged — the same reason section 22 gives for every other figure on this
   site. It is text rather than a counter; the pattern documents why. */
.zm-op__num {
	font-variant-numeric: tabular-nums lining-nums;
}

/* The evidence block: the coverage tally and the project that documents it,
   set as one unit at the foot of the text column and divided from the
   description by the same hairline the rest of the page uses. */
.zm-op__evidence {
	border-top: var(--zm-hairline) solid var(--zm-rule);
	padding-top: var(--wp--preset--spacing--30);
}

/* THE EVIDENCE LINK IS A TARGET, NOT A WORD IN A SENTENCE. WCAG 2.5.8's
   inline exception covers a link inside running prose; this one is a line of
   its own under the coverage tally, so it is measured like a control. At the
   `small` size it rendered 15px tall — the padding takes it to 25 without
   moving the text, because `inline-block` padding grows the box around the
   baseline the line already sits on. */
.zm-op__link a {
	display: inline-block;
	padding-block: 0.35em;
	color: var(--wp--preset--color--foreground);
	text-decoration-color: var(--zm-rule);
	text-underline-offset: 0.2em;
	transition: color var(--zm-duration) ease, text-decoration-color var(--zm-duration) ease;
}

.zm-op__link a:hover,
.zm-op__link a:focus-visible {
	color: var(--wp--preset--color--primary);
	text-decoration-color: currentColor;
}

@media (prefers-reduced-motion: reduce) {
	.zm-op__link a {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
   37. WHERE AN OPERATION ROW STOPS BEING A ROW
   --------------------------------------------------------------------------
   MEASURED BEFORE THIS RULE (/diensten/, document height by viewport width):

       1440  7.073    900  4.935    782  4.387    640  4.612
       1280  6.462    880  4.848    781  4.804    639  8.973   <- +94.6%
       1024  5.467    879  4.789    768  4.749    390  7.548

   One pixel cost 4.361px. Section 26 gives the editorial row two columns from
   640px up; below it core stacks, and a stacked row is a full-viewport
   photograph followed by its text. Four of those was the old page's worst
   case. Seven is this one's, and two of the seven are portrait: at 639 the
   metselwerk row rendered a 639x852 photograph and prefab a 576x1024, which is
   1.9 screens for one operation.

   THE ROW NOW SURVIVES TO 560, WHICH IS A RUNG THIS SITE ALREADY HAS. Section
   26's ladder puts the mobile state below 560 and uses that width for every
   card raster on the site; /diensten/ was the only page whose rows gave up at
   640, so it had a boundary of its own for no reason. Moving it to 560 deletes
   the stacked state from the 560-639 band outright — that is where the 4.361px
   went — and leaves /diensten/ with exactly one transition, at the width the
   rest of the site already transitions at.

   50/50 RATHER THAN THE TABLET'S 55/45. At 560 a 55% media track is 256px and
   a 45% text track is 210px, which is too narrow for a heading like "Metselwerk
   in doorstrijktechniek". Equal tracks give both 233px. Section 8 abandoned
   four-up cards below roughly 230px because the photograph stops showing
   brickwork — that threshold was measured on a CARD, where the photograph
   carries the whole item; here the heading, the description and the coverage
   line carry it and the photograph supports them, so 233px is a support image
   at its floor rather than a card below its floor. Below 560 it stacks and the
   photograph gets the whole screen, which is the better answer once there is
   no room for two of anything.
   -------------------------------------------------------------------------- */
/* THE TABLET BAND TAKES THE SAME EQUAL TRACKS. Section 26 gives every
   editorial row 55/45 between 640 and 879, which is right for a band that has
   one row and a lot of text beside one photograph. Measured on this page at
   879 with that ratio: 4.807px against 4.465px at 880 — a +7,7% step at the
   desktop/tablet rung, made of a text column dropping from 50% to 45% across
   seven rows at once. Equal tracks here too and the rung measures -0,1%: the
   page keeps the proportion it has at 1440 until it runs out of room for two
   columns at all, which is the one boundary section 37 leaves it. */
@media (min-width: 640px) and (max-width: 879.98px) {
	.zm-ops > .wp-block-columns:not(.zm-editorial--tall),
	.zm-ops > .wp-block-columns.zm-editorial--reverse:not(.zm-editorial--tall),
	.zm-ops-intro > .wp-block-columns {
		grid-template-columns: 1fr 1fr;
	}
}

@media (min-width: 560px) and (max-width: 639.98px) {
	.zm-ops > .wp-block-columns,
	.zm-ops-intro > .wp-block-columns {
		display: grid;
		grid-template-columns: 1fr 1fr;
		align-items: center;
	}

	/* Section 8 bleeds the editorial media past the root gutter below 640,
	   which is the mobile design and is wrong for a two-column row: the
	   photograph would overlap the text beside it. Undone here, and only for
	   the rows this section governs. */
	.zm-ops .zm-editorial__media,
	.zm-ops-intro .zm-editorial__media {
		margin-inline: 0;
		width: auto;
	}

	/* Same order swap as sections 8 and 26: the DOM order is media then text on
	   every row, so a screen reader and a keyboard user meet the seven
	   operations in one sequence and only the visual order alternates. */
	.zm-ops .zm-editorial--reverse .zm-editorial__media {
		order: 2;
	}

	.zm-ops .zm-editorial--reverse .zm-editorial__text {
		order: 1;
	}

	/* The two portrait sources still take the narrower track — the same rule
	   sections 20 and 26 state at their own proportions, restated here at this
	   one's. A 3:4 photograph in an equal track is 311px tall against a text
	   column of about 200; at 40/60 it is 233 and the pair finishes together. */
	.zm-ops > .wp-block-columns.zm-editorial--tall:not(.zm-editorial--reverse) {
		grid-template-columns: 40fr 60fr;
	}

	.zm-ops > .wp-block-columns.zm-editorial--tall.zm-editorial--reverse {
		grid-template-columns: 60fr 40fr;
	}
}

/* BELOW 560 THE PORTRAIT PHOTOGRAPHS ARE NARROWED, NOT CROPPED. This is the
   stacked analogue of `--tall`, and it exists for the same reason: at 390 the
   metselwerk source renders 390x520 and prefab 390x693, so two of the seven
   operations cost as much as four of the others. `cover` at a shorter ratio
   would fix the height by throwing away the top of a chimney, which is the one
   thing that photograph is of. Narrowing the frame keeps every pixel of the
   image and costs only the margin either side — and a portrait held to two
   thirds of a phone screen still reads as a photograph of brickwork, which a
   200px-wide one would not.

   THE LANDSCAPE ROWS KEEP THE FULL WIDTH. Section 8's edge-to-edge mobile
   treatment is a design decision ("the photograph takes the whole screen"), not
   an accident, and at 4:3 it costs 293px on a 390px phone — the same as every
   other page on this site. Only the two portraits are exceptional, and only
   they are treated. */
@media (max-width: 559.98px) {
	.zm-ops .zm-editorial--tall .zm-editorial__media {
		margin-inline: 0;
	}

	.zm-ops .zm-editorial--tall .zm-frame {
		max-width: 64%;
		margin-inline: auto;
	}
}

/* --------------------------------------------------------------------------
   38. SEVEN PHOTOGRAPHS IS NOT FOUR — THE DESKTOP TRACK
   --------------------------------------------------------------------------
   MEASURED at 1440 with the rows on the site's usual 7fr/5fr: the photograph
   renders 783x587 and the text column beside it 212. Five of those in a column
   is a slide show with captions, and the thing a hoofdaannemer came for — the
   name of the operation, what it is, how often we have done it and where it
   can be checked — is a third of the row it sits in. That proportion was
   correct when this page led with FOUR services and the rest were cards; it is
   not correct for seven equal operations.

   EQUAL TRACKS. The photograph is evidence and the text is the specification,
   and on a capability page neither outranks the other. Measured after: the
   photograph is 688x516 — still six times the area of the card it replaced —
   and the seven rows cost about 900px less than they did.

   `--tall` KEEPS ITS OWN TRACKS. A portrait source is narrowed rather than
   cropped (sections 20 and 26), and that judgement does not change because the
   landscape rows moved; the `:not()` here is what leaves it alone. The reversed
   selector is spelled out because section 20's `.wp-block-columns.zm-editorial
   --reverse` is a two-class selector and would otherwise win.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
	.zm-ops > .wp-block-columns:not(.zm-editorial--tall),
	.zm-ops > .wp-block-columns.zm-editorial--reverse:not(.zm-editorial--tall) {
		grid-template-columns: 6fr 6fr;
	}

}

/* THE 9:16 SOURCE, EVERYWHERE THERE ARE TWO COLUMNS. `prefab-schoorsteen` is
   1152x2048 — a chimney photographed whole, which is why it is that shape and
   why cropping it is not an option. In the 4fr `--tall` track it renders
   499x887 against rows of about 540, so the operation with the smallest
   coverage figure gets the largest block on the page. Capping the frame brings
   it to about 350 wide and 622 tall: the tallest row, which is honest, rather
   than twice its neighbours.

   THE CAP RUNS DOWN TO 560, NOT DOWN TO 880, AND THAT WAS MEASURED. Held to
   the desktop only, it created a step of its own at the desktop/tablet rung:
   879 measured 4.801px against 4.465 at 880 (+7,5%), and +314 of the +336 was
   these two rows — prefab alone going from 191x340 to 328x582 the moment the
   cap stopped applying. A treatment that exists because a photograph is 9:16
   cannot switch off at a width where it is still 9:16. Below 560 the row
   stacks and section 37 caps it again, at that state's own proportion.

   THE NARROWED FRAME STAYS ON THE BLEED EDGE. Centred inside a column that
   already runs past the gutter, the photograph sat 75px in from the viewport
   edge — measured — which lines it up with nothing: not the content gutter at
   120, not the bleeding rows at 0. It is pushed to whichever edge its row
   bleeds towards, so every photograph on the page starts at the same place and
   only its width differs. */
@media (min-width: 560px) {
	.zm-op--extra-tall .zm-frame {
		max-width: 70%;
		margin-inline: 0 auto;
	}

	.zm-op--extra-tall.zm-editorial--reverse .zm-frame {
		margin-inline: auto 0;
	}
}

/* ==========================================================================
   PHASE 4.1 — /over-zuidema/ JOINS THE LADDER
   Added 2026-09-24. One page, one defect: this was the last page never brought
   onto the responsive ladder, and below 782 it received core's stacking whole.
   No copy, no image, no colour, no type and no section order changed.
   ========================================================================== */

/* --------------------------------------------------------------------------
   39. THE TWO TEXT PAIRS, ON THE GRID VOCABULARY
   --------------------------------------------------------------------------
   MEASURED BEFORE (/over-zuidema/, document height by viewport width):

       1440  2.452   1024  2.370    880  2.385    782  2.368    640  3.691
       1280  2.447   1000  2.352    879  2.384    781  3.817    560  3.661
       1200  2.436    900  2.294                  768  3.792    390  3.754

   One pixel cost 1.449px (+61,2%). Broken down at that boundary:

       main    1.880 -> 2.908   (+1.028)
         intro pair      154 ->   177  (+23)
         materieel pair  385 -> 1.335  (+950)
         regions pair    216 ->   271  (+55)
       footer    382 ->   804   (+422)   <- the shared template part, not this page

   The mechanism is the one Phase 2 documented and it is not page-specific:
   core stacks EVERY `wp-block-columns` below 782 with `flex-wrap: wrap` on the
   container and `flex-basis: 100% !important` on the column. Nothing on this
   page had ever told it otherwise — the three column blocks carried no class
   at all, which is why the ladder's `:has()` and `.zm-editorial` rules never
   matched them. This page was not broken by a rule; it was never reached by
   one.

   THE MEDIA PAIR IS FIXED IN THE PATTERN, NOT HERE. Its two columns are an
   image, a heading and a line of text — a card — so they now carry `zm-card`
   and section 26's existing raster rule matches them: two across from 560,
   one below. That is the whole of the +950, and it cost no CSS, because the
   rule was already written and was only missing the marker that section 26's
   own docblock says is the marker ("their children carry `.zm-card`").

   WHAT IS LEFT IS THE TWO TEXT PAIRS, and they need a rule because they are
   not cards: below 782 core would stack them like everything else.

   T5, 2026-10-03: THE PAIRS NOW USE THE GRID VOCABULARY. Until T5 they were
   authored at 52/48 and 42/58 and this section reproduced those proportions
   with `52% auto` / `42% auto` between 640 and 781, leaving core's flex row
   above. They are normalised to the 12-column splits the site's editorial
   rows already use — 6/6 for the intro, 5/7 for the regions — as `fr` tracks
   dividing what remains after the row's own gap, exactly as `zm-editorial`
   does. The change is 2 and 0.3 percentage points of width; content, order,
   gap and alignment are unchanged.

   640 IS STILL THE FLOOR. Section 26 puts two-column text rows at 640 and up;
   below it core stacks the pairs, as before. Above 782 the grid now holds
   too, so one rule states the layout at every width it applies to. The
   pattern's column widths say 50/50 and 41.67/58.33 so the editor shows the
   same proportions.
   -------------------------------------------------------------------------- */
@media (min-width: 640px) {
	.wp-block-columns.zm-about-intro,
	.wp-block-columns.zm-about-regions {
		display: grid;
		align-items: start;
	}

	.wp-block-columns.zm-about-intro {
		grid-template-columns: 1fr 1fr;
	}

	.wp-block-columns.zm-about-regions {
		grid-template-columns: 5fr 7fr;
	}
}

/* ==========================================================================
   PHASE 4.2 — THE SHARED FOOTER GETS A TABLET STATE
   Added 2026-09-24. One template part, one rule. No copy, no link, no heading,
   no colour, no type size and no information architecture changed.
   ========================================================================== */

/* --------------------------------------------------------------------------
   40. THE FOOTER'S FOUR COLUMNS BECOME TWO BEFORE THEY BECOME ONE
   --------------------------------------------------------------------------
   MEASURED BEFORE (footer height by viewport width, identical on all 13 pages):

       800  384    781  804    640  819    560  845
       782  382    780  804    639  819    559  845    390  883

   One pixel cost 422px (+110,5%), and it is the whole of the +422 this phase
   was opened for: the colophon row is flat across the boundary (47 -> 47) and
   the columns block is 228 -> 649. The mechanism is core's, not this footer's
   — `flex-wrap: wrap` plus `flex-basis: 100% !important` below 782 — and this
   template part had never been given a rule of its own, exactly like
   /over-zuidema/ in Phase 4.1. Four columns became four stacked blocks at once.

   WHY THE ANSWER IS NOT "HOLD FOUR COLUMNS LOWER". Measured min-content per
   column, which is what decides whether a word breaks:

       col 1  brand      162px   the logo
       col 2  contact    219px   "info@zuidemametselwerken.nl"
       col 3  snel naar   89px   "Steenzagerij"
       col 4  werkgebied 155px   "Werkgebied", uppercase at 0.12em tracking

   Against the widths the 34/30/18/18 row actually produces, four columns are
   only clean at 1024 and above (157px in the 18% track against the 155px the
   heading needs). At 900 and 880 the Werkgebied heading already breaks mid-word
   — "WERKGEBI / ED" — and below about 880 the email address breaks too. AT 782,
   TODAY, THE FOOTER IS ALREADY BROKEN: 119px tracks, "WERKGEB / IED",
   "Gedocumenteer / de", "info@zuidemametselwerke / n.nl". Extending that state
   downwards would have extended a defect; the four-column composition has to
   END at a rung, and 880 is the rung the ladder already uses for
   desktop-to-tablet.

   TWO COLUMNS, 560 TO 879. In this band the narrower track measures 296px at
   640 and 411px at 879, against the 219px the widest unbreakable token needs,
   so nothing breaks anywhere in it. It reaches down to 560 — the rung every
   card raster on this site already uses (section 26) — rather than stopping at
   640, because two 236px tracks at 560 still clear 219 and stopping earlier
   would have added a second step for nothing.

   The pairing is DOM order and nothing is moved: brand beside contact, then
   the two link columns. No `order`, no `grid-area`, no reordering — so the
   keyboard path through the footer is the same at every width.

   RESULT: 782 and 781 are now the same composition and measure within a pixel
   of each other. The transitions move to 879/880 and 559/560, which are the
   ladder's own rungs and where the rest of the site already changes shape.

   T8-B3, 2026-10-04 — THE TWO-COLUMN BAND NOW RUNS TO 1023.98. Ending the four
   columns at 880 left the defect named above in place: from 880 to 1019 the
   18% track is 134-156px, under the ~155px the uppercase "WERKGEBIED" heading
   needs, so every page showed "WERKGEBI / ED" (measured by word rectangles).
   The four-column row is clean from 1020 (156px) and this band now ends just
   below 1024, so the existing two-row composition is kept, unchanged, through
   that range. At 880 the tracks measure 412px against the 219px the e-mail
   address needs. The T6 normalisation to 4/4/2/2 was measured and abandoned:
   its narrower fourth track breaks the same heading up to 1024. Below 880 and
   from 1024 up the footer is byte-identical to before.
   -------------------------------------------------------------------------- */
@media (min-width: 560px) and (max-width: 1023.98px) {
	.wp-block-columns.zm-footer-cols {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		align-items: start;
	}
}

/* THE FOOTER TELEPHONE IS TYPOGRAPHY AT REST. It keeps its size and weight;
   interaction red is the hover and focus state only, per the T4 red rule.
   T8-B1: the home contact band's number follows the same rule now that the
   band sits on the light surface instead of the dark one. */
.zm-footer-tel a,
.zm-contact-tel a {
	color: var(--zm-text);
}

.zm-footer-tel a:hover,
.zm-footer-tel a:focus-visible,
.zm-contact-tel a:hover,
.zm-contact-tel a:focus-visible {
	color: var(--zm-interaction);
}

/* ==========================================================================
   PHASE 4.3 — /steenzagerij/ JOINS THE LADDER
   Added 2026-09-24. One page, three column blocks, no content change.
   ========================================================================== */

/* --------------------------------------------------------------------------
   41. THE SAWING PAGE KEEPS ITS PAIRS TO 560
   --------------------------------------------------------------------------
   MEASURED BEFORE (/steenzagerij/, document height by viewport width):

       1440  4.056   1024  3.819    880  3.495    782  3.481    640  5.289
       1280  4.056   1000  3.762    879  3.673    781  5.454    639  5.287
       1200  4.034    900  3.542                  768  5.431    560  5.061
       1180  4.007                                                559  5.349

   One pixel cost 1.973px (+56,7%), all of it in `main` — Phase 4.2 had already
   made the footer flat across this boundary. Split by block:

       intro pair  (48/auto)      154 ->   152      -2
       zm-process  (66/30)        461 -> 1.683  +1.222
       closing     (46/auto)      626 -> 1.380    +754

   Three `wp-block-columns`, none of them carrying a class, all stacking at once
   under core's `flex-wrap: wrap` and `flex-basis: 100% !important`. The same
   diagnosis as /over-zuidema/ in Phase 4.1 and the shared footer in Phase 4.2,
   and the same answer: the rows are given the ladder's mechanism at their own
   authored proportions, in the band where core has taken it away.

   `%` AND `auto` FOR THE TWO ROWS THAT ARE AUTHORED THAT WAY. A percentage
   track resolves against the container exactly as `flex-basis: 48%` does and
   `auto` takes what is left after the gap, so the grid reproduces the flex row
   to the pixel. Verified at 782: 352/378 before and after on the closing row.

   `fr` FOR THE PROCESS ROW, BECAUSE IT IS AUTHORED 66/30 AND NOT 66/34. Those
   do not sum to 100, so `66% auto` would widen the second column by four points
   and quietly re-proportion a row whose two photographs were balanced by
   measurement (see the Z-13 note above: "the two finish within about 30px of
   each other"). `66fr 30fr` divides what remains after the gap in the authored
   ratio and measures 505/229 at 782 — identical to the flex row.

   ABOVE 782 NOTHING IS DECLARED. Core's own flex row already produces all three
   compositions there, and restating it would be a second implementation of a
   layout that works.
   -------------------------------------------------------------------------- */
@media (min-width: 560px) and (max-width: 781.98px) {
	.wp-block-columns.zm-saw-intro {
		display: grid;
		grid-template-columns: 48% auto;
	}

	/* T7, 2026-10-03: the application row is the grid's 4/8 (was 46/auto),
	   text top-aligned, so the 9:16 chimney no longer stands beside a short
	   centred column. `1fr 2fr` divides what remains after the gap, which is
	   what the 33.33/66.67 flex row does from 782 up. */
	.wp-block-columns.zm-saw-closing {
		display: grid;
		grid-template-columns: 1fr 2fr;
	}
}

/* T8-B4, 2026-10-04: THE PROCESS ROW IS 64:27 FROM 560 UP. The bench photograph
   is 4:3 and the saw cut 9:16; two images are equally tall when their widths
   stand in the ratio of their aspect ratios, (4/3)/(9/16) = 64/27. The old
   66fr 30fr here, and core's 66%/30% flex row from 782, left the portrait
   46-60px taller. `fr` divides what remains after the gap, so the two images
   end level at every width with no crop, and the row reaches the content edge.
   The row is top-aligned in the pattern so a wrapping caption cannot move an
   image. Below 560 the row stacks as before. */
@media (min-width: 560px) {
	.wp-block-columns.zm-process {
		display: grid;
		grid-template-columns: 64fr 27fr;
	}
}

/* BELOW 560 THE TWO 9:16 PHOTOGRAPHS ARE NARROWED, NOT CROPPED. This is the
   rule section 37 states for /diensten/, applied to the two images on this page
   that have the same problem. `steenzagerij-zaagsnede` and `prefab-schoorsteen`
   are both 1152x2048; at full width on a phone they render 374x665 each, so two
   of the page's five photographs cost more than the other three together.
   Measured at 559 before this rule: 543x965 each, 1.930px of a 5.349px page.

   A shorter ratio would fix the height by cutting the top off a chimney and the
   blade out of a saw cut, which is exactly what the Z-13 pass above refused to
   do. Narrowing the frame keeps every pixel and costs only the margin beside
   it. The image is held to the START of its column rather than centred so that
   it and its caption share one left edge. */
@media (max-width: 559.98px) {
	.zm-process > .wp-block-column:nth-child(2) .wp-block-image,
	.zm-saw-closing > .wp-block-column:nth-child(1) .wp-block-image {
		max-width: 64%;
		margin-inline: 0 auto;
	}
}

/* --------------------------------------------------------------------------
   42. (REMOVED IN P3-2, 2026-10-02)
   --------------------------------------------------------------------------
   Phase 4.4's contact sheet for /project-spuistraat/ (`zm-gallery--contactsheet`,
   requested through `gallery_class`). Its rules were scoped to pre-P2 gallery
   markup; P2's rows replaced that markup on every project page, so they matched
   nothing. The rules and the page's `gallery_class` were removed together; a
   stack of two portraits now stays paired on a phone through section 50. */

/* ==========================================================================
   PHASE 5 — THE COMPANY PROFILE
   Added 2026-09-24. /over-zuidema/ only. Two additions to the existing record
   component and one text-link style. No new colour, no new font size, no new
   breakpoint, no new layout primitive.
   ========================================================================== */

/* --------------------------------------------------------------------------
   43. A SECOND LINE IN A RECORD VALUE
   --------------------------------------------------------------------------
   The project record on the company page carries two things per row that are
   not the same thing: WHAT the project is, which is a link into its dossier,
   and WHAT WE DID there, which is the scope line. Stacking them inside one
   `<dd>` keeps the `<dt>`/`<dd>` pairing honest — one term, one description —
   where a third column would have meant inventing a `<dl>` that is not a `<dl>`.

   The note takes the size and colour the record's LABEL already uses, so the
   row reads as value-then-note rather than as two values, and the whole
   component still has exactly two type sizes.
   -------------------------------------------------------------------------- */
.zm-record__note {
	display: block;
	margin-top: 0.35em;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.5;
	color: var(--wp--preset--color--muted);
}

/* The record's values are plain text everywhere else on this site; on the
   company page seven of them are links into the project dossier.

   THE UNDERLINE IS THE AFFORDANCE, AND IT HAS TO BE DECLARED. The theme sets
   `text-decoration: none` on links and `theme.json` paints a link inside a
   heading `foreground` so a page does not read as a wall of red — which is
   right, and which left these seven project names looking like plain text:
   measured before this rule, `text-decoration-line: none` and the same colour
   as the label beside them. Nothing distinguished them but the pointer.

   So they take section 4b's answer, on a light ground: keep the neutral colour,
   carry the affordance on a 1px underline, and let hover and focus move the
   whole thing — text and underline together — to `primary`. That is WCAG 2.1
   SC 1.4.1 satisfied by something other than colour, and it is the treatment
   this site already uses wherever a link has been painted neutral. */
.zm-record__value a {
	color: var(--wp--preset--color--foreground);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.2em;
	transition: color var(--zm-duration) ease;
}

.zm-record__value a:hover,
.zm-record__value a:focus-visible {
	color: var(--wp--preset--color--primary);
	text-decoration-thickness: 2px;
}

/* A link on its own line, under a paragraph it belongs to — the typographic
   form of a secondary action, used where a button would overstate it. Same
   arrow, easing and 4px travel as `.zm-cta` and the card titles, so "this is a
   link you follow" is one gesture across the site. */
.zm-textlink a {
	display: inline-block;
	padding-block: 0.35em;
	color: var(--wp--preset--color--foreground);
	font-weight: 600;
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.2em;
	transition: color var(--zm-duration) ease;
}

.zm-textlink a::after {
	content: "\2192";
	display: inline-block;
	margin-inline-start: 0.4em;
	color: var(--wp--preset--color--primary);
	transition: transform var(--zm-duration) var(--zm-ease-out);
}

.zm-textlink a:hover,
.zm-textlink a:focus-visible {
	color: var(--wp--preset--color--primary);
	text-decoration-thickness: 2px;
}

.zm-textlink a:hover::after,
.zm-textlink a:focus-visible::after {
	transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
	.zm-record__value a,
	.zm-textlink a,
	.zm-textlink a::after {
		transition: none;
	}

	.zm-textlink a:hover::after,
	.zm-textlink a:focus-visible::after {
		transform: none;
	}
}

/* --------------------------------------------------------------------------
   44. A RECORD THAT MEASURES ITSELF, NOT THE WINDOW
   --------------------------------------------------------------------------
   Section 23 gives `.zm-record` its label column from 560px of VIEWPORT up.
   That assumption holds on a project page, where the record is the width of the
   content column and the viewport is a fair proxy for it. It does not hold on
   this page, where two records sit inside columns of an editorial row.

   MEASURED WITHOUT THIS RULE, at 640: the operational record's column was 267px
   wide, the 13rem label took 208 of it, and the four values wrapped in the 27px
   that were left. That one block rendered 3.642px tall and the page measured
   7.228px against 4.841 one pixel lower — a component failing because it asked
   the window how much room it had while standing in a 267px box.

   `@container` IS THE MECHANISM AND IT ADDS NO BREAKPOINT. The record asks its
   own width, which is the thing that actually decides whether a label fits
   beside a value. Below the threshold it falls back to the stacked form section
   23 already draws below 560 — the same component, one state lower, not a
   second implementation.

   28rem WAS MEASURED, NOT CHOSEN. Four thresholds were tried and the page
   measured at twelve widths for each. 24rem keeps the label column down to a
   200px value and costs 120px at 782 and 191 at 768; 31rem gives it up too
   early and costs 17px at 880; 34rem costs 115 at 1024. 28rem is the only one
   that is best or equal at every width. It is also where the arithmetic lands:
   an 11rem label plus the gap plus a value wide enough for a readable line.

   SCOPED WITH A MODIFIER on purpose. The project dossier's record keeps
   section 23's viewport rule exactly as Phase 2 wrote it; nothing on a project
   page changes. An engine without container queries gets the stacked form at
   every width, which is a legible record rather than a broken one.

   12rem RATHER THAN 13, AND NOT 11. The labels here are places, and the
   longest single token among them is "Koningdwarsstraat", which measures 182px
   set in the label's uppercase at 0.08em tracking. 11rem is 176 and broke it
   mid-word; 12rem is 192 and holds it, while still returning 16px to the value
   against section 23's 13rem.
   -------------------------------------------------------------------------- */
.zm-record--fit {
	container-type: inline-size;
}

.zm-record--fit .zm-record__row {
	grid-template-columns: 1fr;
}

@container (min-width: 28rem) {
	.zm-record--fit .zm-record__row {
		grid-template-columns: 12rem 1fr;
		align-items: baseline;
	}

	.zm-record--fit .zm-record__label {
		/* Section 23's optical correction, restated for this state. */
		transform: translateY(0.1em);
	}
}

/* ==========================================================================
   PHASE 6 — THE CONTACT PAGE
   Added 2026-09-24. /zuidema-contact/ only. Two column blocks put on the
   ladder, at their authored proportions. No new colour, no new font size, no
   new breakpoint, no new component: the guidance list is the record, the three
   cross-links are section 43's text link.
   ========================================================================== */

/* --------------------------------------------------------------------------
   45. THE INTAKE AND THE CONTEXT ROW JOIN THE LADDER
   --------------------------------------------------------------------------
   MEASURED BEFORE (/zuidema-contact/, document height by viewport width):

       1440  1.934   1024  1.899    880  1.883    782  2.109    640  2.284
       1280  1.930   1000  1.889    879  2.109    781  2.240    639  2.565

   The +6,2% at 781 is the mechanism Phases 4.1 to 4.4 each answered: the
   intake band is a plain `wp:columns` at 40/60 with no class, so core stacks
   it — the form leaves the side of the question it belongs to and the band
   costs 131px. The hero above it was never affected; it has been `zm-editorial`
   since Z-11 and has had the ladder since section 26.

   `%` PLUS `auto`, the Phase 4.1 device, so the grid reproduces the flex row it
   replaces rather than re-proportioning it. Above 782 nothing is declared:
   core's flex row already produces both compositions there.

   THE CONTEXT ROW IS AUTHORED THE SAME WAY and joins on the same rung, so the
   page has one shape from 640 up and one below — the ladder's own boundary,
   not core's.
   -------------------------------------------------------------------------- */
@media (min-width: 640px) and (max-width: 781.98px) {
	.wp-block-columns.zm-intake-cols {
		display: grid;
		grid-template-columns: 40% auto;
		align-items: start;
	}

	.wp-block-columns.zm-contact-context {
		display: grid;
		grid-template-columns: 52% auto;
		align-items: start;
	}
}

/* --------------------------------------------------------------------------
   46. THE ENQUIRY FIELDS KEEP A MEASURE
   --------------------------------------------------------------------------
   Section 18 holds the form to 44rem so a "Naam" input is not a metre wide.
   Inside the intake band's 60% column that cap is already met by the column
   itself at every width — but the guidance record now sits opposite the fields
   and the two must read as one band rather than as two unrelated lists, so the
   record takes the same hairline rhythm the fields have. Nothing here restyles
   a form control: the platform renders them and this page does not reach into
   them beyond the measure section 18 already set.
   -------------------------------------------------------------------------- */
.zm-intake .zm-record {
	max-width: 32rem;
}

/* ==========================================================================
   PHASE 7 — THE PRIVACY STATEMENT
   Added 2026-09-24. /zuidema-privacy/ only, and it is the LAST page of the
   fourteen still carrying the stacking cliff. No sentence, no colour, no font
   size, no breakpoint and no section order changed; the page was built on
   2026-09-21, before Phase 1 existed, which is the whole reason it was still
   outside the ladder and still drew its own labelled values by hand.
   ========================================================================== */

/* --------------------------------------------------------------------------
   47. THE SIX PAIRS JOIN THE LADDER
   --------------------------------------------------------------------------
   MEASURED BEFORE (/zuidema-privacy/, document height by viewport width):

       1440  3.150   1024  3.117    880  3.123    782  3.286    640  4.051
       1280  3.139    900  3.140    879  3.328    781  4.117    559  4.414
                                                   768  4.107    375  5.096

   One pixel cost 831px (+25,3%). Broken down at that boundary, by the six
   `wp-block-columns` this page is composed of:

       lede (58/42)        128 ->   171   (+43)
       fields (52/48)      637 ->   928  (+291)
       steps (three-up)    219 ->   322  (+103)
       rights (42/58)      421 ->   535  (+114)
       identity (52/48)    406 ->   600  (+194)
       close (62/38)        89 ->   178   (+89)

   The mechanism is the one section 39 documented for /over-zuidema/ and it is
   not page-specific: core stacks EVERY `wp-block-columns` below 782 with
   `flex-wrap: wrap` on the container and `flex-basis: 100% !important` on the
   column. None of these six carried a class, so no rule in this file — the
   ladder's `:has()` raster, `.zm-editorial`, section 30's gallery — could ever
   have matched them. The page was not broken by a rule; it was never reached
   by one.

   `%` AND `auto`, NOT `fr`, for the five pairs, exactly as section 39 sets out:
   a percentage track resolves against the container the way `flex-basis: 52%`
   does and `auto` takes what is left after the gap, so the grid reproduces the
   flex row it replaces rather than re-proportioning it at widths that were
   never broken.

   THE STEPS BAND IS THE ONE THAT IS NOT A PAIR. Its three columns are authored
   with no width at all, which core renders as three equal flex children, so
   `repeat(3, 1fr)` is that same composition and not a new one. Three text
   columns at 700px are about 205px each — the same measure they already have
   at 782, where nothing is declared and nothing is wrong. Section 26's
   250px-wide-card reasoning does not apply here: these columns hold a heading
   and two lines of prose, not a photograph of brickwork.

   640 IS THE FLOOR BECAUSE THE LADDER SAYS SO. Section 26 puts two-column
   editorial rows at 640 and up; every block here is a text row, so every one
   takes that rung and the page has one shape from 640 up and one below. Above
   782 nothing is declared at all — core's own flex row already holds.
   -------------------------------------------------------------------------- */
@media (min-width: 640px) and (max-width: 781.98px) {
	.wp-block-columns.zm-privacy-lede,
	.wp-block-columns.zm-privacy-fields,
	.wp-block-columns.zm-privacy-steps,
	.wp-block-columns.zm-privacy-rights,
	.wp-block-columns.zm-privacy-identity,
	.wp-block-columns.zm-privacy-close {
		display: grid;
		align-items: start;
	}

	.wp-block-columns.zm-privacy-lede {
		grid-template-columns: 58% auto;
		align-items: end; /* the authored `verticalAlignment: bottom` on the date column */
	}

	.wp-block-columns.zm-privacy-fields,
	.wp-block-columns.zm-privacy-identity {
		grid-template-columns: 52% auto;
	}

	.wp-block-columns.zm-privacy-steps {
		grid-template-columns: repeat(3, 1fr);
	}

	.wp-block-columns.zm-privacy-rights {
		grid-template-columns: 42% auto;
	}

	.wp-block-columns.zm-privacy-close {
		grid-template-columns: 62% auto;
		align-items: center; /* the authored `verticalAlignment: center` on both cells */
	}
}

/* --------------------------------------------------------------------------
   48. A RECORD WHOSE TERM IS LONGER THAN ANY LABEL COLUMN
   --------------------------------------------------------------------------
   The statutory identification is a record like any other, with one term that
   no label column on this site can hold. MEASURED, by cloning each `<dt>` and
   letting it size itself:

       Wat u ons stuurt     longest label  "Telefoonnummer"                155px
       Wat u kunt vragen    longest label  "Verwijdering"                  123px
       Wie verantwoordelijk longest label  "Verwerkingsverantwoordelijke"  299px

   Section 44's column is 12rem (192px) and section 23's is 13rem (208px), so
   the first two records fit every label on one line and the third broke its
   term MID-WORD across two: "VERWERKINGSVERANT / WOORDELIJKE". That is the
   failure section 44 already names when it explains why 11rem was rejected
   there.

   WIDENING THE COLUMN WAS THE OBVIOUS ANSWER AND IT IS THE WRONG ONE. 299px is
   18.7rem inside a 624px container; the other three terms in this record are
   "Adres" (55px), "KvK" (31px) and "E-mail" (54px), so a column wide enough for
   the first would leave three rows running a quarter of the way across and then
   stopping. The record would fit its longest label by making its other three
   look broken.

   SO IT TAKES THE STATE THE COMPONENT ALREADY HAS. `.zm-record` stacks its term
   above its value below section 23's 560px and below section 44's 28rem
   container — this record simply stays there at every width. Nothing new is
   drawn: same hairline, same two type sizes, same spacing. The term is a legal
   one and cannot be shortened to fit a column, which is exactly the case the
   stacked state is for.
   -------------------------------------------------------------------------- */
.zm-record--stacked .zm-record__row {
	grid-template-columns: 1fr;
	gap: var(--wp--preset--spacing--20) 0;
}

/* --------------------------------------------------------------------------
   49. THE PROJECT LEAD PHOTOGRAPH, AND THE ONE SINGLE PORTRAIT
   --------------------------------------------------------------------------
   P1, 2026-10-02 — the image system. Project pages no longer open on a cover:
   the title band sits on the page ground and the lead photograph follows it,
   uncovered and undimmed (functions.php, zuidemametselwerken_project()).

   ROLES, AND WHERE EACH ONE'S GEOMETRY LIVES:

     Lead        here. A role class chosen from the source's own ratio by
                 zuidemametselwerken_lead_class(); never an inline ratio, so the
                 phone rule below can override it without `!important`.
     Card        section 9 and zuidemametselwerken_project_card(): 4:3, which is
                 the source ratio of every card photograph — no crop.
     Gallery     the file's own orientation (zuidemametselwerken_ratio()), with
                 the row widths measured so cells end level (section 30). No
                 4/5 or 16/9 overrides since P1.
     Detail      natural ratio wherever it appears.

   `zm-lead--crop` is a 4:3-class landscape. 3:2 from 640px up keeps 89% of
   its height, centred; below 640 it returns to 4:3, which for these sources
   is the whole photograph — a phone never gets a portrait crop of a
   landscape. `zm-lead--natural` keeps the file's ratio at every width.

   A PORTRAIT IS CAPPED AT HALF THE CONTENT WIDTH from 640px up — as a lead,
   and as the single body photograph of a two-photo project (Binnenhaven's
   3:4 street view rendered 1200 x 1600 before P1). Below 640 it spans the
   column like everything else; section 30's 72vh guard still applies inside
   galleries. No radius, shadow, border or overlay: the photograph is the
   object.
   -------------------------------------------------------------------------- */
.zm-project-lead .zm-lead {
	margin-block: 0;
}

.zm-lead img {
	display: block;
	width: 100%;
	height: auto;
}

.zm-lead--crop img {
	aspect-ratio: 3 / 2;
	object-fit: cover;
}

@media (max-width: 639.98px) {
	.zm-lead--crop img {
		aspect-ratio: 4 / 3;
	}
}

/* GORREDIJK IS ANCHORED AT THE TOP. Its chimney top sits ~4% below the
   photograph's top edge, inside the 5.5% the centred 3:2 crop removes; the
   house base sits at ~73% with mud below. Top-anchored, the crop takes the
   whole 11% from the foreground. Keyed on the attachment file (media.json),
   since the lead carries no project class; below 640 nothing is cropped. */
@media (min-width: 640px) {
	.zm-lead--crop img[src*="/gorredijk-gevelrij."] {
		object-position: 50% 0%;
	}
}

/* P2 FIX: `50%` was half of the full-width BAND, not of the content column —
   Binnenhaven's portrait measured 760px at 1536, not 600. Half the global
   content size is what was meant — or half the band, whichever is smaller,
   so a tablet does not get 600px of a 752px column. */
@media (min-width: 640px) {
	.zm-lead--portrait,
	.zm-gallery > .zm-photo--portrait {
		max-width: min(50%, calc(var(--wp--style--global--content-size) / 2));
		margin-inline: auto;
	}
}

/* --------------------------------------------------------------------------
   50. THE PROJECT PAGE AS AN EDITORIAL SEQUENCE
   --------------------------------------------------------------------------
   P2, 2026-10-02. Title band → lead photograph (49) → project information →
   gallery → one action. Markup: zuidemametselwerken_project().

   GALLERY COMPOSITIONS. Four, chosen by how many photographs follow the lead:
   `--single`, `--pair`, `--feature` and `--feature-rows`. Inside each, the cell
   widths come from the files' own ratios, so every ROW ends level by
   arithmetic. The one edge arithmetic cannot close is the feature beside a
   stack of two: the gap inside the stack is not in the sums, and the bottoms
   ended 16px apart at 1536 (44px on Emmeloord). So from 640px the feature
   fills its column — the column is stretched to the stack's height by flex —
   and `object-fit: cover` trims about 2% of its width. Nothing else crops.

   Gallery rows carry `is-not-stacked-on-mobile`: core keeps them side by side
   at every width, so the old 640-781 two-column sheet (section 30), whose cells
   ended at different heights, no longer applies to them. Below 640 they become
   one column; a stack of two portraits stays a pair. Section 30's 72vh guard
   still caps a tall photograph on a phone.
   -------------------------------------------------------------------------- */
@media (min-width: 640px) {
	.zm-gallery__feature > figure.wp-block-image {
		height: 100%;
		margin-block: 0;
	}

	.zm-gallery__feature > figure.wp-block-image img {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}
}

@media (max-width: 639.98px) {
	.zm-gallery .wp-block-columns.is-not-stacked-on-mobile {
		flex-direction: column;
	}

	.zm-gallery__stack--portraits {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: var(--wp--preset--spacing--40);
		align-items: start;
	}

	.zm-gallery__stack--portraits figure.wp-block-image {
		margin-block: 0;
	}

	.zm-gallery .zm-gallery__stack--portraits figure.wp-block-image img {
		max-height: none;
		width: 100%;
	}
}

/* A single landscape spans the content width; the file is 1024px wide at
   the `large` size and core would otherwise draw it at that width, left-aligned
   (Gorredijk, measured 1024 of 1200). */
.zm-gallery > .zm-photo--wide img {
	width: 100%;
	height: auto;
}

/* PROJECT INFORMATION. The record (left) and the quantities (right) are one
   block; the quantities become a single column of figures so a third figure
   never wraps under the first two. The quantities keep their own hairline. */
.zm-project-info .zm-quantities {
	grid-template-columns: 1fr;
	max-width: none;
}

/* ONE ACTION. The button and the text link sit on one baseline; the gap is the
   section's own step. */
.zm-project-actions {
	align-items: center;
}

/* --------------------------------------------------------------------------
   51. A TEXT LINK ON A DARK BAND KEEPS ITS COLOUR ON HOVER
   --------------------------------------------------------------------------
   P3-1, 2026-10-02. The home page's featured project now ends on a `zm-textlink`
   inside a `foreground` band. Section 4 makes that link `background` with an
   underline, but section 43's hover — later in this file, same specificity —
   turned it `primary`: 3.2:1 on `foreground`, below the 4.5:1 its 600-weight
   text needs. On a dark band the hover keeps the band's link colour; the
   thicker underline (section 43) and the arrow's movement remain the hover
   cue. The arrow stays `primary`, a decorative glyph at 3.2:1 (3:1 floor). */
.has-foreground-background-color .zm-textlink a:hover,
.has-foreground-background-color .zm-textlink a:focus-visible {
	color: var(--wp--preset--color--background);
}

/* --------------------------------------------------------------------------
   52. THE DESIGN FOUNDATION — the roles later tranches compose with
   --------------------------------------------------------------------------
   T4, 2026-10-03. A role names what a value MEANS on this site. Every role
   aliases a preset the site already declares, so declaring them changes no
   rendered value; only the button corner and the transition time below are
   new. Spacing stays platform policy: the rhythm tiers are section 21's
   existing steps — tight `--zm-band-sm`, standard `--zm-band-md`, showcase
   `--zm-band-lg` — and no step is added here.

   TYPE. The seven steps map onto presets that exist: body `normal`, lead
   `lead`, H3 `subsection`, H2 `section`, H1 `title`, display `display`,
   figure `stat`; label (`eyebrow`) and caption (`small`) support them. Values
   are NOT changed in T4: home H2s still use both `section` and `title`, 25
   labels sit at 19px/700 (the large-text contrast rule recorded under D14),
   and 32 inline weights are 700 against the 400/600/800 roles. Reconciling
   those moves every page, so it belongs to the layout work, not to tokens.

   RED. `--zm-brand-red` is one deliberate accent per page and never small
   text (3.83:1 on white); `--zm-interaction` is the primary button and the
   link hover/focus colour (5.07:1). Nothing here draws red.
   -------------------------------------------------------------------------- */
:root {
	--zm-surface: var(--wp--preset--color--background);
	--zm-surface-muted: var(--wp--preset--color--secondary); /* #f6f6f7, owner-fixed (D14) */
	--zm-surface-dark: var(--wp--preset--color--foreground); /* at most one per page */
	--zm-text: var(--wp--preset--color--foreground);
	--zm-text-muted: var(--wp--preset--color--muted);
	--zm-brand-red: var(--wp--preset--color--accent);
	--zm-interaction: var(--wp--preset--color--primary);

	--zm-type-body: var(--wp--preset--font-size--normal);
	--zm-type-lead: var(--wp--preset--font-size--lead);
	--zm-type-h3: var(--wp--preset--font-size--subsection);
	--zm-type-h2: var(--wp--preset--font-size--section);
	--zm-type-h1: var(--wp--preset--font-size--title);
	--zm-type-display: var(--wp--preset--font-size--display);
	--zm-type-figure: var(--wp--preset--font-size--stat);
	--zm-type-label: var(--wp--preset--font-size--eyebrow);
	--zm-weight-body: 400;
	--zm-weight-ui: 600;
	--zm-weight-heading: 800;

	--zm-duration: 200ms; /* the approved 150-200ms; every site transition reads it */
	--zm-ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
	--zm-radius: 0;
}

/* THE BUTTON IS SQUARE. The platform button takes the small radius preset
   (4px); this site's geometry is the brick's, so every button here — CTA
   groups, the mobile menu's call button, the 404 — has a square corner.
   Same specificity as the platform rule, later in the cascade. Colour, size,
   weight, padding and the two-tone focus ring (owner decision 2026-09-12)
   stay the platform's. */
.fse-blocks-button-link {
	border-radius: var(--zm-radius);
}

/* RHYTHM TIERS, applied by class instead of inline padding (T5, 2026-10-03,
   /over-zuidema/ first). Tight is a section that continues the one before it;
   standard is every other band. Showcase (`--zm-band-lg`) gets its class when
   a page first needs it. The values are section 21's steps, unchanged. */
.zm-rhythm--tight {
	padding-block: var(--zm-band-sm);
}

.zm-rhythm--standard {
	padding-block: var(--zm-band-md);
}
