/* ==========================================================================
   Modulon — modern layer
   Loaded AFTER site.css by all fourteen pages. Deleting this file (and
   the <link>/<script> lines on each page) restores the pre-modern
   prototype exactly; site.css is untouched.

   Every colour is mixed from the existing :root tokens — no new hex values.
   All entrance motion is gated behind the .js class and disabled under
   prefers-reduced-motion, same contract as site.css §9.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Sticky glass header
   The header leaves the flow and pins to the viewport. At the top of the
   page it is transparent on the navy hero, exactly as designed; after 8px
   of scroll (class set by modern.js) it gains a blurred navy surface, a
   soft shadow, and compresses so it takes less of the screen.
   -------------------------------------------------------------------------- */

:root { --header-h: 131px; }
@media (max-width: 1000px) { :root { --header-h: 95px; } }
@media (max-width: 480px)  { :root { --header-h: 88px; } }

html { scroll-padding-top: var(--header-h); }

.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  transition: background 300ms var(--ease), box-shadow 300ms var(--ease),
              padding 300ms var(--ease);
}

.site-header.is-scrolled,
.site-header:has(.site-nav.is-open) {
  background: color-mix(in srgb, var(--navy-deep) 90%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 10px 30px color-mix(in srgb, var(--navy) 35%, transparent);
}

.site-header.is-scrolled {
  padding-top: 14px;
  padding-bottom: 14px;
}
.site-header__logo img { transition: height 300ms var(--ease); }
.site-header.is-scrolled .site-header__logo img { height: 56px; }
@media (max-width: 480px) {
  .site-header.is-scrolled .site-header__logo img { height: 44px; }
}

/* With the header fixed, an opened mobile menu can be taller than the
   screen; the header itself scrolls so the last item stays reachable. */
.site-header:has(.site-nav.is-open) {
  max-height: 100dvh;
  overflow-y: auto;
}

/* Every page's hero compensates for the header leaving the flow, so the
   fold is pixel-identical to the approved layout on load. The header is
   the first child of .hero on all fourteen pages. */
.hero { padding-top: var(--header-h); }

/* The sticky aside card (de contactkaart op contact.html) pins at 24px in
   the base layer — correct there, but with
   the header out of the flow it parks the card heading permanently behind
   the blur. Pinned below the at-rest header instead; the scrolled header
   is shorter than --header-h, so this always clears it, per breakpoint.
   Below 1024px site.css makes the card static and this rule is inert. */
.contact-card { top: calc(var(--header-h) - 24px); }

/* Het offerte-anker krijgt een marge bovenop de scroll-padding-top die
   hierboven al voor de hele pagina staat. De offerte-CTA's midden op de
   pagina's ("Bespreek je project", "Plan een demo in onze fabriek", "Vraag
   een doorrekening aan") springen sinds 17 augustus 2026 naar
   contact.html#offerte — de
   enige ankerlink op de site. De oranje pil in de header niet: die heet nu
   Contact en gaat naar de bovenkant van die pagina.
   Twee redenen voor die extra 24px: onder 1000px is de vastgezette header in
   gescrolde staat juist iets hóger dan --header-h (96 tegen 88 op een
   telefoon, 108 tegen 95 op een tablet), zodat de kop er anders net achter
   valt; en een kop die exact tegen de headerrand aan landt leest als
   afgesneden. In de basislaag staat de header gewoon in de flow, dus deze
   correctie hoort hier en niet in site.css.

   Er komt onder 1000px nog wat bij: het anker draagt data-reveal, en die
   onthulling schuift het blok na de sprong nog ~20px omhoog — precies de
   kant op waar de header zit. Gemeten landt de kop dan op 96/108px vrij van
   de headerrand in plaats van erachter. */
#offerte { scroll-margin-top: 24px; }
@media (max-width: 1000px) { #offerte { scroll-margin-top: 64px; } }

/* Desktop nav: the hover state draws a 2px underline from left to right
   alongside the colour shift. The rule is currentColor, so it fades from
   --ink-on-navy-bright to white with the label instead of introducing a
   third colour into the header. */
@media (min-width: 1001px) {
  .site-nav__link,
  .site-nav__parent {
    position: relative;
    padding-bottom: 4px;
  }
  .site-nav__link::after,
  .site-nav__parent::after {
    content: "";
    position: absolute;
    left: 0; right: 100%;
    bottom: 0;
    height: 2px;
    background: currentColor;
    transition: right 240ms var(--ease);
  }

  .site-nav__link:hover::after,
  .site-nav__link[aria-current="page"]::after { right: 0; }

  /* Bij de ouder stopt de streep vóór het chevron (9px breed, 7px gap), zodat
     hij onder het woord ligt en niet onder het pijltje. Hij blijft staan
     zolang het paneel open is en op de pagina's die eronder hangen — dezelfde
     afspraak als aria-current bij een gewone menulink. */
  .site-nav__parent:hover::after,
  .site-nav__item--has-sub:focus-within > .site-nav__parent::after,
  .site-nav__item--has-sub.is-open > .site-nav__parent::after,
  .site-nav__item--has-sub:has(a[aria-current="page"]) > .site-nav__parent::after {
    right: 16px;
  }
}

/* --------------------------------------------------------------------------
   2. Hero — depth + entrance choreography
   A soft radial glow of the gradient's own light stop gives the navy band
   depth behind the render. The headline lines rise out of their own line
   boxes; lead, buttons and render follow. Base state is fully visible —
   the animations only ever *play*, they never gate visibility.
   -------------------------------------------------------------------------- */

.home-hero {
  background:
    radial-gradient(56% 70% at 76% 32%,
      color-mix(in srgb, var(--blue-bright) 52%, transparent),
      transparent 72%),
    var(--gradient-navy);
}

/* Geen gloed achter de video op mobiel geprobeerd en teruggedraaid
   (11 aug 2026): een backdrop die lichter is dan de donkere delen van de
   wand maakt die delen doorschijnend — lighten (site.css) wist alles wat
   donkerder is dan wat eronder ligt. De backdrop achter de video moet dus
   navy blijven. */

.hero-line { display: block; overflow: hidden; }
.hero-line > span { display: block; }

@media (prefers-reduced-motion: no-preference) {
  .js .hero-line > span { animation: hero-line-up 0.9s var(--ease) both; }
  .js .hero-line:nth-child(2) > span { animation-delay: 0.12s; }

  .js .home-hero .hero__lead--lg {
    animation: hero-fade-up 0.8s var(--ease) both 0.35s;
  }
  .js .home-hero .btn-pair {
    animation: hero-fade-up 0.8s var(--ease) both 0.5s;
  }

  .js .hero-visual { animation: hero-visual-in 1s var(--ease) both 0.25s; }
  /* Once landed, the render drifts gently — the one continuous motion on
     the page, and it is 12px over 7 seconds. Applies to the render on
     every page hero, not just the homepage. */
  .js .hero .hero-render {
    animation: hero-render-float 7s ease-in-out 1.4s infinite alternate;
  }

  /* De hero-video blendt met `lighten` in het verloop (site.css), en een
     opacity-animatie maakt van .hero-visual een stacking context die dat
     blenden isoleert — het vierkante kader zou terugkomen. De binnenkomst
     verhuist daarom naar de video zelf, samen met de drift. */
  .js .hero-visual--video { animation: none; }
  .js .hero .hero-visual--video .hero-render {
    animation:
      hero-visual-in 1s var(--ease) both 0.25s,
      hero-render-float 7s ease-in-out 1.4s infinite alternate;
  }
}

@keyframes hero-line-up   { from { transform: translateY(112%); } }
@keyframes hero-fade-up   { from { opacity: 0; transform: translateY(22px); } }
@keyframes hero-visual-in { from { opacity: 0; transform: translateX(48px); } }
@keyframes hero-render-float { to { transform: translateY(-12px); } }

/* --------------------------------------------------------------------------
   3. Buttons — lift + glow
   The pill rises 2px and casts a soft glow in its own colour. Mixed from
   the token, per the no-loose-hex rule.
   -------------------------------------------------------------------------- */

.btn {
  transition: background 200ms var(--ease), color 200ms var(--ease),
              border-color 200ms var(--ease), transform 200ms var(--ease),
              box-shadow 200ms var(--ease);
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn--primary:hover {
  box-shadow: 0 10px 22px color-mix(in srgb, var(--orange-ink) 32%, transparent);
}
.btn--primary:active {
  box-shadow: 0 4px 10px color-mix(in srgb, var(--orange-ink) 20%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .btn:hover, .btn:active { transform: none; }
}

/* --------------------------------------------------------------------------
   4. Partnerbalk — bewust GEEN marquee meer
   Stond hier als doorlopende loop met een zachte fade aan beide randen en
   een grijsfilter over de logo's. Alle drie zijn eruit op verzoek van de
   vormgever (feedback 30-07-2026): de rand mag solide zijn, en met een
   solide rand ploppen de logo's zichtbaar in en uit beeld — dus staat de
   rij weer stil, precies zoals in de geanimeerde mockup.

   Het grijsfilter was ook de oorzaak van de afwijkende logokleur: de
   bronbestanden zijn al zachtblauw, en grayscale() maakte daar neutraal
   grijs van. Zonder filter kloppen ze weer met de mockup.

   De rij zelf komt volledig uit site.css (.partner-row) — hier is niets
   meer nodig.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   5. Testimonial — the quote reads as display type
   -------------------------------------------------------------------------- */

.home-testimonial blockquote {
  font-size: clamp(19px, 0.7vw + 16px, 26px);
  line-height: 1.5;
  max-width: 640px;
  font-weight: 500;
  color: var(--navy-deep);
}

/* --------------------------------------------------------------------------
   6. Blauwe balk — bewust GEEN tickertape
   Stond hier als langzaam lopende claim met sterretjes ertussen. Eruit op
   verzoek van de vormgever (feedback 30-07-2026): er is te weinig tekst om
   te laten lopen. De balk komt nu volledig uit site.css (.blue-strip) —
   één statische, gecentreerde zin, zoals in de geanimeerde mockup.

   De keyframe marquee-scroll die deze ticker en de partner-marquee deelden
   is met §4 verdwenen; er is geen andere consument.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   8. Arrows — primary buttons and the project name plates point forward
   The arrow is present at rest (no layout shift) and slides 4px on hover.
   -------------------------------------------------------------------------- */

.btn--primary:not(.btn--submit):not(.btn--sm)::after {
  content: "→";
  display: inline-block;
  margin-left: 10px;
  transition: transform 200ms var(--ease);
}
.btn--primary:not(.btn--submit):not(.btn--sm):hover::after {
  transform: translateX(4px);
}

.rail--plain .rail__caption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  text-align: left;
}
.rail--plain .rail__caption::after {
  content: "→";
  color: var(--orange-ink);
  font-weight: 700;
  font-size: 16px;
  transition: transform 300ms var(--ease);
}
/* Het pijltje draait mee met het paneel dat eronder omhoog schuift: een
   kwartslag, van vooruit naar recht omhoog, precies de richting van de
   beweging. Alleen draaien — de 4px schuif die hier stond liep dwars tegen
   die richting in. 300ms zodat hij samen met het tekstvlak aankomt in plaats
   van erop vooruit te lopen. */
.rail__item:hover .rail__caption::after,
.rail__item:focus-within .rail__caption::after {
  transform: rotate(-90deg);
}

@media (prefers-reduced-motion: reduce) {
  .btn--primary:not(.btn--submit):not(.btn--sm):hover::after,
  .rail__item:hover .rail__caption::after { transform: none; }
}

/* --------------------------------------------------------------------------
   9. More air, more photography
   Sections breathe wider on desktop; the promo tiles carry taller crops;
   the hero render takes a little more of its column; leads step up 1px.
   -------------------------------------------------------------------------- */

@media (min-width: 1201px) {
  :root { --section: 116px; --section-tight: 100px; }
}

/* Homepagina-hero: neemt meer ruimte in en valt deels over de blauwe balk,
   zoals in de geanimeerde mockup (feedback 30-07-2026). De render ging van
   41vw naar 47vw en zijn kolom van 1.1fr naar 1.3fr (die staat in index.html).

   --hero-pad-b  bodemruimte onder de tekstkolom. Los van --section-tight,
                 zodat de balk dichter op de knoppen komt zonder de andere
                 twaalf hero's te raken.
   --hero-dip    hoeveel de render ONDER de onderrand van de hero uitkomt,
                 en dus over de blauwe balk heen valt.

   De render hangt met align-self: end aan de onderkant van zijn rij en
   krijgt een negatieve ondermarge van pad-b + dip. Daarmee steekt hij
   precies --hero-dip voorbij de rand van het blauwe blok. z-index 2 zet hem
   boven de balk — die staat statisch in de flow en heeft dus geen eigen
   z-index — en ruim onder de vaste header, die op 60 zit.

   Alleen vanaf 769px. Daaronder klapt de hero naar één kolom en zou de
   render bovenop de zin in de balk landen. */
.home-hero {
  /* 17 aug 2026: een kwart groter, op verzoek. De clip is vierkant en staat
     in een kader dat altijd breder is dan hoog, dus met object-fit: contain
     bepaalt de HOOGTE hoe groot de animatie in beeld staat — alle drie de
     waarden van de clamp zijn daarom maal 1,25 (was 240 / 41vw / 600). */
  --render-h: clamp(300px, 51.25vw, 750px);   /* geldt alleen onder 769px */
  --hero-pad-b: clamp(40px, 5vw, 72px);
  --hero-dip: clamp(18px, 2.7vw, 44px);
}

@media (min-width: 769px) {
  /* Geen vaste hoogte meer. Met object-fit: contain in een kader dat hoger is
     dan de verhouding van de render toelaat, tekent de browser het beeld
     gecentreerd met loze ruimte boven en onder: gemeten was dat een kader van
     751x677 met een beeld van 751x500 en 88px lucht eronder. De onderrand van
     het KADER lag daarmee wel over de balk, maar de zichtbare plaat bleef er
     45px boven hangen — de overhang bestond alleen op papier.

     Zonder vaste hoogte volgt het kader de verhouding van de render (1400x933)
     en valt zijn onderrand samen met de plaat zelf. De hoogte wordt dan
     volledig bepaald door de kolombreedte, en dat is ook de reden dat de
     kolom van 1.1fr naar 1.3fr ging: breder is hier hoger.

     Onder 769px staat de hero in één kolom en geldt de clamp hierboven; daar
     is de render bewust een lage strook en geen halve schermvulling. */
  .home-hero .hero-render { height: auto; }

  .home-hero__inner { padding-bottom: var(--hero-pad-b); }
  .home-hero .hero-visual {
    align-self: end;
    position: relative;
    z-index: 2;
    margin-bottom: calc(-1 * (var(--hero-pad-b) + var(--hero-dip)));
  }

  /* De video-variant (11 aug 2026, het uitvouwende pakket) is vierkant.
     position en z-index gaan terug naar de defaults: die maakten een
     stacking context, en die isoleert de lighten-blend van de video
     (site.css).

     17 aug 2026 — zo groot als kan, binnen twee grenzen (vraag: kom zo dicht
     mogelijk bij de animatie in Home.png, waar het beeld 82% van de
     hero-hoogte vult tegen 46% bij ons).

     BREEDTE. Het kader is precies zo groot als de kolom breed is. Dat is alle
     ruimte die er is zonder de tekstkolom verder te versmallen, en
     aspect-ratio raakt die maat vanzelf op elke schermbreedte — een vw-clamp
     kan hem alleen benaderen, want de zijmarge verspringt per breakpoint
     (64px op 1440, 278px op 1920).

     HOOGTE. Niet de hoogte van het KADER telt, maar die van het OBJECT: de
     hele animatie moet in beeld staan zonder te scrollen (vormgever, 17 aug
     2026 — op een venster van 1470x798 liep de plaat tot 892px en werd er 94
     afgesneden).

     Over de hele lus gemeten reikt het object tot y=948 van de 1080, dus tot
     87,8% van het kader; de onderste 12,2% is lucht. De onderkant van het
     object ligt daarmee op kaderTop + 0,878 x H, en die moet binnen 100vh
     vallen. Omgekeerd: H <= (100vh - kaderTop - marge) / 0,878, en 1/0,878 is
     de 1,139 hieronder. kaderTop is gemeten op 152-157px (de vastgezette
     header plus de bovenmarge van de hero); 157 + 32px marge = 189. Die 32
     is niet willekeurig: met 12px eronder páste het wel (7px over op een
     venster van 798), maar stond de plaat zo strak tegen de schermrand dat
     het alsnog afgesneden oogde. Een maat die klopt op de rekenmachine is
     hier niet genoeg.

     Twee plafonds dus, en het laagste wint: de kolombreedte op een breed maar
     laag scherm, de schermhoogte op een smal maar hoog scherm. Onder de 425px
     stopt het krimpen — daar is het beeld belangrijker dan de vouw.

     Wat hier NIET aan op te lossen is: de clip is vierkant en de wand vult
     maar 61% van die hoogte — de rest is de lucht die het dak nodig heeft om
     omhoog te zwaaien. Over de hele lus gemeten loopt het object van y=0 tot
     948 en van x=9 tot 1071 van de 1080, dus wegsnijden kan niet. Elke pixel
     wand kost daardoor 1,64 pixel kaderhoogte. Een export met het object
     strakker in beeld haalt het ontwerp wél, zonder dat de hero groeit. */
  .home-hero .hero-render--video {
    height: auto;
    aspect-ratio: 1;
    min-height: 425px;
    max-height: calc((100vh - 189px) * 1.139);
  }
  /* De dip. Het kader zakt 20% van zijn eigen hoogte onder de hero uit, zodat
     de animatie kan groeien zonder de hele pagina naar beneden te duwen — de
     blauwe balk valt eroverheen in plaats van eronder te wijken.

     Waarom precies 12%: over de hele lus reikt het object tot 87,8% van de
     clip (gemeten y=948 van 1080), dus daaronder zit 12,2% lucht. Precies
     die lucht wordt weggetrokken en geen pixel meer — de hero reserveert er
     dus geen hoogte voor, en de balk kan er overheen vallen zonder ooit het
     object zelf te raken. Tussen de voet van de wand en de balk blijft de
     ondermarge van de hero staan (--hero-pad-b, 40-72px); dat is geen
     restfout maar de bestaande ademruimte van de band.

     Stond eerst op 20%, gerekend op het rustbeeld (daar eindigt de wand op
     78,9%). Dat klopte alleen zolang de animatie stilstond: tijdens het
     uitklappen schoof de plaat achter de balk langs. Reken op het laagste
     punt van de héle lus, niet op het beeld dat je toevallig voor je hebt.

     Een procent en geen pixelmaat: een verticale marge in procenten rekent
     met de BREEDTE van de cel, en die is door aspect-ratio: 1 gelijk aan de
     hoogte van het kader. De dip schaalt daardoor vanzelf mee met de
     animatie, op elke schermbreedte, zonder tweede clamp.

     position en z-index blijven op de defaults: die maakten een stacking
     context, en die isoleert de lighten-blend van de video (site.css). Dat de
     balk er toch overheen ligt, regelt .blue-strip hieronder. */
  .home-hero .hero-visual--video {
    align-self: end;
    margin-bottom: -12%;
    position: static;
    z-index: auto;
  }
}

/* De tekstkolom naar zijn ondergrens, zodat de animatie de rest krijgt.
   Gemeten is de knoppenrij het breedste element in die kolom: "Bespreek je
   project" en "Ontdek de Modulonwand" zijn samen 505px, en daaronder klappen
   ze onder elkaar. De kop is met 493px inkt ("ALTIJD." op 118px) net smaller.
   520px houdt 15px marge op beide — genoeg dat een iets langere knoptekst of
   kop niet meteen omvalt, en de kop vult de kolom nu rand tot rand, net als
   in Home.png (daar 680px kop in een kolom van ~685).

   Pas vanaf 1280px. Daaronder geeft de verhouding 1fr 1.3fr de tekstkolom
   uit zichzelf al minder dan 520px (op 1200px: 483px), en zou een vaste
   520px de animatie juist kleiner maken in plaats van groter. Het omslagpunt
   ligt op 1274px; 1280 is de eerstvolgende ronde maat.

   !important omdat --split inline op het element staat (base-laag) en een
   inline custom property elke stylesheet-regel verslaat. Zo blijft het
   ongedaan maken van deze laag één handeling: modern.css weghalen. */
@media (min-width: 1280px) {
  .home-hero__inner { --split: 520px 1fr !important; }
}

/* De balk over de video, niet ernaast. Zonder positionering wint de canvas:
   een vervangen element als <canvas> tekent in de inline-laag en komt
   daarmee bóven de achtergrond van een later blok. position: relative met
   z-index tilt de balk daar weer overheen. De homepage is sinds 19 augustus
   2026 de enige pagina met deze balk — op contact stond hij ook, maar die is
   daar op klantwens weggehaald. */
.blue-strip {
  position: relative;
  z-index: 1;
}

.home-benefits { --tile-img-h: 230px; }
.home-benefits .tile { max-width: 340px; }

.section__lead { font-size: 16px; }

/* --------------------------------------------------------------------------
   10. Schuine sectieveeg — de hoofdanimatie uit de mockup
   Een sectie fadet niet omhoog maar wordt onthuld achter een schuine rand:
   een gekleurd gordijn schuift onder 45 graden van de sectie af. Vier keer op
   de homepage, precies zoals in Website-mockup 1 (2).mp4.

   DE HOEK IS EXACT 45 GRADEN, niet ongeveer. Gemeten uit de videoframes: in
   frame 13,20s loopt de rand van x=588 op y=180 naar x=869 op y=460 (dx 281,
   dy 280), in 13,50s van 177/180 naar 497/500 (dx 320, dy 320) en in 12,60s
   van 142/240 naar 202/300 (dx 60, dy 60). Drie keer dx/dy = 1,00. De rand
   loopt overal naar rechtsonder — "\" — alleen de looprichting verschilt.

   Daarom een geroteerd vlak en géén clip-path: rotate(-45deg) geeft een rand
   die in schermpixels exact 45 graden is, ongeacht hoe breed of hoog de sectie
   is. Een polygon() met procenten kantelt mee met de verhouding van het vak en
   zou op mobiel een heel andere hoek tekenen dan op desktop.

   Het gordijn beweegt langs zijn eigen x-as, dus haaks op zijn rand. Bij
   rotate(-45deg) wijst die as naar rechtsboven: translateX positief = het
   gordijn wordt naar rechtsboven weggeduwd (zoals het felblauw bij 3,3-3,9s),
   negatief = het trekt zich terug naar linksonder (zoals het marineblauw bij
   12,9-13,7s).

   --wipe-s (zijde van het vierkante gordijn), --wipe-from en --wipe-to (begin-
   en eindstand) worden alle drie door modern.js gezet, en zo dat de rand precies
   van "net bedekt" naar "net vrij" loopt — geen millimeter meer. Dat luistert
   nauw: stond het gordijn ruimer opgesteld, dan speelt een deel van de animatie
   zich buiten de sectie af en is de zichtbare veeg veel korter dan de opgegeven
   duur. Dat was de eerste versie, en die ging dan ook veel te snel: van de 0,7s
   was maar ongeveer 0,2s te zien.

   TIMING IS LINEAIR, en dat is gemeten, niet gekozen. In de mockup legt de rand
   bij de quote elke 50ms 68 pixels af — 60/70/67/70/68/75/71/59/70/67/70/68 over
   twaalf opeenvolgende frames — en bij de split elke 50ms 95 pixels. Constante
   snelheid dus, zonder aanloop of uitloop. De site-easing --ease vertraagt naar
   het einde toe en versnelt daardoor het begin; die hoort hier niet.

   De DUUR staat niet vast maar volgt uit die snelheid: modern.js rekent hem per
   sectie uit, zodat de rand overal even snel over het scherm gaat. Een lage
   balk is dus eerder klaar dan een hoog contactblok, precies zoals wanneer één
   gordijn over het hele scherm zou schuiven. Wil je het als geheel rustiger of
   sneller: één getal, EDGE_SPEED bovenin modern.js.
   -------------------------------------------------------------------------- */

.js .wipe {
  position: relative;
  /* Knipt het geroteerde gordijn af op de sectierand. Geen van de vier
     secties laat iets buiten zijn eigen kader uitsteken — de render die over
     de blauwe balk valt hangt aan .hero, niet aan .split. */
  overflow: hidden;
}

.js .wipe__curtain {
  position: absolute;
  top: 50%;
  left: 50%;
  /* Boven de inhoud van de sectie, ruim onder de vaste header (60). */
  z-index: 5;
  width: var(--wipe-s, 250vmax);
  height: var(--wipe-s, 250vmax);
  margin: calc(var(--wipe-s, 250vmax) / -2) 0 0 calc(var(--wipe-s, 250vmax) / -2);
  pointer-events: none;
}

/* Ruststand: de rand ligt precies op de verste hoek van de sectie, dus de
   sectie is net helemaal bedekt. De veeg loopt van hier naar --wipe-to. */
.js .wipe__curtain--right { transform: rotate(-45deg) translateX(var(--wipe-from, 0px)); }
.js .wipe__curtain--left  { transform: rotate(-45deg) translateX(calc(var(--wipe-from, 0px) * -1)); }

.js .wipe.is-wiped .wipe__curtain--right {
  animation: wipe-out-right var(--wipe-duration, 0.7s) linear forwards;
}
.js .wipe.is-wiped .wipe__curtain--left {
  animation: wipe-out-left var(--wipe-duration, 0.7s) linear forwards;
}

@keyframes wipe-out-right {
  from { transform: rotate(-45deg) translateX(var(--wipe-from, 0px)); }
  to   { transform: rotate(-45deg) translateX(var(--wipe-to, 250vmax)); }
}
@keyframes wipe-out-left {
  from { transform: rotate(-45deg) translateX(calc(var(--wipe-from, 0px) * -1)); }
  to   { transform: rotate(-45deg) translateX(calc(var(--wipe-to, 250vmax) * -1)); }
}

/* Drie van de vier secties staan in de mockup al volledig getekend klaar
   achter het gordijn: op 12,3s staat het bpd-logo meteen op volle sterkte
   achter de rand, op 13,5s de quote, op 15,8s de formuliervelden. Daar mag de
   standaard fade-up van site.css §9 dus niet ook nog eens overheen.

   .split is de uitzondering en houdt zijn fade: daar komt "DE WINST ZIT" ná de
   veeg op — op 3,7s staat de kop nog lichtpaars, op 4,1s pas vol, en de knop
   is op 3,9s nog een vaag blokje. Dat is de enige sectie waar de mockup de
   twee bewegingen ná elkaar zet in plaats van tegelijk. */
.js .wipe--paint[data-reveal],
.js .wipe--paint [data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}

/* modern.js hangt onder deze voorkeur helemaal geen gordijn op. Dit vangt het
   geval af waarin de bezoeker de voorkeur midden in de sessie omzet, nadat er
   al een gordijn klaarhangt — dezelfde afspraak als site.css §9. */
@media (prefers-reduced-motion: reduce) {
  .js .wipe__curtain { display: none; }
}

/* --------------------------------------------------------------------------
   11. Beeld met diepte — foto's liggen óp de pagina, niet erin
   Feedback 19 augustus 2026: de foto's stonden er te plat op. Ze lagen alle
   drie de kenmerken van een plat vlak: geen eigen schaduw, geen enkele
   overlap met de band eronder, en overal precies binnen de grid. Daardoor
   lazen ze als behang in plaats van als beeld.

   Drie ingrepen, in volgorde van effect:
   a) een schaduw onder elke losse foto (niet onder de kaarten — die hadden er
      al een, alleen te licht voor een vlak dat voor de helft uit foto bestaat)
   b) de grote splitfoto steekt over de rand van de band eronder heen
   c) een trage inzoom bij hover op de kaartfoto's

   Alle kleur komt uit --navy via color-mix; geen enkele nieuwe hexwaarde.
   -------------------------------------------------------------------------- */

:root {
  /* Twee lagen, net als elke echte schaduw: een korte donkere naad die de
     onderrand vastzet, en een brede zachte die de hoogte doet. Eén laag van
     18px (--card-shadow) geeft wel een randje maar geen hoogte — dat was
     precies het probleem.

     --navy en niet zwart: de bestaande --card-shadow rekent al met
     rgba(16,26,80), en dat is --navy op één tint na. Grijze schaduw onder een
     blauw kleurenschema oogt vuil. */
  --photo-shadow:
    0 2px 6px color-mix(in srgb, var(--navy) 10%, transparent),
    0 18px 44px color-mix(in srgb, var(--navy) 20%, transparent);

  /* Hoever de splitfoto onder zijn eigen sectie uitsteekt. Meeschalend, want
     dit is een verhouding tot de sectiehoogte en geen vaste maat: 24px op een
     telefoon zou net zo goed een uitlijnfout kunnen zijn, 56px op 1440 leest
     als bedoeld. */
  --photo-overhang: clamp(24px, 3.5vw, 56px);
}

/* --- a. Schaduw onder de losse foto's ----------------------------------- */

/* De grote foto naast een tekstkolom (Home, Modulonwand, Modulonvloer,
   Project detail) en de sticky beeldkolom op Over Modulon.

   NIET op .hero--photo: die foto is met mix-blend-mode: multiply ín het
   navyverloop verwerkt (site.css) en is daar geen los object maar de band
   zelf. Een schaduw eromheen zou het kader terugbrengen dat die blend juist
   weghaalt.

   Op de homepage valt de boven- en onderrand van deze schaduw weg: .split is
   daar een veegsectie en die knipt af op zijn eigen kader (§10). Wat overblijft
   is de binnenrand naar de tekst toe, en dat is de rand die daar telt. */
.split__media,
.about-photo {
  box-shadow: var(--photo-shadow);
}

/* De beeldkolom op Over Modulon is de logo-animatie op een egaal lichtblauw
   veld — de videobox ís het paneel (site.css). Dat paneel staat op wit en had
   dus geen enkele rand; nu ligt het er zichtbaar bovenop. */

/* Foto's in een artikel. Staan op wit, over de volle tekstbreedte, en waren
   daarmee de platste beelden van de site. */
.article-body figure :is(img, video) {
  box-shadow: var(--photo-shadow);
}

/* --- b. De splitfoto steekt over de band eronder heen -------------------- */

/* Het sterkste diepte-signaal dat er is: een vlak dat over de grens van een
   ander vlak heen ligt, kán niet in hetzelfde vlak liggen.

   :has(+ .section--surface) en geen losse class in de HTML, want de regel is
   inhoudelijk precies dat: overhang mag alléén waar er een kleurgrens is om
   overheen te steken. Dat is nu op drie plekken zo — Modulonwand, Modulonvloer
   en Project detail, alle drie een .split die direct op een --surface-band
   uitkomt. Komt er een vierde bij, dan doet hij automatisch mee.

   De homepage valt hier vanzelf buiten en dat is maar goed ook: die .split is
   een veegsectie met overflow: hidden (§10), dus daar zou een uitstekende foto
   worden afgeknipt in plaats van uitsteken. De tweede split op Modulonwand en
   Modulonvloer valt er ook buiten — daar volgt de fotorail, geen kleurvlak.

   De truc met de hoogte: min-height gaat met de overhang omhóóg en de
   ondermarge er met dezelfde waarde vanaf. Wat de rij-indeling van de grid
   ziet blijft daarmee exact wat het was; alleen het zichtbare kader is
   --photo-overhang langer. Zonder die twee tegen elkaar in zou een negatieve
   marge de rijhoogte van de hele split laten krimpen en de tekst ernaast mee
   omhoog trekken. De band eronder krijgt diezelfde overhang bij zijn
   bovenmarge, zodat de kop daar op exact dezelfde afstand van de foto blijft
   staan: de foto steekt over de kleurgrens heen, het ritme van de pagina
   verandert niet.

   z-index 1 omdat de sectie eronder statisch in de flow staat en dus geen
   eigen z-index heeft: zonder dit schildert die achtergrond gewoon over de
   uitstekende foto heen. Ruim onder de vaste header (60). */
/* Vanaf 769px, en dat is geen afronding maar een voorwaarde. Daaronder klapt
   .split naar één kolom en zet site.css de foto op min-height: 0 met een vaste
   4/3 — de min-height hieronder is specifieker en zou die nul overschrijven,
   waarna 4/3 er een kader van 480x640 van maakt op een scherm van 375 breed.
   Gemeten: 640px documentbreedte op 375px venster, op alle drie de pagina's.
   Uitsteken heeft daar sowieso geen betekenis: in één kolom staat er geen
   kleurvlak naast de foto maar tekst eronder. */
@media (min-width: 769px) {
  .split:has(+ .section--surface) .split__media {
    min-height: calc(var(--media-h, 520px) + var(--photo-overhang));
    margin-bottom: calc(var(--photo-overhang) * -1);
    z-index: 1;
  }

  .split:has(+ .section--surface) + .section--surface {
    padding-top: calc(var(--section-tight) + var(--photo-overhang));
  }
}


/* --- c. Trage inzoom op de kaartfoto's ---------------------------------- */

/* De projectenrail deed dit al (site.css: scale(1.05) over 500ms) en het is
   daar het enige beeld dat niet plat aandoet. Dezelfde beweging dus op de
   kaarten, iets trager omdat een kaartfoto kleiner is en de beweging anders
   schokkerig oogt.

   overflow: hidden is de voorwaarde: zonder clip groeit de foto over de
   kaarttekst heen. Kan hier veilig, want het enige wat ooit buiten een kaart
   valt is de focusring, en die zit op de titel of de pill binnen 24px
   binnenmarge — ruim binnen het kader. De .card__overlay die de hele kaart
   klikbaar maakt heeft tabindex="-1" en krijgt zelf nooit focus.

   :focus-within naast :hover, om dezelfde reden als de schaduwregel in
   site.css: wie met het toetsenbord op de kaart landt, moet dezelfde kaart
   zien reageren. */
.card,
.post-card__media {
  overflow: hidden;
}

.card img,
.post-card__media img {
  transition: transform 600ms var(--ease);
}

.card--link:hover img,
.card--link:focus-within img,
.post-card--link:hover .post-card__media img,
.post-card--link:focus-within .post-card__media img {
  transform: scale(1.06);
}

/* Een kaart die voor de helft uit foto bestaat heeft een zwaardere schaduw
   nodig dan een kaart met alleen tekst — anders zweeft de tekst wel en het
   beeld niet. Alleen de kaarten mét beeld, de rest houdt --card-shadow. */
.card:has(img),
.post-card:has(img) {
  box-shadow: var(--photo-shadow);
}

/* En de hover er meteen achteraan, want de regel hierboven staat ná
   .card--link:hover in site.css en heeft dezelfde specificiteit: zonder deze
   twee regels zou de rustschaduw de hover-schaduw overschrijven en reageert de
   kaart niet meer op de muis. Zelfde vorm als hierboven, een slag dieper —
   de kaart komt omhoog, hij verandert niet van karakter. */
.card--link:has(img):hover,
.card--link:has(img):focus-within,
.post-card--link:has(img):hover,
.post-card--link:has(img):focus-within {
  box-shadow:
    0 3px 8px color-mix(in srgb, var(--navy) 12%, transparent),
    0 26px 56px color-mix(in srgb, var(--navy) 26%, transparent);
}

/* --- Uitzonderingen op smal scherm en bij reduce ------------------------ */

@media (prefers-reduced-motion: reduce) {
  .card img,
  .post-card__media img { transition: none; }
  .card--link:hover img,
  .card--link:focus-within img,
  .post-card--link:hover .post-card__media img,
  .post-card--link:focus-within .post-card__media img { transform: none; }
}
