/* ==========================================================================
   Modulon — prototype stylesheet
   Shared by contact.html, 404.html, bedankt.html and nieuws-detail.html.

   Design tokens are taken verbatim from CLAUDE.md and from the .dc.html
   design files. One typeface, and one added colour: an error red, because a
   form error must not be signalled in the brand orange. See :root below.

   This is a sign-off prototype, not the WordPress theme. When the block
   theme is built these values move into theme.json.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Colour — the nine swatches the client supplied are the palette. Every
     token below is either one of those nine verbatim, or a value derived from
     one of them for a role the nine do not cover: the readable orange, the
     form-error red, and the text greys. Derived values say so and carry their
     measured contrast. */
  --navy: #10295b;
  --navy-deep: #2733a4;
  /* Same value as --navy-deep, and deliberately so: the palette holds three
     blues where the previous set held five, and the heading blue on white and
     the opening stop of the navy gradient both land on this one. Two tokens
     because they are two jobs — if a fourth blue is ever added, this is the
     one that moves. 10,04:1 on white, where the old heading blue read 10,11. */
  --blue: #2733a4;
  /* The closing stop of --gradient-navy. Also #2733a4, and for the same reason
     --blue is: the previous set held a blue between the navy and the strip
     (9,00:1 on white) and the new one does not. Of the two candidates #2733a4
     reads 10,04 and #0b6bf7 reads 4,71, so this is the faithful one — a hero
     ending on #0b6bf7 is a brighter band than the design the client approved,
     and it took the two lowest spec labels on Project Detail and het berichtsjabloon
     under 4,5:1. Decided 30 juli 2026. */
  --blue-bright: #2733a4;
  /* The one place the bright blue is used: the thin banner above the footer,
     the footer bar, the active carousel dot, the testimonial eyebrow. */
  --blue-strip: #0b6bf7;
  /* The brand orange. GRAPHICS ONLY — rules, underlines, focus rings, dashed
     borders, the success mark. Use --orange-ink for anything a visitor reads.
     It carries 4,79:1 on navy but only 2,94:1 on white, a hair under the 3:1
     WCAG asks of a non-text element — the previous orange sat at 3,36. Flagged
     in README.md: the white-background graphics (focus ring, success circle,
     tab underline) either accept that, or move to --orange-ink. */
  --orange: #fc663f;
  /* No consumer at the moment: the rail controls on navy, which is what this
     was for, now fill with --orange on hover instead of lightening their glyph.
     Kept anyway — it is a value out of the palette family, so theme.json wants
     it rather than only the part of it this prototype happens to use. */
  --orange-hover: #fd805e;
  --orange-light: #fdb09b;

  /* The readable orange — derived, not in the palette. Same hue as
     --orange, taken down until it clears 4,5:1 on white (5,21) AND on
     --surface (4,51), which is also what white text on top of it scores.
     This is link text, button fills, badges — every orange a visitor reads
     rather than looks at.

     Hover goes DARKER, against the usual instinct and against the direction
     --orange-hover takes: there is no room to go lighter, because #ce2f03 is
     already the lightest value that clears the bar on both backgrounds.
     Deeper on press reads as intent; illegible does not. */
  --orange-ink: #ce2f03;
  --orange-ink-hover: #af2703;

  /* The greys — derived. The palette has no neutral, so these carry the hue of
     --navy at low saturation and are pitched at the same contrast the previous
     greys had, which keeps the weight of the body copy unchanged. */
  --ink: #37435a;
  --ink-strong: #28334a;
  --ink-on-navy: #c9e5f8;
  /* One step brighter than --ink-on-navy, for the 22px hero lead only: at that
     size the paragraph is display type sharing the fold with a white h1, and
     --ink-on-navy reads as a greyed-out version of it rather than as the second
     line of the same thought. Small print on navy stays on --ink-on-navy. */
  --ink-on-navy-bright: #dcf2ff;
  /* The footer credit line, which is the only text standing on --blue-strip.
     The new strip is a far brighter blue than the old one and leaves no
     headroom at all: white itself scores 4,71:1 there, and the next swatch
     down (#dcf2ff) already fails at 4,08. So this is white — not because the
     credit line wants to shout, but because nothing quieter passes. If the
     strip ever darkens, this token can come back down with it. */
  --ink-on-strip: #ffffff;
  /* Derived. 5,51:1 on white and 4,78 on --surface — placeholders, hints and
     captions need the same 4,5:1 body copy does. */
  --muted: #5c6984;
  --muted-on-navy: #aacce2;
  --surface: #dcf2ff;
  /* Derived. This token draws the border of every input and the inactive
     carousel dots, and both are non-text UI that WCAG 1.4.11 holds to 3:1 —
     a field boundary is not decoration, it is what tells you where to type.
     The palette's lightest blue (#aacce2) reads 1,69:1 on white and cannot do
     this job, so this is that swatch darkened to 3,49:1 on white and 3,02 on
     --surface. */
  --line: #618faa;
  --white: #ffffff;

  --surface-blue: #c9e5f8;   /* partner logo band */
  --ink-soft: #68748d;       /* inactive tab, result count — De Modulonwand,
                                Projecten. Derived, 4,70:1 on white. */

  /* Form validation. The palette's red is the parent of all five: a form error
     cannot be signalled in the brand orange, because that colour means "do
     this", not "this is wrong". --error is the swatch verbatim (4,87:1 on
     white); the other four are derived from it. Flagged in README.md as an
     open question for the client. */
  --error: #d13547;
  --error-ink: #8f212d;
  --error-surface: #feeef0;
  --error-field: #fef7f7;
  /* The same role on navy, where --error scores 2,89:1 and is effectively
     invisible — the one place a message must not be lost. 6,83:1 on --navy and
     4,87 on --navy-deep, so it holds across the whole footer gradient. */
  --error-on-navy: #ef9fa8;

  /* Built from the four tokens above rather than from their hex values again.
     Written out, these three lines were the only place --navy and
     --blue-bright appeared at all, which made two of the five brand colours
     look unused and put the site's largest surface — every navy band on every
     page — outside the palette. Custom properties resolve at the point of use,
     so the substitution here is lazy and the gradients stay live if a stop
     changes. */
  --gradient-navy: linear-gradient(160deg, var(--navy-deep) 0%, var(--navy) 55%, var(--blue-bright) 100%);
  --gradient-navy-soft: linear-gradient(160deg, var(--navy-deep) 0%, var(--navy) 70%);
  /* The "DIT STAAT ER AL" band on the homepage runs the other way. */
  --gradient-navy-alt: linear-gradient(175deg, var(--navy) 0%, var(--navy-deep) 100%);

  /* Rhythm — 64px horizontal page padding, 88–96px section spacing on desktop.
     Both step down at the breakpoints below; every rule reads them from here
     so spacing stays in sync across all four pages. */
  --pad-min: 64px;
  --section: 96px;
  --section-tight: 88px;

  /* The grid, measured out of Modulon-v4.xd rather than taken on description.
     All six artboards carry the same layout grid:

       artboard        1920
       margins          140 left and right   →  content area 1640
       columns           12 of 122px
       gutter            16

     12 × 122 + 11 × 16 = 1640. Text blocks sit one column in from each side of
     that content area — 140 + 122 + 16 = 278 on the left, the mirror on the
     right — which is the 1364px the designer quoted. Verified: every text
     block on all six artboards lands on 278 / 554 / 814 / 1366 etc, exactly on
     a column line, and "DIT STAAT ER AL" measures 278 → 1642 = 1364.

     NOT 12 columns of 99px. 12 × 99 + 11 × 16 also equals 1364, which is why
     that reading survived a first check, but it puts the column lines in the
     wrong places and makes "one column" 23px too small.

     --col and --gutter exist so a block that stops one column short of the
     right edge can say so in grid terms rather than in a bare pixel value.

     --pad is the distance from the window edge to the content, and it is the
     larger of two things: the page padding at this breakpoint, or whatever it
     takes to centre a 1364px column. Below 1492px (1364 + 2 × 64) the padding
     wins and the page behaves exactly as it did before; above it the margins
     grow. Deliberately the same token every rule already read, so the seven
     full-bleed tricks that offset by -1 × --pad keep reaching the window edge
     without knowing any of this changed.

     100vw includes the scrollbar, so with one present the column lands ~15px
     under 1364 rather than overflowing — an error on the safe side.

     Two distances, because the design has two: text stops at 1364 (--wrap),
     but its card grids and specification rows run to the full 1640 content
     area (--content).

       --pad-outer    window edge → content area.  140 at 1920.
       --text-inset   content area → text column.  138 at 1920.
       --pad          window edge → text column.   278 at 1920.

     --pad is the sum of the other two, and that sum is algebraically identical
     to the max(--pad-min, (100vw - --wrap) / 2) it replaces — at every width,
     in all three cases of the two max()es. So all 35 rules that already read
     --pad, including the seven full-bleed offsets, keep behaving exactly as
     before. The split only buys a name for the difference, so a block can opt
     out to the content edge with `margin-inline: calc(var(--text-inset) * -1)`.

     Both collapse below 1492px (1364 + 2 × 64): --text-inset hits 0 and --pad
     falls back to --pad-min, so the whole grid is inert on tablet and phone
     and nothing below that width can move. */
  --wrap: 1364px;
  --content: 1640px;
  --col: 122px;
  --gutter: 16px;
  --pad-outer: max(var(--pad-min), calc((100vw - var(--content)) / 2));
  --text-inset: max(0px, calc((100vw - 2 * var(--pad-outer) - var(--wrap)) / 2));
  --pad: calc(var(--pad-outer) + var(--text-inset));

  /* Type scale — fluid.
     Every display size in the design is a clamp() rather than a stepped
     font-size, so headings scale continuously instead of jumping at each
     breakpoint. Each one is a straight line through (375px, mobile size) and
     (1440px, the exact size in the design file): the max is the design value
     verbatim, the min is what still fits a 375px screen.

       token          design   375px
       --fs-display     118      44     Homepage h1
       --fs-h1-lg        84      38     Project Detail h1
       --fs-h1-md        76      36     De Modulonwand / Projecten h1
       --fs-h1-sm        72      34     Over Modulon h1
       --fs-h1-xs        62      32     Nieuws / Contact / 404 / bedankt h1
       --fs-h2-xl        52      30     "DE WINST ZIT VÓÓR DE BOUW."
       --fs-h2-lg        46      28     "ZO WERKT HET", "DE PROJECTEN", "BERICHTEN"
       --fs-h2           40      26     section h2 (the default)
       --fs-h2-sm        34      24     "Waar wij voor staan.", "Wat levert het op?"
       --fs-h2-xs        30      22     "In het echt nog beter!", "DOWNLOADS"
       --fs-h2-32        32      24     "Meer bewijs nodig?", "Meer lezen?"
       --fs-h2-42        42      28     footer contact h2
       --fs-lead         22      17     lead paragraph
       --fs-stat         52      36     the big number on a stat tile
  */
  --fs-display: clamp(44px, 6.95vw + 18px, 118px);
  /* Floor is 34 rather than 38: a project name is the longest single word of
     any h1 in the design ("NOORDERDUIN,") and at 38px it sat hard against
     both edges of a 375px screen. */
  --fs-h1-lg:   clamp(34px, 4.70vw + 17px, 84px);
  --fs-h1-md:   clamp(36px, 3.76vw + 22px, 76px);
  --fs-h1-sm:   clamp(34px, 3.57vw + 21px, 72px);
  --fs-h1-xs:   clamp(32px, 2.80vw + 22px, 62px);
  --fs-h2-xl:   clamp(30px, 2.10vw + 22px, 52px);
  --fs-h2-lg:   clamp(28px, 1.70vw + 22px, 46px);
  --fs-h2:      clamp(26px, 1.35vw + 21px, 40px);
  --fs-h2-sm:   clamp(24px, 1.00vw + 20px, 34px);
  --fs-h2-xs:   clamp(22px, 0.78vw + 19px, 30px);
  --fs-h2-32:   clamp(24px, 0.78vw + 21px, 32px);
  --fs-h2-42:   clamp(28px, 1.35vw + 23px, 42px);
  --fs-lead:    clamp(17px, 0.50vw + 15px, 22px);
  --fs-stat:    clamp(36px, 1.55vw + 30px, 52px);

  /* Letter-spacing has to shrink with the type or a 6px-tracked caps heading
     overflows a 375px screen long before the font-size does. */
  --track-display: clamp(1px, 0.28vw, 4px);
  --track-hero:    clamp(0.5px, 0.14vw, 2px);
  --track-xl:      clamp(0.5px, 0.42vw, 6px);

  /* Shape */
  --pill: 999px;
  --card-shadow: 0 4px 18px rgba(16, 26, 80, 0.10);
  --card-shadow-sm: 0 3px 14px rgba(16, 26, 80, 0.10);

  /* Motion */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --reveal-duration: 0.7s;
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* No min-width. The existing .dc.html designs lock the root at 1100px;
     that is the one thing these pages deliberately do differently. */
}

