/* ==========================================================================
   WARMVAST — main stylesheet
   Design language: engineering-grade, calm, factual. Deep teal-green as the
   trust/primary color; warm yellow reserved as the energy accent (the live
   subsidy figure, the verdubbelaar). System font stack for zero render-blocking.
   ========================================================================== */

/* ---------- Self-hosted fonts ---------- */
@font-face {
	font-family: "Inter";
	font-style: normal;
	font-weight: 380 620;
	font-display: swap;
	src: url("../fonts/inter-latin-var.woff2") format("woff2");
}
@font-face {
	font-family: "Space Grotesk";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("../fonts/space-grotesk-latin-var.woff2") format("woff2");
}

/* ---------- Design tokens ---------- */
:root {
	--ink:            #1b1f21;
	--ink-2:          #3c4348;
	--ink-3:          #666e74; /* WCAG AA vs both --paper (4.79:1) and --surface (5.19:1); was #6c757b at 4.34:1 on paper (fails) */
	--paper:          #f4f4ef; /* corrected: what the page has always actually been painted */
	--surface:        #ffffff;
	--surface-2:      #fbfbf9;
	--line:           #e6e7e1;
	--line-strong:    #d6d8d0;

	--primary:        #0b6b5b;
	--primary-600:    #0a5d4f;
	--primary-700:    #074a3f;
	--primary-050:    #e7f2ef;
	--primary-ink:    #ffffff;
	/* The offset "glass plate" behind a real photo (.story-row__media::after,
	   .photo-slot::after) pairs this border with --primary-050's fill. It
	   was a bare hex at the call site before this token existed. */
	--primary-050-border: #d9e8e3;

	--accent:         #fed03d;
	--accent-strong:  #f6c200;
	--accent-700:     #e0a900;
	--accent-ink:     #2b2400;

	--success:        #1c7a45;
	--success-bg:     #e8f6ed;
	--warn:           #a35314;
	--warn-bg:        #fbf1e6;

	--radius-xs:  6px;
	--radius-sm:  8px;
	--radius-md:  11px;
	--radius:     14px;
	--radius-lg:  22px;
	--radius-pill: 999px;

	--shadow-sm:  0 1px 2px rgba(20,30,28,.06), 0 1px 3px rgba(20,30,28,.06);
	--shadow:     0 6px 20px rgba(20,40,36,.08), 0 2px 6px rgba(20,40,36,.05);
	--shadow-lg:  0 24px 60px rgba(12,40,34,.16), 0 6px 18px rgba(12,40,34,.08);


	/* Fine paper-grain only -- the ruled engineering-grid layer that used to
	   sit under this (measurement lines every 32px) read as technical
	   "kladpapier" against the hero/CTA's soft glow, a mismatched visual
	   language no amount of edge-fading could fix. Zero image requests: a
	   tiny inline SVG feTurbulence data-URI. */
	--noise-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.028 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");

	/* Glass system: a gradient hairline (cool->warm, echoing the thermal theme)
	   over a near-opaque fill so text contrast is never sacrificed for the effect. */
	--glass-fill:   rgba(255,255,255,.74);
	--glass-border: linear-gradient(150deg, rgba(255,255,255,.95), rgba(255,255,255,.18) 46%, rgba(11,107,91,.30));
	--glass-border-hot: linear-gradient(150deg, rgba(255,255,255,1), rgba(254,208,61,.5) 46%, rgba(11,107,91,.5));
	--shadow-glass: 0 22px 46px -22px rgba(12,40,34,.30), 0 8px 18px -12px rgba(12,40,34,.16);
	--shadow-float: 0 30px 60px -24px rgba(0,0,0,.55), 0 12px 26px -16px rgba(0,0,0,.4);

	--container: 1180px;
	--gutter: clamp(1rem, 4vw, 2.5rem);

	/* Motion system: one easing/duration vocabulary, used everywhere instead of bare "ease". */
	--ease-out:    cubic-bezier(.22,.61,.36,1);
	--ease-io:     cubic-bezier(.65,0,.35,1);
	--ease-spring: cubic-bezier(.34,1.4,.64,1);
	--dur-1: .16s;
	--dur-2: .26s;
	--dur-3: .5s;

	--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
	--font-display: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
	--font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

	--step--1: clamp(.82rem, .8rem + .1vw, .88rem);
	--step-0:  clamp(1rem, .96rem + .2vw, 1.075rem);
	--step-1:  clamp(1.15rem, 1.08rem + .35vw, 1.3rem);
	--step-2:  clamp(1.4rem, 1.28rem + .6vw, 1.7rem);
	--step-3:  clamp(1.75rem, 1.5rem + 1.1vw, 2.3rem);
	--step-4:  clamp(2.1rem, 1.7rem + 1.9vw, 3.2rem);
	--step-5:  clamp(2.5rem, 1.9rem + 2.8vw, 3.9rem);

	--header-h: 70px;
	/* Logo render height. Below ~55px the ISOLATIE descriptor's hairline stem
	   (8.359 units inside a 462.07-unit viewBox, so its bars are only
	   (h / 462.07) * 8.359 px wide) drops under 1 physical pixel and gets a
	   soft antialiasing fringe instead of a crisp edge -- see warmvast_logo_svg()
	   in inc/template-tags.php for why that no longer means a fully smeared
	   mark (inlining fixed the worse bug where the whole logo rasterised soft).
	   46px is a deliberate trade of that one fine descriptor line for a more
	   compact header: the icon and WARMVAST wordmark are both heavy enough to
	   stay clean at this size, only the small tagline picks up the fringe. Do
	   not go materially below this without re-checking that line is still
	   legible. */
	--logo-h: 46px;

	/* Ambient haze channels, set by main.js. --wv-mx/my = smoothed pointer;
	   --wv-fx/fy = card autonomous float; --wv-gx/gy = CTA autonomous float.
	   All -1..1, default 0 (centred / static for reduced-motion). */
	--wv-mx: 0;
	--wv-my: 0;
	--wv-fx: 0;
	--wv-fy: 0;
	--wv-gx: 0;
	--wv-gy: 0;

	/* ==================================================================
	   SEMANTIC ROLES - components reference these, never the primitives.
	   Surface-scoped: the dark block below redefines the same names.
	   ================================================================== */
	--surface-base:    var(--paper);
	--surface-raised:  var(--surface);
	--surface-sunken:  var(--surface-2);
	--text-primary:    var(--ink);
	--text-secondary:  var(--ink-2);
	--text-quiet:      var(--ink-3);
	--border-hairline: var(--line);
	--border-strong:   var(--ink-3);   /* not --line-strong: 1.44:1 fails WCAG 1.4.11 */
	--accent-hover:    var(--accent-strong);
	--accent-text:     var(--accent-ink);
	--focus-ring:      var(--primary);
	--error:           #a3201c;        /* the one added colour; sibling of --warn */

	/* ---- Ambient light: the brand's two atmospheric colours ----
	   The cool teal and warm amber the hero used to glow with. Restored at
	   their original values after the step-5 demolition removed them along
	   with the per-section gradients they were feeding. */
	--ambient-cool: rgba(11,107,91,.22);
	--ambient-warm: rgba(254,208,61,.24);

	/* The light hero's warm cluster (right side): light orange into red, the
	   paper counterpart of the dark hero's thermal glow. Tokens because the
	   dark hero's own values can't be reused -- see .hero__thermal on light
	   heroes for why. */
	--thermal-amber:  rgba(255,190,90,.55);
	--thermal-orange: rgba(247,140,60,.42);
	--thermal-red:    rgba(226,88,34,.32);
	--orb-warm:       rgba(254,208,61,.45);   /* light hero's yellow orb (dark: .22) */
	--orb-cool:       rgba(11,107,91,.34);    /* light hero's teal orb (dark: .6) -- lower so
	                                             the warm cluster over it stays peach, not mud */

	/* ---- The page background. Owned by .page-canvas, nothing else. ----
	   Two soft radial washes anchored to the TOP of the document, where the
	   hero sits, dissolving into --paper within ~1.4 viewport heights.

	   Sized in vh, not px and not %: a % stretches with document length (the
	   bug the step-5 pools had), and a fixed px ramp spread over 4000px
	   renders as FLAT on a 1,900px page -- which is exactly how the previous
	   linear version failed on /contact/, where the whole visible page was
	   one beige. vh ties the wash to the opening screen instead, so the same
	   proportion of the first view carries colour on a short page and a long
	   one alike.

	   Radial, not a vertical linear ramp, so the colour has depth and no
	   horizontal banding. Each gradient reaches full transparency well
	   inside its own 140vh box (cool by ~60vh, warm by ~92vh), so the layer
	   never terminates on a visible straight edge -- the failure mode the
	   whole canvas exists to avoid.

	   Position/size/repeat live INSIDE the token, per gradient: the token
	   expands into two background layers, so a single trailing `/ size` on
	   the shorthand would attach to the last layer only and silently leave
	   the first one auto-sized. */
	--canvas-wash:
		radial-gradient(120% 95% at 88% -8%, var(--ambient-cool), transparent 72%)
			top center / 100% 140vh no-repeat,
		radial-gradient(100% 85% at 0% 25%, var(--ambient-warm), transparent 78%)
			top center / 100% 140vh no-repeat;

	/* The one owned dark gradient, defined here and reused. Value is the
	   homepage hero's existing stack, extracted verbatim so the frozen hero
	   renders byte-identically. */
	--dark-gradient:
		radial-gradient(120% 80% at 85% -10%, rgba(11,107,91,.55), transparent 55%),
		radial-gradient(90% 70% at 10% 110%, rgba(254,208,61,.10), transparent 50%),
		linear-gradient(180deg, #171b1d, #1d2426);

	/* ---- Spacing: 4px scale ---- */
	--space-1: .25rem;  --space-2: .5rem;   --space-3: .75rem;  --space-4: 1rem;
	--space-5: 1.5rem;  --space-6: 2rem;    --space-7: 3rem;    --space-8: 4rem;
	--space-9: 6rem;    --space-10: 8rem;
	--rhythm-tight:  clamp(2rem, 4vw, 3rem);
	--rhythm-normal: clamp(3rem, 6vw, 5rem);
	--rhythm-loose:  clamp(5rem, 9vw, 8rem);

	/* ---- Type: base 17px, ratio 1.25. Steps 2-5 compress one step on phones. ---- */
	--text--1: .85rem;
	--text-0:  1.0625rem;
	--text-1:  1.3281rem;
	--text-2:  clamp(1.3281rem, 1.198rem + .577vw, 1.6602rem);
	--text-3:  clamp(1.6602rem, 1.498rem + .722vw, 2.0752rem);
	--text-4:  clamp(2.0752rem, 1.872rem + .902vw, 2.594rem);
	--text-5:  clamp(2.594rem,  2.340rem + 1.128vw, 3.2425rem);
	--tracking--1: 0;      --tracking-0: 0;      --tracking-1: -.005em;
	--tracking-2: -.01em;  --tracking-3: -.02em; --tracking-4: -.025em; --tracking-5: -.03em;
	--leading--1: 1.6;     --leading-0: 1.6;     --leading-1: 1.5;
	--leading-2: 1.35;     --leading-3: 1.22;    --leading-4: 1.12;     --leading-5: 1.05;
	--measure: 65ch;

	/* One shared floor for every LIGHT hero's content box. Sized against the
	   shortest real case (breadcrumb + h1 + lead, no aside -- e.g. contact) so
	   it is a safety net, not a forced height: pages with more content (an
	   aside, a longer lead) simply exceed it. This is what replaces heights
	   that used to be pure accidents of content length (454px on contact,
	   642px on spouwmuurisolatie, for no designed reason). The dark home hero
	   keeps its own 100svh sizing -- unrelated mechanism, see .hero--dark. */
	--hero-min: 21rem;

	/* ---- Radius: 4px nested scale. Outer = child radius + padding between. ---- */
	--radius-1: 4px;  --radius-2: 8px;  --radius-3: 12px;
	--radius-4: 16px; --radius-5: 20px; --radius-6: 24px;

	/* ---- Elevation: exactly two. Hairline is default, shadow the exception. ---- */
	--elevation-1: 0 1px 2px rgba(20,30,28,.06), 0 2px 6px rgba(20,30,28,.05);
	--elevation-2: 0 4px 8px rgba(12,40,34,.08), 0 12px 24px rgba(12,40,34,.10);

	/* ---- Motion ---- */
	--duration-1: 120ms; --duration-2: 180ms; --duration-3: 240ms;
	--ease-in-out: var(--ease-io);

	/* ---- Hairline width. Every border on the site is 1px, but it was never a
	   token, so new components had no way to say it without a literal. ---- */
	--border-width: 1px;

	/* ---- Photo slots (see .photo-slot). One ratio per slot kind, so a
	   placeholder and the real photo that replaces it occupy the same box.
	   3:2 matches .story-row__media img and .team-card__photo already.
	   --ratio-photo-hero was square (1:1) at first -- close to the text
	   column's own height at 1440, so neither side set the hero's height by
	   accident -- but object-fit:cover then had to crop a two-person,
	   landscape-framed photo down to a square, which cut into both
	   subjects' shoulders no matter how generous the source crop was. Same
	   as --ratio-photo now (2026-09-24): still close enough to the text
	   column's height not to look accidental, and one photo only ever needs
	   one crop, not two different ones per breakpoint. ---- */
	--ratio-photo: 3 / 2;
	--ratio-photo-hero: 3 / 2;
	/* The worked-example photo on /subsidie-service/, stacked: 5:4 is the
	   shape of the table + disclaimer column it sits beside at 1440, so the
	   stacked and side-by-side states show nearly the same crop. */
	--ratio-photo-calc: 5 / 4;
}

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
/* Safety net: position:fixed elements that also carry a CSS transform (the
   off-canvas mobile nav, translateX(100%) when closed) can still register
   as horizontal overflow on some mobile engines even though they're
   visually off-screen -- the viewport then "zooms out" to fit that phantom
   width, which is the actual cause of "half the page is blank" on mobile.
   `clip` (not `hidden`) on both html and body: hidden alone on just html
   isn't enough to stop it (verified live), but hidden on body makes body a
   scroll container in its own right, which breaks .site-header's
   position:sticky (it stopped pinning to the top at all -- verified
   locally). `overflow-x: clip` gives the same paint-time clipping without
   turning the element into a scroll container, so sticky keeps working. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; overflow-x: clip; background-color: var(--surface-base); }
/* anchor targets clear the sticky header when jumped to */
:target, #warmvast-woningscan, [id].section { scroll-margin-top: 90px; }
@media (prefers-reduced-motion: reduce) {
	html { scroll-behavior: auto; }
	*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
body {
	margin: 0;
	overflow-x: clip;
	font-family: var(--font-sans);
	font-size: var(--step-0);
	line-height: 1.62;
	color: var(--text-primary);
	/* No background here. body's own background paints in the block-background
	   phase, which is ABOVE a z-index:-1 child -- an opaque body would hide
	   .page-canvas completely. position:relative makes body the canvas's
	   containing block, so the canvas spans the full document height. */
	position: relative;
	background: transparent;
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
}

/* ---------- The page canvas ----------
   ONE element owns the entire page background. Absolute, not fixed, so it
   spans the document's full scroll height rather than the viewport -- and
   not on <body>, because main.js pins body with position:fixed during the
   mobile nav scroll-lock, which would jump a body background by the whole
   scroll offset the moment the menu opens.

   Layer order is gradient -> noise -> content. The FIRST listed background
   image paints on top, so noise is listed first and sits over the gradient;
   both sit under every element in normal flow.

   z-index:-1 makes this its own stacking context, but as a SIBLING of the
   header -- it is an ancestor of nothing, so it traps no sticky header, no
   dropdown, no off-canvas panel. No new z-index value is introduced. */
.page-canvas {
	position: absolute;
	inset: 0;
	min-height: 100vh;      /* short pages (404, legal) still get full cover */
	z-index: -1;
	pointer-events: none;
	background:
		var(--noise-texture) 0 0 / 220px 220px repeat,
		var(--canvas-wash),
		var(--surface-base);
}

/* ---------- Surface scopes ----------
   A component asks for --text-secondary; the surface decides the value.
   Dark is rare and deliberate: homepage hero, CTA band, footer.
   .hero--dark, not bare .hero: .hero is now the universal hero wrapper for
   EVERY variant (see template-parts/hero.php), light included -- scoping
   this to bare .hero forced every light hero dark too (white text on white
   data-card values, exactly the white-on-white failure the comment below
   warns about, just triggered from the opposite direction). */
.surface--dark, .hero--dark, .cta-band, .site-footer {
	--surface-base:    var(--ink);
	--text-primary:    #fff;
	--text-secondary:  #b9c4c0;
	--text-quiet:      #8b9893;
	--border-hairline: rgba(255,255,255,.12);
	--border-strong:   var(--line);
	--focus-ring:      var(--accent);   /* --primary is 2.58:1 on --ink, fails 3:1 */
}
/* Light islands sitting INSIDE a dark surface must re-assert paper, or a
   white card inherits --text-primary:#fff and goes white-on-white.
   .usp-bar__notch belongs here too, easy to miss because it's a sibling of
   .usp-bar__grid rather than its descendant: its ::after fill reads
   --surface-base to blend into the page below the hero, and without this it
   inherited --ink from .hero--dark same as the card would have -- the
   concave fillet meant to read as "page" painted as solid dark instead,
   right at the one edge (the mask curve) where it wasn't already covered by
   the hero's own background showing through the transparent two-thirds of
   the mask. Same failure as the white-on-white one above, just dark-on-dark
   and easier to miss since most of the shape still looked right. */
.surface--light, .ws, .usp-bar__grid, .usp-bar__notch {
	--surface-base:    var(--paper);
	--text-primary:    var(--ink);
	--text-secondary:  var(--ink-2);
	--text-quiet:      var(--ink-3);
	--border-hairline: var(--line);
	--border-strong:   var(--ink-3);
	--focus-ring:      var(--primary);
}

h1, h2, h3, h4 { font-family: var(--font-display); line-height: 1.1; margin: 0 0 .5em; font-weight: 700; letter-spacing: -.02em; color: var(--ink); }
/* numeric "instrument" elements: display font + tabular figures */
.hero__title, .section-head__title,
.verdub__amount, .tariff__value,
.stat__num { font-family: var(--font-display); }
.verdub__amount, .tariff__value, .stat__num {
	font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; letter-spacing: -.01em;
}
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); }
p { margin: 0 0 1rem; }
a { color: var(--primary-600); text-decoration: none; }
a:hover { color: var(--primary-700); }
img, svg, video { max-width: 100%; height: auto; display: block; }
ul, ol { margin: 0 0 1rem; padding-left: 1.2rem; }
strong { font-weight: 700; }
hr { border: 0; border-top: 1px solid var(--line); margin: 2rem 0; }
:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; border-radius: 4px; }