body {
  margin: 0;
  background: var(--white);
  color: var(--ink);
  font-family: 'Montserrat', 'Helvetica Neue', Arial, sans-serif;
  font-size: 15px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Headings are SemiBold across the site. Extrabold is the exception, not the
   default: only the page heroes and "ZO WIL JE BOUWEN" on Home use 800, and
   each of those says so on its own rule. Set here at element level rather
   than only on `:where(.section) h2`, so a heading that sits outside a
   .section (.home-benefits, .footer-contact, .article-body) does not silently
   fall back to the browser's bold 700. */
h1, h2, h3, h4, h5, h6 { font-weight: 600; }

/* height: auto keeps the width/height attributes (set for CLS-free loading)
   from distorting an image the moment max-width shrinks it; every component
   that wants a fixed crop still overrides this with its own height. */
img { max-width: 100%; height: auto; display: block; }

a { color: var(--orange-ink); text-decoration: none; }
a:hover { color: var(--orange-ink-hover); }

/* The focus ring runs on --orange-ink, not the brand orange: #fc663f reads
   2,94:1 on white, under the 3:1 WCAG 1.4.11 asks of a focus indicator, where
   --orange-ink reads 5,21. On navy the relationship inverts — 2,70 against
   4,79 — so every navy context takes the brand value back below. */
:focus-visible {
  outline: 3px solid var(--orange-ink);
  outline-offset: 3px;
  border-radius: 2px;
}
.hero :focus-visible,
.section--navy :focus-visible,
.footer-contact :focus-visible,
.site-header :focus-visible,
.site-footer :focus-visible { outline-color: var(--orange); }
/* ...except a form field, which is a white box wherever it stands: the footer
   contact form is on navy but its inputs are not. */
.footer-contact .form :focus-visible { outline-color: var(--orange-ink); }

/* Visible only to screen readers. */
.sr-only {
  position: absolute;
  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: 16px; top: -60px;
  z-index: 100;
  background: var(--orange-ink);
  color: var(--white);
  font-size: 14px;
  font-weight: 600;
  padding: 10px 20px;
  border-radius: var(--pill);
  transition: top 160ms var(--ease);
}
.skip-link:focus { top: 16px; color: var(--white); }

/* --------------------------------------------------------------------------
   3. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-block;
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  padding: 12px 28px;
  border-radius: var(--pill);
  border: 1.5px solid transparent;
  cursor: pointer;
  text-align: center;
  transition: background 200ms var(--ease), color 200ms var(--ease),
              border-color 200ms var(--ease);
}

.btn--primary { background: var(--orange-ink); color: var(--white); }
.btn--primary:hover { background: var(--orange-ink-hover); color: var(--white); }

/* Secondary buttons are 1.5px outlines — orange on light, white on navy. */
.btn--outline { border-color: var(--orange-ink); color: var(--orange-ink); background: none; }
.btn--outline:hover { background: var(--orange-ink); color: var(--white); }

.btn--outline-light { border-color: var(--white); color: var(--white); background: none; }
.btn--outline-light:hover { background: rgba(255, 255, 255, 0.12); color: var(--white); }

/* A .btn--outline standing on navy. The design system already has the right
   component for that context (.btn--outline-light), but the rail controls on
   the homepage reach for the light-background one, and orange text on navy
   only manages 3.97:1 — worse once the orange is darkened for legibility on
   white. Rather than correcting the class in the markup and waiting for the
   next person to make the same choice, the component adapts to where it
   stands. Same for the required-field asterisk in the navy footer form. */
.section--navy .btn--outline,
.hero .btn--outline,
.footer-contact .btn--outline {
  border-color: var(--white);
  color: var(--white);
}
/* Van lijn naar gevuld, precies zoals de ronde railpijl ernaast — dezelfde
   drie waarden als .section--navy .rail-nav__btn:hover verderop. De vormgever
   wees die overgang aan als het gedrag dat klopt en wilde hem ook op "Bekijk
   meer projecten" (feedback 30-07-2026); die stond hier op een licht wit
   waas, waardoor de twee knoppen naast elkaar iets anders deden.
   Navy op --orange is dezelfde combinatie als de pijl al gebruikt. */
.section--navy .btn--outline:hover,
.hero .btn--outline:hover,
.footer-contact .btn--outline:hover {
  background: var(--orange);
  border-color: var(--orange);
  color: var(--navy);
}

.btn--sm { font-size: 13px; padding: 9px 24px; }

/* A .btn--sm is 37px tall, which is the size in the design and is fine for a
   mouse. Below the nav breakpoint we are on a touch device, so the same
   button grows to 44px on padding alone — the pill keeps its proportions and
   nothing else on the page moves. .btn itself is already 45px. */
@media (max-width: 1000px) {
  .btn--sm { padding-top: 13px; padding-bottom: 13px; }
}

.btn[disabled],
.btn[aria-disabled="true"] {
  background: var(--muted);
  border-color: transparent;
  color: var(--white);
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Text link: a call to action that is not the end of the page ---------
   A solid pill is the site's "you have read enough, act now" signal, so a
   pill halfway down a page reads as a closing move and the copy under it
   stops being read. This is the same action at the weight of a sentence: it
   keeps its own line and its arrow, but it does not stop the eye. The arrow
   is present at rest, so hovering shifts nothing but the arrow.
   ---------------------------------------------------------------------- */

.text-link {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--orange-ink);
  border-bottom: 1.5px solid transparent;
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}
.text-link::after {
  content: "→";
  transition: transform 200ms var(--ease);
}
.text-link:hover {
  color: var(--orange-ink-hover);
  border-bottom-color: var(--orange-ink-hover);
}
.text-link:hover::after { transform: translateX(4px); }

/* Same context adaptation as .btn--outline above: --orange-ink is a
   white-background value and only reaches 2.70:1 on navy. */
.section--navy .text-link,
.hero .text-link,
.footer-contact .text-link { color: var(--white); }
.section--navy .text-link:hover,
.hero .text-link:hover,
.footer-contact .text-link:hover {
  color: var(--white);
  border-bottom-color: var(--white);
}

@media (prefers-reduced-motion: reduce) {
  .text-link:hover::after { transform: none; }
}

/* --------------------------------------------------------------------------
   4. Site header
   -------------------------------------------------------------------------- */

.site-header {
  position: relative;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 28px var(--pad);
}

/* The logo shrinks to 48px on a phone; the link keeps a 44px minimum box
   around it so the tap target never falls below the minimum. */
.site-header__logo { display: flex; align-items: center; min-height: 44px; }
.site-header__logo img { height: 75px; width: auto; }

.nav-toggle {
  display: none;
  min-height: 44px;
  align-items: center;
  gap: 10px;
  background: none;
  border: 1.5px solid rgba(255, 255, 255, 0.45);
  border-radius: var(--pill);
  color: var(--white);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 9px 18px 9px 14px;
  cursor: pointer;
}
.nav-toggle:hover { border-color: var(--white); }

.nav-toggle__bars {
  position: relative;
  width: 18px; height: 2px;
  background: currentColor;
  transition: background 160ms var(--ease);
}
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 18px; height: 2px;
  background: currentColor;
  transition: transform 200ms var(--ease);
}
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after { top: 6px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after { transform: translateY(-6px) rotate(-45deg); }

/* Header-level Contact pill (tot 17 aug 2026 de Offerte-pil; de losse
   menulink Contact is toen vervallen en deze knop is die route geworden).
   Hidden on desktop (the pill is the last nav item there) and below 640px,
   where the header row cannot hold the 213px logo, the pill and the menu
   button at their approved sizes — the in-menu pill takes over on those
   widths. */
.nav-cta-header { display: none; }

.site-nav__list {
  display: flex;
  align-items: center;
  gap: 28px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav__link,
.site-nav__parent {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-on-navy-bright);
  cursor: pointer;
  transition: color 160ms var(--ease);
}
.site-nav__link:hover,
.site-nav__parent:hover { color: var(--white); }

.site-nav__link[aria-current="page"] { color: var(--white); font-weight: 600; }

.site-nav__chevron {
  width: 9px; height: 9px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 200ms var(--ease);
}

/* Oplossingen submenu — desktop hover/focus panel.
   The parent is a <button>: Oplossingen has no page and no URL. */
.site-nav__sub {
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav__sub a {
  display: block;
  color: var(--ink-on-navy-bright);
  font-size: 14px;
  transition: color 160ms var(--ease), background 160ms var(--ease);
}

.site-nav__sub a[aria-current="page"] { color: var(--white); font-weight: 600; }

/* De ouder licht mee op zodra een van zijn kinderen de huidige pagina is.
   Dat kan niet met aria-current: de ouder is een <button> zonder URL. Vandaar
   :has() — zo hoeft de gedeelde header geen aparte markering per pagina te
   dragen en klopt de staat vanzelf, ook in de WordPress-build. */
.site-nav__item--has-sub:has(a[aria-current="page"]) > .site-nav__parent {
  color: var(--white);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   5. Hero (shared by all four pages)
   -------------------------------------------------------------------------- */

.hero {
  background: var(--gradient-navy);
  color: var(--white);
  position: relative;
  /* Deliberately not `overflow: hidden` — the Producten dropdown panel is
     positioned inside the header, which sits inside the hero. */
}

.hero__inner {
  padding: 44px var(--pad) var(--section-tight);
}

/* Every page hero in the design is [heading | image] at a slightly different
   ratio (1fr 1.1fr on Home, 1.2fr 1fr on De Modulonwand, 1.3fr 1fr on
   Projecten, 1.4fr 1fr on Nieuws, 1fr 1fr on Over Modulon). That is one
   component with one variable, not six classes — set --split on the page. */
.hero__inner--split {
  display: grid;
  grid-template-columns: var(--split, 1.25fr 1fr);
  gap: 48px;
  align-items: var(--split-align, end);
}

/* Extrabold — one of the two exceptions to the SemiBold heading default.
   The same goes for the two heroes that are not .hero: .detail-hero h1 and
   .article-hero h1 below. */
.hero h1 {
  font-size: var(--fs-h1-xs);
  line-height: 1.05;
  font-weight: 800;
  letter-spacing: var(--track-hero);
  margin: 0;
  color: var(--white);
  text-wrap: balance;
}

.hero__lead {
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink-on-navy);
  margin: 0;
  max-width: 460px;
  text-wrap: pretty;
}

/* Een fotohero van één kolom met de zin direct onder de kop (De Modulonwand).
   De andere leads staan in een eigen kolom van .hero__inner--split of in
   .centered-hero, en die zetten zelf hun marge; alleen deze staat als
   volgende regel onder de h1. Dezelfde 24px als .centered-hero. */
.hero h1 + .hero__lead { margin-top: 24px; }

/* The other three h1 sizes the designs use. Modifiers on the same element, so
   there is one hero component rather than five. There is no .h1--lg: the one
   page at that size is Project Detail, whose title sits on a photo rather than
   in a .hero, so it reads --fs-h1-lg through .detail-hero h1 instead. */
.hero h1.h1--display { font-size: var(--fs-display); line-height: 0.95; letter-spacing: var(--track-display); }
.hero h1.h1--md      { font-size: var(--fs-h1-md); }
.hero h1.h1--sm      { font-size: var(--fs-h1-sm); }

/* The homepage hero lets the render bleed off the right edge. */
.hero__inner--bleed-end { padding-right: 0; }
@media (max-width: 768px) {
  .hero__inner--bleed-end { padding-right: var(--pad); }
}

/* A contained render, never cropped (the design uses fit="contain"). */
.hero-render {
  width: 100%;
  height: var(--render-h, clamp(240px, 39vw, 560px));
  object-fit: contain;
  object-position: center;
}

/* De homepage-hero draagt sinds 11 aug 2026 een video — het platte pakket
   dat zich uitvouwt tot zolderwand — in plaats van de statische render.
   Een mp4 heeft geen alfakanaal: het pakket staat op een navy achtergrond
   met een felblauwe gloed. site.js §10b tekent de video daarom naar een
   WebGL-canvas en keyt dat vlakke navy eruit — echte transparantie, exacte
   kleuren, en de gloed blijft staan als licht over het verloop (17 aug
   2026; daarvoor werd hij mee weggekeyd). De canvas draagt dezelfde classes
   en erft dus de maatvoering van .hero-render en modern.css; de regels
   hieronder staan bewust op het video-élement en raken de canvas niet.

   Dit blok is alleen nog de fallback zonder WebGL of zonder JS: `lighten`
   laat elke pixel die donkerder is dan het verloop eronder erin verdwijnen
   (de clip-gloed blijft dan zichtbaar staan, net als op de canvas — de
   twee liggen daarmee dichter bij elkaar dan voorheen). Wegmaskeren van de
   randen kan niet — de wand raakt in sommige frames bijna de rand van het
   beeld. Let op: geen stacking context tussen deze video en .hero, anders
   blendt hij tegen transparant en staat het vierkante kader er weer — zie
   modern.css §2 en §9. Safari kent geen blend op een <video>-element en
   toont in deze fallback het kader; met WebGL (overal aanwezig sinds
   jaren) komt het zover niet. */
video.hero-render--video {
  mix-blend-mode: lighten;
  /* Een fractie donkerder: de filmkorrel in de clip-achtergrond piekt net
     boven het donkerste deel van het verloop uit en zou als vierkante waas
     blijven staan. 0.92 legt de korrel eronder; op de wand zelf is het
     verschil onzichtbaar. */
  filter: brightness(0.92);
  /* Restje: waar de clip-achtergrond nét lichter is dan het verloop laat
     lighten een flauwe rechthoekrand staan. Een smalle fade over de
     buitenste 6% haalt die weg; de wand komt in zijn uiterste frames tot
     ~94% en blijft er dus buiten. */
  mask-image:
    linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%),
    linear-gradient(0deg,  transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-composite: intersect;
}

/* site.js §10b zet deze class op de video zodra de keying-canvas staat:
   uit beeld, maar wel doorspelend als bron. Geen display: none — dan stopt
   het decoderen. De blend-regels hierboven blijven erop staan; met opacity
   0 doen ze er niet toe. */
video.hero-render--source {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* De logo-animatie stond hier in de Over Modulon-hero en staat sinds
   30 juli 2026 naast het verhaal; de regels ervoor zitten nu bij
   .about-photo--anim in sectie 20. */

/* The 22px/1.45 lead from the homepage hero. */
.hero__lead--lg {
  font-size: var(--fs-lead);
  line-height: 1.45;
  color: var(--ink-on-navy-bright);
  max-width: 340px;
}

/* --- A hero photo across the full width ----------------------------------
   Vormgeversfeedback 30 juli 2026: elke hero op de homepage na draagt een
   foto over de volle breedte in plaats van een ingesloten render in de
   rechterkolom.

   De foto is niet gewoon doorschijnend gezet, en dat is de kern van deze
   regel. Op dezelfde 35% als de Project Detail-hero werkt het daar wel — die
   projectfoto's zijn donker — maar op een lichte foto (een afgewerkte zolder,
   een grijze lucht) trekt hij het marineblauw naar lichtblauw en zakt de
   witte kop naar 2,1:1. Gemeten, niet geschat.

   `multiply` maakt dat onmogelijk: het resultaat van vermenigvuldigen is
   nooit lichter dan de laag eronder, dus de kop staat altijd op minstens
   hetzelfde marineblauw als in het goedgekeurde ontwerp — 16,99:1 gemeten op
   het donkerste punt, en welke foto de klant ook uploadt kan dat alleen maar
   donkerder worden. Precies de bedoeling van de editability-afspraak in
   CLAUDE.md: een wissel door de klant mag het ontwerp niet kunnen breken.
   Bijkomend voordeel is dat de foto in de merkkleur getint raakt in plaats
   van als losse afbeelding achter de kop te hangen.

   Geen `isolation: isolate` op .hero: dat maakt er een stacking context van
   en dan is de z-index 30 van de vaste header niet meer paginabreed. De
   backdrop van de foto is de dekkende --gradient-navy van .hero zelf, dus de
   blend heeft die afscherming niet nodig.
   ---------------------------------------------------------------------- */

.hero__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.9;
  mix-blend-mode: multiply;
}

/* Browsers zonder mix-blend-mode (geen enkele actuele, maar het is één regel)
   krijgen de foto zo licht dat de kop er in het slechtste geval nog op kan
   staan. Zonder deze fallback zou daar een dekkende foto onder de kop komen. */
@supports not (mix-blend-mode: multiply) {
  .hero__photo { opacity: 0.18; }
}

/* De foto staat vóór .hero__inner in de DOM en is absoluut gepositioneerd;
   deze regel is het enige dat de kop erboven houdt. De header hoeft niets:
   die heeft in sectie 4 al position: relative met z-index 30. */
.hero--photo .hero__inner { position: relative; }

/* Zonder render is de rechterkolom leeg en bepaalt alleen de kop nog de
   hoogte van de band — te weinig voor een foto om nog als foto te lezen.
   Deze ondergrens houdt de hero op de hoogte die hij mét de render had.
   404 en bedankt zijn uitgezonderd: die hero's zijn uit zichzelf al hoger
   (het 404-cijfer, het vinkje) en zouden alleen lege ruimte krijgen. */
.hero--photo:not(.centered-hero) .hero__inner {
  min-height: var(--hero-photo-h, clamp(300px, 26vw, 420px));
}

/* --------------------------------------------------------------------------
   6. Generic sections
   -------------------------------------------------------------------------- */

.section { padding: var(--section-tight) var(--pad) var(--section); }
.section--surface { background: var(--surface); }
.section--white { background: var(--white); }

/* Een sectie waarvan het beeld zijn eigen marge meebrengt.

   De uitschuif-animatie in het promoblok op Nieuws staat op --surface: de lege
   rand binnen het videokader is dezelfde kleur als de band eromheen en dus niet
   van sectiepadding te onderscheiden. Die twee bij elkaar optellen geeft precies
   het probleem dat op 21 augustus 2026 werd aangewezen — "er kan boven en onder
   nog flink wat witruimte vanaf". Gemeten op 1440 stond er 217px blauw boven de
   kop en 233px eronder, tegen de 100/116 die de rest van de site aanhoudt.

   Het antwoord is niet het beeld bijsnijden — dat kostte de nok van het dak —
   maar de padding aan het beeld overlaten. De mediakolom is hoger dan de
   tekstkolom, dus de tekst staat gecentreerd al met ruimte boven en onder zich:
   op 1440 117px, op 1200 88px. Dat ís het ritme, alleen gedragen door het beeld
   in plaats van door de sectie. Vandaar nul.

   Boven is dat nul en onder niet, en dat is geen slordigheid maar het beeld: de
   nok van het dak staat in rust op 18% van de bovenrand, terwijl het platte
   pakket aan begin en einde van de lus de ónderrand van het beeld raakt. Boven
   is er dus altijd marge, onder tijdelijk geen. Met nul aan beide kanten liep de
   vloerplaat in het beginframe tegen de navy footerband aan (aangewezen op
   21 augustus 2026). De 32px hieronder is wat de clip zelf op zijn krapste
   moment niet levert; in de rusttoestand komt er zijn eigen 41px bovenop.

   Alleen boven nul zodra het blok stapelt (<=1024): daar staat het beeld bovenaan
   en de knop onderaan, dus alleen de bovenkant valt samen met de marge van de
   clip en telt onder gewoon --section. */
.section--hug-media { padding-block: 0 32px; }

@media (max-width: 1024px) {
  .section--hug-media { padding-block: 0 var(--section); }
}

/* :where() drops these to element-level specificity on purpose. They are
   section-wide defaults, and a component that sits inside a section — a white
   .tile on a navy band, a .card, a .contact-card — must always win over them
   with its own single class. Without this a white card inside .section--navy
   inherits white text and disappears. */
:where(.section) h2 {
  font-size: var(--fs-h2);
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--blue);
  margin: 0 0 22px;
  text-wrap: balance;
}

/* The four other h2 sizes the designs use, as modifiers on the same element
   rather than as separate components. A single class is enough to beat the
   :where() default above, so these work inside a .section or outside one. */
.h2--xl { font-size: var(--fs-h2-xl); line-height: 1.12; letter-spacing: var(--track-hero); }
.h2--lg { font-size: var(--fs-h2-lg); }
.h2--sm { font-size: var(--fs-h2-sm); letter-spacing: var(--track-hero); }
.h2--xs { font-size: var(--fs-h2-xs); letter-spacing: 0.5px; }

:where(.section) p { color: var(--ink); text-wrap: pretty; }

.blue-strip {
  background: var(--blue-strip);
  color: var(--white);
  text-align: center;
  padding: 16px 24px;
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

/* --------------------------------------------------------------------------
   7. Site footer
   -------------------------------------------------------------------------- */

.footer-contact {
  background: var(--gradient-navy-soft);
  padding: var(--section) var(--pad);
  color: var(--ink-on-navy);
}

.footer-contact__grid {
  display: grid;
  grid-template-columns: 1.1fr 1fr 0.8fr;
  gap: 64px;
  align-items: start;
}

.footer-contact h2 {
  font-size: var(--fs-h2-42);
  line-height: 1.2;
  margin: 0 0 24px;
  color: var(--white);
  text-wrap: pretty;
}

.footer-contact__intro p { font-size: 15px; color: var(--ink-on-navy); margin: 0; }

.footer-contact__details {
  font-size: 13px;
  line-height: 1.7;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.footer-contact__details strong { color: var(--white); }

/* Underlined, and that is not decoration. These links are --ink-on-navy, which
   is the exact colour of the address text sitting directly above them: at rest
   "info@modulon.nl" and "Kolkweg 6" were pixel-for-pixel the same kind of
   text, and the only thing that ever distinguished them was a hover colour —
   which tells a phone user nothing at all. The whole column is short,
   self-contained lines rather than running prose, so an underline here costs
   nothing in readability and is the one cue that survives on touch.
   (WCAG 1.4.1: colour alone must not carry meaning — here not even colour
   was carrying it.) */
.footer-contact__details a {
  color: var(--ink-on-navy);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.footer-contact__details a:hover { color: var(--white); }

/* The two social marks live inside .footer-contact__details too, and they are
   icon tiles, not text — an underline under a 26px square reads as a stray
   rule. They keep their existing shape-based affordance. */
.footer-social a { text-decoration: none; }

/* 13px text on a 1.7 line-height is a 22px row. WCAG 2.2 puts the floor for a
   target at 24px, and these three only cleared it on the spacing exception —
   22px of link plus the 2px gap landed on exactly 24px centre-to-centre, with
   nothing to spare. 2px of padding makes each row a 26px target in its own
   right, which costs 6px of stack height in total. On touch they grow to 44px;
   see the pointer query below.
   align-items keeps each row as wide as its own text. Stretched to the column
   they would be 335px-wide invisible hit strips, and a tap on blank footer
   space would open the Disclaimer. */
.footer-contact__legal {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.footer-contact__legal a { padding: 2px 0; }

/* E-mail and phone number, in the footer and on the contact card. They used to
   be separated by a <br>, which stacks the two boxes edge to edge with nothing
   between them — and these are two different actions: one opens a mail client,
   the other dials. A shared boundary line means a thumb aimed at the bottom of
   the address places a call. A column with a real gap, and the same
   shrink-to-fit reasoning as the legal links above. */
.footer-contact__actions,
.contact-card__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.footer-social { display: flex; gap: 14px; }
.footer-social a {
  position: relative;
  width: 26px; height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 160ms var(--ease), color 160ms var(--ease),
              border-color 160ms var(--ease);
}
/* The mark used to be the letters "in" typed out — a stand-in that read as a
   missing asset. It is the real brand path now, filled with currentColor so the
   hover rule below keeps working untouched. Only the letters: the rounded
   square the LinkedIn logo knocks them out of is drawn as this link's border,
   so the mark is the brand mark in outline rather than a filled tile. */
.footer-social svg { display: block; width: 14px; height: 14px; }
/* Qualified with .footer-social on purpose. These marks sit inside
   .footer-contact__details, whose `a` rule is (0,1,1) and was quietly beating
   the (0,1,0) modifier below — so the LinkedIn "in" rendered in --ink-on-navy
   at 1.5:1, near enough to invisible, and the hover lost its colour the same
   way. Matching the parent's specificity is what makes it win; drop the
   qualifier and the bug comes straight back. Both marks are named rather than
   folded into a shared `.footer-social a`: that selector is (0,1,1), a tie
   with `.footer-contact__details a` that it wins only by sitting later in this
   file, where the class-qualified form is (0,2,1) and wins outright. */
.footer-social a.footer-social__linkedin,
.footer-social a.footer-social__instagram {
  border: 2px solid var(--white);
  color: var(--white);
}
/* The only thing that differs between the two marks, and it is the whole
   point of drawing them this way: the border IS the shape the brand knocks
   its letters out of, so it follows the brand. LinkedIn and Instagram both
   knock their mark out of a rounded square, so both borders are one: the
   svg carries only the "in" and the Instagram lens plus dot. The Instagram
   square is rounder in the brand than 6px, but 6px is the site's ceiling for
   a rounded corner and the two tiles side by side read as a pair.
   (Instagram replaced a Facebook stub with a 50% circle, 25 september 2026.) */
.footer-social a.footer-social__linkedin,
.footer-social a.footer-social__instagram { border-radius: 6px; }
/* Keeps the brand orange: against navy it reads 4.79:1, where --orange-ink
   would fall to 2.70. This is the one place where the darker orange is the
   worse choice. */
.footer-social a.footer-social__linkedin:hover,
.footer-social a.footer-social__instagram:hover {
  border-color: var(--orange);
  color: var(--orange);
}

/* The social mark is a 26px square in the design and stays that size. This
   invisible pad makes the *hit* area 44x44 without touching the look — the
   alternative would be an oversized box that no longer matches the design.
   With a second mark beside it (Facebook on 2 september 2026, Instagram
   since 25 september) the two pads do overlap:
   9px of overhang on each side of a 14px gap is 18px into 14. The earlier
   note here claimed they would not, by comparing the gap to one overhang
   instead of both — it was wrong, and the overlap is 4px. It is left as is
   because that 4px sits in the blank gutter between the two icons, where
   nobody is aiming at either: the boundary lands at 31px instead of the 33px
   midpoint, a 2px bias towards the second icon. Shrinking the pad to clear it would
   cost the 44px width for nothing. */
.footer-social a::after {
  content: "";
  position: absolute;
  inset: -9px;
}

/* The same links on a mouse, where 44px would be wasted height. 13px on a 1.7
   line-height is a 16px box and they sat 22px apart — under 24px in both
   directions, so they missed the WCAG 2.2 minimum on the spacing exception as
   well as on their own size. They are not running text either: each is alone
   on its line, so the inline exception does not cover them. 4px top and bottom
   is the smallest thing that clears it, and it costs 8px in one footer
   column. */
.footer-contact__details a[href^="mailto:"],
.footer-contact__details a[href^="tel:"],
.receipt dd a {
  display: inline-block;
  padding: 4px 0;
}

/* Phone number and e-mail address are actions, not running text, so they get
   a real target on touch. So do the three legal links: they were the one place
   in this prototype still sitting under 44px, left there on the argument that
   three tall rows would triple the height of that stack. They do — 82px
   becomes 136px — and it buys the two links a visitor is most likely to want
   on a phone, in the one part of the page where nothing else competes for the
   space. The height is not the scarce thing at the bottom of a footer.

   The query is deliberately two conditions, not one. Width alone was wrong:
   an iPad in landscape is 1024px wide, has no mouse, and fell straight through
   `max-width: 1000px` — every link in this block stayed at ~19px on exactly
   the device that needs 44 most. `pointer: coarse` asks what is actually
   pointing at the screen. The width half stays for the fine-pointer case (a
   narrow desktop window), so nothing that already cleared 44px loses it. */
@media (max-width: 1000px), (pointer: coarse) {
  .footer-contact__details a[href^="mailto:"],
  .footer-contact__details a[href^="tel:"],
  /* Standalone action links elsewhere in the prototype, same reasoning:
     they sit on their own line, so they are targets rather than running
     text and need the same 44px on touch. */
  .contact-card__route,
  .article-foot__back,
  /* The contact card carries the site's real bel- and mailknop, and it sits
     outside .footer-contact__details, so the rule above never reached it —
     these measured 131x19 on a phone. They are the most important targets on
     the page and were the smallest. */
  .contact-card__value a[href^="mailto:"],
  .contact-card__value a[href^="tel:"],
  .footer-contact__legal a,
  .receipt dd a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* The 2px was tuned to a 22px row. At 44px it is a hairline between two
     targets; 6px is the smallest gap that reads as a separation without
     turning the stack into a list of buttons. */
  .footer-contact__legal { gap: 6px; }
  .footer-contact__legal a { padding: 0; }
  .footer-contact__actions,
  .contact-card__actions { gap: 6px; }
}

/* Contact-page variant: no form (see README — the footer form is suppressed
   on contact.html so the page never shows two forms). Two columns instead of
   three, and the details column carries a bit more weight. */
.footer-contact--nofrom .footer-contact__grid {
  grid-template-columns: 1.2fr 1fr;
  align-items: center;
}

.site-footer {
  background: var(--blue-strip);
  text-align: center;
  padding: 36px 24px;
}
.site-footer img {
  height: 36px;
  width: auto;
  display: inline-block;
  margin: 0 auto 10px;
}
.site-footer small { font-size: 12px; color: var(--ink-on-strip); display: block; }

/* --------------------------------------------------------------------------
   8. Forms
   -------------------------------------------------------------------------- */

.form { display: flex; flex-direction: column; gap: 12px; }

/* Staat onder de verstuurknop, niet erboven (feedback vormgever 30-07-2026).
   Boven het formulier duwde de melding de eerste labelregel weg en las je een
   voorwaarde over velden die je nog niet gezien had. De 12px gap van .form
   zet hem nu op afstand van de knop, dus hier geen eigen marge meer. */
.form__legend-note {
  font-size: 13px;
  color: var(--muted);
  margin: 0;
}

.field { display: flex; flex-direction: column; gap: 6px; }

/* Dezelfde valkuil als .form__group verderop: een author-`display` wint van
   de UA-regel `[hidden] { display: none }`, hoe laag de specificiteit ook is.
   Zonder deze regel blijft een veld dat niet bij de gekozen reden hoort
   gewoon staan als form.js het verbergt. */
.field[hidden] { display: none; }

.field__label {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink-strong);
}

.field__req { color: var(--orange-ink); }

.field__hint { font-size: 12px; line-height: 1.5; color: var(--muted); }

/* The character counter escalates for sighted visitors on the same two steps
   the live region speaks on: close to the limit, and cut short by it. Weight
   carries the first step and colour the second, so the counter never turns red
   for a message that is merely getting long — red means something went wrong,
   and at 1.350 characters nothing has. */
.field__hint[data-counter-state="near"] { color: var(--ink-strong); font-weight: 600; }
.field__hint[data-counter-state="over"] { color: var(--error); font-weight: 600; }

.field__control,
.form input[type="text"],
.form input[type="email"],
.form input[type="tel"],
.form input[type="number"],
.form select,
.form textarea {
  width: 100%;
  font-family: inherit;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-strong);
  background: var(--white);
  border: 1.5px solid var(--line);
  border-radius: 3px;
  padding: 12px 14px;
  transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}

.form textarea { resize: vertical; min-height: 140px; }

/* Uploadveld (tekeningen bij de offerteaanvraag). Het veld zelf krijgt géén
   invoerkader: de bladerknop die de browser er zelf in zet is al een kader,
   en twee randen om elkaar heen leest als een fout. Alleen de knop wordt
   aangekleed, in dezelfde maat als de andere velden. `::file-selector-button`
   wordt door alle huidige browsers gelezen; valt hij weg, dan blijft de
   standaardknop staan en werkt het veld gewoon. */
.form input[type="file"] {
  width: 100%;
  font-family: inherit;
  font-size: 14px;
  color: var(--ink-strong);
}
.form input[type="file"]::file-selector-button {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--blue);
  background: var(--surface);
  border: 1.5px solid var(--line);
  border-radius: 3px;
  padding: 10px 16px;
  margin-right: 14px;
  cursor: pointer;
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.form input[type="file"]::file-selector-button:hover {
  border-color: var(--blue);
  background: var(--surface-blue);
}

.form select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--blue) 50%),
    linear-gradient(135deg, var(--blue) 50%, transparent 50%);
  background-position: right 20px center, right 14px center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 40px;
  cursor: pointer;
}

.field__control::placeholder,
.form input::placeholder,
.form textarea::placeholder { color: var(--muted); }

.field__control:hover,
.form input:hover,
.form select:hover,
.form textarea:hover { border-color: var(--blue); }

/* The focus state is the field's OWN border going orange and thickening, not a
   ring drawn beside it. The global :focus-visible rule (§3) paints a 3px
   outline at 3px offset, which on an input reads as a second frame around a
   box that already has one — the vormgever flagged exactly that on the
   Projecten search field (30 juli 2026). `outline: none` here is safe because
   the indicator is not being removed, it is moving onto the border: 1.5 → 2px
   plus the glow. Note the selector list carries `.field__control` as well as
   the typed `.form input` variants — the Projecten search box is a standalone
   .field__control with no surrounding .form, which is why it was the one field
   on the site still falling through to the global outline. */
.field__control:focus,
.form input:focus,
.form select:focus,
.form textarea:focus {
  outline: none;
  border-width: 2px;
  border-color: var(--orange-ink);
  /* Mixed from the token rather than written out as rgba(206, 47, 3, .18).
     The error red is still awaiting client sign-off (README, "Openstaande
     vragen"), and a hand-expanded glow is exactly the copy that gets missed
     when one of them changes. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--orange-ink) 18%, transparent);
}

/* Error state. Written per element type so it outranks the typed selectors
   above (`.form input[type="text"]`) — an [aria-invalid] rule alone loses the
   border colour on exactly the fields that need it most. */
.form input[aria-invalid="true"],
.form select[aria-invalid="true"],
.form textarea[aria-invalid="true"] {
  border-color: var(--error);
  background: var(--error-field);
}
.form input[aria-invalid="true"]:focus,
.form select[aria-invalid="true"]:focus,
.form textarea[aria-invalid="true"]:focus {
  border-color: var(--error);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--error) 18%, transparent);
}

.field__error {
  display: none;
  align-items: flex-start;
  gap: 6px;
  font-size: 13px;
  line-height: 1.45;
  font-weight: 500;
  color: var(--error);
}
/* The alt-text half of `content` is what keeps this glyph out of the spoken
   message. The error text is announced through aria-describedby, and generated
   content inside the element it points at is part of that string — without the
   `/ ""` a screen reader prefixes every message with "uitroepteken". */
.field__error::before { content: "!" / ""; font-weight: 700; }
.field.has-error .field__error { display: flex; }

.form__summary {
  display: none;
  background: var(--error-surface);
  /* Bordered on all four sides at the same weight and radius as an input,
     not accented with a thick left stripe. The stripe reads as decoration
     applied to a box; a full border reads as the box itself being the alert. */
  border: 1.5px solid var(--error);
  border-radius: 3px;
  padding: 16px 20px;
  margin: 0 0 8px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--error-ink);
}
.form__summary.is-visible { display: block; }
.form__summary ul { margin: 8px 0 0; padding-left: 18px; }
.form__summary a { color: var(--error-ink); text-decoration: underline; }

/* Two fields side by side where it reads better. */
.field-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
}

/* Valt één van de twee weg bij de gekozen reden — e-mail verdwijnt bij een
   terugbelverzoek — dan is het geen rij meer. Zonder deze regel blijft het
   overgebleven veld op de linkerhelft staan met een gat ernaast. */
.field-row:has(> .field[hidden]) { grid-template-columns: minmax(0, 1fr); }

/* Submitting state — the button keeps its width so nothing jumps. */
.btn--submit { align-self: flex-start; min-width: 240px; margin-top: 8px; }
.btn--submit .btn__spinner { display: none; }
.form.is-submitting .btn--submit .btn__label { display: none; }
.form.is-submitting .btn--submit .btn__spinner { display: inline-block; }

/* Copy that depends on the submitted form — the confirmation text on
   bedankt.html, arriving as ?onderwerp=offerte from the offerte form or
   ?onderwerp=terugbellen from the terugbelstrook under it. Switched in CSS
   rather than JS so it is already correct on first paint. Written as
   :not() pairs so that without the attribute (JavaScript off, so the
   inline script in <head> never ran) the algemeen variant shows and the
   others stay hidden. */
html[data-onderwerp="offerte"] [data-when="algemeen"] { display: none; }
html[data-onderwerp="terugbellen"] [data-when="algemeen"] { display: none; }
html:not([data-onderwerp="offerte"]) [data-when="offerte"] { display: none; }
html:not([data-onderwerp="terugbellen"]) [data-when="terugbellen"] { display: none; }

/* --------------------------------------------------------------------------
   9. Scroll reveal
   Single IntersectionObserver in site.js. The `js` class is set by an inline
   script in <head>, so nothing is hidden when JavaScript is unavailable.
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity var(--reveal-duration) var(--ease),
              transform var(--reveal-duration) var(--ease);
}
.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* --------------------------------------------------------------------------
   9b. Tile extend
   The three promo tiles on Home ([data-extend]) perform the product's own
   move: each tile rises and is revealed from its base upward — the way a
   Modulonwand is extended up to the kap — then stops dead, with a 2px
   orange rule drawn along the top edge at the moment it seats: the click.
   The rule then fades out, so at rest the page is pixel-identical to the
   approved design and the animation itself needs no separate sign-off.

   Driven by initExtend() in site.js. The hidden start state (.is-primed)
   is only ever added by that script, and only when the animation will
   actually run — without JavaScript, or under reduced motion, the tiles
   are simply visible.

   The easing is deliberately not --ease. The site curve is a soft
   deceleration; this move has to hold its speed and then stop — the
   contact is the message. Expo-out, scoped here and used nowhere else.
   -------------------------------------------------------------------------- */

.js [data-extend] { position: relative; }

/* Primed = waiting for the observer. Opacity, deliberately not clip-path:
   Chrome reports intersectionRatio 0 for a fully clipped element, so an
   observer waiting for 30% of a clipped tile would never fire. Opacity
   leaves the geometry intact — the same fact the [data-reveal] system
   relies on. The clipped start state lives in the keyframes' from-frame,
   which `backwards` fill shows from the moment .is-extended lands. */
.js [data-extend].is-primed {
  opacity: 0;
}

.js [data-extend].is-extended {
  animation: tile-extend 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}

@keyframes tile-extend {
  from { clip-path: inset(100% 0 0 0); transform: translateY(18px); }
  to   { clip-path: inset(0 0 0 0);    transform: none; }
}

/* The click. Graphic orange — this is exactly the role --orange is for.
   scaleX(0) at rest on both ends of the animation, so it leaves no trace. */
.js [data-extend]::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--orange);
  transform: scaleX(0);
}
.js [data-extend].is-extended::after {
  /* Starts 450ms into that tile's extend, when the top edge is seating.
     1050ms total: drawn by 14% (150ms), held to 52% (+400ms), faded by 100%. */
  animation: tile-click 1.05s ease-out backwards;
  animation-delay: calc(var(--i, 0) * 90ms + 0.45s);
}

@keyframes tile-click {
  0%   { transform: scaleX(0); opacity: 1; }
  14%  { transform: scaleX(1); opacity: 1; }
  52%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js [data-reveal] { opacity: 1; transform: none; transition: none; }
  /* initExtend() never primes under reduced motion; this covers the
     preference flipping on mid-session, after a tile was already primed. */
  .js [data-extend].is-primed { opacity: 1; }
  * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
}

/* --------------------------------------------------------------------------
   10. Stub links
   Pages that already exist as .dc.html designs but are not part of this
   four-page prototype. Rather than shipping dead 404s into a client review,
   they explain themselves. Replace with real URLs in the WordPress build.
   -------------------------------------------------------------------------- */

.stub-toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  z-index: 200;
  transform: translate(-50%, 16px);
  max-width: min(460px, calc(100vw - 32px));
  background: var(--navy-deep);
  color: var(--white);
  font-size: 13px;
  line-height: 1.55;
  padding: 14px 20px;
  border-radius: 6px;
  box-shadow: 0 10px 30px rgba(10, 16, 60, 0.35);
  opacity: 0;
  visibility: hidden;
  transition: opacity 220ms var(--ease), transform 220ms var(--ease), visibility 220ms;
}
.stub-toast.is-visible { opacity: 1; visibility: visible; transform: translate(-50%, 0); }

/* ==========================================================================
   11. Responsive

   ONE breakpoint set, used by every page in this folder and retro-fitted into
   the three legal .dc.html pages. Each value has exactly one job:

     1200px  page padding steps 64 → 40
     1024px  tablet landscape — three columns become two, side-by-side
             content blocks stay side by side
     1000px  navigation only: the six nav items plus the logo stop fitting on
             one line, so the header switches to the hamburger. Kept separate
             from 1024 on purpose — at 1024 the desktop nav still fits
             (290px logo + 599px nav inside a 944px content width, with the
             item gap tightened to 20px between 1001 and 1140px), and an
             iPad in landscape should not get a mobile menu.
     768px   tablet portrait — two-column content blocks become one
     640px   card grids go from two columns to one
     480px   small phone — padding steps 40 → 20, inputs go to 16px

   Display type is NOT stepped here; it is a clamp() in :root, so headings
   scale continuously between these breakpoints rather than jumping at them.
   Verified at 375 / 768 / 1024 / 1440px.
   ========================================================================== */

@media (max-width: 1200px) {
  :root { --pad-min: 40px; }
  .site-nav__list { gap: 22px; }
  .footer-contact__grid { gap: 40px; }
}

@media (max-width: 1024px) {
  :root { --section: 76px; --section-tight: 68px; }
  .footer-contact__grid { grid-template-columns: 1fr 1fr; gap: 40px 48px; }
  .footer-contact__intro { grid-column: 1 / -1; }
  .footer-contact--nofrom .footer-contact__grid { grid-template-columns: 1fr 1fr; }
}

/* --- Mobile navigation ------------------------------------------------- */

@media (min-width: 1001px) {
  /* Desktop: hover / keyboard-focus panel. */
  .site-nav__item--has-sub { position: relative; }

  .site-nav__sub {
    position: absolute;
    top: calc(100% + 14px);
    left: -20px;
    min-width: 220px;
    background: var(--navy-deep);
    border-radius: 6px;
    padding: 10px;
    box-shadow: 0 14px 34px rgba(8, 14, 50, 0.4);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity 180ms var(--ease), transform 180ms var(--ease), visibility 180ms;
  }
  /* Bridges the gap between the parent and the panel so the pointer can
     travel into the submenu without it closing. */
  .site-nav__sub::before {
    content: "";
    position: absolute;
    left: 0; right: 0; top: -14px;
    height: 14px;
  }
  .site-nav__sub a { padding: 9px 14px; border-radius: 4px; }
  .site-nav__sub a:hover { background: rgba(255, 255, 255, 0.1); color: var(--white); }

  .site-nav__item--has-sub:hover .site-nav__sub,
  .site-nav__item--has-sub:focus-within .site-nav__sub,
  .site-nav__item--has-sub.is-open .site-nav__sub {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  .site-nav__item--has-sub:hover .site-nav__chevron,
  .site-nav__item--has-sub:focus-within .site-nav__chevron,
  .site-nav__item--has-sub.is-open .site-nav__chevron {
    transform: translateY(1px) rotate(-135deg);
  }
}

/* The 75px logo is 290px wide, which leaves the desktop nav 9px short of
   fitting on one line at 1024px. Tightening the item gap in this narrow band
   buys it back, so an iPad in landscape keeps the desktop nav (see 1000px). */
@media (min-width: 1001px) and (max-width: 1140px) {
  .site-nav__list { gap: 20px; }
  .site-nav__link, .site-nav__parent { white-space: nowrap; }
}

@media (max-width: 1000px) {
  .nav-toggle { display: inline-flex; }

  .site-header { padding: 20px var(--pad); flex-wrap: wrap; }
  .site-header__logo img { height: 55px; }

  /* `visibility` is doing the accessibility work here, not the decoration.
     max-height: 0 + overflow: hidden hides the panel from the eye but leaves
     all seven controls in the tab order, so a keyboard visitor tabs off the
     hamburger straight into an invisible menu. visibility: hidden is what
     actually takes them out of it — the same thing the desktop dropdown
     already does. It animates discretely, so it flips to visible at the start
     of the opening transition and only back to hidden once the closing one has
     finished; the panel is never clipped mid-slide. */
  .site-nav {
    flex: 0 0 100%;
    max-height: 0;
    overflow: hidden;
    visibility: hidden;
    transition: max-height 300ms var(--ease), visibility 300ms var(--ease);
  }
  .site-nav.is-open { max-height: 640px; visibility: visible; }

  .site-nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 12px 0 4px;
  }
  .site-nav__list > li { border-bottom: 1px solid rgba(255, 255, 255, 0.16); }
  .site-nav__list > li:last-child { border-bottom: 0; padding: 18px 0 6px; }

  .site-nav__link,
  .site-nav__parent {
    width: 100%;
    justify-content: space-between;
    font-size: 16px;
    padding: 15px 0;
  }

  /* Mobile accordion: tapping Producten opens the submenu instead of
     navigating, so the parent is never a dead end. */
  /* Same reasoning as .site-nav above, one level down: with the menu open but
     this accordion collapsed, Modulonwand is invisible and still tabbable. */
  .site-nav__sub {
    max-height: 0;
    overflow: hidden;
    visibility: hidden;
    transition: max-height 260ms var(--ease), visibility 260ms var(--ease);
  }
  .site-nav__item--has-sub.is-open .site-nav__sub { max-height: 320px; visibility: visible; }
  .site-nav__item--has-sub.is-open .site-nav__chevron { transform: translateY(2px) rotate(-135deg); }
  .site-nav__sub a { padding: 12px 0 12px 18px; font-size: 15px; color: var(--ink-on-navy); }
  .site-nav__sub a:hover { color: var(--white); }
  .site-nav__sub li:last-child a { padding-bottom: 16px; }

  .nav-cta { display: inline-block; width: auto; }
}

/* In the tablet band the primary CTA stays visible next to the menu button
   instead of collapsing into it; the in-list pill hides so the action never
   appears twice in the tab order — en dus ook nooit twee keer als
   "huidige pagina". */
@media (max-width: 1000px) and (min-width: 641px) {
  .nav-cta-header { display: inline-block; margin-left: auto; }
  .site-nav__item--cta { display: none; }
}

@media (max-width: 768px) {
  :root { --pad-min: 28px; --section: 60px; --section-tight: 56px; }

  .hero__inner { padding: 32px var(--pad) var(--section-tight); }
  .hero__inner--split { grid-template-columns: 1fr; gap: 26px; align-items: start; }

  .footer-contact__grid { grid-template-columns: 1fr; gap: 40px; }
  .footer-contact--nofrom .footer-contact__grid { grid-template-columns: 1fr; }

  .field-row { grid-template-columns: 1fr; }
  .btn--submit { align-self: stretch; width: 100%; }
}

@media (max-width: 480px) {
  :root { --pad-min: 20px; }
  body { font-size: 15px; }
  .site-header__logo img { height: 48px; }

  /* Inputs at 16px stop iOS Safari zooming in on focus. */
  .form input, .form select, .form textarea { font-size: 16px; }
}

/* ==========================================================================
   12. Contact page
   ========================================================================== */

.contact-layout {
  display: grid;
  grid-template-columns: 1.45fr 1fr;
  gap: 64px;
  align-items: start;
}

/* Dit is de verhouding waar contact.html op staat: het ene formulier links,
   de contactgegevenskaart rechts (klantwens 19 augustus 2026 — de kaart naast
   het formulier en niet eronder). Op 1364px is dat 769px formulier tegen
   531px kaart, en 769px is precies de breedte waarop een invoerveld nog als
   invoerveld leest; over de volle tekstkolom wordt het een fout.

   Twee modifiers stonden hier — `--even` (1fr 1fr) voor de twee formulieren
   naast elkaar van 17 augustus, en `--single` voor de kaart die er even
   alleen onder stond. Beide zijn met die opzet verdwenen; de basisregel doet
   het werk weer. */

/* Kop plus introparagraaf boven het formulier. De alinea is sinds
   19 augustus 2026 de introtekst die eerst los onder de hero stond; hij is
   daarom afgetopt op dezelfde 560px als .section__intro had, niet op de 520
   die hier stond voor de korte regel die hij vervangt. */