.screen-reader-text {
	position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
	overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.skip-link {
	position: absolute; left: -9999px; top: 0; z-index: 100;
	background: var(--ink); color: #fff; padding: .8rem 1.2rem; border-radius: 0 0 var(--radius-sm) 0;
}
.skip-link:focus { left: 0; color: #fff; }

/* ---------- Layout ---------- */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
/* Slightly tighter than before (was clamp(3rem, 6vw, 5.5rem)) -- adjacent
   sections share this same padding top+bottom, so the gap between any two
   was 2x it; a modest ~18% cut across the board reads as "a bit closer
   together" without collapsing the rhythm. */
.section { padding-block: clamp(2.5rem, 5vw, 4.5rem); }
.section--tight { padding-block: clamp(2rem, 4vw, 3.5rem); }

/* .site-main no longer paints anything. The page background belongs to
   .page-canvas; this is now just a positioning context. */
.site-main { position: relative; }
.section--ink h2, .section--ink h3 { color: #fff; }


.section-head { max-width: 62ch; margin-bottom: clamp(1.5rem, 3vw, 2.5rem); }
.section-head--center { margin-inline: auto; text-align: center; }
.section-head__title { font-size: var(--step-3); text-wrap: balance; }
.section-head__intro { font-size: var(--step-1); color: var(--ink-2); margin-bottom: 0; }
.section--ink .section-head__intro { color: #b9c4c0; }

.kicker {
	display: inline-flex; align-items: center; gap: .5rem;
	font-size: var(--step--1); font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
	color: var(--primary); margin: 0 0 .7rem;
}
.kicker::before { content: ""; width: 22px; height: 2px; background: var(--accent); border-radius: 2px; }
.kicker--light { color: var(--accent); }
.kicker--light::before { background: var(--accent); }

/* ---------- Icons ---------- */
.wv-icon { width: 22px; height: 22px; flex: 0 0 auto; }
.wv-icon--sm { width: 16px; height: 16px; }
.wv-icon--end { width: 18px; height: 18px; }

/* ---------- Buttons ---------- */
.btn {
	display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
	font: inherit; font-weight: 650; line-height: 1; text-align: center; cursor: pointer;
	padding: .95rem 1.5rem; border-radius: var(--radius-pill); border: 2px solid transparent;
	/* transform only, 120ms -- CLAUDE.md rule 6. Colour/border/shadow now
	   switch instantly on hover; only the lift animates. */
	transition: transform var(--duration-1) var(--ease-out);
	white-space: nowrap; max-width: 100%;
}
.btn:hover { transform: translateY(-2px); }
@media (max-width: 560px) {
	/* nowrap is fine for short labels but a few CTAs run long ("Twijfelt u
	   welke maatregel past? Start de scan") -- on a narrow phone that just
	   pushes the pill past the viewport edge instead of ever wrapping.
	   Letting it wrap below this width keeps every button visually
	   contained; the short labels most buttons use don't reach a second
	   line anyway. */
	.btn { white-space: normal; line-height: 1.25; }
}
.btn:active { transform: translateY(0); }
.btn--sm { padding: .62rem 1rem; font-size: var(--step--1); }
.btn--lg { padding: 1.1rem 1.9rem; font-size: var(--step-1); }

.btn--primary { background: var(--primary); color: #fff; box-shadow: var(--shadow-sm); }
.btn--primary:hover { background: var(--primary-700); color: #fff; box-shadow: var(--shadow); }

.btn--accent { background: var(--accent); color: var(--accent-ink); box-shadow: 0 6px 16px rgba(246,194,0,.28); }
.btn--accent:hover { background: var(--accent-strong); color: var(--accent-ink); }

.btn--secondary { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn--secondary:hover { background: var(--surface); border-color: var(--ink); color: var(--ink); }

.btn--ghost { background: rgba(255,255,255,.08); color: #fff; border-color: rgba(255,255,255,.35); }
.btn--ghost:hover { background: rgba(255,255,255,.16); color: #fff; }

.btn:disabled { opacity: .6; cursor: not-allowed; transform: none; }

.link-arrow { display: inline-flex; align-items: center; gap: .4rem; font-weight: 650; color: var(--primary-600); }
/* The arrow nudges by transform, not by growing `gap` -- a gap change is a
   layout shift on hover, and not a property CLAUDE.md rule 6 allows to move. */
.link-arrow .wv-icon { transition: transform var(--duration-1) var(--ease-out); }
.link-arrow:hover .wv-icon { transform: translateX(var(--space-1)); }

/* ---------- Header / nav ---------- */
/* Transparent-over-hero header, on every page (2026-09-24; previously only
   pages with a dark hero got this -- see git history for the old
   body.has-dark-hero-scoped version). At the very top of the page the
   header floats over the hero with no fill of its own; scrolling even a
   little brings back the usual frosted-white bar and hairline border.
   position:fixed (not sticky) removes it from flow, so every hero below
   reserves --header-h of its own top padding to compensate for the space
   the header used to occupy in flow -- see .hero--dark's padding-top
   (already accounted for it) and .hero:not(.hero--dark)'s (added
   alongside this change). */
.site-header {
	position: fixed; left: 0; right: 0; top: 0; z-index: 60;
	border-bottom: 1px solid transparent;
	transition: box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
/* The frosted-glass fill lives on a pseudo-element, not .site-header itself.
   backdrop-filter (like transform/filter/perspective) makes the element that
   carries it a new containing block for any position:fixed descendant --
   which trapped the off-canvas mobile nav (a real descendant, #primary-nav)
   inside the header's own ~70px box instead of the viewport, collapsing it
   to a sliver. A ::before has no descendants of its own, so it can carry
   the same visual effect without turning .site-header into that trap. */
.site-header::before {
	content: ""; position: absolute; inset: 0; z-index: -1;
	background: rgba(255,255,255,.9);
	backdrop-filter: saturate(1.4) blur(10px);
	opacity: 0; transition: opacity var(--dur-2) var(--ease-out);
}
.site-header.is-scrolled { box-shadow: var(--shadow-sm); border-bottom-color: var(--line); }
.site-header.is-scrolled::before { opacity: 1; }
.site-header__inner { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1.5rem; min-height: var(--header-h); }
/* min-width, not unscoped: below 960px .nav__list only ever renders inside
   the off-canvas panel (its own permanent white background, open or not),
   never inline over the hero -- white text there would just be invisible
   white-on-white the moment the panel opens.
   `> .menu-item > a` (direct children only), not a bare `.nav__list a`: the
   dropdown panels (.dropdown, .dropdown--wide) are ALSO descendants of
   .nav__list, and ALSO have their own permanent white background -- the
   same white-on-white trap as the off-canvas panel, just missed here the
   first time because it only breaks while unscrolled, at >=961px, with a
   dropdown actually open (the Gemeentes list going fully blank). Only the
   top-level trigger link ("Isolatie", "Gemeentes", ...) is an `a` that's a
   direct child of a `.menu-item` -- every dropdown-nested link is a
   grandchild-or-deeper, so this excludes them structurally rather than
   needing to know every dropdown class by name. */
@media (min-width: 961px) {
	body.has-dark-hero .site-header:not(.is-scrolled) .nav__list > .menu-item > a { color: rgba(255,255,255,.86); }
	body.has-dark-hero .site-header:not(.is-scrolled) .nav__list > .menu-item > a:hover,
	body.has-dark-hero .site-header:not(.is-scrolled) .nav__list > .current-menu-item > a {
		color: #fff; background: rgba(255,255,255,.14);
	}
}
/* logo crossfade: the dark wordmark reads fine on the white scrolled bar,
   but disappears into the hero's own near-black background when floating
   transparently -- swap to the light variant (already used in the footer)
   for that state instead of leaving it illegible. */
.brand { position: relative; display: inline-flex; align-items: center; }
.brand__logo--light {
	position: absolute; left: 0; top: 0; opacity: 0; pointer-events: none;
	transition: opacity var(--dur-2) var(--ease-out);
}
.brand__logo--dark { transition: opacity var(--dur-2) var(--ease-out); }
body.has-dark-hero .site-header:not(.is-scrolled) .brand__logo--dark { opacity: 0; }
body.has-dark-hero .site-header:not(.is-scrolled) .brand__logo--light { opacity: 1; }

/* Mobile menu button follows the same floating-over-dark-hero treatment as
   the logo/nav links above -- without this it stayed a solid white square
   (its always-on .nav-toggle background) sitting on top of the transparent
   header, the one element that never adapted to the dark backdrop. */
body.has-dark-hero .site-header:not(.is-scrolled) .nav-toggle { background: rgba(255,255,255,.1); border-color: rgba(255,255,255,.35); }
body.has-dark-hero .site-header:not(.is-scrolled) .nav-toggle__bars span { background: #fff; }

.brand { display: inline-flex; align-items: center; }
.brand__logo { display: block; height: var(--logo-h); width: auto; }
.brand__logo--footer { height: var(--logo-h); }

.site-nav { display: contents; }
.nav__list { display: flex; align-items: center; gap: .25rem; list-style: none; margin: 0; padding: 0; }
.nav__list a {
	display: inline-flex; align-items: center; gap: .3rem; padding: .55rem .7rem; border-radius: var(--radius-sm);
	color: var(--ink-2); font-weight: 600; white-space: nowrap;
	transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.nav__list a:hover, .nav__list .current-menu-item > a { color: var(--ink); background: var(--primary-050); }
.nav__caret { transition: transform var(--dur-2) var(--ease-out); }

.menu-item--has-children { position: relative; }
.dropdown {
	position: absolute; top: calc(100% + 10px); left: 50%; transform: translateX(-50%) translateY(6px);
	width: min(560px, 92vw); background: var(--surface); border: 1px solid var(--line);
	border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: .8rem;
	opacity: 0; visibility: hidden; transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out); z-index: 70;
}
/* transparent bridge over the gap so the pointer never leaves the hover area */
.dropdown::before { content: ""; position: absolute; left: 0; right: 0; top: -14px; height: 14px; }
.menu-item--has-children:hover .dropdown,
.menu-item--has-children:focus-within .dropdown,
.dropdown.is-open { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
/* min-width, not unscoped: mobile taps don't fire a real mouseleave, so touch
   browsers (iOS Safari especially) can leave :hover "stuck" on the tapped
   <li> after the tap ends -- and since this selector's specificity beats the
   mobile aria-expanded-driven rule below (.nav-toggle etc.), that stuck hover
   would win and leave the caret rotated regardless of the dropdown's real
   open/closed state. Only meaningful >=861px anyway, where dropdowns are
   genuinely hover-driven (below that .dropdown is forced position:static and
   opened by tap, see the mobile block). */
@media (min-width: 861px) {
	.menu-item--has-children:hover .nav__caret { transform: rotate(180deg); }
}
/* minmax(0, 1fr), not a bare 1fr: a grid track's default minimum is auto (its
   content's own min-content size), so a long unbroken subtitle like
   "Uitleg over subsidie en isolatie" was demanding more than its fair half
   and pushing the whole grid -- and the item boxes with it -- past the
   dropdown panel's right edge instead of wrapping. minmax(0, 1fr) lets the
   track shrink below that and forces the text to wrap within its column. */
.dropdown__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3rem; }
.dropdown__grid--single { grid-template-columns: 1fr; }
.dropdown--simple { width: min(300px, 92vw); }
.dropdown__item { display: flex; align-items: center; gap: .8rem; padding: .7rem .8rem !important; border-radius: var(--radius); background: transparent !important; transition: background-color var(--dur-1) var(--ease-out); min-width: 0; }
.dropdown__item:hover { background: var(--primary-050) !important; }
/* the text span is itself a flex item (of .dropdown__item) with the same
   min-width:auto default -- without this it also refuses to shrink and the
   overflow reappears one level down even after the grid track is fixed. */
.dropdown__item > span:last-child { min-width: 0; }
.dropdown__item strong { display: block; color: var(--ink); font-size: .98rem; }
.dropdown__item em { font-style: normal; color: var(--ink-3); font-size: .82rem; }
.dropdown__icon { display: inline-flex; width: 40px; height: 40px; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.dropdown__item--all { grid-column: 1 / -1; background: var(--surface-2) !important; }

/* compact link-list variant: for menus with many plain items (e.g. Gemeentes)
   where the heavy icon+title+subtitle card style would make the dropdown
   unwieldy. */
.dropdown--wide { width: min(680px, 94vw); }
.dropdown__lead { padding: .3rem .5rem .7rem; color: var(--ink-3); font-size: var(--step--1); border-bottom: 1px solid var(--line); margin-bottom: .5rem; }
.dropdown__links { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .1rem .3rem; }
.dropdown__links a {
	display: block; padding: .5rem .7rem !important; border-radius: var(--radius-sm);
	color: var(--ink-2); font-weight: 600; font-size: .92rem;
	transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.dropdown__links a:hover { background: var(--primary-050); color: var(--ink); }
.dropdown__links-all { grid-column: 1 / -1; margin-top: .3rem; padding-top: .6rem; border-top: 1px solid var(--line); }
.dropdown__links-all a { color: var(--primary) !important; }

.site-nav__cta { display: flex; align-items: center; justify-content: flex-end; gap: 1rem; }
.nav__phone { display: inline-flex; align-items: center; gap: .45rem; font-weight: 700; color: var(--ink); white-space: nowrap; }
.nav__phone:hover { color: var(--primary); }
.nav__phone .wv-icon { width: 18px; height: 18px; color: var(--primary); }

.nav-toggle { display: none; }

/* ---------- Hero ---------- */
/* flex + justify-content:center (not a fixed top offset) so hero__inner
   sits centered in the space between the header and the floating USP bar,
   with equal breathing room above and below, instead of pinned to the top
   with whatever's left over dumped below it. padding-top/-bottom are that
   space's boundaries, not the content's own offset: top clears the fixed
   header (see .site-header), bottom reserves the USP bar's real height
   (main.js measures it live and writes --usp-clearance, since the bar's
   height depends on wrapped text that varies by viewport/content). */
/* ---------- Hero: one component, two surfaces ----------
   .hero is the shared wrapper for every hero on the site. Bare .hero is the
   LIGHT default (home does not use it bare). .hero--dark is the single
   documented dark exception (see docs/DESIGN-SYSTEM.md #1) -- its own rule
   below carries the homepage hero's existing values UNCHANGED; nothing in
   this split alters what the homepage renders. */
.hero {
	position: relative; overflow: hidden;
	display: flex; flex-direction: column; justify-content: center;
	min-height: var(--hero-min);
}
.hero--dark {
	background: var(--ink); color: #eef2f0;
	min-height: 100vh; min-height: 100svh;
	padding-top: clamp(5.5rem, 10vh, 8rem);
	padding-bottom: var(--usp-clearance, clamp(7rem, 14vh, 10rem));
}
.hero--dark::before {
	content: ""; position: absolute; inset: 0; z-index: 0;
	background: var(--dark-gradient);
}
.hero__thermal {
	position: absolute; inset: 0; z-index: 0; opacity: .5; mix-blend-mode: screen;
	background:
		radial-gradient(closest-side at 78% 30%, rgba(254,208,61,.35), transparent),
		radial-gradient(closest-side at 90% 55%, rgba(226,88,34,.25), transparent),
		radial-gradient(closest-side at 68% 12%, rgba(11,107,91,.35), transparent);
	filter: blur(6px);
	animation: hero-drift 16s ease-in-out infinite alternate;
}
@keyframes hero-drift {
	from { transform: translate3d(0,0,0) scale(1); }
	to { transform: translate3d(-3%, 2%, 0) scale(1.08); }
}

/* sprawling ambient orbs: soft thermal light-sources that give the dark hero
   real depth without an image. Contained by the hero's overflow:hidden. */
.hero__orb {
	position: absolute; z-index: 0; border-radius: 50%; pointer-events: none;
	filter: blur(90px); mix-blend-mode: screen; will-change: transform;
}
.hero__orb--1 {
	width: 44vw; height: 44vw; max-width: 660px; max-height: 660px; top: -12%; right: -8%;
	background: radial-gradient(circle at center, rgba(11,107,91,.6), transparent 68%);
}
.hero__orb--2 {
	width: 36vw; height: 36vw; max-width: 540px; max-height: 540px; bottom: -16%; left: -6%;
	background: radial-gradient(circle at center, rgba(254,208,61,.22), transparent 68%);
}
@media (prefers-reduced-motion: reduce) { .hero__orb { will-change: auto; } }

/* ---------- Hero orbs + thermal cluster on light heroes ----------
   Deliberate exception to CLAUDE.md rules 4 (no section gradients) and 6
   (no decorative motion), asked for by Alex 2026-09-23: the homepage's
   orbs and thermal cluster, copied onto every light hero. Same markup
   (hero.php), same colours, sizes, positions, blur and motion -- the rules
   above apply unchanged. The one thing that cannot transfer is
   mix-blend-mode:screen, which only lightens: over near-white --paper it
   renders almost nothing, so light heroes composite normally instead.
   Two further paper-only changes, asked for by Alex the same day ("a
   little more prominent", "light orange to red on the right"): the yellow
   orb is stronger, and the thermal cluster is warm-only. Its teal layer is
   dropped because under normal compositing red over teal mixes to a grey-
   brown smudge instead of the glow screen produces on --ink; the teal orb
   already carries the cool side. */
.hero:not(.hero--dark) .hero__orb,
.hero:not(.hero--dark) .hero__thermal {
	mix-blend-mode: normal;
}
/* Smudged across the whole hero rather than pooled in two corners: each orb
   keeps its round shape and anchor corner but is ~1.4x the dark hero's
   size, and blur(128px) instead of 90px spreads its colour further. */
.hero:not(.hero--dark) .hero__orb { filter: blur(var(--space-10)); }
.hero:not(.hero--dark) .hero__orb--1 {
	width: 62vw; height: 62vw; max-width: none; max-height: none;
	background: radial-gradient(circle at center, var(--orb-cool), transparent 68%);
}
.hero:not(.hero--dark) .hero__orb--2 {
	width: 52vw; height: 52vw; max-width: none; max-height: none;
	background: radial-gradient(circle at center, var(--orb-warm), transparent 68%);
}
.hero:not(.hero--dark) .hero__thermal {
	opacity: 1;
	/* Heavy blur and farthest-side falloff so the three stops melt into one
	   amber -> red wash that fades out across the hero, not three discs. */
	filter: blur(var(--space-9));
	background:
		radial-gradient(farthest-side at 84% 30%, var(--thermal-amber), transparent 70%),
		radial-gradient(farthest-side at 90% 52%, var(--thermal-orange), transparent 62%),
		radial-gradient(farthest-side at 94% 74%, var(--thermal-red), transparent 55%);
}
/* No hard containment on light heroes: unlike the dark hero, a light hero
   paints no background of its own -- it sits on the same continuous
   .page-canvas wash as the section below it. Clipping the glow at the
   hero's own edge (overflow:hidden) cut it off in a dead straight line
   exactly at that boundary, which read as a seam because everything either
   side of the line is otherwise the same surface. Letting it bleed past the
   edge instead means it simply finishes fading out over the page below, the
   same way it already fades out well inside the hero on every other side.
   html/body's own overflow-x:clip (near the top of this file) is what
   actually prevents this from ever producing a horizontal scrollbar, the
   same safety net every other off-edge-positioned element on the site
   already relies on. */
.hero:not(.hero--dark) { overflow: visible; }
/* Header clearance, light heroes: .site-header is now position:fixed on
   every page (see "Header / nav" above), removed from flow everywhere, not
   just on the dark hero -- which already had its own padding-top sized for
   this. Adding the header's own height here as top padding replaces
   exactly the space the header used to occupy in flow, so hero content
   lands in precisely the same place as before; nothing shifts, only the
   header's fill becomes transparent until scrolled. */
.hero:not(.hero--dark) { padding-top: var(--header-h); }

@media (prefers-reduced-motion: reduce) {
	.hero__thermal { animation: none; }
}
.hero__inner {
	position: relative; z-index: 1;
	display: grid; grid-template-columns: 1.02fr .98fr; gap: clamp(1.5rem, 4vw, 3.5rem);
	/* top-align, and both columns start flush at the exact same row line —
	   copy and scan card line up tightly. top-align (not center) is deliberate:
	   the scan card's height varies a lot across its steps (address -> result ->
	   subsidie -> lead), so centering would make the left copy visibly jump
	   every time the user advances. */
	align-items: start;
	/* no padding of its own any more -- .hero itself now owns the top/bottom
	   reserve and centers this whole block within it (see .hero). Full width
	   comes for free: flex items default to align-self:stretch on the cross
	   axis (horizontal, since .hero is flex-direction:column). */
}
/* Shared across every variant -- the ONLY h1, the action row, the trust
   row. text-wrap:balance + display tracking per docs/DESIGN-SYSTEM.md #3.
   .hero--dark below restates the homepage's pre-existing exact values. */
.hero__title {
	font-size: var(--text-4); letter-spacing: var(--tracking-4); line-height: var(--leading-4);
	color: var(--text-primary); text-wrap: balance; max-width: 20ch; margin: 0;
}
.hero__title em { font-style: normal; color: var(--accent); }
.hero--dark .hero__title {
	/* Restates the frozen original exactly -- including what it DIDN'T set:
	   no letter-spacing of its own (inherits -.02em from the global h1 reset,
	   not the shared rule's --tracking-4), and text-wrap:wrap, not balance
	   (balance can change where lines break; on every other variant that's a
	   pure improvement, but home's wrap is explicitly frozen). */
	font-size: var(--step-5); letter-spacing: -.02em; line-height: 1.1; text-wrap: wrap;
	color: #fff; max-width: none; margin-bottom: .8rem;
}
/* .hero__sub: home's lead paragraph. Frozen values, unchanged -- the light
   variants' lead uses .hero__lead (own rule above) instead of this class, so
   there is no shared base to restate here, only home's own original numbers.
   (This rule was dropped by mistake during the initial split and caught by
   the same pixel-diff: with it gone, .hero__sub fell back to plain <p>
   spacing/size, wrapped at the full column width instead of 44ch, and came
   out 17px shorter -- which is what actually produced the vertical shift
   the earlier version of this comment mis-blamed on the h1.) */
.hero--dark .hero__sub { font-size: var(--step-1); color: #c6d0cc; max-width: 44ch; margin-bottom: 1.6rem; }
.hero__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); margin: 0; }
.hero--dark .hero__actions { gap: .9rem; margin-bottom: 1.6rem; }
.hero__phone { display: inline-flex; align-items: center; gap: .5rem; font-weight: 700; color: var(--text-primary); }
.hero__phone .wv-icon { color: var(--primary); }
.hero__trust { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--text--1); color: var(--text-quiet); margin: 0; }
.hero__trust span { display: inline-flex; align-items: center; gap: .45rem; }
.hero__trust .wv-icon { width: 16px; height: 16px; color: var(--primary); }
.hero--dark .hero__trust { gap: 1.2rem; font-size: var(--step--1); color: #aeb9b5; }
.hero--dark .hero__trust .wv-icon { color: var(--accent); }

/* ---------- Light hero: grid, body, lead, eyebrow, aside ----------
   .hero__grid replaces .hero__inner for every non-dark variant (dark keeps
   its own .hero__inner, untouched, below). One shared padding pair from
   the spacing scale, per docs/DESIGN-SYSTEM.md #4 -- this is what makes
   hero height a function of content, not of an accidental clamp(). */
/* The grid has exactly TWO states, and both are fluid -- every track is an
   fr or a minmax, never a fixed pixel width.

   STATE 1 (default) -- WITHOUT aside: one fluid column, capped at the
   reading measure. The cap is what stops the column spanning the full
   1100px container and leaving the copy stranded beside ~560px of dead
   space. One track, so there is no empty second column to leave behind.

   STATE 2 -- WITH aside (.hero--has-aside): two fluid columns. The measure
   cap is lifted, because here the container width is shared rather than
   given over to one run of text.

   Both states start their first column at the same x -- the container's
   own padding edge -- because a max-width on a block-level grid trims from
   the END, never the start. Verified: h1 left edge is identical on
   /contact/ (no aside) and /spouwmuurisolatie/ (aside). */
.hero__grid {
	position: relative; z-index: 1;
	display: grid;
	/* The TRACK is capped at the measure, not the element: .hero__grid IS
	   the .container element, so a max-width here would fight .container's
	   own max-width + margin-inline:auto -- it centres the single-column
	   state and lets the two-column state escape the container entirely.
	   min(100%,...) also guarantees the track can never exceed the space
	   available, so it cannot overflow a narrow viewport. */
	grid-template-columns: min(100%, var(--measure));
	justify-content: start;
	gap: var(--space-8);
	align-items: start;
	padding-block: var(--rhythm-normal);
	width: 100%;
}
.hero--has-aside .hero__grid {
	grid-template-columns: minmax(0, 1.35fr) minmax(280px, .95fr);
	gap: clamp(1.5rem, 4vw, 3rem);
}
.hero__body {
	min-width: 0; display: flex; flex-direction: column; align-items: flex-start;
	gap: var(--space-5);
}
/* every direct child manages spacing via the flex gap above, never its own
   margin -- so the gap between slots is identical regardless of which
   optional slots are present (rule: "vertical rhythm... identical on
   every page"). */
.hero__body > * { margin: 0; }
.hero__eyebrow-row { display: flex; align-items: center; gap: var(--space-3); }
.hero__icon {
	display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
	width: 40px; height: 40px; border-radius: var(--radius-3);
	background: var(--primary); color: #fff;
}
.hero__icon .wv-icon { width: 20px; height: 20px; }
.breadcrumb {
	display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
	font-size: var(--text--1); color: var(--text-quiet);
}
.breadcrumb a { color: var(--text-quiet); }
.breadcrumb a:hover { color: var(--primary); }
.hero__lead {
	font-size: var(--text-1); letter-spacing: var(--tracking-1); line-height: var(--leading-1);
	color: var(--text-secondary); max-width: var(--measure); text-align: left;
}
.hero__meta { font-size: var(--text--1); color: var(--text-quiet); }
.hero__aside { min-width: 0; }
.hero__scan {
	position: relative;
	z-index: 0;
	isolation: isolate;
	/* Tuned for a hero__copy column sitting beside this one, back when both
	   pages' heroes had a visible eyebrow+h1 above the fold -- neither does
	   any more (see body.is-front/.is-scan-landing overrides below), so this
	   is really just "some top breathing room" now, kept as the shared
	   default rather than deleted since a future hero__copy could still
	   want it. margin-top (not padding-top): the ::before glow backdrop
	   below is positioned relative to this box's own edge, so the whole
	   box -- glow and card together -- must shift as one unit, not just the
	   inner content. */
	margin-top: 3rem;
}
/* Scan landing page: the card is the entire hero now, no copy column beside
   it to align with or share width against -- single centered column. The
   940px match to .usp-bar__grid belongs to the GLOW backdrop (.hero__scan
   ::before below), same as the homepage's proportions: the actual white
   card is narrower than its own glow by 44px (22px reveal on each side),
   so capping .hero__scan itself at 940px would have matched the card, not
   the glow, to the USP bar -- 896 (940-44) is what makes the glow land on
   940 once its own "+44px" is added back. */
body.is-scan-landing .hero__inner { grid-template-columns: 1fr; justify-items: center; }
body.is-scan-landing .hero__scan { width: 100%; max-width: 896px; margin-top: 0; }
/* ...and the glow's own 684px ceiling (tuned for the homepage's narrower
   grid-column context) needs to come off here, or it'd clamp smaller than
   the 896px card it's meant to frame. */
body.is-scan-landing .hero__scan::before { width: calc(100% + 44px); }
/* The homepage hero dropped its eyebrow pill -- h1 is hero__copy's first
   child there now, so hero__scan needs only a small manual nudge -- not the
   3rem tuned for the eyebrow's own height (that stays correct for
   template-scan.php, which still has one) -- to sit a touch below the h1's
   top line instead of dead-flush with it. */
body.is-front .hero__scan { margin-top: 2rem; }
.hero__scan::before {
	content: "";
	position: absolute;
	left: 50%;
	top: -18px;
	width: min(calc(100% + 44px), 684px);
	height: calc(100% + 36px);
	transform: translateX(-50%);
	border-radius: calc(var(--radius-lg) + 8px);
	background:
		linear-gradient(135deg, rgba(254,208,61,.13), rgba(255,255,255,.035) 24%, rgba(11,107,91,.18) 100%),
		rgba(255,255,255,.035);
	border: 1px solid rgba(255,255,255,.11);
	box-shadow:
		0 34px 90px -42px rgba(0,0,0,.74),
		inset 0 1px 0 rgba(255,255,255,.14);
	z-index: -1;
}

/* ---------- Shared form bits (used by the woningscan lead step) ---------- */
.wv-consent { display: flex; align-items: flex-start; gap: .6rem; font-weight: 500 !important; font-size: var(--step--1); color: var(--ink-2); margin-top: 1rem; }
.wv-consent input { margin-top: .2rem; accent-color: var(--primary); }
.wv-consent--inline { align-items: center; justify-content: space-between; gap: .9rem; margin-top: .15rem; }
.wv-consent--inline input { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }

.wv-success__mark { width: 56px; height: 56px; margin: 0 auto 1rem; border-radius: 50%; background: var(--success); color: #fff; display: flex; align-items: center; justify-content: center; }

/* ---------- Cards / services ---------- */
.grid { display: grid; gap: clamp(1rem, 2.5vw, 1.6rem); }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }

.card {
	background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
	padding: 1.5rem; box-shadow: var(--shadow-sm);
	transition: transform var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.card--link:hover { transform: translateY(-4px); box-shadow: var(--shadow); border-color: var(--line-strong); }

.problem-card { display: flex; align-items: flex-start; gap: .9rem; }
.problem-card__icon { flex: 0 0 auto; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.problem-card p { margin: 0; font-weight: 600; }
.problem-card__body { min-width: 0; flex: 1; }
.problem-card__body p + p { margin-top: .3rem; font-weight: 400; color: var(--ink-3); font-size: .92rem; }

.service-card { display: flex; flex-direction: column; height: 100%; }
.service-card__icon { width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius); background: var(--primary); color: #fff; margin-bottom: 1rem; }
.service-card__icon .wv-icon { width: 26px; height: 26px; }
.service-card h3 { margin-bottom: .5rem; }
.service-card__problem { color: var(--ink-3); font-size: var(--step--1); margin-bottom: .5rem; }
.service-card__solution { margin-bottom: 1rem; }
.service-card__subsidy {
	display: inline-flex; align-items: center; gap: .45rem; align-self: flex-start;
	font-weight: 700; font-size: var(--step--1); color: var(--primary-700);
	background: var(--primary-050); padding: .4rem .75rem; border-radius: var(--radius-pill); margin-bottom: 1.2rem;
}
.service-card__foot { margin-top: auto; }

/* ---------- Verdubbelaar ---------- */
.verdub { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
.verdub__badge { display: inline-flex; align-items: center; gap: .5rem; background: var(--accent); color: var(--accent-ink); font-weight: 750; padding: .4rem .85rem; border-radius: var(--radius-pill); margin-bottom: 1rem; }
.verdub__calc { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: clip; }
.verdub__row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.1rem 1.4rem; border-bottom: 1px solid var(--line); }
.verdub__row:last-child { border-bottom: 0; }
.verdub__row--total { background: var(--ink); color: #fff; }
.verdub__row--total .verdub__amount { color: var(--accent); font-size: var(--step-2); }
.verdub__label { font-weight: 600; }
.verdub__label small { display: block; color: var(--ink-3); font-weight: 500; font-size: .82rem; }
.verdub__row--total .verdub__label small { color: #a9b6b1; }
.verdub__amount { font-weight: 800; font-size: var(--step-1); letter-spacing: -.02em; }
.verdub__note { font-size: .82rem; color: var(--ink-3); padding: .9rem 1.4rem; background: var(--surface-2); margin: 0; }

/* ---------- Stapelen: ISDE + gemeentelijke subsidie ---------- */
.stapel { position: relative; display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; gap: clamp(1.25rem, 3vw, 2rem); }
.stapel__card {
	background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
	padding: clamp(1.5rem, 3vw, 2rem); box-shadow: var(--shadow-sm); display: flex; flex-direction: column;
}
.stapel__kicker {
	display: inline-flex; align-self: flex-start; align-items: center; gap: .4rem;
	font-size: var(--step--1); font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
	color: var(--primary-700); background: var(--primary-050); padding: .35rem .7rem; border-radius: var(--radius-pill);
	margin-bottom: 1rem;
}
.stapel__icon {
	width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
	border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); margin-bottom: 1rem;
}
.stapel__icon .wv-icon { width: 24px; height: 24px; }
.stapel__card h3 { font-size: var(--step-1); margin-bottom: .3rem; }
.stapel__card > p { color: var(--ink-3); font-size: var(--step--1); margin: 0; }
.stapel__amount { font-family: var(--font-display); font-weight: 800; font-size: var(--step-4); letter-spacing: -.03em; line-height: 1; color: var(--ink); margin: .2rem 0 .6rem; font-feature-settings: "tnum"; font-variant-numeric: tabular-nums; }
/* a plain colored line, not a filled pill: the full "bij energiearmoede..."
   sentence used to sit in a solid accent-yellow badge, which for the longer
   gemeenten wrapped to 2-3 lines of small bold text on a bright block -- the
   single biggest contributor to the card reading as cluttered. Same color
   coding, much less visual weight. */
.stapel__amount-sub { display: flex; align-items: flex-start; gap: .4rem; font-weight: 600; font-size: .82rem; color: var(--accent); margin: 0 0 .9rem; }
.stapel__amount-sub .wv-icon { flex: 0 0 auto; margin-top: .15rem; }
.stapel__desc { margin-bottom: .7rem !important; }
.stapel__card .link-arrow { margin-top: auto; padding-top: .5rem; }

/* voorwaarden/looptijd: real, carefully-sourced regulatory detail that must
   stay -- but shown by default it's what made the card feel like a wall of
   text. Tucked behind a native <details> so the card reads as a scannable
   headline number first; the fine print is one tap away, not gone. */
.stapel__details { margin: 0 0 1rem; }
.stapel__details summary {
	list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .35rem;
	font-size: .82rem; font-weight: 700; color: var(--ink-3);
}
.stapel__details summary::-webkit-details-marker { display: none; }
.stapel__details summary .wv-icon { width: 14px; height: 14px; transition: transform var(--dur-2) var(--ease-out); }
.stapel__details[open] summary .wv-icon { transform: rotate(180deg); }
.stapel__details__body { margin-top: .6rem; }
.stapel__details__body p { font-size: .82rem; color: var(--ink-3); margin: 0 0 .5rem; }
.stapel__card--lokaal .stapel__details summary { color: #9fb0ab; }
.stapel__card--lokaal .stapel__details__body p { color: #9fb0ab; }

.stapel__card--lokaal { background: var(--ink); border-color: var(--ink); color: #fff; }
.stapel__card--lokaal .stapel__kicker { background: rgba(254, 208, 61, .16); color: var(--accent); }
.stapel__card--lokaal .stapel__icon { background: rgba(254, 208, 61, .16); color: var(--accent); }
.stapel__card--lokaal h3 { color: #fff; }
.stapel__card--lokaal > p { color: #b9c4c0; }
.stapel__card--lokaal .stapel__amount { color: var(--accent); }
.stapel__card--lokaal .link-arrow { color: var(--accent); }
.stapel__card--lokaal .link-arrow:hover { color: var(--accent-strong); }

.stapel__plus {
	align-self: center; justify-self: center; width: 44px; height: 44px; flex: 0 0 auto;
	display: flex; align-items: center; justify-content: center; border-radius: 50%;
	background: var(--accent); color: var(--accent-ink); font-family: var(--font-display); font-weight: 800; font-size: 1.4rem;
	box-shadow: var(--shadow); z-index: 1;
}

.stapel__foot { display: flex; align-items: flex-start; gap: .6rem; margin-top: 1.5rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.stapel__foot .wv-icon { flex: 0 0 auto; margin-top: .15rem; color: var(--ink-3); }
.stapel__foot p { margin: 0; font-size: .82rem; color: var(--ink-3); }

@media (max-width: 720px) {
	.stapel { grid-template-columns: 1fr; }
	.stapel__plus { justify-self: center; margin: -.4rem 0; }
}

/* ---------- Gemeenten (regio Zaandam) ---------- */
.kernen-list { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem .8rem; }
.kernen-list li { display: flex; align-items: center; gap: .5rem; font-weight: 600; font-size: var(--step--1); }
.kernen-list .wv-icon { color: var(--primary); flex: 0 0 auto; }
@media (max-width: 480px) { .kernen-list { grid-template-columns: 1fr; } }

.gemeenten-inline { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem .7rem; }
.gemeenten-inline a {
	display: inline-flex; padding: .5rem .9rem; border-radius: var(--radius-pill);
	background: var(--surface); border: 1px solid var(--line); color: var(--ink-2); font-weight: 600; font-size: var(--step--1);
	transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.gemeenten-inline a:hover { background: var(--primary-050); border-color: var(--primary); color: var(--primary); }

/* ---------- Checklist ---------- */
.checklist { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .6rem; }
.checklist li { position: relative; padding-left: 1.9rem; }
.checklist li::before {
	content: ""; position: absolute; left: 0; top: .15rem; width: 1.25rem; height: 1.25rem;
	background: var(--primary-050); border-radius: 50%;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%230b6b5b' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
	background-repeat: no-repeat; background-position: center;
}

/* Two-up variant: keeps each item's text left-aligned (checkmarks need a
   consistent left edge to scan) inside an otherwise centered block. */
.checklist--2 { grid-template-columns: repeat(2, 1fr); gap: .7rem 1.6rem; text-align: left; margin-bottom: 0; }
@media (max-width: 620px) { .checklist--2 { grid-template-columns: 1fr; } }

/* ---------- Werkwijze (steps) ---------- */
.steps { list-style: none; counter-reset: step; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(1rem, 3vw, 1.8rem); }
.steps li { counter-increment: step; position: relative; padding-left: 3.4rem; }
.steps li::before {
	content: counter(step); position: absolute; left: 0; top: -.15rem;
	width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center;
	background: var(--primary); color: #fff; font-weight: 800; border-radius: var(--radius-md); font-size: 1.05rem;
}
.steps li :is(h3, h4) { font-size: var(--step-0); margin-bottom: .25rem; }
.steps li p { color: var(--ink-3); font-size: var(--step--1); margin: 0; }

/* ---------- Trust ---------- */
.trust-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.trust-item { display: flex; align-items: center; gap: .9rem; padding: 1.1rem 1.3rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.trust-item__icon { flex: 0 0 auto; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.trust-item__icon .wv-icon { width: 22px; height: 22px; }
.trust-item span:last-child { font-weight: 600; }

/* keurmerken: only rendered at all once warmvast_certificeringen() is non-empty */
.keurmerken { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .8rem 1.4rem; }
.keurmerken__item { display: flex; align-items: center; gap: .7rem; }
.keurmerken__item img { width: 44px; height: 44px; object-fit: contain; }
.keurmerken__item .wv-icon { width: 32px; height: 32px; color: var(--primary); flex: 0 0 auto; }
.keurmerken__item span { display: flex; flex-direction: column; line-height: 1.25; }
.keurmerken__item strong { font-size: .92rem; }
.keurmerken__item em { font-style: normal; color: var(--ink-3); font-size: .78rem; }
.site-footer .keurmerken__item strong { color: #fff; }
.site-footer .keurmerken__item em { color: #93a09b; }

/* trust-stats: only rendered once at least one of founded/homes/warranty is set */
.compare { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: clip; margin: 1.5rem 0; }
.compare__side { background: var(--surface); padding: 1.4rem; }
.compare__side--hot { background: var(--primary-050); }
.compare__icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); margin-bottom: .8rem; }
.compare__title { margin: 0 0 .4rem; font-size: var(--step-0); }
.compare__side p:last-child { margin: 0; color: var(--ink-3); font-size: var(--step--1); }

.trust-stats { display: flex; flex-wrap: wrap; gap: clamp(1.6rem, 5vw, 3.2rem); }
.trust-stats__item { text-align: left; }
.trust-stats__value { display: block; font-family: var(--font-display); font-size: var(--step-3); font-weight: 800; color: var(--primary); line-height: 1; letter-spacing: -.02em; }
.trust-stats__label { display: block; margin-top: .4rem; color: var(--ink-3); font-size: var(--step--1); font-weight: 600; }
.section--ink .trust-stats__value { color: var(--accent); }
.section--ink .trust-stats__label { color: #b9c4c0; }

/* trust-facts: one bordered card combining warmvast_trust_stats() (if any real
   ones are configured) with the always-on kernwaarden badges -- see
   warmvast_the_trust_facts(). auto-fit (not a fixed column count) because the
   item count varies with how many trust stats are configured; the divider
   rule below can draw a stray hairline at the left edge of a wrapped row's
   first item, an accepted trade-off for not hardcoding a count that drifts
   out of sync with the content. */
.trust-facts {
	list-style: none; margin: clamp(1.5rem, 3vw, 2.2rem) 0 0; padding: 0;
	display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
	background: var(--surface); border: 1px solid var(--line);
	border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden;
}
.trust-facts__item { display: flex; align-items: center; gap: .8rem; padding: 1.15rem 1.4rem; position: relative; }
/* when an item is a real link (the scan landing page's "meer weten" tiles
   reuse this exact card for that) instead of the usual plain <li>, it needs
   its own text color reset (link color would otherwise tint strong/em) and
   a hover cue. */
a.trust-facts__item { color: inherit; transition: background-color var(--dur-1) var(--ease-out); }
a.trust-facts__item:hover { background: var(--primary-050); }
.trust-facts__item + .trust-facts__item::before { content: ""; position: absolute; left: 0; top: 22%; height: 56%; width: 1px; background: var(--line); }
.trust-facts__icon { flex: 0 0 auto; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.trust-facts__item strong { display: block; font-size: .95rem; }
/* the homepage's only other user of this component (kernwaarden badges)
   never pairs strong with em, so this never got styled -- without it, the
   scan landing page's quick-link tiles (the first real strong+em use) ran
   both onto one line with no separation. */
.trust-facts__item em { display: block; margin-top: .3rem; font-style: normal; color: var(--ink-3); font-size: .82rem; }
.trust-facts__item--stat strong { display: block; font-family: var(--font-display); font-size: var(--step-2); font-weight: 800; color: var(--primary); line-height: 1; letter-spacing: -.02em; }
.trust-facts__item--stat span { display: block; margin-top: .35rem; color: var(--ink-3); font-size: .82rem; font-weight: 600; }

/* ---------- FAQ ---------- */
.faq { max-width: 820px; }
.faq__item { border-bottom: 1px solid var(--line); }
.faq__item summary {
	list-style: none; cursor: pointer; padding: 1.1rem 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem;
	font-weight: 700; font-size: var(--step-1); color: var(--ink);
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary .wv-icon { color: var(--primary); transition: transform var(--dur-2) var(--ease-out); flex: 0 0 auto; }
.faq__item[open] summary .wv-icon { transform: rotate(180deg); }
.faq__item .faq__answer { padding: 0 0 1.2rem; color: var(--ink-2); }
.faq__item .faq__answer p:last-child { margin-bottom: 0; }

/* ---------- CTA band ---------- */
/* Same dark surface as the hero, from the same token -- not a second,
   slightly-different copy of it. The hairline is the one deliberate line
   between this band and the footer, which share a colour. */
.cta-band {
	position: relative;
	overflow: clip;
	background-color: var(--ink);
	background-image: var(--dark-gradient);
	color: #fff;
	border-bottom: 1px solid var(--border-hairline);
}

/* Focused split: message left, one prominent action right, vertically aligned. */
.cta-band__inner {
	position: relative; z-index: 1;
	display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
	gap: clamp(1.4rem, 3.5vw, 3.2rem);
	padding-block: clamp(1.8rem, 3vw, 2.5rem);
}
.cta-band__copy { flex: 1 1 34ch; }
.cta-band h2 {
	color: #fff; margin: .15rem 0 .45rem; max-width: 24ch;
	font-size: var(--step-2); line-height: 1.14;
}
.cta-band h2 em { font-style: normal; color: var(--accent); }
.cta-band__sub { color: #cfe0da; margin: 0; max-width: 50ch; font-size: var(--step-0); }
.cta-band__proof {
	list-style: none; margin: .95rem 0 0; padding: 0;
	display: flex; flex-wrap: wrap; gap: .35rem 1.4rem;
}
.cta-band__proof li {
	display: inline-flex; align-items: center; gap: .45rem;
	color: #bcd3cc; font-size: var(--step--1); font-weight: 600;
}
.cta-band__proof .wv-icon { color: var(--accent); width: 16px; height: 16px; }

/* action cluster: one glowing primary CTA + a quiet phone fallback */
.cta-band__actions {
	flex: 0 0 auto; display: flex; flex-direction: column; align-items: stretch; gap: .7rem;
	text-align: center;
}
.cta-band__cta {
	font-size: var(--step-1); white-space: nowrap;
	padding-inline: clamp(1.5rem, 2.6vw, 2.4rem);
	box-shadow: 0 14px 36px -12px rgba(254,208,61,.6), 0 3px 10px rgba(0,0,0,.2);
	transition: transform var(--duration-1) var(--ease-out);
}
.cta-band__cta:hover { transform: translateY(-2px); box-shadow: 0 18px 44px -12px rgba(254,208,61,.75), 0 3px 10px rgba(0,0,0,.22); }
.cta-band__phone {
	display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
	color: #cfe0da; font-weight: 700; font-size: var(--step--1);
	transition: color var(--dur-1) var(--ease-out);
}
.cta-band__phone:hover { color: #fff; }
.cta-band__phone .wv-icon { color: var(--accent); width: 18px; height: 18px; }
.cta-band .keurmerken { margin-top: 1.1rem; gap: .6rem 1.6rem; }
.cta-band .keurmerken__item .wv-icon { color: var(--accent); width: 26px; height: 26px; }
.cta-band .keurmerken__item strong { color: #fff; font-size: .88rem; }
.cta-band .keurmerken__item em { color: #b9c4c0; }
@media (max-width: 620px) {
	.cta-band__actions { width: 100%; }
}

/* ---------- Footer ---------- */
.site-footer { background: var(--ink); color: #b9c4c0; }
.site-footer__grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 2rem; padding-block: clamp(2.5rem, 5vw, 3.8rem); }
.site-footer__tag { margin: 1rem 0 1.2rem; max-width: 40ch; font-size: var(--step--1); }
.site-footer__contact { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.site-footer__contact li { display: flex; align-items: center; gap: .5rem; font-size: var(--step--1); }
.site-footer__contact a { display: inline-flex; align-items: center; gap: .5rem; color: #dfe7e4; }
.site-footer__contact a:hover { color: var(--accent); }
.site-footer__contact .wv-icon { color: var(--accent); width: 18px; height: 18px; }
.site-footer__title { color: #fff; font-size: var(--step-0); margin-bottom: .9rem; }
.site-footer__col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.site-footer__col a { color: #b9c4c0; font-size: var(--step--1); }
.site-footer__col a:hover { color: #fff; }
.site-footer__badge { display: flex; align-items: flex-start; gap: .5rem; margin-top: 1.2rem; font-size: .78rem; color: #93a09b; }
.site-footer__badge .wv-icon { color: var(--accent); width: 18px; height: 18px; flex: 0 0 auto; }
.site-footer__bar { border-top: 1px solid rgba(255,255,255,.1); }
.site-footer__bar-inner { display: flex; justify-content: space-between; gap: 1rem; padding-block: 1.2rem; font-size: .8rem; color: #8b9893; flex-wrap: wrap; }
.site-footer__bar p { margin: 0; }

/* ---------- Prose ---------- */
/* .page-hero is retired -- see .hero/.hero__grid earlier in this file,
   built from template-parts/hero.php. One template, one component, every
   page (see docs/DESIGN-SYSTEM.md). */
.page-body { padding-block: clamp(2.2rem, 5vw, 3.5rem); }
.prose { max-width: 760px; }
.prose > * + * { margin-top: 1.1rem; }
.prose h2 { font-size: var(--step-2); margin-top: 2.2rem; }
.prose h3 { font-size: var(--step-1); margin-top: 1.6rem; }
.prose ul, .prose ol { padding-left: 1.3rem; }
.prose li { margin-bottom: .4rem; }
.prose img { border-radius: var(--radius); }
.prose blockquote { border-left: 4px solid var(--accent); background: var(--surface); padding: 1rem 1.3rem; margin: 1.5rem 0; border-radius: 0 var(--radius) var(--radius) 0; }
.prose a { text-decoration: underline; text-underline-offset: 2px; }
.prose .wp-block-table table { width: 100%; border-collapse: collapse; }
.prose table th, .prose table td { border: 1px solid var(--line); padding: .6rem .8rem; text-align: left; }
.prose table th { background: var(--surface-2); }

/* callout box usable inside prose */
/* article illustrations (custom SVG diagrams, not stock photography) */
.article-visual {
	margin: 0 0 1.8rem; border-radius: var(--radius-lg); overflow: clip;
	border: 1px solid var(--line); box-shadow: var(--shadow-sm); background: var(--surface);
}
.article-visual svg { width: 100%; height: auto; display: block; }
.article-visual--card { margin-bottom: 0; border-radius: var(--radius); aspect-ratio: 640/260; }
.article-visual--card svg { width: 100%; height: 100%; object-fit: cover; }

.wv-callout { background: var(--primary-050); border: 1px solid #cde3dd; border-radius: var(--radius); padding: 1.2rem 1.4rem; }
.wv-callout--warn { background: var(--warn-bg); border-color: #edd7c2; }
.wv-disclaimer { font-size: .82rem; color: var(--ink-3); border-top: 1px solid var(--line); padding-top: 1rem; }

/* article inline cta */
.article-cta { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; background: var(--surface); border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: var(--radius); padding: 1.5rem; margin-top: 2rem; }
.article-cta h2 { font-size: var(--step-1); margin-bottom: .25rem; }
.article-cta p { margin: 0; color: var(--ink-3); }

/* posts grid */
.post-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.card--post { padding: 0; overflow: clip; display: flex; flex-direction: column; }
.card--post__media img { width: 100%; height: 190px; object-fit: cover; }
.card--post__media--icon { position: relative; height: 190px; display: flex; align-items: center; justify-content: center; background: var(--surface-2); color: var(--primary); }
.card--post__media--icon .wv-icon { width: 44px; height: 44px; }
.card--post__body { padding: 1.3rem; display: flex; flex-direction: column; gap: .5rem; }
.card--post__title { font-size: var(--step-1); margin: 0; }
.project-card__tags { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; }
.project-card__tag { display: inline-flex; padding: .3rem .7rem; border-radius: var(--radius-pill); background: var(--primary-050); color: var(--primary); font-weight: 700; font-size: .78rem; }
.project-card__badge { position: absolute; top: .8rem; left: .8rem; padding: .3rem .7rem; border-radius: var(--radius-pill); background: var(--accent); color: var(--ink); font-weight: 700; font-size: .7rem; letter-spacing: .03em; text-transform: uppercase; }
.pagination { margin-top: 2.5rem; }
.pagination .nav-links { display: flex; gap: .5rem; flex-wrap: wrap; }
.pagination a, .pagination .current { padding: .5rem .9rem; border-radius: var(--radius-sm); border: 1px solid var(--line); }
.pagination .current { background: var(--primary); color: #fff; border-color: var(--primary); }

/* utilities */
.u-mt-0 { margin-top: 0; }
.u-center { text-align: center; }
.hidden { display: none !important; }
/* A small trailing note inside a card or block -- replaces the retired
   .facts__note for its two non-hero uses (deliberately NOT .data-card__note,
   which is a hero-aside-specific component; coupling unrelated body content
   to it would make a future edit to one silently affect the other). */
.u-note { font-size: var(--text--1); color: var(--text-quiet); margin: var(--space-4) 0 0; }

/* ---------- Data card ----------
   A tabular label/value surface, not a marketing card. Used as the hero
   aside on maatregel (ISDE tariffs) and gemeente (local facts) pages --
   same component, different rows, per docs/DESIGN-SYSTEM.md's "everything
   that differs is a parameter." Hairline rows, elevation-1 maximum, no
   gradient; --accent reserved for the single headline figure, if any. */
.data-card {
	background: var(--surface-raised); border: 1px solid var(--border-hairline);
	border-radius: var(--radius-4); box-shadow: var(--elevation-1);
	padding: var(--space-5);
}
.data-card__kicker { margin: 0 0 var(--space-3); }
.data-card__rows { margin: var(--space-3) 0 0; display: grid; gap: 0; }
.data-card__row {
	display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4);
	padding: var(--space-3) 0; border-bottom: 1px solid var(--border-hairline);
}
.data-card__row:last-child { border-bottom: 0; }
.data-card__label { margin: 0; color: var(--text-quiet); font-size: var(--text--1); }
/* the fix: "10 m2" and "170 m2" wobble against each other because digits
   are proportionally spaced by default -- tabular-nums gives every digit
   the same advance width, lining-nums keeps them all on the same baseline
   (no old-style figures with descenders). */
.data-card__value {
	margin: 0; font-family: var(--font-display); font-weight: 800;
	font-size: var(--text-1); letter-spacing: var(--tracking-1); color: var(--text-primary);
	font-variant-numeric: tabular-nums lining-nums;
}
.data-card__value--headline { color: var(--accent); }
.data-card__note { font-size: var(--text--1); color: var(--text-quiet); margin: var(--space-4) 0 0; }

.tariff { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
.tariff__cell { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.3rem; display: flex; flex-direction: column; gap: .5rem; }
.tariff__cell--hot { background: var(--ink); color: #fff; border-color: var(--ink); }
.tariff__label { font-size: var(--step--1); color: var(--ink-3); }
.tariff__cell--hot .tariff__label { color: #b7c2be; }
.tariff__value { font-size: var(--step-3); font-weight: 800; letter-spacing: -.03em; line-height: 1; }
.tariff__cell--hot .tariff__value { color: var(--accent); }
.tariff__value small { font-size: 1rem; font-weight: 600; color: var(--ink-3); margin-left: .2rem; }
.tariff__cell--hot .tariff__value small { color: #9fb0ab; }

.steps--4 { grid-template-columns: repeat(4, 1fr); }

/* rate table */
.table-wrap { overflow-x: auto; border-radius: var(--radius); border: 1px solid var(--line); }
.rate-table { width: 100%; border-collapse: collapse; background: var(--surface); min-width: 560px; }
.rate-table th, .rate-table td { padding: .95rem 1.1rem; text-align: left; border-bottom: 1px solid var(--line); }
.rate-table thead th { background: var(--surface-2); font-size: var(--step--1); text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); }
.rate-table tbody tr:last-child td { border-bottom: 0; }
.rate-table td { font-weight: 600; }
.rate-table__hot { color: var(--primary); font-weight: 800; }

/* contact */
.contact-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.contact-card { display: flex; align-items: center; gap: .9rem; text-decoration: none; color: var(--ink); }
.contact-card__icon { flex: 0 0 auto; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.contact-card strong { display: block; }
.contact-card em { font-style: normal; color: var(--ink-3); font-size: var(--step--1); }

.service-card--wide { flex-direction: column; }
@media (max-width: 560px) { .contact-cards { grid-template-columns: 1fr; } }

/* ==========================================================================
   Woningscan (address-driven)
   ========================================================================== */
.ws {
	color: var(--ink); border: 1px solid transparent;
	/* solid fill (padding-box) keeps the form razor-legible; gradient hairline
	   (border-box) + float shadow + top light-catch make it read as glass. */
	background:
		linear-gradient(var(--surface), var(--surface)) padding-box,
		var(--glass-border) border-box;
	border-radius: var(--radius-lg); overflow: clip;
	box-shadow: var(--shadow-float), inset 0 1px 0 rgba(255,255,255,.65);
	padding: clamp(1.3rem, 3vw, 1.9rem);
}
.ws-head { margin-bottom: 1.1rem; }
.ws-head h2 { font-size: var(--step-2); margin-bottom: .3rem; }
.ws-head > p { color: var(--ink-3); font-size: var(--step--1); margin: 0; }
.ws-stage[hidden] { display: none; }

/* address form */
.ws-address__fields { display: grid; grid-template-columns: 1.4fr 1fr .8fr; gap: .7rem; margin-bottom: .9rem; }
.ws-address__fields label { display: block; font-size: var(--step--1); font-weight: 600; color: var(--ink-2); }
.ws-address input { width: 100%; font: inherit; margin-top: .3rem; padding: .75rem .8rem; background: var(--surface-2); border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm); }
.ws-address input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-050); }
.ws-address__btn { width: 100%; }
.ws-privacy { font-size: .8rem; color: var(--ink-3); text-align: center; margin: .8rem 0 0; }
.ws-error { color: var(--warn); background: var(--warn-bg); border-radius: var(--radius-sm); padding: .55rem .75rem; margin: .8rem 0 0; font-size: var(--step--1); }
.ws-error:empty { display: none; }

/* loading */
.ws-loading { text-align: center; padding: 2rem 1rem 2.35rem; color: var(--ink-3); }
.ws-loading[hidden] { display: none; }
.ws-loader {
	position: relative;
	width: 92px;
	height: 82px;
	margin: 0 auto .8rem;
	overflow: visible;
}
.ws-loader svg { position: relative; width: 100%; height: 100%; overflow: visible; }
.ws-loader__heat path {
	opacity: 0;
	animation: ws-heat-rise 1.8s ease-in-out infinite;
}
.ws-loader__heat path:nth-child(2) { animation-delay: .22s; }
.ws-loader__heat path:nth-child(3) { animation-delay: .44s; }
.ws-loader__house { filter: drop-shadow(0 4px 9px rgba(11,107,91,.13)); }
.ws-loading p {
	max-width: 30ch;
	margin: 0 auto;
	font-weight: 700;
	color: var(--ink-2);
}
@keyframes ws-heat-rise {
	0% { opacity: 0; transform: translateY(5px); }
	22% { opacity: 1; }
	68% { opacity: .75; }
	100% { opacity: 0; transform: translateY(-10px); }
}
@keyframes ws-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
	.ws-loader__heat path { animation: none; opacity: 1; }
}

/* back link */
.ws-back { display: inline-flex; align-items: center; gap: .3rem; background: none; border: 0; color: var(--ink-3); font: inherit; font-weight: 600; cursor: pointer; padding: 0 0 .6rem; }
.ws-back:hover { color: var(--primary); }
.ws-back .wv-icon { transform: rotate(90deg); }

/* verdict */
.ws-verdict { font-weight: 700; }
.ws-verdict.is-ok { color: var(--success); }
.ws-verdict.is-warn { color: var(--warn); }

/* visuals: house + aerial */
.ws-visuals { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: 1.1rem; }
.ws-house { background: linear-gradient(160deg, #f2f6f4, #e7efec); border: 1px solid var(--line); border-radius: var(--radius); aspect-ratio: 4/3; display: flex; align-items: center; justify-content: center; }
.ws-house__svg { width: 100%; height: 100%; }
.ws-house__wall { fill: var(--primary); stroke: var(--primary-700); stroke-width: .4; stroke-linejoin: round; }
.ws-house__roof { fill: var(--accent); stroke: var(--accent-700); stroke-width: .4; stroke-linejoin: round; }
/* crisp ridge line along the roof/wall junction — reinforces the technical-drawing feel */
.ws-house__outline { fill: none; stroke: var(--primary-700); stroke-width: .6; opacity: .55; }
.ws-house__shadow { fill: rgba(10,40,34,.10); }
.ws-aerial { position: relative; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); aspect-ratio: 4/3; background: var(--surface-2); }
.ws-aerial img { width: 100%; height: 100%; object-fit: cover; }
.ws-aerial__overlay {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	pointer-events: none;
}
.ws-aerial__footprint {
	fill: rgba(254,208,61,.18);
	stroke: var(--accent);
	stroke-width: 1.15;
	vector-effect: non-scaling-stroke;
	filter: drop-shadow(0 0 4px rgba(0,0,0,.55));
}

/* details / surfaces */
.ws-details, .ws-label { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1rem; }
.ws-details h3, .ws-label h3 { display: flex; align-items: baseline; flex-wrap: wrap; gap: .25rem; font-size: var(--step-0); margin-bottom: .85rem; }
.ws-hint { font-weight: 500; font-size: .76rem; line-height: 1.3; color: var(--ink-3); }
.ws-surfaces { display: grid; gap: .5rem; }
.ws-surfaces label { display: flex; align-items: center; justify-content: space-between; gap: 1rem; font-weight: 600; }
.ws-suffix { display: inline-flex; align-items: center; gap: .4rem; color: var(--ink-3); font-weight: 500; }
.ws-suffix input { width: 92px; text-align: right; font: inherit; font-weight: 700; color: var(--ink); padding: .45rem .6rem; background: var(--surface); border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm); }
.ws-suffix input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-050); }
.ws-meta { font-size: .8rem; color: var(--ink-3); margin: .7rem 0 0; }

/* energy label scale */
.ws-label__scale {
	display: grid;
	grid-template-columns: repeat(11, minmax(0, 1fr));
	gap: 6px;
	margin-bottom: 1.45rem;
}
.ws-label__cell {
	position: relative;
	isolation: isolate;
	min-width: 0;
	min-height: 48px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: var(--radius-sm);
	background: color-mix(in srgb, var(--c) 32%, #fff);
	color: color-mix(in srgb, var(--c) 58%, #333);
	font-family: var(--font-display);
	font-size: .76rem;
	font-weight: 800;
	line-height: 1;
	letter-spacing: 0;
	white-space: nowrap;
}
.ws-label__cell[data-label^="A+"] { font-size: .66rem; }
.ws-label__cell.is-active {
	background: var(--c);
	color: #fff;
	outline: 2px solid #fff;
	box-shadow: 0 0 0 4px var(--c);
	z-index: 2;
}
.ws-label__cell em {
	position: absolute;
	top: calc(100% + 7px);
	left: 50%;
	transform: translateX(-50%);
	font-family: var(--font-sans);
	font-style: normal;
	font-size: .61rem;
	font-weight: 800;
	line-height: 1;
	color: var(--ink-2);
	white-space: nowrap;
	pointer-events: none;
}
.ws-label__source { margin: 0; font-size: .8rem; line-height: 1.45; color: var(--ink-3); }

/* measures */
.ws-measures { list-style: none; margin: 0 0 1.1rem; padding: 0; display: grid; gap: .5rem; }
.ws-measure { display: flex; align-items: center; gap: .7rem; padding: .8rem 1rem; border: 1.5px solid var(--line-strong); border-radius: var(--radius); cursor: pointer; font-weight: 600; }
.ws-measure:has(input:checked) { border-color: var(--primary); background: var(--primary-050); }
.ws-measure input { accent-color: var(--primary); width: 1.15rem; height: 1.15rem; }
.ws-measure__name { display: inline-flex; align-items: center; gap: .45rem; }
.ws-measure__name .wv-icon { color: var(--primary); }
.ws-measure__m2 { margin-left: auto; font-weight: 700; color: var(--primary-700); }

/* subsidie / besparing figures */
.ws-figures { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
.ws-figure { border-radius: var(--radius); padding: 1.1rem 1.2rem; }
.ws-figure { display: flex; flex-direction: column; align-items: flex-start; }
.ws-figure--subsidie { background: var(--ink); color: #fff; }
.ws-figure--besparing { background: var(--primary-050); border: 1px solid #cde3dd; }
.ws-figure__label { display: block; font-size: var(--step--1); color: var(--ink-3); margin-bottom: .35rem; }
.ws-figure--subsidie .ws-figure__label { color: #b7c2be; }
.ws-figure__val { font-family: var(--font-display); font-size: clamp(1.7rem, 5vw, 2.3rem); font-weight: 800; letter-spacing: -.02em; line-height: 1; font-variant-numeric: tabular-nums; }
.ws-figure--subsidie .ws-figure__val { color: var(--accent); }
.ws-figure--besparing .ws-figure__val { color: var(--primary-700); }
.ws-badge { display: none; margin-top: .6rem; align-items: center; gap: .3rem; background: var(--accent); color: var(--accent-ink); font-weight: 750; font-size: .72rem; padding: .25rem .55rem; border-radius: var(--radius-pill); }
.ws-badge.is-on { display: inline-flex; }
.ws-disclaimer { font-size: .78rem; color: var(--ink-3); margin: 1rem 0 1.1rem; }

/* lead form */
.ws-lead { display: grid; gap: .7rem; }
.ws-lead label { display: block; font-weight: 600; font-size: var(--step--1); }
.ws-lead .wv-consent--inline {
	display: block;
	margin-top: .1rem;
	font-weight: 500 !important;
	color: var(--ink-2);
	line-height: 1.5;
}
.ws-lead .wv-consent--inline span { display: inline; }
.ws-lead .wv-consent--inline input {
	display: inline-block;
	width: 16px;
	height: 16px;
	margin: 0 0 0 .45rem;
	vertical-align: -2px;
	accent-color: var(--primary);
}
.ws-lead input, .ws-lead textarea { width: 100%; font: inherit; margin-top: .25rem; padding: .7rem .8rem; background: var(--surface-2); border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm); }
.ws-lead input:focus, .ws-lead textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-050); }
.ws-honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.ws-full { width: 100%; }
.ws-success { text-align: center; padding: 1.5rem 1rem; }
.ws-success .wv-success__mark { margin-inline: auto; }

@media (max-width: 520px) {
	.ws-address__fields { grid-template-columns: 1fr; }
	.ws-visuals { grid-template-columns: 1fr; }
	.ws-figures { grid-template-columns: 1fr; }
	.ws-details, .ws-label { padding: .95rem; }
	.ws-label__scale { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 5px; row-gap: 1.65rem; margin-bottom: 1.5rem; }
	.ws-label__cell { min-height: 42px; font-size: .7rem; border-radius: 7px; }
	.ws-label__cell[data-label^="A+"] { font-size: .61rem; }
	.ws-label__cell em { top: calc(100% + 6px); font-size: .58rem; }
}

/* ---------- Reviews ---------- */
.reviews__head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1.5rem; margin-bottom: 2rem; }
.reviews__head .section-head { margin-bottom: 0; }
.reviews__score { display: flex; align-items: center; gap: .9rem; }
.reviews__num { font-family: var(--font-display); font-size: 3rem; font-weight: 700; line-height: 1; letter-spacing: -.03em; color: var(--ink); }
.reviews__score em { display: block; font-style: normal; color: var(--ink-3); font-size: var(--step--1); margin-top: .2rem; }
.stars { display: inline-flex; gap: 2px; }
.star { width: 18px; height: 18px; fill: var(--line-strong); }
.star--on { fill: var(--accent-strong); }
/* position:relative so .review__link (a full-card overlay link, see below)
   has something to fill. A CSS grid row stretches every cell in it to the
   tallest one by default, so clamping the blockquote to a fixed line count
   is what actually keeps a one-sentence review and a five-paragraph one the
   same card height -- the grid stretch alone can't do that when the content
   itself is what varies. */
.review { display: flex; flex-direction: column; gap: .8rem; position: relative; }
.review .stars .star { width: 16px; height: 16px; }
/* -webkit-line-clamp (despite the name, supported in every current engine,
   not just WebKit) truncates to a fixed number of lines with an ellipsis --
   6 lines is roughly the height the shortest real review already renders
   at, so short reviews are visually unaffected and long ones stop growing
   the card past that. */
.review blockquote {
	margin: 0; font-size: 1.02rem; color: var(--ink); line-height: 1.55;
	display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden;
}
/* Purely a visual affordance, NOT its own link -- .review__link below (the
   one real, whole-card link) already covers this text, and two overlapping
   links to the same place would just double the focus stops for a screen
   reader / keyboard user for no benefit. */
.review__cta { font-size: .82rem; font-weight: 700; color: var(--primary); }
.review figcaption { display: flex; align-items: center; gap: .7rem; margin-top: auto; }
.review__avatar { flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--primary); color: #fff; font-weight: 700; font-family: var(--font-display); }
.review figcaption strong { display: block; }
.review figcaption em { font-style: normal; color: var(--ink-3); font-size: var(--step--1); }
/* The whole-card "stretched link" pattern: inset:0 over a position:relative
   .review makes the entire card one click target, same as clicking a blog
   card. It's transparent, so the text underneath still renders and reads
   normally -- clicking IT also activates the link, which is the expected
   behaviour for a clickable card, not a bug. aria-label carries the link's
   purpose since it has no visible text of its own -- see
   template-parts/reviews.php. */
.review__link { position: absolute; inset: 0; border-radius: inherit; }

/* ---------- USP bar ---------- */
/* A centred, rounded overview card that floats fully inside the hero,
   anchored flush to its bottom edge (bottom:0, not offset) and sharing the
   page sheet's own background color -- so the card's bottom simply becomes
   the page starting underneath it, no visible seam, instead of a gap of
   dark hero showing between the card and the page below. Absolutely
   positioned (not a sibling with negative margin) so it can never end up
   below the fold: the hero is exactly 100svh, so anything outside its own
   box would need scrolling to reach. Below the breakpoint where the hero
   stacks to one column, it drops back into normal flow. */
.usp-bar {
	position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
}
/* Concave corner fill: the card's square bottom corners (see .usp-bar__grid)
   sit flush against the hero's own flat bottom edge, which continues past
   the card's narrower width -- that junction (card's straight side edge
   meeting the hero's straight bottom edge) is itself a sharp inward corner,
   even though both the card and the hero-bottom are individually "seamless"
   now. mask-image (not a plain radial-gradient background) so the actual
   fill can be the card's real background -- flat color AND the same noise
   grain (color alone still mismatched the page, same reason as the card
   itself) -- clipped to a quarter-circle instead of trying to paint a
   second, separate approximation of it. The circle is centered on the
   corner AWAY from the card (0%/100% of this small box, i.e. deeper into
   the hero) with radius var(--radius-lg): everything closer than that stays
   masked out (hero's own background shows through, unaltered), everything
   at-or-beyond it is filled -- the fill reaches the junction point itself
   and curves away from it, not a bump centered ON the junction (that read
   as a ball, not a fillet -- the first version had the circle backwards).
   A thin border ring (::before, unmasked, 1px larger) gives it the same
   edge definition as the card's own top/side border. Two fixed
   940px-card-width halves (470px from center), not measured -- the card's
   max-width is itself a fixed value, so this stays exact at any viewport
   wide enough for the card to be at full width. */
.usp-bar__notch {
	position: absolute; bottom: 0; z-index: 1;
	width: var(--radius-lg); height: var(--radius-lg);
	pointer-events: none;
}
.usp-bar__notch::before {
	content: ""; position: absolute; inset: 0;
	background: var(--line);
}
.usp-bar__notch::after {
	content: ""; position: absolute; inset: 1px;
	background: var(--noise-texture) 0 0 / 220px 220px, var(--surface-base);
}
.usp-bar__notch--left {
	right: calc(50% + 470px);
}
.usp-bar__notch--left::before {
	-webkit-mask-image: radial-gradient(circle at 0% 0%, transparent calc(var(--radius-lg) - .5px), #fff var(--radius-lg));
	mask-image: radial-gradient(circle at 0% 0%, transparent calc(var(--radius-lg) - .5px), #fff var(--radius-lg));
}
.usp-bar__notch--left::after {
	--r: calc(var(--radius-lg) - 1px);
	-webkit-mask-image: radial-gradient(circle at 0% 0%, transparent calc(var(--r) - .5px), #fff var(--r));
	mask-image: radial-gradient(circle at 0% 0%, transparent calc(var(--r) - .5px), #fff var(--r));
}
.usp-bar__notch--right {
	left: calc(50% + 470px);
}
.usp-bar__notch--right::before {
	-webkit-mask-image: radial-gradient(circle at 100% 0%, transparent calc(var(--radius-lg) - .5px), #fff var(--radius-lg));
	mask-image: radial-gradient(circle at 100% 0%, transparent calc(var(--radius-lg) - .5px), #fff var(--radius-lg));
}
.usp-bar__notch--right::after {
	--r: calc(var(--radius-lg) - 1px);
	-webkit-mask-image: radial-gradient(circle at 100% 0%, transparent calc(var(--r) - .5px), #fff var(--r));
	mask-image: radial-gradient(circle at 100% 0%, transparent calc(var(--r) - .5px), #fff var(--r));
}
@media (max-width: 960px) {
	.usp-bar__notch { display: none; }
	/* position:static drops out of z-index entirely (it only applies to
	   positioned elements), so without this the always-on .hero__thermal /
	   .hero__orb glow layers (position:absolute, z-index:0) paint ABOVE this
	   card regardless of DOM order -- a translucent dark wash sitting on top
	   of a white card, exactly the "unreadable dark text" bug. `relative` +
	   a z-index keeps it in its own stacking layer above them. */
	.usp-bar { position: relative; z-index: 2; margin-top: clamp(1.5rem, 4vw, 2.5rem); }
	/* the square-bottom "cutout" look only makes sense flush against the
	   page below it (see .usp-bar__grid); here the card floats in normal
	   flow with a gap under it, so a squared-off bottom just reads as
	   accidentally clipped. Round it back. */
	.usp-bar__grid { border-radius: var(--radius-lg); border-bottom: 1px solid var(--line); }
}
.usp-bar__grid {
	display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
	max-width: 940px; margin-inline: auto;
	/* explicit reset: .usp-bar now lives inside .hero, which sets a near-white
	   color for its dark-background copy. Without this, .usp__text strong (no
	   color of its own) would inherit that near-white straight onto this white
	   card and become unreadable. */
	color: var(--ink);
	/* same literal color AND the same grain as body/.site-main's sheet
	   background (no shared variable exists for either yet) -- deliberately
	   not var(--surface)/white, so the card's bottom blends straight into
	   the page instead of reading as a white card sitting in front of it.
	   The color alone still measurably mismatched: .site-main's noise
	   texture (5% black grain) darkens its #f4f4ef by a few RGB points on
	   average, and this flat background didn't have that same grain --
	   without it, the seam was still visible as a faint but real color
	   step even with matching base colors. */
	background: var(--surface-base) var(--noise-texture); background-size: 220px 220px;
	/* no border-bottom: a full 1px border on all four sides drew a visible
	   grey hairline right across the seam -- exactly the cut the color-match
	   was meant to erase. Top/sides still get one, for definition against
	   the hero. */
	border: 1px solid var(--line); border-bottom: none;
	/* rounded top only, square bottom -- the card visually rises out of the
	   page it shares a color with, instead of floating as a fully-rounded
	   card that happens to touch it. */
	border-radius: var(--radius-lg) var(--radius-lg) 0 0;
	/* var(--shadow-lg) is tuned for light backgrounds and all but disappears
	   against the hero's own near-black tone, so this card gets its own,
	   dark-tuned shadow instead. Two things this needs to keep true:
	   1) spread 0 on every layer -- a box-shadow's own corners follow the
	      element's border-radius shrunk by the spread (max(0, radius +
	      spread)), so any negative spread here would give the shadow a
	      tighter corner than the card's real one and it would show as a
	      flat/square nick under the curve. At spread 0 they always match,
	      at any blur.
	   2) every layer's reach (offset + blur/2) safely below 0 -- .hero's
	      overflow:hidden clips right at this card's own bottom edge, so a
	      layer reaching past that gets sliced mid-fade into a hard line.
	   The top layer is centred and vertical-only, so it reads across the
	   whole top edge, not just the middle. The two side layers are
	   horizontal-only (no Y offset) rather than diagonal from the top
	   corners, so the card reads as shadowed along its FULL height on both
	   sides, not just near the top -- a diagonal pair fades out well before
	   reaching the bottom. Their own reach past the clip line only matters
	   in the last ~8px above it, which the corner notch (z-index above this
	   card) already covers regardless. Kept deliberately light -- enough to
	   read as "the dark hero sits behind this card", not a heavy cutout. */
	box-shadow:
		0 -14px 20px 0 rgba(0,0,0,.22),
		-14px 0 16px 0 rgba(0,0,0,.16),
		14px 0 16px 0 rgba(0,0,0,.16);
	overflow: hidden;
}
.usp { display: flex; align-items: center; gap: .8rem; padding: 1.15rem 1.4rem; position: relative; }
.usp + .usp::before { content: ""; position: absolute; left: 0; top: 22%; height: 56%; width: 1px; background: var(--line); }
.usp__icon { flex: 0 0 auto; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--primary-050); color: var(--primary); }
.usp__text { display: flex; flex-direction: column; line-height: 1.25; }
.usp__text strong { font-size: .98rem; }
.usp__text em { font-style: normal; color: var(--ink-3); font-size: .82rem; }

/* ---------- Button sheen ----------
   Reserved for the single primary hero CTA (.btn--sheen) — a sweep on every
   accent/primary button site-wide reads as a cheap SaaS-promo tic, not premium. */
.btn--sheen { position: relative; overflow: hidden; }
.btn--sheen::after {
	content: ""; position: absolute; top: 0; left: -120%; width: 55%; height: 100%;
	background: linear-gradient(100deg, transparent, rgba(255,255,255,.38), transparent);
	transform: skewX(-18deg); transition: left var(--dur-3) var(--ease-out); pointer-events: none;
}
.btn--sheen:hover::after { left: 130%; }
@media (prefers-reduced-motion: reduce) { .btn--sheen::after { display: none; } }

/* ---------- Service card: warm top glow + full-perimeter hover trace ---------- */
@property --wv-trace { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

.service-card { position: relative; isolation: isolate; }

/* warm hue glowing up from the top (echoes "trapped heat rising"): subtle by
   default so the brand hue is always a touch present, intensifies on hover. */
.service-card::before {
	content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
	background: radial-gradient(120% 78% at calc(50% + var(--wv-mx) * 30% + var(--wv-fx) * 14%) calc(-24% + var(--wv-my) * 16% + var(--wv-fy) * 9%), rgba(254,208,61,.24), transparent 60%);
	opacity: .38; transition: opacity var(--dur-3) var(--ease-out); pointer-events: none;
}
.card--link.service-card:hover::before { opacity: 1; }

/* a cool→warm light continuously traces the full outline on hover. inset:-1px
   + explicit radius makes the ring sit exactly on the card's rounded edge (no
   corner gap); a seamless linear loop means it never stops abruptly. A bright
   comet head plus a second, dimmer chaser keep it lively. */
.service-card::after {
	content: ""; position: absolute; inset: -1px; border-radius: var(--radius-lg); padding: 2px;
	background: conic-gradient(from var(--wv-trace),
		transparent 0deg,
		rgba(11,107,91,.5) 34deg,
		var(--primary) 60deg,
		var(--accent) 84deg,
		#fff7db 98deg,
		transparent 110deg,
		transparent 206deg,
		rgba(11,107,91,.32) 228deg,
		var(--accent) 252deg,
		transparent 272deg,
		transparent 360deg);
	-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
	        mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
	-webkit-mask-composite: xor; mask-composite: exclude;
	opacity: 0; pointer-events: none; z-index: 2;
	transition: opacity .3s var(--ease-out);
}
.card--link.service-card:hover::after {
	opacity: 1;
	/* slower, and eased per quarter-turn so the light lingers ("magnet") as it
	   rounds each corner, then pulls smoothly along the straight edges. The 37°
	   phase places each slow-point with the comet head roughly on a corner. */
	animation: wv-card-trace 3.6s cubic-bezier(.66,0,.34,1) infinite;
}
@keyframes wv-card-trace {
	0%   { --wv-trace: 37deg; }
	25%  { --wv-trace: 127deg; }
	50%  { --wv-trace: 217deg; }
	75%  { --wv-trace: 307deg; }
	100% { --wv-trace: 397deg; }
}

@media (prefers-reduced-motion: reduce) {
	.service-card::after { display: none; }
}

/* ---------- Smooth FAQ open ---------- */
@keyframes wv-faq-down { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.faq__item[open] .faq__answer { animation: wv-faq-down var(--duration-2) var(--ease-out) both; }
@media (prefers-reduced-motion: reduce) { .faq__item[open] .faq__answer { animation: none; } }

/* ---------- Staggered reveal within grids ---------- */
/* capped 2-tier cascade: enough to read as a sequence without leaving the tail
   of a long row still fading in once it's already fully on screen */
.wv-js .grid > [data-reveal]:nth-child(n+2) { transition-delay: .04s; }
.wv-js .grid > [data-reveal]:nth-child(n+4) { transition-delay: .08s; }
.wv-js .steps > [data-reveal]:nth-child(n+2) { transition-delay: .04s; }
.wv-js .steps > [data-reveal]:nth-child(n+4) { transition-delay: .08s; }

/* ==========================================================================
   Premium layering: glass modules, asymmetric flow, ambient depth
   Scoped to the named high-value modules only — glass reads as hierarchy,
   not decoration, and stays cheap (≈5 backdrop-filters, not every chip).
   ========================================================================== */

/* Glass elevation for the 4 measures cards + the ISDE verdubbelaar logic card.
   Translucency is justified here: these sit over the ambient section gradients,
   so the blur samples real colour and reads as depth rather than frost. */
.section--surface .service-card,
.section--paper .verdub__calc {
	border: 1px solid transparent;
	background:
		linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
		var(--glass-border) border-box;
	-webkit-backdrop-filter: blur(12px) saturate(1.1);
	backdrop-filter: blur(12px) saturate(1.1);
	box-shadow: var(--shadow-glass), inset 0 1px 0 rgba(255,255,255,.6);
}
/* hover: a higher lift and the hairline warms up (echoing "trapped heat"). */
.card--link.service-card:hover {
	transform: translateY(-6px);
	background:
		linear-gradient(rgba(255,255,255,.88), rgba(255,255,255,.88)) padding-box,
		var(--glass-border-hot) border-box;
	box-shadow: 0 32px 56px -26px rgba(12,40,34,.42), 0 12px 24px -14px rgba(12,40,34,.22), inset 0 1px 0 rgba(255,255,255,.7);
}
@supports not (backdrop-filter: blur(1px)) {
	.section--surface .service-card, .section--paper .verdub__calc { --glass-fill: rgba(255,255,255,.96); }
}

/* ---------- Responsive ---------- */
@media (max-width: 960px) {
	/* .hero--dark's flex-centering is a desktop-only trick to balance space
	   around .hero__inner while .usp-bar floats absolutely outside the flex
	   flow. Below this breakpoint the bar drops into normal flow as a second
	   flex item (see .usp-bar's own mobile rule) -- centering both together
	   would bunch them in the middle instead of the expected top-to-bottom
	   stack, so back to plain block flow with a modest fixed bottom gap
	   instead of reserving the bar's full height. */
	.hero--dark { display: block; padding-bottom: clamp(2.5rem, 6vw, 3.5rem); }
	.hero__inner { grid-template-columns: 1fr; }
	.hero--has-aside .hero__grid { grid-template-columns: 1fr; }
	.verdub { grid-template-columns: 1fr; }
	.grid--4 { grid-template-columns: repeat(2, 1fr); }
	.grid--3 { grid-template-columns: repeat(2, 1fr); }
	.steps { grid-template-columns: repeat(2, 1fr); }
	.post-grid { grid-template-columns: repeat(2, 1fr); }
	.site-footer__grid { grid-template-columns: 1fr 1fr; }
	.site-footer__brand { grid-column: 1 / -1; }
	.tariff { grid-template-columns: repeat(2, 1fr); }
	.steps--4 { grid-template-columns: repeat(2, 1fr); }
	.usp-bar__grid { grid-template-columns: repeat(2, 1fr); }
	.usp:nth-child(3)::before { display: none; }
	.usp:nth-child(odd)::before { display: none; }
}

/* hide the header phone at mid widths to prevent nav crowding (still in mobile menu + footer) */
@media (max-width: 1120px) and (min-width: 861px) {
	.site-nav { gap: 1rem; }
	.nav__phone { display: none; }
	.site-nav__cta { gap: .6rem; }
}

@media (max-width: 860px) {
	.site-header__inner { display: flex; }
	.nav-toggle {
		display: inline-flex; align-items: center; justify-content: center; margin-left: auto;
		width: 46px; height: 46px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); cursor: pointer;
		transition: background-color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
	}
	.nav-toggle__bars { position: relative; width: 22px; height: 16px; }
	.nav-toggle__bars span { position: absolute; left: 0; width: 100%; height: 2.2px; background: var(--ink); border-radius: var(--radius-xs); transition: transform var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-out), top var(--dur-2) var(--ease-out), background-color var(--dur-2) var(--ease-out); }
	.nav-toggle__bars span:nth-child(1) { top: 0; }
	.nav-toggle__bars span:nth-child(2) { top: 7px; }
	.nav-toggle__bars span:nth-child(3) { top: 14px; }
	.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { top: 7px; transform: rotate(45deg); }
	.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
	.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { top: 7px; transform: rotate(-45deg); }

	.site-nav {
		display: flex;
		position: fixed; inset: var(--header-h) 0 0 auto; width: min(360px, 88vw);
		flex-direction: column; align-items: stretch; gap: 0; margin: 0; padding: 1.2rem;
		background: var(--surface); border-left: 1px solid var(--line); box-shadow: var(--shadow-lg);
		transform: translateX(100%); transition: transform var(--dur-2) var(--ease-out);
		overflow-y: auto; overscroll-behavior: contain;
	}
	.site-nav.is-open { transform: translateX(0); }

	/* Dimmed scrim behind the off-canvas panel -- standard mobile-drawer
	   pattern, and gives the "click outside to close" behaviour (already
	   handled by the document click listener in main.js, since this sits
	   outside both .site-nav and .nav-toggle) a visible, tappable surface
	   instead of an invisible dead zone. Always in the DOM so opacity can
	   transition both in and out; a class-gated element could only fade in.
	   z-index 59, one below .site-header's 60, so it dims the page content
	   without darkening the header bar/logo/toggle sitting above it. */
	body::after {
		content: ""; position: fixed; inset: 0; z-index: 59;
		background: rgba(10, 18, 16, .55);
		opacity: 0; visibility: hidden; pointer-events: none;
		transition: opacity var(--dur-2) var(--ease-out), visibility 0s linear var(--dur-2);
	}
	body.nav-open::after { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity var(--dur-2) var(--ease-out); }

	.nav__list { flex-direction: column; align-items: stretch; gap: .1rem; }
	.nav__list a { padding: .85rem .6rem; font-size: var(--step-1); }
	/* Cascading reveal as the panel opens, instead of every link/CTA just
	   being instantly "there" the moment the slide-in finishes -- items sit
	   translated/faded until .site-nav.is-open, then step in with a short
	   per-item delay. transition-delay isn't reset to 0 on close, so items
	   also step back out in the same order the panel slides away, rather
	   than all vanishing at once. */
	.nav__list > .menu-item, .site-nav__cta { opacity: 0; transform: translateX(16px); transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
	.site-nav.is-open .nav__list > .menu-item, .site-nav.is-open .site-nav__cta { opacity: 1; transform: translateX(0); }
	.site-nav.is-open .nav__list > .menu-item:nth-child(1) { transition-delay: .05s; }
	.site-nav.is-open .nav__list > .menu-item:nth-child(2) { transition-delay: .09s; }
	.site-nav.is-open .nav__list > .menu-item:nth-child(3) { transition-delay: .13s; }
	.site-nav.is-open .nav__list > .menu-item:nth-child(4) { transition-delay: .17s; }
	.site-nav.is-open .nav__list > .menu-item:nth-child(5) { transition-delay: .21s; }
	.site-nav.is-open .nav__list > .menu-item:nth-child(n+6) { transition-delay: .25s; }
	.site-nav.is-open .site-nav__cta { transition-delay: .27s; }
	/* !important throughout: the desktop rule this overrides
	   (.dropdown.is-open { transform: translateX(-50%) ... }, meant to
	   center a hover dropdown under its trigger) kept winning here even
	   after matching this on equal specificity with later source order --
	   empirically confirmed via getComputedStyle, cause not fully pinned
	   down. Left unresolved it shifts the open mobile submenu half its own
	   width off-screen to the left.
	   Open/closed used to be driven by `display` alone (an instant show/
	   hide) specifically to dodge opacity/visibility -- those are what the
	   desktop hover-fade uses (.menu-item--has-children:hover .dropdown,
	   .dropdown.is-open below, unscoped by width), and an earlier, overly
	   broad attempt at animating this forced opacity/visibility to "shown"
	   unconditionally on BOTH .dropdown and .dropdown.is-open, cancelling
	   the JS's classList.toggle() out entirely -- every submenu got stuck
	   permanently open. This animates instead via max-height (display:none
	   can't transition), but stays correctly *gated*, unlike that earlier
	   attempt: the closed state below still resolves to opacity:0/hidden
	   -- the same values the desktop rule already uses for a closed
	   dropdown -- so there's nothing to fight, only .is-open flips them,
	   same as before. transition-delay on visibility keeps a closed
	   submenu's links out of tab order/screen readers (an inert max-height:0
	   box alone doesn't), instant on open, deferred on close until the
	   collapse finishes. 700px max-height ceiling: comfortably above every
	   current dropdown's real content height (~450px, the widest being
	   Gemeentes' lead + 8 links) with headroom for text wrapping -- bump it
	   if a dropdown's content grows enough to risk clipping. */
	.dropdown {
		position: static !important; transform: none !important; box-shadow: none !important;
		width: auto !important; border: 0 !important; padding: 0 0 .5rem .6rem !important; background: transparent !important;
		overflow: hidden !important; max-height: 0 !important; opacity: 0 !important; visibility: hidden !important;
		/* !important applies to the whole declaration, not per comma-separated
		   value -- can only appear once, at the end. Wins over the desktop
		   .dropdown transition (opacity/transform, --dur-1) by source order
		   alone (equal specificity, this rule is later in the file), so no
		   !important is actually needed here; kept unqualified so the syntax
		   stays valid. */
		transition: max-height var(--dur-2) var(--ease-out), opacity var(--dur-1) var(--ease-out), visibility 0s linear var(--dur-2);
	}
	.dropdown.is-open {
		max-height: 700px !important; opacity: 1 !important; visibility: visible !important;
		transition: max-height var(--dur-2) var(--ease-out), opacity var(--dur-1) var(--ease-out), visibility 0s linear;
	}
	/* Desktop only flips the caret on :hover (see .menu-item--has-children:hover
	   .nav__caret above) -- meaningless on a tap-to-open mobile submenu, which
	   has no hover state. Key it off the trigger's own aria-expanded instead,
	   which the JS below already keeps in sync with .is-open. */
	[aria-expanded="true"] > .nav__caret { transform: rotate(180deg); }
	.dropdown__grid { grid-template-columns: 1fr; }
	.dropdown__item--all { grid-column: auto; }
	.dropdown__links { grid-template-columns: 1fr; }
	.site-nav__cta { flex-direction: column; align-items: stretch; gap: .8rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
	.site-nav__cta .btn { width: 100%; }
	.nav__phone { justify-content: center; padding: .6rem; }
	/* No body.nav-open .site-header override needed here any more: the
	   header is position:fixed sitewide now (see "Header / nav" above), so
	   body's own scroll-lock position:fixed (main.js setOpen) never displaces
	   it the way it used to when the header was position:sticky. */
}

@media (max-width: 560px) {
	.grid--4, .grid--3, .grid--2, .steps, .trust-grid, .post-grid, .dropdown__grid, .compare { grid-template-columns: 1fr; }
	.hero__scan::before { top: -12px; width: calc(100% + 20px); height: calc(100% + 24px); border-radius: calc(var(--radius-lg) + 4px); }
	.usp-bar__grid { grid-template-columns: 1fr; }
	.usp::before { display: none !important; }
	.usp + .usp { border-top: 1px solid var(--line); }
	.site-footer__grid { grid-template-columns: 1fr; }
	.cta-band__actions .btn { width: 100%; }
	.hero__actions .btn { width: 100%; }
}

/* body scroll lock when mobile nav open. overflow:hidden alone doesn't
   reliably block touch-drag scrolling on mobile, so JS (main.js) also
   freezes body with position:fixed + a negative top offset matching the
   scroll position at open time -- the standard mobile scroll-lock pattern,
   restored again on close. */
body.nav-open { overflow: hidden; }

/* ---------- scroll progress bar ---------- */
.scroll-progress {
	position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 200;
	background: linear-gradient(90deg, var(--primary), var(--accent));
	transform: scaleX(0); transform-origin: 0 50%; will-change: transform;
}
@media (prefers-reduced-motion: reduce) { .scroll-progress { display: none; } }

/* ---------- scroll reveal (progressive enhancement) ----------
   Only hidden when JS is present (html.wv-js). Without JS, or with reduced
   motion, content is fully visible. */
.wv-js [data-reveal] { opacity: 0; transform: translateY(12px); transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out); }
.wv-js [data-reveal="left"] { transform: translateX(-22px); }
.wv-js [data-reveal="right"] { transform: translateX(22px); }
.wv-js [data-reveal="scale"] { transform: scale(.96); }
/* CTA: action reveals a beat after the copy for a premium cascade */
.wv-js .cta-band__actions[data-reveal] { transition-delay: .12s; }
.wv-js [data-reveal].is-revealed { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
	.wv-js [data-reveal] { opacity: 1; transform: none; }
}

/* ==========================================================================
   OVER WARMVAST + ONS WERK — page makeovers
   Both pages open on the SAME bedrijf-variant .hero every other inner page
   uses (Contact, Subsidie, Kwaliteit, ...) -- no aside, no full-bleed
   banner. Photos throughout are deliberately SMALL: one shared .story-row
   component (photo max 300px wide, text beside it, sides alternating) used by
   About's origin story, Ons Werk's process story, and Subsidie's
   technische-schouw section. Reuses shared tokens/.card/.btn/kicker/checklist/
   data-reveal rather than inventing a parallel system.
   ========================================================================== */

/* ---------- Shared "small photo + text" story row ----------
   Used by: About's origin story, Ons Werk's process story, Subsidie's
   technische-schouw section. One pattern, one place to tune it.

   The photo column is deliberately SMALL (max 300px, 4:3) so text carries the
   page and the eye zigzags down it -- the row is a paragraph with a picture,
   not a picture with a caption.

   Movement is three effects, and each gets its OWN element, because all three
   animate `transform` and would otherwise overwrite each other (the JS
   parallax writes an inline transform, which beats any CSS transform on the
   same node -- including [data-reveal]'s slide-in and .is-revealed's reset):
     .story-row__media  -> data-reveal left/right  (slides in from its side)
     .story-row__drift  -> data-parallax           (drifts against the scroll)
     img                -> :hover                  (lifts + scales)
   Bonus from that split: the accent plate belongs to __media, so it stays put
   while the photo drifts across it -- reads as real depth, not a moving slab. */
/* Image column ~40% of the row: the convention comfort-isolatie.nl (the agreed
   reference) uses for every text-beside-photo block, at the restrained end of
   its 40-60% range. Never a percentage of the VIEWPORT -- that is what turns a
   photo into a banner on a wide screen. */
.story-row {
	/* The media column is a custom property, not a literal, so a variant can
	   widen it by redefining ONE value instead of restating the whole
	   grid-template-columns -- which would need a compound selector
	   (.story-row--lead.story-row--alt) that then outranks the single-class
	   mobile reset below and strands the row in two columns on a phone. */
	--story-media: clamp(240px, 40%, 470px);
	display: grid;
	grid-template-columns: var(--story-media) minmax(0, 1fr);
	gap: clamp(1.5rem, 4vw, 3.5rem);
	align-items: center;
}
.story-row--alt { grid-template-columns: minmax(0, 1fr) var(--story-media); }
/* Lead variant: the homepage intro photo is the first photograph a visitor
   sees and carries the handshake, so it gets more room than the standard
   column. Nothing else about the component changes. */
.story-row--lead { --story-media: clamp(260px, 46%, 560px); }
.story-row--alt .story-row__media { order: 2; }
.story-row + .story-row { margin-top: clamp(2.5rem, 6vw, 4.5rem); }
/* Same gap when a row follows the werkwijze step list, or when a block
   follows a row, so the homepage needs no inline styles to space these. */
.steps + .story-row,
.story-row + .trust-grid { margin-top: clamp(2.5rem, 6vw, 4.5rem); }
/* A closing action under a section's grid -- replaces the inline
   style="margin-top:2rem" this pattern used to carry. */
.section__foot { margin-top: var(--space-6); }

.story-row__media { position: relative; isolation: isolate; }
.story-row__drift { position: relative; z-index: 1; will-change: transform; }
.story-row__media img {
	display: block;
	/* Sources are pre-cropped to this exact 3:2 with face-aware framing (see
	   the build step in the repo notes), so cover is a no-op and there is no
	   object-position guess left to clip anyone's head. */
	width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
	border-radius: var(--radius-lg); box-shadow: var(--shadow);
	transition: transform var(--dur-3) var(--ease-out), box-shadow var(--dur-3) var(--ease-out);
}
.story-row__media:hover img { transform: translateY(-5px) scale(1.015); box-shadow: var(--shadow-lg); }
/* offset accent plate peeking out behind the photo -- gives a small image
   presence without making it bigger, and reads as intentional composition. */
.story-row__media::after {
	content: ""; position: absolute; z-index: 0; inset: 14px -14px -14px 14px;
	border-radius: var(--radius-lg); background: var(--primary-050);
	border: 1px solid var(--primary-050-border);
}
.story-row--alt .story-row__media::after { inset: 14px 14px -14px -14px; }

.story-row__step {
	display: inline-flex; align-items: center; gap: .5rem;
	font-family: var(--font-display); font-size: var(--step--1); font-weight: 700;
	letter-spacing: .09em; text-transform: uppercase; color: var(--primary); margin-bottom: .6rem;
}
.story-row__step::before { content: ""; width: 22px; height: 2px; background: var(--accent); border-radius: 2px; }
.story-row h3 { font-size: var(--step-2); margin-bottom: .5rem; }
.story-row p { color: var(--ink-2); margin: 0; max-width: 58ch; }
/* A story row that opens a page carries an h2 and a bold standfirst above
   the body copy. The emphasised clause is --primary, NOT --accent: accent
   yellow on paper measures 1.36:1 and the design system bans it as text
   (docs/DESIGN-SYSTEM.md 2.4). --primary is 5.82:1 on paper. */
/* balance so the emphasised clause is not split across a line break -- an
   <em> that starts mid-line and finishes on the next reads as a wrapping
   accident rather than a deliberate second phrase. */
.story-row h2 { font-size: var(--step-3); margin-bottom: .5rem; text-wrap: balance; }
.story-row h2 em { font-style: normal; color: var(--primary); }
/* .story-row p is (0,1,1), so the standfirst needs two classes to win. */
.story-row .story-row__sub {
	font-size: var(--step-1); font-weight: 700; color: var(--ink);
	margin: 0 0 .9rem; max-width: 58ch;
}
.story-row__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .9rem 1.5rem; margin-top: 1.5rem; }

@media (max-width: 720px) {
	.story-row, .story-row--alt { grid-template-columns: 1fr; gap: 1.4rem; }
	.story-row--alt .story-row__media { order: 0; }
	/* cap the photo on mobile too -- edge-to-edge here is exactly the "huge
	   banner" look this component exists to avoid. */
	.story-row__media { max-width: 340px; }
	.story-row__media::after, .story-row--alt .story-row__media::after { inset: 12px -12px -12px 12px; }
}

/* Two related work photos side by side, one section -- reuses .story-row__media
   for the photo treatment (crop, radius, accent plate) but stacks heading/copy
   below rather than beside, since a 2-up grid has no room for a text column
   next to each. Collapses to one column with .grid--2's existing breakpoint. */
.work-pair__title { font-size: var(--step-1); margin: 1rem 0 .5rem; }
.work-pair__item p { color: var(--ink-2); margin: 0; max-width: 58ch; }

/* ---------- Story pull-quote ---------- */
.about-story__quote {
	max-width: 640px; margin: 0 auto; padding: 1.7rem 1.9rem; background: var(--ink); color: #fff;
	border-radius: var(--radius-lg); font-family: var(--font-display); font-weight: 700;
	font-size: var(--step-2); line-height: 1.32; letter-spacing: -.01em; text-align: left; position: relative;
}
.about-story__quote::before { content: ""; position: absolute; left: 1.9rem; top: -.55rem; width: 26px; height: 4px; background: var(--accent); border-radius: 2px; }

/* ---------- Team grid ---------- */
.team-grid { grid-template-columns: repeat(3, 1fr); }
.team-card { padding: 0; overflow: clip; display: flex; flex-direction: column; transition: transform var(--dur-2) var(--ease-spring), box-shadow var(--dur-2) var(--ease-out); }
.team-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); }
/* 3:2, not the source photos' own 4:5 -- a portrait-shaped crop made each card
   ~425px of photo before a word of text, which is what stacked them into a
   column of pictures. The shipped files are already cropped to this ratio
   around the detected face, so cover never has to guess. */
.team-card__photo { position: relative; aspect-ratio: 3 / 2; overflow: hidden; background: var(--ink); }
.team-card__photo img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--ease-out); }
.team-card:hover .team-card__photo img { transform: scale(1.06); }
.team-card__photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 55%, rgba(10,14,14,.55) 100%); }
.team-card__role-tag {
	position: absolute; left: 1rem; bottom: 1rem; z-index: 1;
	display: inline-flex; align-items: center; gap: .4rem;
	background: rgba(255,255,255,.16); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
	color: #fff; font-weight: 700; font-size: var(--step--1); letter-spacing: .01em;
	padding: .35rem .8rem; border-radius: var(--radius-pill); border: 1px solid rgba(255,255,255,.28);
}
.team-card__body { padding: 1.4rem 1.5rem 1.6rem; }
.team-card__name { font-size: var(--step-1); margin-bottom: .3rem; }
.team-card__bio { color: var(--ink-3); font-size: var(--step--1); margin: 0; }
@media (max-width: 860px) { .team-grid { grid-template-columns: 1fr; max-width: 420px; margin-inline: auto; } }

/* ---------- Ons werk: small extra-shots filmstrip ---------- */
.werk-filmstrip { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: clamp(2rem, 5vw, 3rem); }
.werk-filmstrip__item { position: relative; width: 118px; height: 118px; border-radius: var(--radius); overflow: hidden; flex: 0 0 auto; box-shadow: var(--shadow-sm); }
.werk-filmstrip__item img { width: 100%; height: 100%; object-fit: cover; transition: transform .5s var(--ease-out); }
.werk-filmstrip__item:hover img { transform: scale(1.08); }
.wv-js .werk-filmstrip[data-reveal] .werk-filmstrip__item { transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out); }
.wv-js .werk-filmstrip[data-reveal] .werk-filmstrip__item:nth-child(2) { transition-delay: .07s; }
.wv-js .werk-filmstrip[data-reveal] .werk-filmstrip__item:nth-child(3) { transition-delay: .14s; }

/* ---------- Scroll-linked parallax (data-parallax="0..1"; see main.js) ----------
   Only ever used on the small .story-row photos, at a low factor: enough that
   the page feels alive as you scroll, never enough to shift layout. */
[data-parallax] { will-change: transform; }
@media (prefers-reduced-motion: reduce) { [data-parallax] { transform: none !important; } }

/* ==========================================================================
   Page-level switches, set by a template via body_class
   ========================================================================== */

/* has-page-cta previously muted the header's own accent button here, to
   keep exactly one yellow CTA per viewport on this page (the hero already
   has one). Alex asked for the header button to match every other page
   instead (2026-09-24) -- overriding CLAUDE.md's "one accent colour per
   viewport" rule specifically for this page, explicitly, not by accident.
   The class still exists (see functions.php/body_class) in case a future
   page wants the muted treatment back; it currently styles nothing. */

/* No scroll-entrance fades and no hover sweep: text is simply there. */
body.has-static-entrance [data-reveal] { opacity: 1; transform: none; transition: none; }
body.has-static-entrance .btn--sheen::after { display: none; }

/* ---------- Page flow: one rhythm gap between blocks, never two paddings ----------
   docs/DESIGN-SYSTEM.md #4. The hero's own bottom padding is the gap above
   the first block; loose before the dark CTA band, which is a declared edge. */
.page-flow { padding-bottom: var(--rhythm-loose); }
.page-flow > * + * { margin-top: var(--rhythm-normal); }
/* ---------- "Zo werkt de ISDE": questions, then a comparison ----------
   One 4-column grid for the whole section, lines at 0/25/50/75%. Columns
   carry their gutter as padding-right instead of a grid gap, so each column
   starts exactly on a line and every item's top rule joins into one
   unbroken hairline across the row. The comparison table below uses the
   same trick (cell padding, fixed 25/25/50 widths), so its ISDE and
   gemeente columns start on the 25% and 50% lines of the questions above. */
.isde-facts {
	display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
	row-gap: var(--space-6); margin: 0;
}
.isde-facts__item {
	padding: var(--space-5) var(--space-6) 0 0;
	border-top: var(--border-width) solid var(--border-hairline);
}
.isde-facts dt {
	font-family: var(--font-display); font-weight: 700; font-size: var(--text-1);
	line-height: var(--leading-2); color: var(--text-primary); margin-bottom: var(--space-3);
}
.isde-facts dd { margin: 0; color: var(--text-secondary); }

.isde-compare__title { font-size: var(--step-2); text-wrap: balance; margin: var(--space-8) 0 var(--space-5); }
.isde-compare { width: 100%; border-collapse: collapse; table-layout: fixed; }
.isde-compare :is(th, td) {
	padding: var(--space-4) var(--space-6) var(--space-4) 0;
	text-align: left; vertical-align: baseline;
	border-bottom: var(--border-width) solid var(--border-hairline);
}
.isde-compare thead :is(th, td) { width: 25%; padding-top: 0; }
.isde-compare thead :is(th, td):last-child { width: 50%; }
.isde-compare thead th { font-family: var(--font-display); font-weight: 700; font-size: var(--text-1); color: var(--text-primary); }
.isde-compare tbody th { font-weight: 600; color: var(--text-quiet); }
.isde-compare tbody td { color: var(--text-secondary); }
.isde-compare__note { margin: var(--space-5) 0 0; max-width: var(--measure); color: var(--text-secondary); }
.isde-compare__links { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-6); margin: var(--space-4) 0 0; }

@media (max-width: 960px) {
	/* Two questions per row: lines at 0/50%, which is where the table's
	   gemeente column already starts. */
	.isde-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
	.isde-facts { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-5); }
	.isde-facts__item { padding-right: 0; }
	/* Stacked comparison: one ruled block per row, each value labelled by
	   the column it came from (data-label), header row kept for screen
	   readers only. Same pattern as .rate-table--stack. */
	.isde-compare, .isde-compare tbody, .isde-compare tr, .isde-compare :is(th, td) { display: block; }
	.isde-compare thead {
		position: absolute; width: var(--border-width); height: var(--border-width);
		overflow: hidden; clip-path: inset(50%); white-space: nowrap;
	}
	.isde-compare tbody tr {
		padding-block: var(--space-4);
		border-top: var(--border-width) solid var(--border-hairline);
	}
	.isde-compare tbody tr:last-child { border-bottom: var(--border-width) solid var(--border-hairline); }
	.isde-compare tbody :is(th, td) { padding: 0; border: 0; }
	/* Stacked, the row name becomes the group heading and the column names
	   become quiet labels inside it -- the reverse of the desktop weights. */
	.isde-compare tbody th {
		margin-bottom: var(--space-3);
		font-family: var(--font-display); font-weight: 700; color: var(--text-primary);
	}
	.isde-compare tbody td + td { margin-top: var(--space-3); }
	.isde-compare tbody td::before {
		content: attr(data-label); display: block;
		font-size: var(--text--1); font-weight: 600; color: var(--text-quiet);
	}
}

/* ---------- Photo slot ----------
   One component for the real photo and its placeholder: same figure, same
   ratio token, so the swap causes no layout shift. The placeholder is meant
   to look deliberately unfinished and calm -- a quiet tile with a label,
   never a broken-image icon. Built by warmvast_the_photo_slot(). */
.photo-slot {
	position: relative; margin: 0;
	aspect-ratio: var(--ratio-photo);
	/* var(--radius), not --radius-4 (16px vs 14px) -- the only other radius
	   on this page is .table-wrap, at --radius; matched to it rather than
	   sitting 2px apart for no visible reason. */
	border-radius: var(--radius);
	background: var(--surface-sunken);
	border: var(--border-width) solid var(--border-hairline);
}
.photo-slot--hero { aspect-ratio: var(--ratio-photo-hero); }
.photo-slot--calc { aspect-ratio: var(--ratio-photo-calc); }
/* z-index:1, not the figure's old overflow:hidden, is what keeps a real
   photo's own rounded corners crisp now that .photo-slot needs
   overflow:visible for the plate below to peek out: border-radius:inherit
   replaces the clip, and sitting above the plate's z-index:0 is what stops
   the plate showing through the photo itself, not stacking order alone --
   same two-layer mechanism .story-row__drift/::after already use. */
.photo-slot img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; position: relative; z-index: 1; }
/* Offset glass plate behind a real photo. First pass copied
   .story-row__media::after's flat pale-green fill, since that's the other
   diagonally-offset shape behind a photo on the site -- but that shape is
   genuinely flat on the homepage too, not the glass Alex meant. The actual
   glass on this site is --glass-fill/--glass-border + backdrop-filter blur
   (.service-card, .verdub__calc, .ws), reused here instead (2026-09-24).
   NOTE: CLAUDE.md's forbidden list names glassmorphism explicitly; this
   reuses an existing, already-shipped exception rather than introducing a
   new one, but the tension is real and worth knowing about, not silent.
   Real photos only: overflow:visible so the plate can peek past the
   figure's own edge (a placeholder has no plate and keeps overflow:hidden,
   the calmer, unfinished-on-purpose look .photo-slot--placeholder wants). */
.photo-slot:not(.photo-slot--placeholder) { overflow: visible; isolation: isolate; }
.photo-slot--placeholder { overflow: hidden; }
.photo-slot:not(.photo-slot--placeholder)::after {
	content: ""; position: absolute; z-index: 0; inset: 14px 14px -14px -14px;
	border-radius: inherit; border: var(--border-width) solid transparent;
	background:
		linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
		var(--glass-border) border-box;
	-webkit-backdrop-filter: blur(12px) saturate(1.1);
	backdrop-filter: blur(12px) saturate(1.1);
	box-shadow: var(--shadow-glass);
}
.photo-slot__label {
	position: absolute; inset: 0; margin: 0;
	display: flex; align-items: center; justify-content: center;
	padding: var(--space-5); text-align: center; text-wrap: balance;
	font-family: var(--font-mono); font-size: var(--text--1); line-height: var(--leading--1);
	letter-spacing: .06em; text-transform: uppercase; color: var(--text-quiet);
}
/* Hero with a photo aside: the text track still holds the majority share so
   the h1 can hold two lines at 1440 instead of breaking at the old 20ch cap,
   but the aside's share grew from 1fr:2fr (33%) to 1.1fr:1.7fr (~39%,
   2026-09-24, "a little bigger") -- at 1440 that's 351px -> ~421px wide. */
@media (min-width: 961px) {
	.hero__grid:has(.photo-slot--hero) { grid-template-columns: minmax(0, 1.7fr) minmax(280px, 1.1fr); }
	.hero__grid:has(.photo-slot--hero) .hero__title { max-width: none; }
}
/* No separate mobile-ratio rule needed any more: --ratio-photo-hero equals
   --ratio-photo now (both 3:2, see :root), so .photo-slot--hero already
   renders the same shape stacked as it does beside the text. */
/* Subsidie hero: centre the photo on the text column's own height, rather
   than the whole grid's align-items:start (which starts every column flush
   with the breadcrumb, the text column's first line). An earlier pass
   top-aligned it to the H1 instead via a measured margin-top -- superseded
   2026-09-24: centred reads better against a shorter (3:2, not square)
   photo, and align-self:center is exact regardless of how tall the text
   column ends up, rather than a value measured against one specific
   breadcrumb+lead combination. Only meaningful once the aside sits beside
   the copy, not stacked below it on mobile/tablet. */
@media (min-width: 961px) {
	.hero__aside:has(.photo-slot--hero) { align-self: center; }
}

/* ---------- Split: what the homeowner does vs. what Warmvast does ----------
   The homeowner's column is deliberately short; the photo sits under it at
   the normal gap. (Bottom-aligning it to the end of the steps opened a
   ~130px hole under the one-line "Wat u doet", which read as a mistake.) */
.split {
	display: grid; gap: var(--space-7);
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	grid-template-rows: auto 1fr;
	grid-template-areas: "you us" "photo us";
}
.split__you { grid-area: you; }
.split__us {
	grid-area: us;
	border-left: var(--border-width) solid var(--border-hairline);
	padding-left: var(--space-7);
}
.split__photo { grid-area: photo; align-self: start; }
/* Two columns, real photo: the photo fills the space under "Wat u doet", so
   its bottom edge lands on the last step's bottom at every width instead of
   stopping short of it (at 768 a fixed 3:2 left a ~285px hole under it). At
   1440 that space is exactly 3:2, the photo's own shape; narrower, it turns
   taller, and object-position keeps the endoscope screen and probe (left of
   centre) in frame rather than centring on the inspector's back.
   The img is absolute, so the photo adds no height of its own: the steps
   column alone sets the row, and nothing shifts while it loads. */
@media (min-width: 721px) {
	.split__photo:has(> .photo-slot:not(.photo-slot--placeholder)) { align-self: stretch; }
	.split__photo > .photo-slot:not(.photo-slot--placeholder) { aspect-ratio: auto; height: 100%; }
	.split__photo > .photo-slot:not(.photo-slot--placeholder) img { position: absolute; inset: 0; object-position: 40% 50%; }
}
.split__title { font-size: var(--step-1); margin-bottom: var(--space-4); }
.split__item { color: var(--text-secondary); max-width: var(--measure); margin: 0; }
@media (max-width: 720px) {
	.split {
		grid-template-columns: minmax(0, 1fr); grid-template-rows: auto;
		grid-template-areas: "you" "us" "photo"; gap: var(--space-6);
	}
	.split__us {
		border-left: 0; padding-left: 0;
		border-top: var(--border-width) solid var(--border-hairline); padding-top: var(--space-6);
	}
}

/* Linear variant of the homepage .steps: one column, markers joined by a
   hairline. 1.25rem / 2.35rem follow the existing 2.5rem marker in .steps
   (its centre line and its bottom edge). */
.steps.steps--linear { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
.steps--linear li:not(:last-child)::after {
	content: ""; position: absolute;
	left: calc(1.25rem - var(--border-width) / 2);
	top: calc(2.35rem + var(--space-2));
	bottom: calc(var(--space-2) - var(--space-5));
	width: var(--border-width); background: var(--border-hairline);
}

/* ---------- Worked example beside the photo that explains its m² ----------
   .calc__caption carries exactly the styling <caption> had (see .calc
   below) -- it moved to a sibling so the table's own top, not the caption
   sitting above it, is what the photo tile's top edge lines up with. */
.calc__caption { text-align: left; font-weight: 600; color: var(--text-primary); margin: var(--space-7) 0 var(--space-3); }
.calc-row {
	display: grid; gap: var(--space-7); align-items: start;
	grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
}
.calc-row .calc { margin-top: 0; }
/* The disclaimer is the column's last line; its paragraph margin would
   otherwise leave the column 16px taller than its visible text, and the
   photo beside it would end below the words it aligns with. */
.calc-row .wv-disclaimer { margin-bottom: 0; }
/* Stacks at the hero's breakpoint, not 720: at 768 the table needs ~397px
   and its half of the row is 384px. Stacked, the photo takes the table's
   own width cap, so both right edges line up. */
@media (max-width: 960px) {
	.calc-row { grid-template-columns: minmax(0, 1fr); }
	.calc-row > .photo-slot { max-width: var(--measure); }
}
/* Side by side, a real photo spans the whole column beside it: top edge on
   the table's header line, bottom edge on the disclaimer's last line. A
   fixed ratio could match that at one width only; here the text column sets
   the row height and the photo fills it (absolute img, so it adds no height
   of its own and cannot shift the layout while loading). */
@media (min-width: 961px) {
	.calc-row:has(> .photo-slot:not(.photo-slot--placeholder)) { align-items: stretch; }
	.calc-row > .photo-slot:not(.photo-slot--placeholder) { aspect-ratio: auto; }
	.calc-row > .photo-slot:not(.photo-slot--placeholder) img { position: absolute; inset: 0; }
}

/* ---------- Rate table: figures right-aligned in tabular lining numerals ---------- */
.rate-table .rate-table__num { text-align: right; font-variant-numeric: tabular-nums lining-nums; white-space: nowrap; }
.rate-table tbody th { font-weight: 600; text-align: left; }
/* Below 768 the table becomes one stacked block per measure instead of a
   horizontally scrolling table; each value takes its label from data-label. */
@media (max-width: 767px) {
	.table-wrap:has(.rate-table--stack) { overflow: visible; }
	.rate-table--stack { min-width: 0; }
	.rate-table--stack thead {
		position: absolute; width: var(--border-width); height: var(--border-width);
		overflow: hidden; clip-path: inset(50%); white-space: nowrap;
	}
	.rate-table--stack, .rate-table--stack tbody, .rate-table--stack tr,
	.rate-table--stack th, .rate-table--stack td { display: block; }
	.rate-table--stack tbody tr { padding: var(--space-4); border-bottom: var(--border-width) solid var(--border-hairline); }
	.rate-table--stack tbody tr:last-child { border-bottom: 0; }
	.rate-table--stack tbody th, .rate-table--stack td { padding: 0; border: 0; }
	.rate-table--stack tbody th { margin-bottom: var(--space-2); }
	.rate-table--stack td {
		display: flex; justify-content: space-between; gap: var(--space-4);
		padding-block: var(--space-1);
	}
	.rate-table--stack td::before {
		content: attr(data-label); text-align: left;
		font-size: var(--text--1); font-weight: 500; color: var(--text-quiet);
	}
}

/* ---------- Worked example: the doubling as real numbers ----------
   The doubled total is the one emphasised figure, marked with the accent as
   a fill (never as text on paper, 1.36:1). The disclaimer below it is set at
   body size -- the limit carries the same weight as the claim. */
.calc {
	width: 100%; max-width: var(--measure); margin-top: var(--space-7);
	border-collapse: collapse; font-variant-numeric: tabular-nums lining-nums;
}
.calc th, .calc td {
	padding: var(--space-3) 0; text-align: right; white-space: nowrap;
	border-bottom: var(--border-width) solid var(--border-hairline);
}
.calc :is(th, td) + :is(th, td) { padding-left: var(--space-5); }
.calc th:first-child { text-align: left; }
.calc thead th { font-size: var(--text--1); font-weight: 600; color: var(--text-quiet); }
.calc tbody th { font-weight: 600; }
.calc tfoot th, .calc tfoot td { border-bottom: 0; padding-top: var(--space-4); font-weight: 700; }
.calc__single { color: var(--text-secondary); }
.calc__doubled {
	font-family: var(--font-display); font-size: var(--text-2); font-weight: 800; color: var(--text-primary);
	text-decoration: underline; text-decoration-color: var(--accent);
	text-decoration-thickness: var(--space-1); text-underline-offset: var(--space-2);
}
/* At phone width the four columns need ~15px more than a 328px container
   has (measured at 360), and overflow-x:clip on body would hide the loss
   silently. The m² column goes: the caption directly above already states
   both areas, so no figure disappears. */
@media (max-width: 560px) {
	.calc :is(th, td):nth-child(2) { display: none; }
}
.wv-disclaimer--claim {
	font-size: var(--text-0); color: var(--text-secondary);
	max-width: var(--measure); padding-top: var(--space-4);
}