.contact-form-intro h2 { margin-bottom: 14px; }
.contact-form-intro p { font-size: 15px; margin: 0 0 32px; max-width: 560px; }

/* --- Contact: terugbelstrook — VERWIJDERD ------------------------------
   De strook "Liever toch eerst even bellen?" (.callback / .callback--wide)
   en de stappenkaart "Zo werkt je aanvraag" kwamen op 17 augustus 2026 mee
   van de opgeheven offerte-pagina en zijn er op 19 augustus 2026 op verzoek
   van de klant weer uit gehaald. De regels zijn hier weggehaald in plaats van
   ongebruikt te blijven staan; niet opnieuw toevoegen zonder nieuw besluit.
   Terugbellen zelf is er nog wél: het is sinds diezelfde dag de derde keuze
   in het ene contactformulier, en dus stuurt de pagina nog steeds
   ?onderwerp=terugbellen naar bedankt.html. Het directe telefoonnummer staat
   daarnaast in de contactkaart en in de footer, dus bellen blijft één klik. */

/* --- Projectvelden (offerteformulier) ----------------------------------- */

.form__group {
  border: 0;
  margin: 20px 0 4px;
  padding: 26px 0 0;
  border-top: 1.5px solid var(--surface);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.form__group legend {
  font-size: 13px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--blue);
  padding: 0;
  margin: 0 0 4px;
}

.form__group-note {
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  margin: 0 0 8px;
}

/* De belofte die bij de gekozen reden hoort, direct onder de keuze bovenaan
   het formulier. Ze stonden tot 19 augustus 2026 in de introparagraaf boven
   elk van de twee formulieren. Donkerder dan de groepsnotitie hierboven: dit
   is de toezegging waarop iemand besluit het formulier in te vullen, geen
   bijschrift. */
.form__choice-note {
  font-size: 14px;
  line-height: 1.7;
  color: var(--ink);
  margin: -2px 0 4px;
}
.form__choice-note a { color: var(--blue); text-decoration: underline; }
.form__choice-note a:hover { color: var(--orange-ink); }
.form__choice-note[hidden] { display: none; }

/* Waarom `data-mode` en niet `data-when` op contact.html: de vier regels
   hieronder schakelen op de wáárde van data-when, en op contact.html staat er
   geen data-onderwerp op <html>. Een `[data-when="offerte"]` daar zou dus
   permanent op display:none staan, wat de keuzelijst er niet uit krijgt. */

/* An author `display` rule beats the user-agent `[hidden] { display: none }`
   whatever its specificity, so the hidden state has to be restated here. */
.form__group[hidden] { display: none; }

/* --- Contact details card ---------------------------------------------- */

.contact-card {
  background: var(--surface);
  padding: 36px 34px 40px;
  position: sticky;
  top: 24px;
}

.contact-card h2 {
  font-size: 22px;
  letter-spacing: 0.3px;
  color: var(--blue);
  margin: 0 0 22px;
}

.contact-card__block { margin: 0 0 26px; }
.contact-card__block:last-child { margin-bottom: 0; }

.contact-card__label {
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
  margin: 0 0 6px;
}

.contact-card__value {
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink);
  margin: 0;
}
.contact-card__value a { color: var(--blue); text-decoration: underline; }
.contact-card__value a:hover { color: var(--orange-ink); }

.contact-card__route {
  display: inline-block;
  margin-top: 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--orange-ink);
}
.contact-card__route:hover { color: var(--orange-ink-hover); }

/* Static, self-hosted map in the "Bezoek en post" block. One image, no
   third-party embed — see the comment in contact.html. The image is cropped
   with the factory in the exact centre, so the pin is fixed at 50%/50%. */
.contact-card__map {
  display: block;
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);

  /* Wit, want dat is wat de gradient map hieronder van het landvlak maakt:
     tijdens het laden van de video staat er dan geen andere kleur in het
     kader dan erna. */
  background: var(--white);
}
.contact-card__map img,
.contact-card__map video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* De kaart in de huisstijl — wit veld, blauwe lijnen.

   OpenStreetMap tekent in zijn eigen palet: beige woonwijken, roze industrie,
   gele wegen, groene bermen, paarse voorzieningen. Op een pagina die verder
   alleen navy, lichtblauw en oranje kent was dat het enige vlak met vreemde
   kleuren erin, en het las als een screenshot van een andere website.

   De ingreep is een **gradient map**: de kaart wordt eerst tot één grijswaarde
   per pixel teruggebracht, en die waarde wijst vervolgens een kleur aan op een
   schaal die van --navy via --blue en --surface naar --white loopt. Donker
   wordt dus blauw en licht wordt wit — het wegennet, de letters en de
   perceelgrenzen komen als blauwe lijnen op een wit veld te staan, precies
   zoals de kaart getekend zou zijn als hij zelf uit de huisstijl kwam.

   Dat is iets anders dan een tint eroverheen. Een gekleurde laag met multiply
   of een opacity-vlak verschuift álle pixels dezelfde kant op: het wit van de
   wegen wordt dan net zo blauw als de rest en de kaart krijgt een waas. Een
   gradient map verschuift elke waarde afzonderlijk, dus de lichte vlakken
   kunnen naar wit lopen terwijl de donkere naar navy gaan — de kaart wordt
   contrastrijker in plaats van vlakker.

   De schaal zelf staat in de SVG in contact.html, als drie tabellen van 51
   waarden (om de 0.02). Ook de reden dat het geen standaard grijswaarde is —
   water en bebouwing liggen in luminantie 0.02 uit elkaar en zouden op de
   kaart samenvallen — staat daar, bij de feColorMatrix die dat oplost.

   Waarom in CSS/SVG en niet in nieuwe kaartbestanden: in dit slot zitten twéé
   beelden die exact gelijk moeten blijven — de video die van Nederland naar
   Kolkweg 10 inzoomt, en de poster kaart-kolkweg.webp, die het laatste frame
   daarvan is en die blijft staan zonder JS en bij prefers-reduced-motion. Eén
   regel op het element raakt ze allebei met dezelfde waarden; twee opnieuw
   gerenderde bestanden kunnen uit elkaar lopen. De ODbL-bron blijft daarbij
   onaangeroerd: de kaart wordt gekleurd, niet hertekend.

   Valt de filter weg — een browser die url()-filters niet oplost — dan staat
   de kaart er gewoon in OSM-kleuren. Lelijker, maar leesbaar; dat is de
   bedoelde terugval. */
.contact-card__map img,
.contact-card__map video {
  filter: url(#kaart-huisstijl);
}

/* De SVG bestaat alleen om die filter te dragen en mag geen ruimte innemen.
   Niet display: none: een filter uit een verborgen SVG wordt niet in elke
   browser gevonden, en een 0x0-element is even onzichtbaar zonder dat risico. */
.contact-card__map-filter {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

/* The pin marks a place and must read against the map, so it follows
   the on-white rule: --orange-ink with a white ring, like the focus ring
   and the success mark — not the decorative --orange. Circular, like the
   site's other circular marks. */
.contact-card__map-pin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 16px;
  height: 16px;
  transform: translate(-50%, -50%);
  background: var(--orange-ink);
  border: 3px solid var(--white);
  border-radius: 50%;

  /* De halo is er bijgekomen toen de kaart wit werd: op de oude, drukke
     OSM-tegels was een losse stip het rustigste punt van het kader, op een wit
     veld is 16px zo klein dat het oog hem in het uitgezoomde begin van de
     video niet meteen vindt. De ring is --orange-ink op 18% — dezelfde
     mengverhouding als de focusgloed op een invoerveld, zodat er één waarde is
     voor "zachte ring om een oranje mark". Geen animatie: een pulserende pin
     trekt de aandacht weg van de tekst ernaast en zou onder
     prefers-reduced-motion toch weer uit moeten. */
  box-shadow:
    0 0 0 8px color-mix(in srgb, var(--orange-ink) 18%, transparent),
    var(--card-shadow-sm);
  transition: box-shadow 0.3s var(--ease);
}

/* De kaart is een link naar de routeplanner en had geen enkele hoverstaat —
   de ring verbreedt, zodat het beeld antwoordt op de muis zonder te bewegen. */
.contact-card__map:hover .contact-card__map-pin,
.contact-card__map:focus-visible .contact-card__map-pin {
  box-shadow:
    0 0 0 13px color-mix(in srgb, var(--orange-ink) 18%, transparent),
    var(--card-shadow-sm);
}

/* Het frame bestaat alleen om het krediet in de kaart te kunnen leggen: de
   kaart zelf is een <a> naar de routeplanner en kan de link naar de
   OSM-licentie niet in zich dragen. Zie de noot in contact.html. */
.contact-card__map-frame {
  position: relative;
  margin-top: 14px;
}

/* De naamsvermelding is een ODbL-licentievoorwaarde en mag niet weg — wel
   klein en uit het oog, op precies de plek waar elke kaartaanbieder hem zet.
   Als losse regel onder de kaart telde hij mee als tekst in de kolom en las
   hij als een bijschrift bij het adres; in de hoek van de kaart hoort hij
   zichtbaar bij het beeld waar hij over gaat.

   Het schild is --white op 82% en geen effen wit: de kaart is nu een wit veld
   met blauwe lijnen, dus een dichte witte balk zou een gat in het beeld
   snijden. Op 82% blijft de lijntekening er zwak doorheen te zien en is de
   tekst nog steeds 5,3:1. */
.contact-card__map-credit {
  position: absolute;
  right: 1px;
  bottom: 1px;
  margin: 0;
  padding: 2px 6px;
  background: color-mix(in srgb, var(--white) 82%, transparent);
  font-size: 10px;
  line-height: 1.5;
  color: var(--muted);
}
.contact-card__map-credit a {
  color: inherit;
  text-decoration: underline;
}
.contact-card__map-credit a:hover { color: var(--orange-ink); }

/* Open questions, deliberately loud so nobody ships them by accident. The
   dashed border carries the loudness; the text is the same --orange-ink as the
   label inside it, so the whole marker is one colour drawn from the palette
   rather than a one-off hex nobody would think to update. 4,87:1 on the tint. */
.placeholder {
  display: block;
  border: 1.5px dashed var(--orange);
  background: color-mix(in srgb, var(--orange) 6%, transparent);
  border-radius: 3px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--orange-ink);
}
.placeholder strong {
  display: block;
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--orange-ink);
  margin-bottom: 3px;
}

/* The same note under a centred band (the unverified stats on project
   detail): capped and centred so it reads as a footnote to the band,
   not a full-width banner. */
.placeholder--center {
  max-width: 560px;
  margin: 26px auto 0;
  text-align: left;
}

@media (max-width: 1024px) {
  /* Naast een invoerveld van ~300px past geen tweede kolom, dus de kaart gaat
     hier onder het formulier staan — en dan heeft sticky geen betekenis meer. */
  .contact-layout { grid-template-columns: 1fr; gap: 48px; }
  .contact-card { position: static; }
}

/* ==========================================================================
   13. Berichtsjabloon (article template)
   Modelled on Project Detail.dc.html so the two detail pages read as
   siblings: fotohero, centred title, meta row, then the content.

   De kop stond eerst op een kale marineblauwe verlooop, met de foto pas
   daaronder als losse `.article-featured`. De vormgever vond dat een bericht
   daardoor te veel op een projectpagina leek en vroeg om "een foto hero met
   daaroverheen een pakkende titel" (30 juli 2026).

   Dat is niet een tweede fotohero geworden maar dezelfde: de band gebruikt
   `.detail-band` / `.detail-band__photo` uit §19, die de foto naar onderen
   laat oplossen in het bestaande heroverloop in plaats van er een tweede
   kleur overheen te leggen. Zie de lange noot daar.

   Eén verschil met Project Detail: daar is de band een blok bínnen de hero,
   hier ís de hero de band — `.detail-band` staat op hetzelfde element als
   `.hero`, dus de foto loopt door tot achter de header. Dat is niet cosmetisch.
   Een band die pas ónder de header begint, trekt over de volle breedte een
   zichtbare horizontale rand op precies die hoogte, en dan leest het als een
   banner onder de header in plaats van als één hero. Er is geen rand meer om
   te zien als er geen grens meer is.

   De losse `.article-featured` is vervallen: dat was op alle vijf pagina's
   exact dezelfde foto als de hero, dus die stond er nu twee keer.
   ========================================================================== */

/* De band is nu de hele hero, inclusief de ~125px header, dus alle vier de
   stops liggen lager dan bij Project Detail. --photo-in houdt de foto zwak
   achter de navigatie (die is 15px en heeft 4,5:1 nodig), --photo-open dekt
   het titelblok, en --photo-gone zit vóór de meta-rij zodat de vier labels op
   schoon marineblauw liggen. Alle waarden zijn gemeten, niet geschat: zie de
   noot bij ::after hieronder. */
.article-hero.detail-band {
  --photo-in: 26%;
  --photo-open: 62%;
  --photo-gone: 84%;
  --photo-strength: 0.55;
}

/* Verduistering ónder het titelblok, niet over de hele band.

   Waarom dit er is: op 0.55 haalde de titel het net (3,07:1 op Schuytgraaf,
   2,95:1 op Noorderduin — die foto heeft een lichte lucht) en de regel eronder
   nergens. Die regel is lopende tekst van 17–22px en heeft dus 4,5:1 nodig, en
   dat haalt geen enkele foto op deze sterkte. De foto zwakker zetten lost het
   op, maar dan is het geen fotohero meer — precies wat de vormgever vroeg.

   Dus: de foto blijft sterk en de tekst krijgt zijn eigen ondergrond. Twee
   dingen bewust zo:

   1. Het is een radiaal verloop en geen vlakke overlay over de band. De noot
      in §19 legt uit waarom een vlakke marineblauwe overlay hier niet werkt:
      de achtergrond is een diagonaal verloop, dus een tweede kleur eroverheen
      zit onderaan naast de kleur die daar werkelijk staat en dat zie je als
      een naad. Een ovaal dat naar alle randen volledig oplost heeft nergens
      een rand om te zien.
   2. ::after en niet ::before, want beide zitten op z-index -1 en delen dus
      dezelfde stapellaag als de foto: ::before is het eerste kind en zou
      áchter de foto belanden, ::after is het laatste en komt erover.

   De middenwaarde is gemikt op de zwaarste van de vijf foto's; verhoog hem
   niet zonder opnieuw te meten. */
.article-hero.detail-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(115% 62% at 50% 46%,
    color-mix(in srgb, var(--navy) 80%, transparent) 0%,
    color-mix(in srgb, var(--navy) 62%, transparent) 46%,
    transparent 76%);
}

.article-hero__inner {
  position: relative;
  text-align: center;
  padding: 40px var(--pad) 0;
  max-width: 900px;
  margin: 0 auto;
}

.article-hero__category {
  display: inline-block;
  background: var(--orange-ink);
  color: var(--white);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  padding: 6px 14px;
  border-radius: var(--pill);
  margin: 0 0 22px;
}

.article-hero h1 {
  font-size: var(--fs-h2-xl);
  line-height: 1.12;
  font-weight: 800;
  letter-spacing: 0.5px;
  margin: 0;
  color: var(--white);
  text-wrap: balance;
}

/* De "pakkende" regel onder de titel: één zin, de korte versie van de lead
   die eerst pas in de body stond. Bewust --ink-on-navy-bright en niet
   --ink-on-navy: dit is displaytype op een foto, geen lopende tekst. */
.article-hero__lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
  color: var(--ink-on-navy-bright);
  max-width: 660px;
  margin: 20px auto 0;
  text-wrap: balance;
}

/* Same six-across meta row as Project Detail, but with the four things an
   article actually has. */
.article-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
  padding: 56px var(--pad) 52px;
  text-align: center;
  max-width: 1100px;
  margin: 0 auto;
}
.article-meta__label { font-size: 13px; color: var(--muted-on-navy); margin-bottom: 6px; }
.article-meta__value {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--white);
  text-transform: uppercase;
}

/* --- Body ---------------------------------------------------------------
   A single centred column at a fixed measure. This is what makes the
   template survive a 200-word article and a 2000-word one alike: nothing
   depends on the amount of content, and the measure never widens.
   ---------------------------------------------------------------------- */

/* No min-height here. A short bericht is kept from leaving the footer riding
   high by the "Meer lezen?" block below, not by padding out this section — a
   vh-based min-height would open a hole of dead white on a tall monitor. */
.article-wrap {
  background: var(--white);
  padding: var(--section-tight) var(--pad) var(--section);
}

.article-body {
  max-width: 720px;
  margin: 0 auto;
}

.article-body > *:first-child { margin-top: 0; }
.article-body > *:last-child { margin-bottom: 0; }

.article-body p {
  font-size: 16px;
  line-height: 1.8;
  color: var(--ink);
  margin: 0 0 20px;
  text-wrap: pretty;
}

.article-body .lead {
  font-size: 20px;
  line-height: 1.55;
  font-weight: 500;
  color: var(--blue);
  margin-bottom: 32px;
}

.article-body h2 {
  font-size: 30px;
  line-height: 1.2;
  letter-spacing: 0.3px;
  color: var(--blue);
  margin: 56px 0 18px;
  text-wrap: balance;
}

.article-body h3 {
  font-size: 19px;
  line-height: 1.35;
  color: var(--navy-deep);
  margin: 36px 0 12px;
}

.article-body ul,
.article-body ol {
  margin: 0 0 24px;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.article-body li {
  font-size: 16px;
  line-height: 1.75;
  color: var(--ink);
  text-wrap: pretty;
}
.article-body li::marker { color: var(--orange-ink); font-weight: 700; }
.article-body li > ul,
.article-body li > ol { margin: 10px 0 0; }

.article-body a { color: var(--blue); text-decoration: underline; }
.article-body a:hover { color: var(--orange-ink); }

.article-body strong { color: var(--navy-deep); font-weight: 600; }

/* The tinted panel IS the device — there is no accent stripe down the left
   edge. Same reasoning the .form__summary already carries: a thick side rule
   reads as decoration applied to a box, where the box itself is what marks the
   content as set apart. --surface is the material this site already uses for
   that everywhere else (.downloads, .receipt, .contact-card, .related), so a
   quote in a bericht is recognisably the same move rather than a new one.
   Losing the stripe costs nothing: the type carries the weight, and the cite
   underneath says who is speaking. */
.article-body blockquote {
  margin: 36px 0;
  padding: 28px 30px;
  background: var(--surface);
}
.article-body blockquote p {
  font-size: 19px;
  line-height: 1.55;
  color: var(--ink-strong);
  margin: 0 0 12px;
}
.article-body blockquote p:last-child { margin-bottom: 0; }
.article-body blockquote cite {
  display: block;
  font-size: 13px;
  font-style: normal;
  line-height: 1.6;
  color: var(--muted);
}

/* Image inside the body — allowed to breathe past the text measure on wide
   screens, clamped back inside it below 1024px. */
.article-body figure {
  margin: 40px 0;
}
/* :is(img, video) en niet alleen img: het nieuwsbericht over de huisstijl
   zet de logo-animatie in een figuur, en zonder `display: block` op de video
   laat de inline-baseline een streep achtergrond onder de clip staan. */
.article-body figure :is(img, video) {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
/* Drieluiken van de oude site zijn drie foto's naast elkaar in één bestand —
   een 16:9-crop zou er een halve foto afsnijden. */
.article-body figure.figure--natural :is(img, video) { aspect-ratio: auto; }

/* Staande bouwplaatsfoto's — de telefoonfoto's die de klant aanlevert zijn
   vaker portret dan liggend (Heidepost, 21 augustus 2026). Twee dingen gaan
   daar mis zonder eigen regel: de 16:9-crop hierboven snijdt bij een 3:4-foto
   de kop en de voet van het onderwerp weg, en op volle kolombreedte zou zo'n
   foto ruim 1800px hoog worden. Vandaar een eigen maximum, gecentreerd, met
   het bijschrift eronder in dezelfde breedte. */
.article-body figure.figure--portrait img {
  display: block;
  aspect-ratio: auto;
  max-width: 520px;
  margin-inline: auto;
}
.article-body figure.figure--portrait figcaption {
  max-width: 520px;
  margin-inline: auto;
}
.article-body figcaption {
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
  margin-top: 10px;
}

.article-body hr {
  border: 0;
  border-top: 1.5px solid var(--surface);
  margin: 44px 0;
}

/* --- Foot of the article ------------------------------------------------ */

.article-foot {
  max-width: 720px;
  margin: 56px auto 0;
  padding-top: 32px;
  border-top: 1.5px solid var(--surface);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.article-foot__back {
  font-size: 14px;
  font-weight: 600;
  color: var(--blue);
}
.article-foot__back:hover { color: var(--orange-ink); }

/* --- Related cards (the "Meer bewijs nodig?" block, re-used) ------------ */

/* --pad-outer, not --pad: in the XD this row of three cards spans the full
   content area, 140 → 1780 on a 1920 artboard. */
.related { background: var(--surface); padding: var(--section-tight) var(--pad-outer) var(--section); text-align: center; }
.related--white { background: var(--white); }
.related > h2 { font-size: var(--fs-h2-32); color: var(--blue); margin: 0 0 44px; }

.related__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px; /* left at 28 for the same reason as .card-grid — see there */
  margin-bottom: 44px;
  text-align: left;
}

.card {
  position: relative; /* houder voor .media-tag; verder zonder gevolg */
  background: var(--white);
  box-shadow: var(--card-shadow);
  display: flex;
  flex-direction: column;
}
/* projecten.html and over-modulon.html both set --card-img-h on their
   .card-grid (210px and 180px) the same way .tile and .rail__item take their
   size, but this rule was a flat 200px and swallowed both. The related-cards
   block on nieuws-detail.html sets nothing, so the fallback holds it where it
   already was. */
.card img { width: 100%; height: var(--card-img-h, 200px); object-fit: cover; }
.card__body {
  padding: 24px 24px 28px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  flex: 1;
}
.card__meta {
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
}
/* Navy, not --blue. A .card is two different things in this prototype: the
   project and article cards, which are links, and the six process steps on
   over-modulon.html, which are not. --blue on white is the colour a visitor
   reads as "link" — on a step that leads nowhere it is a promise the card
   cannot keep. .card--link takes the blue back below. */
.card__title { font-size: 17px; font-weight: 600; line-height: 1.35; color: var(--navy-deep); margin: 0; }
.card__text { font-size: 13px; line-height: 1.7; color: var(--ink); margin: 0; flex: 1; text-wrap: pretty; }

/* Waarom staat deze kaart in het zoekresultaat? Zoeken op Projecten kijkt ook
   in de zes gegevens uit de specificatiestrook, en die staan niet op de kaart —
   zonder deze regel levert "wouters" een kaart op waar het woord Wouters
   nergens te vinden is. Alleen zichtbaar bij een treffer op zo'n gegeven; zie
   initProjectFilter in site.js.

   De 2px regel is --orange en niet --orange-ink: dit is een markering waar je
   naar kijkt, geen tekst die je leest, en dat is precies de scheiding tussen
   die twee tokens. De tekst zelf is --ink-strong op --surface. */
.card__match {
  align-self: stretch;
  font-size: 12px;
  line-height: 1.5;
  font-weight: 500;
  color: var(--ink-strong);
  background: var(--surface);
  border-left: 2px solid var(--orange);
  padding: 7px 10px;
  margin: 0;
  text-wrap: pretty;
}

/* --- Clickable cards -----------------------------------------------------
   A card whose whole surface leads somewhere. Applied to the 6 project cards
   on Projecten, the related cards on Project Detail and het berichtsjabloon, and
   every .post-card on Nieuws.

   The .btn inside stays the ONLY link a screen reader announces: .card__overlay
   is aria-hidden and tabindex="-1", a plain <a> stretched over the card box so
   the entire card is a hit area for the same destination. The obvious
   alternative — wrapping the image and the title in their own <a> — gives
   every card two or three links to the same page, which a screen reader
   dutifully reads out two or three times; this keeps one accessible name and
   one tab stop while the mouse can land anywhere.

   This used to be a ::after on the .btn instead of a real element: shorter
   markup, no extra tag. It was dropped because it flickered — the pseudo-
   element's z-index only resolves inside a stacking context, .card--link
   never had one, so the browser hit-tested the invisible overlay against the
   card's own image and text on every single frame, and on the tie the answer
   changed: the cursor visibly switched between the pointer and the text
   cursor dozens of times a second while it sat still. Giving .card--link its
   own stacking context and taking every other descendant out of hit-testing
   with pointer-events narrowed it but never fully closed it. A real, direct
   child element does not have this problem — verified with three independent
   tools (elementFromPoint, native :hover matching, and a plain <a> elsewhere
   on the site that never flickered) before rewriting all 28 cards.

   The trade-off is that the overlay sits above the text, so card text is no
   longer selectable. That is the standard cost of the pattern and it is
   worth it here: people click card images and card headlines, and before
   this both were dead.

   Feedback on hover is box-shadow and colour, never transform. A transform
   would lose: most of these cards carry data-reveal, and
   `.js [data-reveal].is-revealed { transform: none }` is (0,3,0) where any
   :hover rule here is (0,2,0). The lift would silently never happen.
   -------------------------------------------------------------------- */

/* Same trap as .form__group further down: .card sets `display: flex`, and an
   author display rule beats the user-agent's `[hidden] { display: none }`. The
   project filter on Projecten hides cards by setting .hidden, and without this
   line every "hidden" card stays on screen. */
.card[hidden] { display: none; }

.card--link,
.post-card--link {
  position: relative;
  transition: box-shadow 200ms var(--ease);
}

.card__overlay,
.post-card__overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* :focus-within as well as :hover — keyboard focus lands on the pill inside,
   and without this the card around it stays flat while the focus ring says
   the whole thing is the target. */
.card--link:hover,
.card--link:focus-within,
.post-card--link:hover,
.post-card--link:focus-within {
  box-shadow: 0 10px 26px rgba(16, 26, 80, 0.18);
}

/* The title is the thing people aim at, so it is the thing that has to
   answer. It is safe to keep --blue here: on these cards it IS a link. */
.card--link .card__title,
.post-card--link .post-card__title {
  color: var(--blue);
  transition: color 200ms var(--ease);
}
.card--link:hover .card__title,
.card--link:focus-within .card__title,
.post-card--link:hover .post-card__title,
.post-card--link:focus-within .post-card__title {
  color: var(--orange-ink);
}

/* The pill has to track the card, or hovering the image lights up the card
   while the button it actually triggers stays inert. */
.card--link:hover .btn--primary,
.card--link:focus-within .btn--primary,
.post-card--link:hover .btn--primary,
.post-card--link:focus-within .btn--primary {
  background: var(--orange-ink-hover);
}

/* --- Wand of vloer, in de hoek van de foto -------------------------------
   Klantwens 24 augustus 2026: op een projectfoto moet meteen te zien zijn of
   het om een Modulonwand of een Modulonvloer gaat. Vier van de zes projecten
   zijn wand, twee zijn vloer, en aan de foto zelf is dat verschil niet af te
   lezen — de zolderelementen op proj-noorderduin.webp lezen voor een leek als
   wanden terwijl het vloeren zijn.

   Eén klasse voor twee plekken: de zes kaarten op Projecten en dezelfde zes
   in de fotobalk op de homepage. Een merkje dat maar op één van de twee
   overzichten staat leest als een fout, niet als een keuze.

   Vlak in de hoek, zonder ronding en zonder marge: het is de hoek van de
   foto, geen label dat erboven zweeft. Navy met wit, omdat de foto eronder
   elke keer een andere bouwplaats is — een halftransparant vlak of een lichte
   tint is op de ene foto leesbaar en op de andere niet. Oranje kan hier niet:
   dat betekent op deze site "doe dit", en dit is een typeaanduiding.

   pointer-events: none, want het merkje ligt boven het klikbare vlak (de
   .card__overlay op Projecten, de <a> om de railfoto). Zonder deze regel is
   de linkerbovenhoek van elke kaart het enige dode plekje erin.

   Het merkje draagt de volledige productnaam voluit — modulonwand of
   modulonvloer (klantwens 24 augustus 2026), in kleine letters. Daarmee
   verviel de .sr-only-span met "Modulon" ervóór: die bestond alleen om een
   schermlezer de hele naam te laten horen terwijl het oog WAND of VLOER las,
   en nu staat er voor beide hetzelfde. Kapitalen kunnen hier niet meer: twee
   woorden van elf en twaalf letters in caps met 1,5px tracking lopen op een
   375px-kaart in de foto. Vandaar text-transform: lowercase — die staat er
   als vangnet voor het geval de tekst ooit met een hoofdletter wordt
   ingevoerd (in WordPress vult de klant dit zelf) — en tracking terug naar
   het minimum dat onderkast verdraagt. */
.media-tag {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  pointer-events: none;
  padding: 8px 12px;
  background: var(--navy);
  color: var(--white);
  font-size: 12px;
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.2px;
  text-transform: lowercase;
}

@media (max-width: 1024px) {
  /* De meta-rij wordt hier twee rijen hoog, dus de band groeit terwijl het
     titelblok gelijk blijft: de foto moet eerder weg zijn, anders schuift hij
     de labels in. Zelfde reden op 768. */
  .article-hero.detail-band { --photo-in: 24%; --photo-open: 56%; --photo-gone: 74%; }
  .article-meta { grid-template-columns: repeat(2, 1fr); gap: 28px 24px; padding: 44px var(--pad) 44px; }
  .related__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 768px) {
  .article-hero.detail-band { --photo-in: 20%; --photo-open: 54%; --photo-gone: 72%; }
  .article-hero__inner { padding: 28px var(--pad) 0; }
  .article-hero__lead { margin-top: 16px; }
  .article-meta { grid-template-columns: 1fr 1fr; text-align: left; padding: 32px var(--pad) 36px; }

  .article-body p, .article-body li { font-size: 16px; }
  .article-body h2 { font-size: 25px; margin-top: 42px; }
  .article-body h3 { font-size: 18px; }
  .article-body blockquote { padding: 22px 22px; margin: 28px 0; }
  .article-body blockquote p { font-size: 17px; }

  .related__grid { grid-template-columns: 1fr; }
  .article-foot { flex-direction: column; align-items: flex-start; }
}

/* ==========================================================================
   14. 404 and bedankt
   Both are short pages. The hero does the work and the section below it
   carries the routes onward, so neither ever looks empty.
   ========================================================================== */

.centered-hero { text-align: center; }
.centered-hero .hero__inner { max-width: 760px; margin: 0 auto; padding-bottom: var(--section); }
.centered-hero h1 { text-align: center; }
.centered-hero .hero__lead { margin: 24px auto 0; }

/* Same slope as --fs-display, but a higher floor: "404" is only three glyphs,
   so it can stay big on a phone where a full hero headline cannot. */
.error-code {
  font-size: clamp(64px, 6.95vw + 18px, 118px);
  line-height: 0.95;
  font-weight: 800;
  letter-spacing: var(--track-display);
  color: rgba(255, 255, 255, 0.16);
  margin: 0 0 4px;
}

.success-mark {
  width: 68px;
  height: 68px;
  margin: 0 auto 28px;
  border-radius: 50%;
  /* --orange-ink, not the brand orange: the white check inside is a graphic on
     an orange fill, and #fc663f carries only 2,94:1 against white. */
  background: var(--orange-ink);
  display: flex;
  align-items: center;
  justify-content: center;
}
.success-mark::after {
  content: "";
  width: 20px;
  height: 34px;
  border-right: 4px solid var(--white);
  border-bottom: 4px solid var(--white);
  transform: translateY(-3px) rotate(45deg);
}

/* Route cards — the way back on 404, the way onward on bedankt. Both pages
   carry their onward links here rather than as buttons under the heading, so
   there is no button row in either hero. */
.routes {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  max-width: 1100px;
  margin: 0 auto;
}
.routes--three { grid-template-columns: repeat(3, 1fr); }

.route {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--white);
  box-shadow: var(--card-shadow-sm);
  padding: 26px 26px 30px;
  border-top: 3px solid var(--orange);
  transition: transform 200ms var(--ease), box-shadow 200ms var(--ease);
}
.route:hover { transform: translateY(-3px); box-shadow: 0 10px 26px rgba(16, 26, 80, 0.16); }

/* Dezelfde vier kaarten, maar als uitspraak in plaats van als link — "Waar
   wij voor staan" op Over Modulon. Een kaart die niet klikt mag ook niet
   optillen bij hover: dat belooft een doorklik die er niet is.

   De opbouw is sinds 24 augustus 2026 die van de statistiektegel op een
   projectbericht (`.stat`, §19): één groot merkteken bovenaan, dan de kop,
   dan de tekst, alles gecentreerd. Vandaar dezelfde celpadding (40/28/44) en
   dezelfde tekstmaat als `.stat__label`, en vandaar dat `text-align: left`
   hier weg is: de sectie is `.section--center` en de kaart laat dat nu door.
   Het verschil met een stat-tegel blijft het oranje streepje bovenaan — dat
   is wat een routekaart een routekaart maakt. */
.routes--static .route { transition: none; padding: 40px 28px 44px; gap: 0; }
.routes--static .route:hover { transform: none; box-shadow: var(--card-shadow-sm); }
.routes--static .route__title { margin-bottom: 10px; }
.routes--static .route__text { font-size: 14px; }

/* Eén icoon boven de kop, alleen op de vier kernwaarden van Over Modulon.
   De routekaarten van 404 en bedankt krijgen er geen: die dragen al een pijl,
   en twee merktekens in één kaart is er één te veel. De glyphs komen uit
   Phosphor Icons (MIT), weight Bold, en staan in --orange — hetzelfde
   oranje als het streepje erboven. Dat mag: net als dat streepje is dit
   een decoratief merkteken zonder status, de uitzondering die in CLAUDE.md
   onder de oranje-splitsing staat beschreven. Wordt het ooit een link of een
   statusmarkering, dan moet het naar --orange-ink.
   De maat is die van het grote getal in een stat-tegel: het icoon leest
   --fs-stat, dus het krimpt van 52px op de desktop naar 36px op een telefoon
   en houdt dezelfde verhouding tot de tekst eronder.
   Kort weg geweest (3 september 2026, verplaatst naar "Zo doen we het" en
   daarna helemaal geschrapt toen die sectie een cijferrail werd); terug nu
   dat sectie geen icoon meer voert en er verder nergens op de pagina een
   dubbeling ontstaat. */
.route__icon { width: var(--fs-stat); height: var(--fs-stat); flex: none;
               margin: 0 auto 18px; color: var(--orange); }

.route__title { font-size: 17px; font-weight: 600; color: var(--blue); }
.route__text { font-size: 13px; line-height: 1.65; color: var(--ink); text-wrap: pretty; }
.route__arrow { font-size: 14px; font-weight: 600; color: var(--orange-ink); margin-top: auto; padding-top: 10px; }

/* What was sent / what happens next. */
.receipt {
  max-width: 720px;
  margin: 0 auto var(--section-tight);
  background: var(--surface);
  padding: 34px 36px 38px;
}
.receipt h2 {
  font-size: 20px;
  color: var(--blue);
  margin: 0 0 18px;
  letter-spacing: 0.3px;
}
.receipt dl { margin: 0; display: grid; grid-template-columns: 170px 1fr; gap: 10px 20px; }
.receipt dt { font-size: 13px; font-weight: 600; color: var(--muted); }
.receipt dd { margin: 0; font-size: 15px; line-height: 1.6; color: var(--ink); }
.receipt__next { margin: 24px 0 0; font-size: 15px; line-height: 1.75; color: var(--ink); }

@media (max-width: 1024px) {
  .routes { grid-template-columns: repeat(2, 1fr); }
  .routes--three { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .routes, .routes--three { grid-template-columns: 1fr; }
  .receipt { padding: 26px 22px 30px; }
  .receipt dl { grid-template-columns: 1fr; gap: 2px; }
  .receipt dd { margin-bottom: 12px; }
}

/* --------------------------------------------------------------------------
   15. Footer form (the default footer, on every page except contact.html)
   Same markup and same validation as the contact form, restyled for navy.
   The design shows placeholder-only inputs; real <label>s are added here,
   because placeholder-only fields fail accessibility.
   -------------------------------------------------------------------------- */

.footer-contact .field__label { color: var(--white); }
/* --orange-ink is tuned for white and --surface; on navy it drops to 2.70:1.
   The asterisk is small text, so it takes the light orange the nav already
   uses against this background (9.27:1). */
.footer-contact .field__req { color: var(--orange-light); }
.footer-contact .field__hint { color: var(--muted-on-navy); }
.footer-contact .form__legend-note { color: var(--muted-on-navy); }
.footer-contact .form input,
.footer-contact .form textarea { border-color: transparent; }
.footer-contact .form input:hover,
.footer-contact .form textarea:hover { border-color: var(--orange-light); }
/* Links uitgelijnd, net als op de contact- en offertepagina. Stond
   gecentreerd, waardoor de knop als enige element uit de linkerkantlijn van
   het formulier liep — dat viel op zodra de verplicht-melding eronder kwam te
   staan (feedback vormgever 30-07-2026). */
.footer-contact .btn--submit { align-self: flex-start; min-width: 0; }
.footer-contact .form__summary { background: var(--error-surface); }

/* Error state on navy. Two things were wrong here and both only show up once
   a field actually fails, which is why the contrast sweep over the ten pages
   missed them — the elements are display:none until then.

   1. `.footer-contact .form input { border-color: transparent }` and
      `.form input[aria-invalid="true"]` are both (0,2,1); the footer rule wins
      on source order, so an invalid field on navy had no border at all. Adding
      the attribute here takes it to (0,3,1) and settles it on specificity
      rather than on which rule happens to sit lower in the file.
   2. --error is tuned for white. On navy it is 2,81:1. */
.footer-contact .field__error { color: var(--error-on-navy); }
.footer-contact .form input[aria-invalid="true"],
.footer-contact .form select[aria-invalid="true"],
.footer-contact .form textarea[aria-invalid="true"] { border-color: var(--error-on-navy); }
.footer-contact .form input[aria-invalid="true"]:focus,
.footer-contact .form select[aria-invalid="true"]:focus,
.footer-contact .form textarea[aria-invalid="true"]:focus { border-color: var(--error-on-navy); }

.footer-contact .field__hint[data-counter-state="near"] { color: var(--white); }
.footer-contact .field__hint[data-counter-state="over"] { color: var(--error-on-navy); }

@media (max-width: 768px) {
  .footer-contact .btn--submit { align-self: stretch; }
}

/* ==========================================================================
   16. Shared layout components for the six rebuilt pages
   Home, De Modulonwand, Projecten, Project Detail, Over Modulon and
   Nieuws & downloads all reduce to the same handful of blocks. Each one is
   defined once here with its collapse order, and every page uses it.
   ========================================================================== */

/* --- Section helpers ---------------------------------------------------- */

/* Same :where() rule as the section defaults in section 6 — these are
   defaults for the band, and any component inside it overrides them. */
.section--navy { background: var(--gradient-navy-alt); color: var(--white); }
:where(.section--navy) h2, :where(.section--navy) h3 { color: var(--white); }
:where(.section--navy) p { color: var(--ink-on-navy); }
.section--partners { background: var(--surface-blue); }

/* Sections whose content bleeds to the viewport edge (the project rail, the
   Nieuws downloads column). They pad vertically only. */
.section--flush { padding-left: 0; padding-right: 0; }
.section--tight-bottom { padding-bottom: 0; }
.section--center { text-align: center; }

/* --- Closing statement: the last word before the footer ------------------
   Over Modulon ran out on process detail — six steps and a BIM note — and
   never said what all of it adds up to. This band puts the story back
   together and carries the one solid call to action on the page, which is
   what lets the CTA halfway up the story column drop to a .text-link.
   Centred text, so it is capped narrower than a left-aligned column: a
   centred line past ~70 characters loses its return sweep.
   ---------------------------------------------------------------------- */

.closing { max-width: 680px; margin: 0 auto; }
.closing p { font-size: 15px; line-height: 1.75; margin: 0 0 18px; text-wrap: pretty; }
.closing p:last-of-type { margin-bottom: 0; }
.closing .btn { margin-top: 30px; }

.section__intro { max-width: 560px; }
.section__lead { font-size: 15px; line-height: 1.75; margin: 0 0 18px; max-width: 460px; }
.section__lead:last-of-type { margin-bottom: 0; }
/* A lead with a button under it needs more air than one paragraph after
   another. Two classes deep so it outranks the :last-of-type reset above —
   this paragraph is usually the only <p> in its block. */
.section__lead.section__lead--cta { margin-bottom: 36px; }

/* --- Buttons: the 15px size the hero uses -------------------------------- */

.btn--lg { font-size: 15px; padding: 13px 28px; }
.btn--outline.btn--lg,
.btn--outline-light.btn--lg { padding: 12px 28px; }

/* Two buttons under a hero heading. They stack full width on a phone so both
   stay a comfortable tap target rather than shrinking side by side. */
.btn-pair { display: flex; flex-wrap: wrap; gap: 16px; }
@media (max-width: 480px) {
  .btn-pair { flex-direction: column; align-items: stretch; }
  .btn-pair .btn { width: 100%; }
}

/* --- Section head: heading beside its controls ---------------------------
   "DIT STAAT ER AL" + carousel arrows on Home, "DE PROJECTEN" + search on
   Projecten. Collapse: 2 columns → 1 at 768px.
   ---------------------------------------------------------------------- */

.head-split {
  display: grid;
  grid-template-columns: var(--split, 1.2fr 1fr);
  gap: var(--head-gap, 48px);
  align-items: var(--head-align, end);
  margin-bottom: var(--head-mb, 0);
}
.head-split > * { min-width: 0; }
@media (max-width: 768px) {
  .head-split { grid-template-columns: 1fr; gap: 28px; align-items: start; }
}

/* --- Split: text beside an image ----------------------------------------
   Collapse: 2 columns → 1 at 768px. The image half is a fixed min-height on
   desktop (as in the design) and a fixed aspect ratio once it goes full
   width, so it never becomes a random-height band.
   ---------------------------------------------------------------------- */

.split {
  display: grid;
  grid-template-columns: var(--split, 1fr 1fr);
  align-items: stretch;
  background: var(--white);
}

/* Een split op de blauwe band in plaats van op wit. "In het echt nog beter!"
   op De Modulonwand stond als .section--surface in de pagina en is 19 augustus
   2026 een split geworden; de band eronder mag daarbij niet omslaan naar wit,
   anders staat er ineens een witte strook tussen de fotobalk en de navy
   footer. Alleen de achtergrond verschilt — alle maatvoering blijft die van
   .split. */
.split--surface { background: var(--surface); }

/* Wins over the .section padding on purpose: a split half is vertically
   centred against the photo next to it, so it uses the same value top and
   bottom rather than the section's 88/96 pair. */
.split__text {
  padding: var(--section) var(--pad);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
}

/* Only the outer side follows --pad; the side facing the photo keeps
   --pad-min. --pad grows with the window above 1492px (598px at 2560), and
   on both sides it squeezed the text half down to ~80px on a wide screen,
   with "VÓÓR" and "BOUW." spilling out over the photo. Below 1492px --pad
   equals --pad-min, so nothing changes there. */
.split__text:first-child { padding-right: var(--pad-min); }
.split__text:last-child { padding-left: var(--pad-min); }

.split > * { min-width: 0; }
.split__media { position: relative; min-height: var(--media-h, 520px); }
.split__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* De Modulonwand insets the left-hand photo from the page edge, and the photo
   in the section above it exactly as far from the right edge. That second half
   was missing: the upper photo ran off the screen while its counterpart below
   stopped neatly inside the margin — the alignment the designer flagged.
   With both inset by --pad the two photos land on the same two vertical lines
   as every heading, paragraph and button on the page. */
.split__media--inset { margin-left: var(--pad); }
.split__media--inset-end { margin-right: var(--pad); }

/* De stapfoto naast "GEBOUWD VOOR DE PRAKTIJK" stond naast een tekstkolom die
   veel hoger is dan de foto nodig heeft: met align-items: stretch rekte hij mee
   tot 712px en werd --media-h nooit gehaald. --small laat de foto los van die
   stretch — hij houdt de 4/3-verhouding die .split__media onder 768px ook al
   aanhoudt, en staat verticaal gecentreerd tegen de tekst. De buitenrand blijft
   op --pad staan, dus de uitlijning met de foto in de sectie erboven verandert
   niet; alleen de binnenrand schuift op. Smaller (19 augustus 2026). */
.split__media--small {
  align-self: center;
  min-height: 0;
  aspect-ratio: 4 / 3;
}

/* --panel is er niet meer. Het was een navy vlak met de uitschuif-animatie
   erin, in "In het echt nog beter!" op De Modulonwand; die sectie draagt sinds
   19 augustus 2026 een foto, zoals de twee splits erboven. Niet opnieuw
   invoeren zonder de reden erbij: een navy vlak onderaan die pagina liep
   tegen de navy footer-band aan en las als één L-vormig blok, wat het vlak
   dwong zijn eigen helft niet te vullen — een uitzondering op alles wat
   .split__media verder doet. */

@media (max-width: 768px) {
  .split { grid-template-columns: 1fr; }
  .split__media { min-height: 0; aspect-ratio: 4 / 3; }
  .split__media--inset { margin-left: 0; }
  .split__media--inset-end { margin-right: 0; }
}

/* --- Rail: full-bleed horizontal scroller -------------------------------
   The project carousel on Home and the photo strips on De Modulonwand and
   Project Detail. This is the one part of the design that was already
   responsive — it stays a scroller at every width, and only the item size
   changes. Snap points and a focusable container are added on top.
   ---------------------------------------------------------------------- */

.rail {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-left: var(--pad);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.rail::-webkit-scrollbar { display: none; }

.rail__item {
  position: relative;
  flex: 0 0 var(--rail-w, clamp(260px, 72vw, 420px));
  height: var(--rail-h, clamp(280px, 26vw, 380px));
  scroll-snap-align: start;
}
.rail__item img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* --- A rail that never reaches an end -----------------------------------
   Vormgeversfeedback 30 juli 2026: de fotobalk moet blijven rondlopen in
   plaats van tegen een eind aan te lopen. site.js kloont de set één keer en
   zet de scrollpositie precies één set terug zodra hij de naad voorbij is —
   onzichtbaar, dus er ís geen eind meer en de pijlen worden nooit grijs.
   Hij schuift ook zelf door, en staat stil bij hover, bij toetsenbordfocus,
   buiten het beeld en onder prefers-reduced-motion.

   Snap staat hier uit, en dat is geen detail: scroll-snap-type: proximity
   trekt een langzaam doorschuivende balk elke keer terug naar het
   dichtstbijzijnde item, en dan loopt hij niet meer maar hikt hij.

   Zonder JavaScript is dit een gewone scroller — dan mist alleen het
   doorlopen, niet de foto's. */
.rail--loop { scroll-snap-type: none; }
.rail--loop .rail__item { scroll-snap-align: none; }

/* .rail zet display: flex, en een author-declaratie verslaat de
   [hidden] { display: none } van de browser. Zonder deze regel blijft een
   verborgen rail zichtbaar staan — dezelfde valkuil als .form__group. */
.rail[hidden] { display: none; }

/* --- A rail photo that is a link ----------------------------------------
   Only the homepage rail wraps its photos in an <a>; the strips on De
   Modulonwand and Project Detail are photos and nothing more. Before this
   the two were indistinguishable — the cursor was the only thing that ever
   said one of them led to a project page.

   The <a> has to be a block filling the item, or it collapses to the height
   of the image's own line box and the bottom of the photo stops responding.
   overflow: hidden is what contains the zoom. It does not clip the caption:
   that sits on .rail__item, which is the parent of this link, not a child. */
.rail__item > a {
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
.rail__item > a img { transition: transform 500ms var(--ease); }
.rail__item > a:hover img,
.rail__item > a:focus-visible img { transform: scale(1.05); }

/* The plate is the obvious thing to aim at, so it answers too. It is not
   inside the link — pointer-events: none below is what lets a click on it
   fall through to the photo — so it has to be driven from the item. */
.rail__item:hover .rail__caption { color: var(--orange-ink); }

@media (prefers-reduced-motion: reduce) {
  .rail__item > a img { transition: none; }
  .rail__item > a:hover img,
  .rail__item > a:focus-visible img { transform: none; }
}

/* The white name plate over a project photo.

   The plate is the positioned box; .rail__caption is only the bar of text
   inside it. That split exists so the plate can hold a second child — the
   hover panel below — and grow upward out of the bar instead of the bar
   itself having to move. The bar stays put at the bottom of the photo at
   every state, which is the whole point: nothing shifts, something appears
   above it. */
.rail__plate {
  position: absolute;
  bottom: 28px;
  left: 24px;
  right: 24px;
  background: var(--white);
  border-radius: 4px;
  /* Lets a click on the plate fall through to the photo underneath, which on
     the homepage is the project link. Without this the most obvious target
     on the card — the one with the project's name on it — would be the one
     dead spot. It is also why the panel holds text and no button: a second
     link inside a card that is already one link is a target that has to
     fight the card for the click. */
  pointer-events: none;
  box-shadow: 0 6px 20px rgba(10, 16, 60, 0.25);
  overflow: hidden;
}
.rail__caption {
  margin: 0;
  color: var(--ink-strong);
  font-size: 15px;
  font-weight: 500;
  text-align: center;
  padding: 14px 12px;
  transition: color 200ms var(--ease);
}

/* --- The hover panel ----------------------------------------------------
   Wireframe-gedrag, 19 augustus 2026: bij hover schuift er een wit tekstvlak
   uit de naambalk omhoog met de projectomschrijving erin.

   0fr → 1fr op grid-template-rows in plaats van een height, want de tekst is
   per project een andere lengte en een vaste hoogte laat de korte omschrijving
   in een te hoog vlak zweven. De transitie loopt over de fr-waarde; het kind
   heeft overflow: hidden en min-height: 0 nodig, anders weigert het te krimpen.

   Alleen waar er echt gehoverd kan worden. Op touch bestaat hover niet en
   blijft er precies de balk staan die er nu ook staat — geen paneel dat aan
   blijft hangen na een tik, en geen tekstvlak dat op een 260px-brede kaart
   de halve foto afdekt. */
.rail__reveal { display: grid; grid-template-rows: 0fr; }
.rail__reveal > div { overflow: hidden; min-height: 0; }
.rail__reveal-text {
  margin: 0;
  padding: 16px 14px 2px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink);
  text-align: left;
}

@media (hover: hover) {
  .rail__reveal { transition: grid-template-rows 380ms var(--ease); }
  .rail__reveal-text {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 260ms var(--ease) 60ms, transform 260ms var(--ease) 60ms;
  }
  .rail__item:hover .rail__reveal,
  .rail__item:focus-within .rail__reveal { grid-template-rows: 1fr; }
  .rail__item:hover .rail__reveal-text,
  .rail__item:focus-within .rail__reveal-text { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .rail__reveal { transition: none; }
  .rail__reveal-text { transition: none; transform: none; }
}

/* Prev / next. 44px targets, not the bare 26px glyph in the design.

   The 44px box was already here; what was missing was any sign that it is a
   button. A bare orange arrow with no border and an 8%-white hover reads as
   punctuation next to the outlined pill beside it. The 1.5px ring is the
   same treatment .nav-toggle already uses, so the two agree.

   Default is the LIGHT-background version. The homepage rail sits on navy,
   but the strips on De Modulonwand and Project Detail sit on white, and this
   component follows the same convention as .btn--outline further up: the
   component adapts to where it stands rather than the markup remembering to
   pick a modifier. */
.rail-nav { display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
.rail-nav__btn {
  width: 44px; height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1.5px solid var(--orange-ink);
  border-radius: 50%;
  color: var(--orange-ink);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  transition: background 160ms var(--ease), color 160ms var(--ease),
              border-color 160ms var(--ease), opacity 160ms var(--ease);
}
.rail-nav__btn:hover { background: var(--orange-ink); border-color: var(--orange-ink); color: var(--white); }
.rail-nav__btn[disabled] { color: var(--muted); border-color: var(--line); opacity: 0.55; cursor: default; background: none; }

/* On navy. Keeps the brand orange, which scores 4.79:1 there where
   --orange-ink drops to 2.70 — the same inversion the footer's LinkedIn
   mark documents. */
.section--navy .rail-nav__btn,
.hero .rail-nav__btn,
.footer-contact .rail-nav__btn {
  border-color: rgba(255, 255, 255, 0.45);
  color: var(--orange);
}
.section--navy .rail-nav__btn:hover,
.hero .rail-nav__btn:hover,
.footer-contact .rail-nav__btn:hover {
  background: var(--orange);
  border-color: var(--orange);
  color: var(--navy);
}
.section--navy .rail-nav__btn[disabled],
.hero .rail-nav__btn[disabled],
.footer-contact .rail-nav__btn[disabled] {
  color: var(--muted-on-navy);
  border-color: rgba(255, 255, 255, 0.2);
  background: none;
}

.rail-nav .btn { margin-left: 12px; }

/* A rail nav standing on its own above a full-bleed strip, rather than in the
   right-hand column of a .head-split. Needs the page padding the strip below
   it deliberately does without. */
.rail-nav--standalone {
  padding: 0 var(--pad) 16px;
}

/* "Bekijk meer projecten" stops one column short of the right edge of the text
   column. One column plus its gutter = 122 + 16 = 138px, in grid terms so it
   tracks --col. Measured in the XD: the button is 254px wide at x=1250, so its
   right edge is 1504 — exactly column 10's right edge, against a text column
   that ends at 1642.

   Scoped to .home-proof__head, which only the homepage carries: projecten.html
   uses the same .head-split and must keep its right edge flush. Held above
   1200px because below that the head-split collapses to a single column and
   the nav aligns left, where an inset on the right means nothing. */
@media (min-width: 1201px) {
  .home-proof__head .rail-nav { margin-right: calc(var(--col) + var(--gutter)); }
}

@media (max-width: 640px) {
  .rail__plate { bottom: 18px; left: 14px; right: 14px; }
  .rail__caption { font-size: 14px; padding: 12px 10px; }
  /* The buttons scroll one card at a time on a phone; the row goes full
     width so "Bekijk meer projecten" is not squeezed against them. */
  .rail-nav { justify-content: flex-start; flex-wrap: wrap; }
  .rail-nav .btn { margin-left: 0; flex: 1 1 100%; text-align: center; }
}

/* --- Tile row: three equal promo cards ----------------------------------
   "ZO WIL JE BOUWEN" on Home. Over Modulon used the same block until de
   definitieve webtekst daar vier kernwaarden vroeg — dat is nu .routes.
   Flex rather than grid so the odd card out centres itself instead of
   hanging off the left edge. Collapse: 3 → 2 → 1.
   ---------------------------------------------------------------------- */

/* --tile-gap so a page can tighten the row without a second component. Over
   Modulon runs both its tile rows and its card grid closer together than the
   homepage does; the 28px default is what every other page keeps. */
.tile-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--tile-gap, 28px);
  text-align: left;
}

.tile {
  background: var(--white);
  flex: 1 1 280px;
  max-width: 320px;
  display: flex;
  flex-direction: column;
}
.tile img { width: 100%; height: var(--tile-img-h, 190px); object-fit: cover; }
.tile__body { padding: 22px 22px 28px; }
/* Navy for the same reason as .card__title: a tile is never a link. The three
   promo tiles on Home and on Over Modulon share one call-to-action below the
   row, so a link-blue heading on each of them offers three destinations that
   do not exist. */
.tile__title { font-size: 21px; font-weight: 600; color: var(--navy-deep); margin: 0 0 10px; }
.tile__text { font-size: 13px; line-height: 1.6; color: var(--ink); margin: 0; text-wrap: pretty; }

/* --- Card grid: the three-across card block -----------------------------
   The 9 project cards on Projecten. Uses the .card component that
   nieuws-detail.html already ships. Collapse: 3 → 2 (1024) → 1 (640).
   The 6 process steps on Over Modulon used this grid until 3 september
   2026 — a photo tile per step, later an icon tile per step, both boxed
   in a repeated three-across card. Neither read as more than a template
   row once the photos were gone; see .process-card below (search
   "Process rail") for what replaced it.
   ---------------------------------------------------------------------- */

/* Widen a block from the text column out to the design's full content area.
   Opt-in, because most blocks should NOT do this: the design holds text to
   1364 and only lets card grids and specification rows run to 1640.

   Above 1492px this pulls 138px out of each side; below it --text-inset is 0
   and the rule is a no-op, so tablet and phone are untouched. Do not put this
   on anything containing running text. */
.to-content-edge {
  margin-inline: calc(var(--text-inset) * -1);
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Left at 28. The XD draws this row as 3 × 534 with 20px gutters, but with
     the grid now spanning the full 1640 content area, 28px gutters give 528px
     cards — 6px off the design and invisible. Going to 20 would also retune
     the row gap between stacked cards on mobile, since --card-gap sets both
     axes, and that is not worth 6px. */
  gap: var(--card-gap, 28px);
}
@media (max-width: 1024px) { .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .card-grid { grid-template-columns: 1fr; } }

/* --- Process rail: "ZO DOEN WE HET." on Over Modulon --------------------
   Fourth version of this section in one day (3 september 2026). Three
   vertical-list attempts — a single rail in a narrow column, that same
   rail beside a photo that didn't fit the copy, two rails side by side —
   all ran into the same wall: a list reads narrow, and narrow inside a
   wide section is empty space no matter how the list itself is drawn.
   Klantvoorbeeld: the Webflow "native-webflow-slider-overflow" showcase.
   What carries over is the mechanic, not its mint-on-forest-green colour
   scheme — a full-bleed rail where the next card visibly hangs off the
   edge, with arrows to page through it. That mechanic already exists on
   this site: .rail + .rail-nav is the exact component "DIT STAAT ER AL"
   uses for the homepage project rail, right down to the same site.js
   (setupRail/initRails runs on every .rail it finds, unmodified). A rail
   does not have this problem in the first place — it is allowed to run
   past the edge of the viewport, so six items never have to fit a fixed
   width. See .process-head / .process-card below for the card itself;
   .rail and .rail-nav are defined once, above, in section 16. */
.process-head { --head-mb: 48px; padding: 0 var(--pad); }

/* White on navy, the same inversion as a .stat tile on a navy detail band:
   the big number carries the step instead of an icon. No icon also settles
   the open question from the earlier versions of this section — a plain
   numbered card cannot read as the generic-construction-site icon row
   CLAUDE.md bans, because there is no icon left to read that way. */
.process-rail .rail__item { height: auto; }
.process-card {
  background: var(--white);
  box-shadow: var(--card-shadow);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 36px 28px 32px;
}
.process-card__number {
  display: block;
  font-size: var(--fs-stat);
  font-weight: 700;
  line-height: 1;
  color: var(--orange-ink);
  margin-bottom: 18px;
}
.process-card__title { font-size: 17px; font-weight: 600; color: var(--navy-deep); margin: 0 0 10px; }
.process-card__text { font-size: 13px; line-height: 1.7; color: var(--ink); margin: 0; text-wrap: pretty; }

/* --- Column set: two text columns ---------------------------------------
   "MINDER WERK OP DE BOUW." op De Modulonwand — die kop heeft precies twee
   alinea's, en in .cols-3 zou de derde kolom leeg blijven staan. Zelfde
   maatvoering als .cols-3 hieronder, met één inklapstap in plaats van twee.
   ---------------------------------------------------------------------- */

.cols-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 48px;
  align-items: start;
}
.cols-2 p { font-size: 14px; line-height: 1.75; margin: 0; text-wrap: pretty; }

@media (max-width: 768px) { .cols-2 { grid-template-columns: 1fr; gap: 24px; } }

/* --- Puntenlijst: een reeks korte regels onder een kop -------------------
   "HET MODULON VLOERSYSTEEM." op De Modulonvloer telt acht regels van één
   zin, die grammaticaal doorlopen op de kop erboven. In één kolom wordt dat
   een smal, hoog lint onder een brede kop; in twee kolommen leest het als
   één blok.

   Multicolumn en geen grid, omdat de regels sterk in lengte verschillen:
   column-fill balanceert ze dan zelf over de twee kolommen in plaats van er
   vier en vier van te maken met een gat onderaan de korte kolom.
   break-inside houdt een regel die wél wikkelt bij elkaar.

   Maat, kleur en de 48px tussenruimte zijn die van .cols-2 hierboven — dit
   is dezelfde tweekolomstekst, alleen met opsommingstekens — en het
   opsommingsteken volgt .tabpanel ul: --orange-ink, want een bolletje op wit
   is geen liniaal maar een grafisch element dat 3:1 moet halen.
   ---------------------------------------------------------------------- */

.bullets {
  margin: 0;
  padding-left: 18px;
  columns: 2;
  column-gap: 48px;
  font-size: 14px;
  line-height: 1.75;
  color: var(--ink);
}
.bullets li { margin-bottom: 12px; break-inside: avoid; text-wrap: pretty; }
.bullets li:last-child { margin-bottom: 0; }
.bullets li::marker { color: var(--orange-ink); }

@media (max-width: 768px) { .bullets { columns: 1; } }

/* --- Column set: three text columns -------------------------------------
   "BIM-tekening" on Over Modulon. Collapse: 3 → 2 (1024) → 1 (768).
   "In het echt nog beter!" on De Modulonwand used this too; that section is
   a two-column .split with the animation beside it since 19 augustus 2026.
   ---------------------------------------------------------------------- */

.cols-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 48px;
  align-items: start;
}
.cols-3 p { font-size: 14px; line-height: 1.75; margin: 0; text-wrap: pretty; }
.cols-3 p + .btn { margin-top: 20px; }

@media (max-width: 1024px) { .cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 40px; } }
@media (max-width: 768px)  { .cols-3 { grid-template-columns: 1fr; gap: 24px; } }

/* --- Carousel ------------------------------------------------------------
   The photo carousel beside "DE WINST ZIT VÓÓR DE BOUW." and the quote carousel in the
   testimonial. Both become ACF repeaters, so the dots are generated from the
   number of slides rather than written into the markup — the client adds a
   fourth photo and the fourth dot appears by itself.

   The track is a scroll-snap container, so swipe, trackpad and arrow keys all
   work natively and without JavaScript. The dots are the extra affordance.
   ---------------------------------------------------------------------- */

.carousel { position: relative; }

.carousel__track {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  height: 100%;
}
.carousel__track::-webkit-scrollbar { display: none; }

.carousel__slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  margin: 0;
  min-width: 0;
}

.carousel__dots {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0;
}

/* 44px tall. 32px rather than 44px wide: at 44 the dots sit so far apart
   they stop reading as one row. 32px still clears the 24px WCAG 2.2 AA
   minimum with room to spare, and no two hit areas touch. */
.carousel__dot {
  width: 32px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.carousel__dot::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--line);
  transition: background 200ms var(--ease), transform 200ms var(--ease);
}
.carousel__dot[aria-current="true"]::before { background: var(--blue-strip); transform: scale(1.15); }
.carousel__dot:hover::before { background: var(--blue); }

/* Dots sitting on top of a photo instead of under it. */
.carousel__dots--overlay {
  position: absolute;
  bottom: 8px;
  left: 0; right: 0;
  z-index: 2;
}
/* Over a photo there is no knowing what is underneath, so these carry their own
   shadow. At 45% white an inactive dot came in around 2.2:1 against a mid-tone
   photo, under the 3:1 WCAG asks of a control you have to be able to see; 75%
   plus the shadow holds against a light sky and a dark roof alike. */
.carousel__dots--overlay { filter: drop-shadow(0 1px 2px rgba(10, 16, 60, 0.55)); }
.carousel__dots--overlay .carousel__dot::before { background: rgba(255, 255, 255, 0.75); }
.carousel__dots--overlay .carousel__dot[aria-current="true"]::before { background: var(--white); }
.carousel__dots--overlay .carousel__dot:hover::before { background: var(--white); }

/* A carousel filling the image half of a .split. */
.split__media .carousel { position: absolute; inset: 0; }
.split__media .carousel__slide { position: relative; }
.split__media .carousel__slide img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.quote-slide { display: flex; flex-direction: column; align-items: center; }

/* --- Testimonial --------------------------------------------------------- */

.testimonial__eyebrow { font-size: 14px; font-weight: 500; color: var(--blue-strip); margin: 0 0 28px; }
.testimonial blockquote {
  font-size: clamp(17px, 0.19vw + 16px, 19px);
  line-height: 1.55;
  color: var(--ink-strong);
  max-width: 460px;
  margin: 0 auto 32px;
  font-weight: 400;
  text-wrap: pretty;
}
.testimonial__cite { font-size: 13px; line-height: 1.6; color: var(--ink); font-style: normal; display: block; }

/* --- Partner logo row ----------------------------------------------------
   Five logos. Collapse: 5 across → wraps to 3 + 2 → 2 per row on a phone.
   The gap does most of the work, so no breakpoint is needed.
   ---------------------------------------------------------------------- */

.partner-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(28px, 4.4vw, 72px);
  padding: 56px var(--pad);
}
.partner-row img {
  height: clamp(28px, 2.9vw, 44px);
  width: auto;
  max-width: 200px;
  object-fit: contain;
}

/* ==========================================================================
   17. De Modulonwand
   ========================================================================== */

/* The five-step tab set. On desktop a wrapped row; on a phone a horizontal
   scroller, because five labels at 15px semibold need ~490px and wrapping
   them onto three lines reads as a broken list rather than a control. */

/* De overdrachtszin boven de tabrij loopt even breed als de kop erboven
   (klantwens 25-08-2026: "even breed als de titel"). De gedeelde maat van
   .section__lead is 460px en dat is hier te smal: de kolom is 837px op 1440
   en 1116px op 1920, dus de zin stapelde vier regels hoog onder een kop van
   613px. 620px is gemeten, niet gekozen — het is de breedte van het blok
   eronder: de kop is 613px breed op 1440 en de tabrij 626px, dus de zin valt
   tussen die twee in en de linkerkolom leest als een blok in plaats van als
   een smalle kolom onder een brede kop.

   Vastgehouden aan de twee-koloms-stand: onder 769px klapt de split om, staat
   de zin in de tekstkolom en valt hij terug op de gedeelde 460px, net als elke
   andere section lead. Boven 1441 wikkelt de kop zelf naar twee regels
   (text-wrap: balance op 560px), en daar is 620px de bovengrens van de zin. */
@media (min-width: 769px) {
  .section__lead--wide { max-width: 620px; }
}

.tabs {
  --tab-gap: 28px;
  display: flex;
  flex-wrap: wrap;
  gap: var(--tab-gap);
  margin: 0 0 28px;
  padding: 0;
  list-style: none;
}

/* ---- Het overdrachtsstreepje ---------------------------------------------
   Fase 1 t/m 3 doet Modulon, fase 4 t/m 6 gebeurt op de bouwplaats. Dat
   moment krijgt een verticaal oranje streepje tussen Transport en Opperen
   (klantwens 21-08-2026). Een liniaal, dus --orange en niet --orange-ink,
   net als de onderstreping van de actieve tab.

   Het streepje hangt aan de tab ERNA en staat niet als eigen element tussen
   de twee in. Dat is het hele punt: deze rij wikkelt op 1920, 1025 en 769 naar
   twee regels, en op 769 valt de breuk precies hier. Een los element zou dan
   als een zwevend streepje aan het eind van de eerste regel achterblijven;
   nu verhuist het mee en staat het vóór Opperen, waar het nog steeds zegt wat
   het moet zeggen.

   De padding is --tab-gap, zodat het streepje midden in de tussenruimte staat
   en op beide maten (28px, en 22px onder 768) even ver van beide labels blijft.
   Het is een pseudo-element en dus decoratie: een schermlezer krijgt het niet
   mee. De zin boven de tabrij draagt dezelfde boodschap in woorden.
   ------------------------------------------------------------------------- */
.tabs__handover { position: relative; padding-left: var(--tab-gap); }
.tabs__handover::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 2px;
  height: 22px;
  background: var(--orange);
}

.tab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 10px 0;
  min-height: 44px;
  cursor: pointer;
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
  transition: color 160ms var(--ease), border-color 160ms var(--ease);
}
.tab:hover { color: var(--blue); }
.tab[aria-selected="true"] { color: var(--blue); border-bottom-color: var(--orange); }

/* ---- De panelen stapelen -------------------------------------------------
   De zes fasepanelen op De Modulonwand verschillen in lengte — drie regels
   tegen vier — en onder elkaar in de flowkolom betekende dat een sectie die
   26px inzakte zodra je op Opperen of Afwerking klikte (klantwens
   21-08-2026: de hoogte moet gelijk blijven).

   Eén gridcel, alle zes erin: de cel is zo hoog als het langste paneel, dus
   de hoogte staat vast en past zich vanzelf aan als de klant de tekst later
   langer maakt. Een vast getal zou dat niet doen.

   Het verborgen paneel moet daarvoor zijn plek houden, en dat is precies wat
   het `hidden`-attribuut niet doet — de browser zet er `display: none` op.
   Hier dus omgekeerd van de regel elders in dit bestand: `display` wordt
   teruggezet en `visibility` neemt het verbergen over. Dat haalt het paneel
   even goed uit de voorleesvolgorde en uit de tabvolgorde (de tabindex="0"
   erop telt niet meer mee), maar laat de ruimte staan.

   De fotokolom hiernaast verspringt sowieso niet: die staat op
   aspect-ratio 4/3 en de foto's erin zijn absoluut geplaatst.
   ------------------------------------------------------------------------- */
.tabpanels { display: grid; }
.tabpanels > .tabpanel { grid-area: 1 / 1; }
.tabpanels > .tabpanel[hidden] { display: block; visibility: hidden; }

.tabpanel p { font-size: 15px; line-height: 1.75; color: var(--ink); margin: 0 0 20px; max-width: 520px; text-wrap: pretty; }
.tabpanel ul { font-size: 15px; line-height: 1.9; color: var(--ink); margin: 0 0 28px; padding-left: 18px; }
.tabpanel li::marker { color: var(--orange-ink); }

/* Hier stond .voordelen: de rij "VOORDEEL 1 · VOORDEEL 2 · VOORDEEL 3" onder
   het stappenplan op De Modulonwand. Een plaatshouder uit het ontwerpbestand
   waar nooit inhoud voor kwam — de webtekst benoemt geen voordelen per stap —
   en op 21-08-2026 op verzoek van de klant geschrapt. Niet terugzetten zonder
   nieuw besluit; dit was de enige plek op de site die de klasse gebruikte. */

/* De foto naast het fasetraject wisselt mee met de gekozen fase. Alle zes
   staan in de DOM en vijf zijn hidden — een verborgen lazy image wordt niet
   opgehaald, dus die vijf kosten niets tot iemand de fase kiest, en met
   JavaScript uit staat de foto van fase 1 er nog steeds.
   .split__media img is al absolute met inset: 0, dus de zes stapelen
   zichzelf; alleen de display-schakelaar moet hier expliciet staan. */
.step-media img[hidden] { display: none; }

/* Hier stond .step-media--render: object-fit: contain plus een --surface vlak,
   voor het besluit van 19-08-2026 dat het beeld naast de fases een render zou
   zijn en geen foto. Op 21-08-2026 leverde de klant per fase eigen fotografie
   aan (de presentatie "Modulon V8 Bezoek Demoruimte") en is dat besluit
   teruggedraaid. Het zijn nu gewone foto's, dus .split__media img met cover
   volstaat en de modifier is nergens meer in gebruik. Komen die renders er
   alsnog, dan hoort dit blok terug — niet een van de twee helften.
   .step-media zelf blijft: dat is de display-schakelaar hierboven. */

/* Hier stond .rail-note: de paginapadding voor de open vraag onder de
   fotobalk op De Modulonwand. Die balk is er op 21-08-2026 op verzoek van de
   klant uit gehaald en de noot ermee, en het was de enige plek op de site die
   deze klasse gebruikte. */

@media (max-width: 768px) {
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    --tab-gap: 22px;
    scrollbar-width: none;
    /* Bleeds past the section padding so the last tab is visibly cut off —
       that edge is what tells you the row scrolls. */
    margin-inline: calc(var(--pad) * -1);
    padding-inline: var(--pad);
    /* Required, not cosmetic. .split__text is a flex column with
       align-items: flex-start, so its children size to their own content —
       a nowrap row of five tabs would be 506px wide inside a 375px screen
       and overflow-x would never engage. Pinning the width to the column
       (plus the two bleed margins) is what makes it scroll instead. */
    width: calc(100% + var(--pad) * 2);
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tabs > li, .tab { flex: 0 0 auto; }
}

/* ==========================================================================
   18. Projecten (overview)
   ========================================================================== */

.search-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin-top: 12px;
  font-size: 13px;
  color: var(--ink-soft);
}

.search-sort {
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--blue);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0;
}
.search-sort:hover { color: var(--orange-ink-hover); }

/* Leeg-resultaat bij zoeken. Staat op --surface zodat het een blok is en niet
   een losse regel in een verder lege sectie. */
.search-empty {
  background: var(--surface);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.7;
  padding: 28px 32px;
  margin: 0;
  text-wrap: pretty;
}
.search-empty[hidden] { display: none; }

/* Pagination. Every control is a 44px target — in the design they are bare
   14px glyphs about 12px tall, which is not tappable. */
.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 56px;
}

.pagination a,
.pagination button,
.pagination span {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0 6px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 400;
  color: var(--ink);
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color 160ms var(--ease), border-color 160ms var(--ease);
}
.pagination a:hover,
.pagination button:hover { color: var(--orange-ink-hover); }
.pagination [aria-current="page"] {
  font-weight: 700;
  color: var(--navy-deep);
  border-bottom-color: var(--navy-deep);
}
/* .pagination is display: flex, which beats the UA `[hidden]` rule — see the
   note on .card[hidden]. The filter hides the pager while a search is active. */
.pagination[hidden] { display: none; }

/* De <span>s in deze rij zijn géén knoppen: het huidige paginanummer en de
   "…". De gedeelde regel hierboven zet cursor: pointer voor de <a>'s, en die
   erfden ze mee — een handje boven iets wat niet reageert. */
.pagination span { cursor: default; }
.pagination__gap { cursor: default; color: var(--muted); min-width: 24px; }
.pagination__next { font-size: 20px; color: var(--navy-deep); }

/* ==========================================================================
   19. Project Detail
   ========================================================================== */

/* De fotoband achter de titel én de specificatiestrook.
   De foto zat eerst op een vlakke 35% dekking binnen alleen het titelvak: een
   rechthoek met een harde rand boven én onder, en overal even sterk vertroebeld.
   De vormgever vroeg om een weglopende overlay over het onderste deel van de
   hero, "zo ademt de pagina meer bouw" (30 juli 2026) — dus: de foto staat
   bovenaan open en lost naar onderen op in het marineblauw, waar de zes
   specificaties op zitten.

   Twee dingen bewust zo:

   1. Het weglopen is een MASK op de foto, geen marineblauwe gradient erover.
      De hero-achtergrond is --gradient-navy, een diagonaal verloop; een
      overlay die naar var(--navy) toewerkt zou onderaan naast de kleur zitten
      die daar werkelijk staat, en dat is precies waar je een naad ziet. Door
      de foto zelf te laten verdwijnen komt de bestaande gradient tevoorschijn
      en is er geen tweede kleur in het spel.
   2. --white in het mask is geen kleur maar een dekkende waarde: bij de
      standaard mask-mode telt alleen het alfakanaal. Het is een token in
      plaats van `black`/`#000` zodat er nergens buiten :root een losse
      kleurwaarde staat. Zie CLAUDE.md, "Design tokens".

   --photo-open is tot waar de foto op volle sterkte staat, --photo-gone waar
   hij helemaal weg is. Die tweede hoort net boven de eerste labelregel te
   liggen, zodat de zes specificaties op schoon marineblauw staan.

   Waarom die twee bij elke breakpoint opnieuw gezet worden en niet één keer:
   de band is overal ongeveer even hoog (414–443px), maar wát die hoogte vult
   verschuift volledig. De kop groeit van 34px naar 84px terwijl de strook van
   twee kolommen naar zes gaat, en daardoor begint de eerste labelregel op 375
   al op 38% van de band en op 1440 pas op 70%. Eén percentage dat op beide
   klopt bestaat niet. Op smalle schermen wordt ook --photo-open meegetrokken,
   anders wordt de overgang zo kort dat het weer een rand is in plaats van een
   verloop. */
.detail-band {
  position: relative;
  /* Een absoluut geplaatst element wordt getekend ná de in-flow inhoud, dus
     zonder dit lag de foto óver de zes specificaties heen — de oude opzet
     omzeilde dat met `position: relative` op alleen de h1, maar nu de foto ook
     achter de strook doorloopt zijn dat zes elementen. `isolation` maakt van
     deze band een eigen stapelcontext, zodat de z-index: -1 hieronder niet
     dóórvalt tot achter het marineblauwe verloop van .hero. */
  isolation: isolate;
  --photo-open: 28%;
  --photo-gone: 68%;
}
.detail-band__photo {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* 0.40, iets boven de oude 0.35: de foto mag sterker staan nu hij niet meer
     over de volle hoogte even zwaar vertroebeld is. Dit is een plafond en geen
     smaakkeuze, dus verhoog het niet zonder opnieuw te meten: op de lichtste
     plek achter de kop haalt wit hier 3,72:1 en de 84px-kop heeft 3:1 nodig.
     Op 0.45 zakt datzelfde punt naar 3,24:1 en is de marge weg. De strook
     eronder zit ruim goed — labels 6,53:1, waarden 10,6:1.

     Per project instelbaar via --photo-strength op .detail-band: een donkere
     bouwfoto kan hoger, en dat is een keuze per foto. In WordPress hoort dat
     dus een ACF-veld te zijn en geen vaste waarde in het template. */
  opacity: var(--photo-strength, 0.40);
  /* --photo-in is de tegenhanger van --photo-gone: waar de foto bovenaan op
     volle sterkte is. Op Project Detail staat hij op 0% — daar begint de band
     bovenin de pagina en is er niets om vanaf te lopen. Op een bericht begint
     de band pas ónder de header, en een foto die daar op volle sterkte inzet
     trekt een zichtbare horizontale naad over de hele breedte: het leest dan
     als een banner onder de header in plaats van als één hero. Zie §13. */
  mask-image: linear-gradient(to bottom,
    transparent 0%,
    var(--white) var(--photo-in, 0%),
    var(--white) var(--photo-open),
    transparent var(--photo-gone));
}

.detail-hero { position: relative; text-align: center; padding: 56px var(--pad) 0; }
.detail-hero h1 {
  position: relative;
  font-size: var(--fs-h1-lg);
  line-height: 1.05;
  font-weight: 800;
  letter-spacing: var(--track-xl);
  margin: 0;
  color: var(--white);
  text-wrap: balance;
}

/* The six-across meta row — the tightest grid in the whole design.
   Collapse: 6 → 3 (1024) → 2 (640). Never one column: six stacked
   label/value pairs is a list, not a spec strip. */
/* Six specification cells across the full content area — measured in the XD at
   140 → 1780, six 276px columns, so --pad-outer rather than --pad. */
.meta-row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 24px;
  padding: 72px var(--pad-outer) 56px;
  text-align: center;
}
.meta-row__label { font-size: 13px; color: var(--muted-on-navy); margin-bottom: 6px; }
.meta-row__value {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--white);
  text-transform: uppercase;
  text-wrap: pretty;
}

/* De fotoband schuift mee met de strook: elke keer dat de strook een kolom
   inlevert wordt hij hoger, begint de eerste labelregel eerder in de band, en
   moet de foto dus eerder weg zijn. Zie de noot bij .detail-band. */
@media (max-width: 1200px) {
  .detail-band { --photo-gone: 64%; }
}
@media (max-width: 1024px) {
  .meta-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px 24px; padding: 48px var(--pad) 44px; }
  .detail-band { --photo-open: 24%; --photo-gone: 54%; }
}
@media (max-width: 768px) {
  .detail-band { --photo-open: 20%; --photo-gone: 48%; }
}
@media (max-width: 640px) {
  .meta-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 20px; padding: 36px var(--pad); text-align: left; }
  .detail-band { --photo-open: 12%; --photo-gone: 40%; }
}
@media (max-width: 480px) {
  .detail-band { --photo-open: 10%; --photo-gone: 36%; }
}

/* Stat band. Collapse: 3 → 1 at 640 — a 52px number cannot share a 640px row
   three ways once the 28px cell padding is taken off. */
.stat-band {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
  max-width: 900px;
  margin: 0 auto;
}
.stat { background: var(--white); padding: 40px 28px 44px; }
.stat__value { font-size: var(--fs-stat); font-weight: 700; color: var(--navy-deep); line-height: 1; margin: 0; }
.stat__unit { font-size: 18px; font-weight: 500; color: var(--navy-deep); margin: 6px 0 18px; }
.stat__label { font-size: 14px; line-height: 1.6; color: var(--ink); text-wrap: pretty; margin: 0; }

@media (max-width: 640px) {
  .stat-band { grid-template-columns: 1fr; }
  .stat { padding: 28px 24px 30px; }
}

/* ==========================================================================
   20. Over Modulon
   ========================================================================== */

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 64px;
  align-items: start;
}
.about-text { max-width: 520px; }
.about-text p { font-size: 14px; line-height: 1.75; color: var(--ink); margin: 0 0 18px; text-wrap: pretty; }
.about-text p.is-first { font-size: 15px; margin-bottom: 20px; }
.about-text h2 { margin: 44px 0 20px; }
/* De kolom opent nu met een h2; de 44px die kopjes verderop van de vorige
   alinea scheidt hoort niet bovenaan te staan, anders zakt de tekstkolom
   ten opzichte van de foto ernaast. */
.about-text > :first-child { margin-top: 0; }
.about-text .about-pull {
  font-size: var(--fs-lead);
  line-height: 1.4;
  font-weight: 500;
  color: var(--blue);
  margin: 0 0 28px;
}
.about-text .btn,
.about-text .text-link { margin-top: 16px; }

.about-photo { position: relative; height: 620px; position: sticky; top: 40px; }
.about-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* De logo-animatie in plaats van een foto. Anders dan een foto mag deze clip
   niet bijgesneden worden: het is een wordmark, en `cover` in een 620px hoge
   kolom hakt de letters aan beide kanten af. De box krijgt daarom de 3:2 van
   de clip zelf in plaats van een vaste hoogte, en het beeld staat gewoon in de
   flow — de absolute positionering hierboven geldt alleen voor `img`.
   De clip is gerenderd op een vlak lichtblauw veld dat drie eenheden van
   --surface afligt, dus de videobox ís het paneel: een wrapper met een eigen
   achtergrond zou een naad geven. --surface dekt alleen het moment vóór de
   eerste frame. Blijft sticky: de animatie loopt terwijl de tekst passeert. */
.about-photo--anim { height: auto; }
.about-photo--anim video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  background: var(--surface);
}

@media (max-width: 1024px) {
  .about-grid { gap: 40px; }
  .about-photo { height: 460px; }
  .about-photo--anim { height: auto; }
}
@media (max-width: 768px) {
  .about-grid { grid-template-columns: 1fr; gap: 36px; }
  /* A 620px photo below 900 words of text never gets seen. It leads instead.
     `relative`, NOT `static`: the <img> inside is absolutely positioned, so
     dropping the parent out of the positioning context entirely sends it to
     the top of the viewport where it covers the hero. `top: auto` cancels
     the sticky offset. */
  .about-photo { order: -1; position: relative; top: auto; height: auto; aspect-ratio: 4 / 3; }
  /* De animatievariant houdt zijn eigen 3:2: een 4/3 op de ouder zou de clip
     in de flow eronder tegenspreken en een strook onder het logo laten staan. */
  .about-photo--anim { aspect-ratio: auto; }
  .about-text { max-width: none; }
}

/* ==========================================================================
   21. Nieuws & downloads
   ==========================================================================

   De klassenamen hieronder beginnen met .kennis- en dat blijft zo. De pagina
   heette tot 2 september 2026 Kennis & downloads; de paginanaam, de bestands-
   naam en de URL zijn toen alle drie Nieuws geworden. Een klassenaam is geen
   van drieën: hij is niet zichtbaar en niet adresseerbaar, en deze stylesheet
   is de goedgekeurde basis die het WordPress-thema deelt. Hernoemen levert
   niets op en raakt twee kopieën tegelijk. Zie prototype/README.md.
   ========================================================================== */

/* The pale downloads panel runs flush to the left viewport edge in the
   design, so the section itself carries no horizontal padding and each
   column pads itself. */
.kennis-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: 48px;
  align-items: start;
}

/* Uitgerekend in plaats van geschat: de sectie is flush, dus de kolommen zijn
   1fr en 2fr met 48px ertussen — de berichtenkolom begint op
   (100% - 48px) / 3 + 48px. De oude `33%` zat daar ~32px naast, waardoor de
   intro net niet boven de eerste tegel uitkwam. De sectie is flush, dus rechts
   moet de intro haar eigen marge nemen of ze loopt tot de schermrand.
   De kop "BERICHTEN" die hier boven de intro stond is eruit (21-08-2026,
   klantwens); `.kennis-heading` bestaat daarmee nergens meer. */
.kennis-intro { margin-left: calc(33.3333% + 32px); margin-right: var(--pad-min); }

/* Introparagraaf uit webtekst v2 (20-08-2026); 560px is dezelfde leesmaat
   als .section__intro. */
.kennis-intro {
  max-width: 560px;
  margin-top: 0;
  margin-bottom: 48px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink);
  text-wrap: pretty;
}

.downloads {
  background: var(--surface);
  padding: 48px 40px 56px var(--pad);
}
/* Links uitgelijnd, niet gecentreerd: de kop staat daarmee op dezelfde
   linkerlijn als de PDF-links eronder (vormgever, 30 juli 2026). */
.downloads h2 {
  font-size: var(--fs-h2-xs);
  letter-spacing: 1px;
  color: var(--blue);
  margin: 0 0 28px;
}
.downloads__list { display: flex; flex-direction: column; gap: 8px; }

/* 44px minimum on every PDF link — in the design these are 14px underlined
   text with no padding at all. */
.download-link {
  display: flex;
  gap: 12px;
  align-items: center;
  min-height: 44px;
  padding: 6px 0;
  color: var(--navy-deep);
  font-size: 14px;
  line-height: 1.5;
  text-decoration: underline;
}
.download-link:hover { color: var(--orange-ink-hover); }
.download-link__badge {
  flex: 0 0 auto;
  background: var(--orange-ink);
  color: var(--white);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.5px;
  padding: 3px 5px;
  border-radius: 2px;
  text-decoration: none;
}

/* Geen padding rechts: de tegels lopen door tot de rechter schermrand, net
   zoals het lichtblauwe downloadblok links tot de rand doorloopt (vormgever,
   30 juli 2026). Vanaf 1024 stapelen de kolommen en krijgt de lijst wél
   zijpadding terug — een tegel die daar over de volle breedte bloedt zet zijn
   schaduw tegen de schermrand aan. */
.post-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-items: flex-start;
}
.post-list > .btn { margin-top: 12px; }

/* The article card. The one grid in the design that is a fixed pixel column
   rather than a fraction, so it is also the one that has to be re-thought
   rather than reflowed. Collapse: 220px 1fr → 180px 1fr (1024) → image
   above text (640), pinned to 16:9 so the crop never changes shape. */
.post-card {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  width: 100%;
  background: var(--white);
  box-shadow: var(--card-shadow-sm);
}
.post-card__media { position: relative; min-height: 190px; }
.post-card__media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.post-card__body {
  padding: 26px 28px 28px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}
.post-card__title { font-size: 18px; font-weight: 600; line-height: 1.35; color: var(--blue); margin: 0; }
.post-card__text { font-size: 13px; line-height: 1.7; color: var(--ink); margin: 0; text-wrap: pretty; }

/* The promo block at the foot of the page. Outer 0.9fr 1.6fr → 1 at 1024;
   the two paragraphs inside it → 1 at 768. */
.kennis-promo {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.6fr);
  gap: 56px;
  align-items: center;
}
/* Tot 18-09-2026 was dit 720/634, de verhouding van de uitschuif-animatie die
   hier stond. De bedrijfsfilm die ervoor in de plaats kwam is een gewone 16:9-
   opname, geen eigen marge zoals de vorige clip had (zie de sectie zelf: die
   is niet meer .section--hug-media), dus de mediakolom volgt nu gewoon de
   verhouding van de video. */
.kennis-promo__media { position: relative; aspect-ratio: 16 / 9; }
.kennis-promo__media img,
.kennis-promo__media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
/* De afspeelknop staat los van de aspect-ratio-regel hierboven (die geldt
   alleen voor img/video) en blijft daardoor gewoon in het midden staan,
   ongeacht de grootte van de mediakolom. Verdwijnt zodra data-video-play in
   site.js .is-playing zet. */
.kennis-promo__play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 76px; height: 76px;
  display: inline-flex; align-items: center; justify-content: center;
  padding-left: 4px; /* optische centrering van de driehoek */
  background: var(--orange-ink);
  color: var(--white);
  border: none;
  border-radius: 50%;
  box-shadow: var(--card-shadow);
  cursor: pointer;
  transition: background 160ms var(--ease), transform 160ms var(--ease);
}
.kennis-promo__play:hover,
.kennis-promo__play:focus-visible { background: var(--orange-ink-hover); transform: translate(-50%, -50%) scale(1.06); }
.kennis-promo__media.is-playing .kennis-promo__play { display: none; }
.kennis-promo h2 { font-size: var(--fs-h2-xs); color: var(--blue); margin: 0 0 22px; }
/* Eén kolom, niet twee (21 augustus 2026). Twee kolommen maakten de tekst
   163px hoog tegen een mediakolom van 398px, en die 235px verschil verdeelt
   align-items: center netjes boven én onder de tekst — dat was de witruimte
   waar het over ging. Onder elkaar is dezelfde tekst ruim 100px hoger en staat
   het beeld dus veel minder ver onder de tekst uit.

   De max-width hoort erbij: de tekstkolom is hier 803px breed en dat is op 14px
   lopende tekst zo'n 130 tekens per regel. Twee kolommen hielden de regellengte
   vanzelf in toom; één kolom moet dat zelf doen. */
.kennis-promo__cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; max-width: 34rem; }
.kennis-promo__cols p { font-size: 14px; line-height: 1.75; color: var(--ink); margin: 0; text-wrap: pretty; }
.kennis-promo__cols p + .btn { margin-top: 20px; }

@media (max-width: 1024px) {
  .kennis-layout { grid-template-columns: 1fr; gap: 40px; }
  .kennis-intro { margin-left: var(--pad); margin-right: var(--pad); }

  /* Gestapeld staan de berichten bovenaan, niet de downloads. De oude volgorde
     volgde de leesrichting van links naar rechts, maar zodra de kolommen op
     elkaar komen zet dat de intro pal boven het downloadblok — een tekst die
     het verkeerde blok inleidt. Dit is wat de vormgever bedoelt met
     "valt vooral op op mobile" (30 juli 2026). De kolommen blijven op desktop
     naast elkaar staan; alleen de stapelvolgorde draait om. */
  .post-list { order: 1; padding-inline: var(--pad); }
  .downloads { order: 2; padding: 40px var(--pad) 44px; }
  .post-card { grid-template-columns: 180px minmax(0, 1fr); }
  .kennis-promo { grid-template-columns: 1fr; gap: 32px; }
  /* Gestapeld is de mediakolom de volle paginabreedte, en op de verhouding van
     de clip zou het beeld daarmee bijna een scherm hoog worden. Afgetopt op
     420px en gecentreerd; onder die breedte loopt het beeld gewoon mee met het
     scherm.

     De width: 100% hoort erbij: met alleen een auto-marge valt een gridcel
     terug op fit-content, en de mediakolom heeft geen inhoud in de flow —
     alleen een absoluut geplaatste video — dus die breedte is nul. */
  .kennis-promo__media { width: 100%; max-width: 420px; margin-inline: auto; }
}

@media (max-width: 640px) {
  .post-card { grid-template-columns: 1fr; }
  .post-card__media { min-height: 0; aspect-ratio: 16 / 9; }
  .post-card__body { padding: 22px 22px 26px; }
}

/* ==========================================================================
   22. Homepage
   Everything below is layout that belongs to index.html alone. The blocks it
   shares with the other five pages — .split, .rail, .tile-row, .head-split,
   .partner-row — are in section 16.
   ========================================================================== */

.home-hero__inner { padding-top: 32px; }
.home-hero .hero__lead--lg { margin: 28px 0 36px; }

/* "DIT STAAT ER AL" — the heading pads itself because the section around it
   is flush, so the rail underneath can bleed to the viewport edge. */
.home-proof__head {
  --head-mb: 48px;
  padding: 0 var(--pad);
}

/* The intro here is three paragraphs, not the usual one, so the shared 460px
   measure on .section__lead stacked them eight lines high while the column
   around them is 706px wide at 1470 and 718px at 1920 (klantwens 21 augustus
   2026: breder maken in plaats van inkorten). 660px is measured, not picked:
   it is the last width at which a line-break is actually won — 460 wraps
   3/3/2, 580 wraps 3/2/1, 660 wraps 2/2/1 and 700, 760 and 820 all wrap 2/2/1
   as well. So the block goes from 328px to 244px tall and anything past 660
   only lengthens the measure for nothing. It stays inside the existing
   1.2fr 1fr split — the rail-nav beside it needs 323px plus its 138px
   column inset and keeps every pixel of that.

   Held to the two-column state: below 769px the .head-split collapses and the
   intro spans the text column, where it falls back to the shared 460px like
   every other section lead. */
@media (min-width: 769px) {
  .home-proof__head .section__lead { max-width: 660px; }
}

.home-benefits {
  padding: var(--section) var(--pad) var(--section-tight);
  text-align: center;
}
/* One of the two Extrabold exceptions to the SemiBold heading default (the
   heroes are the other). Class+element beats the .h2--lg modifier on the
   element itself, so the weight lands whether or not this block sits in a
   .section. "Waar wij voor staan." on Over Modulon sits outside this
   wrapper, so it stays SemiBold like every other section heading. */
.home-benefits h2 {
  color: var(--white);
  font-weight: 800;
  letter-spacing: clamp(0.5px, 0.21vw, 3px);
  margin: 0 0 56px;
}
.home-benefits .tile-row { margin-bottom: 52px; }

/* The quote carousel needs a fixed height, or the track jumps as you move
   between a two-line quote and a four-line one. */
.home-testimonial .carousel__track { align-items: flex-start; min-height: 190px; }
.home-testimonial .carousel__dots { margin-top: 20px; }
@media (max-width: 640px) {
  .home-testimonial .carousel__track { min-height: 240px; }
}

/* The rail is a <ul>; the design has no list semantics to show. */
.rail--plain { list-style: none; margin: 0; padding: 0; }

/* ==========================================================================
   23. Juridische pagina's — AVG-privacyverklaring, Disclaimer, Algemene
   Voorwaarden. Gebouwd uit de drie los aangeleverde .dc.html-ontwerpen
   (Modulon homepage wireframe/), waarvan de breekpunten daar al waren
   teruggebracht naar deze set (1200/1024/768/480) voordat de pagina's hier
   landden. De hero hergebruikt .hero + .hero__inner--split, en de
   tekstkolom hergebruikt .article-body — alleen de inhoudsopgave, de
   gegevenskaart, de sluit-CTA en een tabelstijl voor de AVG-tekst zijn hier
   nieuw. Geen foto in de hero: dat is het eigen ontwerp van deze drie
   pagina's, niet een vergeten .hero__photo — zie prototype/README.md.
   ========================================================================== */

.legal-hero__meta {
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--muted-on-navy);
  margin: 22px 0 0;
}

.legal-layout {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 56px;
  align-items: start;
  padding: var(--section-tight) var(--pad) 0;
}

.legal-toc {
  position: sticky;
  top: 24px;
  background: var(--surface);
  padding: 28px 26px 32px;
}
.legal-toc h2 {
  font-size: 13px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--blue);
  margin: 0 0 18px;
}
.legal-toc ol {
  margin: 0;
  padding: 0 0 0 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.legal-toc li { font-size: 13px; line-height: 1.45; color: var(--muted); }
.legal-toc a { color: var(--navy-deep); text-decoration: none; }
.legal-toc a:hover { color: var(--orange-ink); text-decoration: underline; }

/* De bedrijfskaart (Modulon BV, adres, KvK) — dezelfde --surface-behandeling
   als .downloads en .article-body blockquote: het vlak zelf is het teken,
   geen randstreep. */
.legal-card {
  background: var(--surface);
  padding: 26px 28px;
  margin: 24px 0 8px;
  font-size: 15px;
  line-height: 1.8;
  color: var(--ink);
}
.legal-card strong {
  display: block;
  color: var(--blue);
  font-size: 17px;
  font-weight: 700;
  margin-bottom: 6px;
}

/* Eyebrow boven een h2 in de Algemene Voorwaarden ("Artikel 1"). */
.article-body .artnr {
  display: block;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--orange-ink);
  margin-bottom: 6px;
}

/* Eén tabel, in de AVG-tekst (doeleinden en grondslagen) — generiek genoeg
   om ook in een toekomstig bericht te hergebruiken. */
.article-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 24px;
}
.article-body th {
  text-align: left;
  font-size: 12px;
  letter-spacing: 1px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--blue);
  padding: 0 16px 10px 0;
  border-bottom: 2px solid var(--surface);
}
.article-body td {
  font-size: 14px;
  line-height: 1.65;
  color: var(--ink);
  padding: 14px 16px 14px 0;
  border-bottom: 1px solid var(--surface);
  vertical-align: top;
}
.article-body td:last-child,
.article-body th:last-child { padding-right: 0; }

.legal-cta {
  background: var(--surface);
  margin-top: var(--section-tight);
  padding: 64px var(--pad);
  display: grid;
  grid-template-columns: 1.4fr auto;
  gap: 40px;
  align-items: center;
}
.legal-cta h2 { font-size: var(--fs-h2-32); color: var(--blue); margin: 0 0 12px; }
.legal-cta p { font-size: 15px; line-height: 1.7; color: var(--ink); margin: 0; max-width: 560px; }

@media (max-width: 1024px) {
  .legal-layout { grid-template-columns: 1fr; gap: 32px; padding-top: var(--section-tight); }
  .legal-toc { position: static; }
  .legal-cta { grid-template-columns: 1fr; padding: 48px var(--pad); text-align: left; }
}
@media (max-width: 768px) {
  .article-body table,
  .article-body tbody,
  .article-body tr,
  .article-body td { display: block; width: 100%; }
  .article-body thead { display: none; }
  .article-body tr { border-bottom: 1px solid var(--surface); padding: 12px 0; }
  .article-body td { border: none; padding: 4px 0; }
}

/* ==========================================================================
   24. Cookie consent (29 September 2026). Only rendered by the Astro site,
   and only once a tracking ID (GTM_ID or GA4_ID) is set: without tracking
   the site sets no cookies and needs no banner. A bar at the bottom, not a
   modal: the page stays usable, and nothing loads from Google until the
   visitor presses Accepteren. Both choices are the same size and equally
   easy to reach, as the Autoriteit Persoonsgegevens asks.
   ========================================================================== */
.cookie-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 70; /* above the sticky header (60) — they never overlap anyway */
  max-width: 760px;
  margin: 0 auto;
  background: var(--navy);
  color: var(--ink-on-navy);
  padding: 20px 24px;
  border-radius: 6px;
  box-shadow: var(--card-shadow);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 16px 24px;
  align-items: center;
}
/* An author display overrides the [hidden] attribute; restate it. */
.cookie-banner[hidden] { display: none; }
.cookie-banner p { margin: 0; font-size: 14px; line-height: 1.6; }
.cookie-banner a {
  color: var(--white);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cookie-banner .btn-pair { flex-wrap: nowrap; gap: 12px; }
/* On navy the focus ring takes the brand orange, as in every navy band. */
.cookie-banner :focus-visible { outline-color: var(--orange); }

/* "Cookie-instellingen" in the footer is a button (it opens the banner, it
   goes nowhere) dressed as the legal links beside it. */
.footer-cookie-link {
  font: inherit;
  color: var(--ink-on-navy);
  background: none;
  border: 0;
  padding: 2px 0;
  cursor: pointer;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.footer-cookie-link:hover { color: var(--white); }

@media (max-width: 1000px), (pointer: coarse) {
  .footer-cookie-link { display: inline-flex; align-items: center; min-height: 44px; padding: 0; }
}
@media (max-width: 640px) {
  .cookie-banner { grid-template-columns: 1fr; padding: 18px 20px; }
  .cookie-banner .btn-pair { flex-wrap: wrap; }
  .cookie-banner .btn { flex: 1 1 0; justify-content: center; }
}
@media (prefers-reduced-motion: no-preference) {
  .cookie-banner:not([hidden]) { animation: cookie-in 320ms var(--ease) both; }
  @keyframes cookie-in { from { opacity: 0; transform: translateY(12px); } }
}

/* ==========================================================================
   25. Nieuws & downloads without downloads (30 September 2026). Until
   Modulon supplies the PDFs, downloads without a file are not listed, and
   with none left the DOWNLOADS column is not rendered at all. The page then
   takes the one-column layout it already has below 1024px — intro and
   berichten from the page edge — on every width, so no empty third remains.
   ========================================================================== */
.kennis-layout--no-downloads { grid-template-columns: minmax(0, 1fr); }
.kennis-layout--no-downloads .post-list { padding-inline: var(--pad); }
.kennis-intro--full { margin-left: var(--pad); margin-right: var(--pad); }
