/* ============================================================
   PCG — Components: nav, hero, cards, footer, etc.
   ============================================================ */

/* ---------- Scroll progress ---------- */
.progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:100;background:transparent;pointer-events:none}
.progress__fill{
  height:100%;background:var(--gold-grad);
  transform-origin:left;transform:scaleX(0);
  box-shadow:0 0 18px rgba(201,161,74,.5);
}

/* ---------- Top nav ---------- */
/* The scrolled state used to animate `padding` — a layout property, so
   the whole bar (brand, six links, mega trigger, language toggle, CTA)
   was re-laid-out on every frame of a 600ms transition. The bar now
   keeps ONE padding and simply rides 6px lower until you scroll: the
   content lands on exactly the same baselines as before, but the
   transition is a composited translate.
   `translate` is used rather than `transform` so the auto-hide
   (.nav--hidden, in effects.css) can still own `transform` outright —
   the two compose instead of overwriting each other. */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:90;
  display:flex;align-items:center;justify-content:space-between;
  padding:12px var(--gutter);
  translate:0 6px;
  transition:background .6s var(--ease),translate .6s var(--ease),
             border-color .6s var(--ease),box-shadow .6s var(--ease);
  border-bottom:1px solid transparent;
}
.nav.scrolled{
  background:rgba(8,8,9,.72);backdrop-filter:blur(16px) saturate(1.2);
  -webkit-backdrop-filter:blur(16px) saturate(1.2);
  translate:0 0;border-bottom-color:rgba(201,161,74,.16);
  /* the bar becomes a plane attached to the top edge, so it casts */
  box-shadow:0 1px 0 rgba(255,255,255,.03),var(--lift-1);
}
.nav__brand{display:flex;align-items:center;gap:13px;z-index:2}
.nav__brand img{height:42px;width:auto;filter:drop-shadow(0 2px 14px rgba(201,161,74,.18))}
.nav__brand .bn{display:flex;flex-direction:column;line-height:1}
.nav__brand .bn b{font-family:var(--ff-display);font-weight:700;font-size:17px;letter-spacing:.01em;color:var(--creme-d)}
.nav__brand .bn span{font-size:8.5px;letter-spacing:.32em;text-transform:uppercase;color:var(--steen);margin-top:4px}
.nav__links{display:flex;align-items:center;gap:34px}
.nav__links>a,.nav__trigger{
  font-size:12px;letter-spacing:.18em;text-transform:uppercase;font-weight:400;
  color:var(--creme-d);opacity:.8;position:relative;padding:4px 0;transition:opacity .4s;
  display:inline-flex;align-items:center;gap:.5em;
}
/* Underline draws in on scaleX, not width — width is a layout property
   and every hover was re-laying-out the link's line box. */
.nav__links>a::after,.nav__trigger::after{
  content:"";position:absolute;left:0;bottom:-2px;height:1px;width:100%;
  background:var(--champagne);
  transform:scaleX(0);transform-origin:left;
  transition:transform .45s var(--ease);
}
.nav__links>a:hover,.nav__trigger:hover,
.nav__links>a:focus-visible,.nav__trigger:focus-visible{opacity:1}
.nav__links>a:hover::after,.nav__links>a:focus-visible::after,.nav__links>a.active::after,
.nav__trigger:hover::after,.nav__trigger:focus-visible::after,
.nav__has-menu.open .nav__trigger::after,.nav__trigger.active::after{transform:scaleX(1)}
@media (prefers-reduced-motion:reduce){
  .nav,.nav__links>a::after,.nav__trigger::after{transition-duration:.01ms}
}
.nav__right{display:flex;align-items:center;gap:22px}

/* ---------- Mega-menu ---------- */
.nav__item{position:relative;display:flex;align-items:center}
.nav__has-menu{padding-block:18px;margin-block:-18px}
.nav__chev{width:9px;height:9px;transition:transform .4s var(--ease);opacity:.7}
.nav__has-menu.open .nav__chev{transform:rotate(180deg)}
.mega{
  position:absolute;top:100%;left:50%;
  width:min(720px,88vw);
  transform:translate(-50%,10px);
  opacity:0;pointer-events:none;visibility:hidden;
  transition:opacity .38s var(--ease),transform .45s var(--ease),visibility .38s;
  padding-top:16px;
}
.nav__has-menu.open .mega{opacity:1;transform:translate(-50%,0);pointer-events:auto;visibility:visible}
/* Floating free of the page — the top of the elevation scale, and the
   surface nearest the light, so it gets the brighter sheen too. */
.mega__inner{
  display:grid;grid-template-columns:1fr .78fr;gap:1px;
  background:rgba(201,161,74,.18);
  border:1px solid rgba(201,161,74,.22);
  box-shadow:var(--lift-3);
}
.mega__col,.mega__aside{background-color:#0C0B0C;background-image:var(--sheen-lit);padding:26px}
.mega__ttl{font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--steen);margin-bottom:16px}
.mega__link{display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:start;padding:12px 12px 12px 10px;margin-inline:-10px;transition:background-color .35s var(--ease)}
.mega__link:hover,.mega__link:focus-visible{background-color:rgba(201,161,74,.08)}
.mega__link svg{width:20px;height:20px;color:var(--champagne);margin-top:3px;flex:none}
.mega__txt{display:block;letter-spacing:0;text-transform:none}
.mega__link b{font-family:var(--ff-display);font-weight:600;font-size:17px;color:var(--creme-d);display:block}
.mega__desc{font-size:13px;color:var(--steen);line-height:1.45;display:block;margin-top:3px;font-style:normal}
.mega__aside{display:flex;flex-direction:column;justify-content:space-between;background:linear-gradient(160deg,#131009,#0C0B0C 62%)}
.mega__aside p{font-family:var(--ff-display);font-weight:600;font-size:20px;line-height:1.25;color:var(--creme-d);letter-spacing:0;text-transform:none}
.mega__aside .fine{
  margin-top:10px;text-transform:none;letter-spacing:.02em;
  font-family:var(--ff-body);font-size:var(--fs-fine);line-height:1.5;color:var(--steen);
}
.mega__aside .btn{margin-top:24px;width:100%}

/* language toggle
   THE CONTROL IS A LINK PAIR, NOT A BUTTON PAIR. tools/build-en.js replaces the
   script toggle with a real anchor to the other language's URL plus a <span>
   marking the current one, because a crawler has to be able to follow the site
   from Dutch to English. These selectors therefore cover a, span and button:
   button is kept so any surface still shipping the old toggle looks identical. */
.lang{display:inline-flex;align-items:center;border:1px solid rgba(201,161,74,.34);border-radius:4px;overflow:hidden}
.lang a,
.lang span,
.lang button{
  font-family:var(--ff-body);font-size:11px;letter-spacing:.14em;font-weight:500;
  padding:6px 12px;background:transparent;color:var(--steen);cursor:pointer;
  transition:color .35s var(--ease),background-color .35s var(--ease);
  text-transform:uppercase;text-decoration:none;line-height:1.4;
}
/* Hairline between the two options — a segmented control, not a pill. Drawn as
   an inset shadow so it adds no width and never shifts the second label. */
.lang a+a,.lang a+span,.lang span+a,.lang button+button{box-shadow:inset 1px 0 0 rgba(201,161,74,.30)}
.lang a:hover,
.lang button:hover{color:var(--champagne);background-color:rgba(201,161,74,.10)}
/* The current language: not a link, and marked the way the old .on state was. */
.lang [aria-current],
.lang button.on{background:var(--gold-grad);color:#15110a;cursor:default}
.lang button.on:hover{color:#15110a}
/* The pill is deliberately compact for the pointer nav, but 29px is well under
   the 44px touch minimum. On coarse pointers grow the hit area, not the type. */
@media (hover:none),(pointer:coarse){
  .lang a,
  .lang span,
  .lang button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:48px;padding:6px 14px}
}

/* hamburger (mobile) */
/* The three bars are only 26x15, which is far under the touch minimum for what
   is the sole navigation control below 1000px. Padding grows the hit area to
   44x44 and the negative margin keeps the bars sitting exactly where they were. */
.burger{
  display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;z-index:2;
  align-items:center;justify-content:center;
  padding:15px 9px;margin:-15px -9px;
}
.burger span{width:26px;height:1.5px;background:var(--champagne);transition:transform .4s var(--ease),opacity .3s var(--ease)}
.nav.open .burger span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
.nav.open .burger span:nth-child(2){opacity:0}
.nav.open .burger span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}

/* mobile drawer */
.drawer{
  position:fixed;inset:0;z-index:80;
  background:linear-gradient(160deg,#0F0D0A,#070708 60%);
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(90px,14vh,140px) var(--gutter) clamp(30px,6vh,60px);
  overflow-y:auto;
  opacity:0;pointer-events:none;transition:opacity .45s var(--ease)
}
.drawer.open{opacity:1;pointer-events:auto}
.drawer::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(70% 50% at 100% 0%,rgba(201,161,74,.12),transparent 60%);
}
.drawer__nav{position:relative;display:flex;flex-direction:column;gap:2px}
.drawer a{
  font-family:var(--ff-display);font-size:clamp(30px,9vw,44px);color:var(--creme-d);
  padding:9px 0;font-weight:800;letter-spacing:-.022em;line-height:1.1;display:flex;align-items:baseline;gap:.7em;
  opacity:0;transform:translateY(22px);
  transition:opacity .5s var(--ease),transform .6s var(--ease),color .3s;
}
.drawer.open a{opacity:1;transform:none}
.drawer.open .drawer__nav a:nth-child(1){transition-delay:.08s}
.drawer.open .drawer__nav a:nth-child(2){transition-delay:.14s}
.drawer.open .drawer__nav a:nth-child(3){transition-delay:.2s}
.drawer.open .drawer__nav a:nth-child(4){transition-delay:.26s}
.drawer.open .drawer__nav a:nth-child(5){transition-delay:.32s}
.drawer.open .drawer__nav a:nth-child(6){transition-delay:.38s}
.drawer a:hover,.drawer a.active{color:var(--champagne)}
.drawer a .n{font-family:var(--ff-body);font-size:11px;letter-spacing:.24em;color:var(--steen);flex:none}
.drawer a.active .n{color:var(--champagne)}
.drawer__foot{
  position:relative;margin-top:clamp(28px,6vh,52px);padding-top:26px;
  border-top:1px solid rgba(201,161,74,.18);
  display:flex;flex-direction:column;gap:16px;
  opacity:0;transform:translateY(16px);transition:opacity .5s var(--ease) .42s,transform .6s var(--ease) .42s;
}
.drawer.open .drawer__foot{opacity:1;transform:none}
.drawer__foot .btn{width:100%}
.drawer__meta{display:flex;flex-wrap:wrap;gap:6px 22px;font-size:13.5px;color:var(--steen)}
/* These are standalone controls in a touch-only drawer, not links inside prose,
   so they need a real target. 15px tall failed WCAG 2.5.8. */
.drawer__meta a{
  font-family:var(--ff-body);font-size:13.5px;opacity:1;transform:none;color:var(--steen);
  display:inline-flex;align-items:center;min-height:44px;padding:0
}
@media (prefers-reduced-motion:reduce){
  .drawer a,.drawer__foot{opacity:1 !important;transform:none !important;transition:none !important}
}
/* The floating dock and WhatsApp bubble sit below the drawer —
   get them out of the way while the menu owns the screen. */
body:has(.drawer.open) .pcg-wa,
body:has(.drawer.open) .cta-dock{opacity:0;pointer-events:none;transition:opacity .3s var(--ease)}

/* ---------- Hero ---------- */
.hero{position:relative;min-height:100svh;display:flex;align-items:center;overflow:hidden;background:var(--onyx)}
.hero__bg{position:absolute;inset:-8% 0 0 0;z-index:0;}
.hero__bg .frame__media{
  background:
    radial-gradient(80% 60% at 70% 20%,rgba(201,161,74,.10),transparent 60%),
    linear-gradient(120deg,#14110c 0%,#0a0a0b 45%,#1a140b 100%);
}
.hero__veil{position:absolute;inset:0;z-index:1;background:linear-gradient(90deg,rgba(8,8,9,.92) 0%,rgba(8,8,9,.62) 45%,rgba(8,8,9,.3) 100%)}
.hero__grain{position:absolute;inset:0;z-index:1;opacity:.5;pointer-events:none;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E")}
.hero__inner{position:relative;z-index:3;width:100%;max-width:var(--maxw);margin-inline:auto;padding-inline:var(--gutter)}
.hero__mono{
  /* `translate` keeps the centring independent of `transform`, so the
     scroll-scrub can own `transform` without fighting the layout. */
  position:absolute;right:var(--gutter);top:50%;translate:0 -50%;z-index:2;
  /* Size here drives the guilloche collar: js/pcg-hero-canvas.js:206 reads
     this element's rendered box and sets rIn = side * 0.355, rim = rIn * 1.85.
     So shrinking the mark shrinks the engine-turned rings with it.
     It had been cut to min(27vw,350px) at .55 to stop the mark competing with
     the headline - but that took the rings down with it, and the owner asked
     for the larger collar back. Restored to the original 520px geometry, with
     opacity held at .6 rather than the old .9: the rings regain their scale
     while the mark itself stays a watermark behind the type. */
  /* height:auto is load-bearing, not tidiness. The <img> carries
     width="1024" height="1024" attributes, which map to CSS presentational
     hints. Author CSS overrides the width to 350px but leaves the height
     hint standing, so the mark rendered 350x1024 - squeezed narrow and
     stretched to ~2.9x its true height. */
  width:min(40vw,520px);height:auto;opacity:.6;
  filter:drop-shadow(0 14px 44px rgba(201,161,74,.08));
  pointer-events:none;
}
/* ---------- Hero measure: TWO caps, and both are load-bearing ----------
   The seal is position:absolute (both .hero__mono and the guilloche plate) while
   .hero__inner is in normal flow, so nothing structurally stops the headline
   running into the ornament. A `ch` cap alone cannot fix that, because `ch` is
   the advance of "0" in the current face and has no relationship to where the
   seal is. Measured, before this: 1ch resolved to 56.07px in Bodoni Moda at
   86px, so 15ch was a FIXED 875px, while the seal's left edge slides from
   ~64.5vw at 1920 down to ~48.75vw at 1280. The two closed on each other and
   the headline collided at every desktop width — by 28px at 1920, and by 273px
   at 1280, in Dutch as well as English.

   So: `ch` keeps the line length readable, `vw` keeps it clear of the seal, and
   min() takes whichever binds first. The vw figure is not taste — it is the
   largest value that still clears the measured seal edge at every width from
   1280 up, with ~40px of air left at the tightest point (1440).

   If the seal's anchor (js/pcg-hero-canvas.js, anchor.x) ever moves, re-measure
   this. Below 960px components.css hides .hero__mono and the seal goes to edge
   mode, so the vw cap stops binding and the ch cap takes over on its own. */
.hero h1{max-width:min(13ch,40vw)}
.hero .display{max-width:min(12ch,40vw)}
/* The standfirst is body-size, so it clears the seal on its own measure; it is
   capped here only so it never out-runs the headline above it. */
.hero .lead{max-width:min(46ch,42vw)}

@media (max-width:960px){
  /* Edge mode: the ornament is hung off the right edge and the watermark is
     hidden, so the text may use the full column again. */
  .hero h1,.hero .display,.hero .lead{max-width:none}
}

/* ---------- Standfirst ----------
   REPLACES .hero__proof. The three above-the-fold facts used to be a
   <ul> of big gold figures over small captions, divided by vertical
   rules — the big-number/small-label band, at n=3, in the first fold,
   whose only growth path is a fourth invented number. The FACTS have not
   changed and not one word of copy has: they now sit inside one line at
   READING size, with the figure emphasised and the rosette as
   punctuation between them.
   Baseline-aligned flex, so a figure and the words after it share one
   baseline however either language wraps. The measure matches .lead's
   (pcg.css:772) because this line is read, not scanned.
   The markup change is step 13; until it lands, index.html's <ul> simply
   has no rules of its own — content stays visible, which is the safe
   ordering. The reverse order would flash an unstyled list. */
.standfirst{
  display:flex;flex-wrap:wrap;align-items:baseline;
  gap:.55em 1.1em;
  margin-top:clamp(38px,4.6vw,64px);
  max-width:60ch;
  font-size:var(--fs-lead);line-height:1.55;font-weight:500;
  /* var(--ink): --creme-d #F4ECD8 on --onyx #0B0B0C = 16.71:1;
     --inkt #1A1610 on --papier #F8F3E9 = 16.28:1. Both AAA at any size. */
  color:var(--ink);
  text-wrap:pretty;
}
/* The figure keeps the emphasis the old <b> carried, minus the display
   scale. --ls-figure is the one negative tracking step (pcg.css:256) —
   a number is not a headline.
   var(--gold-text): --champagne #E6CF8F on --onyx = 12.82:1;
   --brons #7A5E1C on --papier = 5.51:1, on --creme = 4.91:1.
   All three clear 4.5:1 as body text. */
.standfirst b{
  font-weight:700;color:var(--gold-text);
  letter-spacing:var(--ls-figure);
  font-variant-numeric:lining-nums tabular-nums;
}
/* A separator sits on the line's optical centre, not on its baseline. */
.standfirst .sep{align-self:center}

/* ---------- The rosette ----------
   A concentric struck mark: engine-turned spokes over two rings, cut to
   a circle by a radial mask — the same lathe as the seal and the seams,
   at punctuation size.
   ADDITIVE ONLY, deliberately: it is the standfirst separator and the
   footer colophon mark. The diamond in .cta-note
   stay exactly as they are — repeating-conic-gradient has no support
   before Safari 16.2, and swapping a working glyph for a hazard is a
   downgrade for no gain.
   The plain `background:var(--gold-text)` is a real fallback, not
   decoration: where repeating-conic-gradient is unsupported the second
   declaration is dropped whole and the mark degrades to a struck dot,
   still circular because the mask is independent of it.
   Decorative (aria-hidden in markup) so no ratio is required, measured
   anyway: --gold-text is 12.82:1 on --onyx, 5.51:1 on --papier. */
.standfirst .rosette,
.footer__bottom .sig::before{
  content:"";
  width:11px;height:11px;flex:none;
  opacity:.85;
  background:var(--gold-text);
  background:
    repeating-conic-gradient(from 0deg,var(--gold-text) 0deg 5deg,transparent 5deg 22.5deg),
    repeating-radial-gradient(circle at 50% 50%,transparent 0 1.4px,var(--gold-text) 1.4px 2.2px);
  -webkit-mask-image:radial-gradient(circle at 50% 50%,#000 0 44%,transparent 50%);
          mask-image:radial-gradient(circle at 50% 50%,#000 0 44%,transparent 50%);
}
/* Its second home. inline-flex is what gives the pseudo a gap to sit in;
   the signature's own text is untouched. */
.footer__bottom .sig{display:inline-flex;align-items:center;gap:.62em}
.footer__bottom .sig::before{width:9px;height:9px}

.scrollcue{
  position:absolute;left:var(--gutter);bottom:34px;z-index:3;
  display:flex;align-items:center;gap:12px;color:var(--steen);
  font-size:10.5px;letter-spacing:.26em;text-transform:uppercase
}
/* Machined, like every other hairline: --rake-soft's specular peak sits
   at 52% of the bar's length, so the track is lit once in its middle
   instead of being an evenly-lit yellow from end to end. The champagne
   runner in ::after is what actually carries the cue. */
.scrollcue .bar{position:relative;width:1px;height:50px;background:var(--rake-soft);overflow:hidden}
.scrollcue .bar::after{content:"";position:absolute;top:-50%;left:0;width:100%;height:50%;background:var(--champagne);animation:cue 2.4s var(--ease) infinite}
@keyframes cue{0%{top:-55%}60%,100%{top:105%}}

/* ---------- The shared divider, machined ----------
   .hairline is declared at pcg.css:922 as a flat `background:var(--rule)`.
   It is a full-width 1px FILL, not a border, so it is one of the few rules
   on the site that can take the gradient with no restructuring — and
   pcg.css:137 already names it as an intended consumer. It is re-declared
   HERE rather than there because pcg.css belongs to another step; both
   selectors are (0,1,0) and components.css loads second in the manifest
   (tools/sync-shell.js: pcg, components, motion, effects), so this wins on
   source order. If the two ever end up in one sheet, delete this block and
   change the token at the declaration instead.
   Decorative divider, no information depends on it; --rake's mean alpha
   (.19) is actually BELOW the flat --rule it replaces (.22), so nothing
   gets louder.
   motion.css's data-draw animations scale this element on X — a background
   gradient scales with its box, so the draw-in still works unchanged. */
.hairline{background:var(--rake)}
/* .gold-tick is deliberately NOT converted: it is a 46px accent and a
   105deg gradient across 46px is a smear, not a rake. Same reasoning that
   keeps every short button/chip/input border flat. */

/* ---------- Service / value cards ---------- */
/* Breakpoint-free: auto-fit drops tracks as the container narrows, so
   the grid answers to its own width instead of to a guessed viewport.
   `min(100%,…)` is the guard that keeps a track from exceeding the
   container at 320px — same pattern as .t-rail below.
   NOTE the specificity trap this replaced: `.cards.cols-3` is (0,2,0)
   and a media query adds no specificity, so the old `1fr 1fr` overrides
   at the bottom of this file only won by source order. They are gone for
   these two selectors; re-adding one would silently defeat auto-fit. */
/* RULED, NOT BOXED. The outer `border:1px solid var(--rule)` is gone and
   only the block edges remain. The four-sided box is a large part of what
   made every page read as a stack of boxes, and now that each cell is a
   milled pocket (--lip-tl, below) the cells describe the plate themselves;
   the top and bottom rules are the plate's edges, the 1px gaps are its
   cuts. Those gaps take --rake, so each cut is lit once along its length.
   Note this MUST stay a `border`, not a fill: a gradient is not a valid
   <color> in a border shorthand — see the warning at pcg.css's --rake.
   index.html's borderless "why us" grid overrides with an inline
   `border:none;background:transparent`; the shorthand still beats
   border-block, so that opt-out keeps working untouched. */
.cards{display:grid;gap:1px;background:var(--rake);border-block:1px solid var(--rule)}
.cards.cols-3{grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr))}
.cards.cols-2{grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr))}
/* THE GOLD RECTANGLE, as a standing guard rather than a page fix.
   The defect: .cards paints --rake as the SHARED grid ground so the cuts
   show through the 1px gaps, and --rake is a translucent gold — so an
   UNOCCUPIED cell renders as a warm gold slab (over.html's four values in
   a three-track grid, fixed in the pages by step 4).
   auto-fit already closes half the class: a track with no items at all
   collapses to 0, so a trailing empty COLUMN can never show. What it
   cannot close is an incomplete last ROW, which needs
   items % tracks == 0 — and with auto-fit the track count is not
   knowable in CSS.
   Where the track count IS known it can be closed, and .grid-split--even
   is the two-track case the pages actually use (over.html's six values,
   aanpak.html's four guarantees). In two tracks an incomplete last row is
   exactly an odd child count, which :has() can see: the odd one out is
   stretched across the row instead of leaving a hole beside it. Dormant
   at today's counts of 6 and 4; it fires the moment someone adds a fifth.
   :has() is already relied on in this codebase (pcg.css:691, below at
   :188). Specificity is (0,3,0) — it beats .grid-split--even (0,1,0) and
   .cards.cols-2 (0,2,0) on specificity rather than on source order,
   which is the trap documented above. An inline grid-template-columns in
   a page still outranks it, so step 4's page fixes stay authoritative.
   The general case cannot be closed from this sheet: it needs the cuts
   painted by the cells instead of by the ground, and a cell-drawn cut
   fires regardless of the container's background — which would put stray
   hairlines on index.html's deliberately borderless grid (inline
   `background:transparent`, 28-56px gaps). That is a page-and-sheet
   change, so it is reported as a follow-up, not smuggled in here. */
.cards.grid-split--even:has(> :last-child:nth-child(odd)) > :last-child{grid-column:1 / -1}
/* A card is a lit surface inside a ruled grid, not a coloured box, so
   --sheen puts light on its top edge.
   Deliberately NO ::before here: motion.css already owns
   [data-spotlight]::before and gives these cards a pointer-tracking
   warm glow. Adding a second wash to the same pseudo-element would
   simply lose the cascade to it and leave a dead rule behind. The
   division is: motion.css owns the moving light, this file owns the
   material it lands on and the state the card settles into. */
.card{
  background-color:var(--bg-card,transparent);
  background-image:var(--sheen);
  /* MILLED POCKET. --sheen was only ever half the treatment: it puts
     light on the top edge but leaves the cell drawn on the plate rather
     than cut into it. --lip-tl (pcg.css:119/393) is the other half — two
     1px inset hairlines, dark top-left and light bottom-right, under the
     same one light source. Decoration only; nothing rests on it. */
  box-shadow:var(--lip-tl);
  padding:clamp(28px,3vw,46px);position:relative;overflow:hidden;
  transition:background-color .6s var(--ease),box-shadow .5s var(--ease);
  /* No min-height. It used to be 300px, and together with
     `margin-top:auto` on the heading it structurally manufactured an
     ~80px void between the icon and the h3 in EVERY card — worse on
     mobile, where a three-line card carried ~200px of nothing. A card
     sizes to its content; grid stretch still levels a row. */
  display:flex;flex-direction:column;
}
.skin-onyx .card,.skin-char .card{background-color:var(--onyx)}
.skin-creme .card,.skin-creme-deep .card{background-color:var(--papier)}
.card:hover{background-color:var(--bg-hover)}
.skin-onyx .card:hover,.skin-char .card:hover{background-color:#100f0d}
.skin-creme .card:hover,.skin-creme-deep .card:hover{background-color:var(--creme)}
/* Lift it clear of the 1px rules it shares with its neighbours.
   THE POCKET STAYS IN THE HOVER VALUE. Two shadows interpolating to two
   shadows is what keeps the .5s transition smooth; `var(--lift-2)` alone
   would animate the lip out of existence and pop a new shadow in from
   nothing on every hover. This exact value is the one handed to step 11
   for css/effects.css — that sheet loads later and owns the .card
   TRANSITION (effects.css:76), which already lists box-shadow, so no edit
   is required there. If it ever restates the hover VALUE, it must restate
   it as `var(--lip-tl),var(--lift-2)`. */
.card:hover,.card:focus-within{z-index:2;box-shadow:var(--lip-tl),var(--lift-2)}
/* opacity:.8 removed — at 15px it put the gold ink at 3.65:1 on --papier
   and 3.36:1 on --creme against a 4.5:1 requirement. The gold already
   reads quieter than --ink; it does not need an alpha as well. */
.card__no{font-family:var(--ff-display);font-weight:600;font-size:15px;color:var(--gold-text);letter-spacing:.1em}
.card__ico{width:34px;height:34px;margin-bottom:18px;color:var(--gold-text);transition:transform .5s var(--ease)}
.card:hover .card__ico{transform:translateY(-3px)}
/* h2 where the card is a top-level item under the page h1 (404), h3 where a
   section heading already introduces the grid. Sized the same either way.
   `margin-top:auto` is gone: the heading belongs directly under whatever
   introduces it, and pushing it to the foot of a fixed box was the void.
   What is left is a real, small gap. */
.card :is(h2,h3){padding-top:clamp(12px,1.4vw,20px);font-size:var(--fs-h3);line-height:1.18}
.card p{color:var(--muted);font-size:15.5px;margin-top:12px;max-width:34ch}
.card__line{position:absolute;left:0;bottom:0;height:2px;width:100%;background:var(--gold-grad);transform:scaleX(0);transform-origin:left;transition:transform .6s var(--ease)}
.card:hover .card__line,.card:focus-within .card__line{transform:scaleX(1)}
/* THIS is the legitimate use of margin-top:auto in the card — the "read
   on" line is the card's last object, so it sits on one baseline across a
   row instead of floating under whichever paragraph ran shortest. */
.card__more{margin-top:auto;padding-top:22px;opacity:0;transform:translateY(8px);transition:opacity .5s var(--ease),transform .5s var(--ease)}
.card:hover .card__more,.card:focus-within .card__more{opacity:1;transform:none}
/* On diensten.html this line carries the price ("Vanaf €59 / maand").
   Hiding it behind :hover means no touch device ever sees it — so where
   there is no hover, it is simply part of the card. */
@media (hover:none),(pointer:coarse){
  .card__more{opacity:1;transform:none}
}
@media (prefers-reduced-motion:reduce){
  .card,.card__ico,.card__line,.card__more{transition-duration:.01ms}
  .card:hover .card__ico{transform:none}
  /* the reveal-side default: never leave content depending on a hover */
  .card__more{opacity:1;transform:none}
}

/* ---------- Homepage services (#diensten): the three service cards ----------
   These used to opt OUT of the card system. The previous block set
   background-image:none, box-shadow:none on hover, card__ico{transition:none}
   and :hover .card__ico{transform:none} — which removed the sheen, the milled
   pocket, the hover lift and the icon motion, i.e. every elevation cue the
   global .card earns. Three flat rectangles is what that produces, and it is
   why the row read as cheap next to the rest of the page.

   The strict 3-column grid and the solid --rule dividers are KEPT: they are the
   right frame, and an auto-fit grid would break the 01/02/03 reading order.
   Everything that flattened the material is gone, and what replaces it is built
   from tokens this sheet already publishes, so the cards are the same metal and
   the same light source as the buttons and the section seams.

   The motion is deliberately ONE gesture per card, composited only (transform
   and opacity), and it reads as the cell being cut deeper rather than as a
   card flying up:
     the plate      lifts 2px and darkens by one step
     the numeral    a large ghost figure behind the cell slides and warms
     the rule       a gold hairline wipes across the foot, left to right
     the icon       rises and its stroke brightens
     the heading    rises with the plate, a beat later
   Durations are staggered (.5s / .6s / .7s) so the parts arrive in sequence
   instead of snapping together, which is the difference between expensive and
   templated. [data-spotlight] in motion.css still owns the pointer-tracking
   glow on top of all of it; nothing here touches ::before. */
#diensten .cards{background:var(--rule);border:1px solid var(--rule)}
/* Four service lines now, so the homepage grid is .cols-4. The strict count is
   deliberate rather than auto-fit: the cards are numbered 01-04 and an auto-fit
   grid reflows them into a reading order that no longer matches the numbers. */
#diensten .cards.cols-4{grid-template-columns:repeat(4,1fr)}

/* ---------- Four-up needs three fixes that three-up did not ----------
   All three are the same underlying problem: a quarter-width cell is narrower
   than the longest Dutch word in it.

   1. min-width:0. A grid item's default min-width is `auto`, which means its
      minimum size is its longest unbreakable word. "Procesoptimalisatie" is 19
      characters with no break opportunity, so the cell refused to shrink, its
      content ran past the cell edge, and .card's overflow:hidden sliced the
      heading and the paragraph mid-word. This is the fix for that, not a tidy-up.
   2. hyphens. With the cell now allowed to shrink, the long compound needs
      somewhere to break.

      THIS IS `manual`, NOT `auto`, AND THAT IS THE WHOLE POINT. hyphens:auto
      breaks at a real Dutch SYLLABLE boundary, which for "procesoptimalisatie"
      gives "procesop-timalisatie" — linguistically valid (pro-ces-op-ti-ma-li-
      sa-tie) and unreadable, because it splits across the compound's seam and
      hides the two words the reader is looking for. The word is proces +
      optimalisatie, so the only acceptable break is "proces-optimalisatie".
      hyphens:manual honours the &shy; placed at that seam in the markup and
      refuses every other break point. If you re-add `auto`, the bad break
      comes straight back.

      overflow-wrap is the last-resort fallback: an ugly break still beats a
      clipped word.
   3. A smaller heading step. --fs-h3 tops out at 30px, which was tuned against a
      third of the measure. Measured at 1440: "Procesoptimalisatie" needs ~256px
      at 30px Bodoni against ~234px of usable cell, so it could never fit. 25px
      brings it to ~214px and it sets on one line after all. */
#diensten .cards.cols-4 > .card{min-width:0}
#diensten .cards.cols-4 > .card h3{
  font-size:clamp(19px,1.7vw,25px);
  hyphens:manual;
  overflow-wrap:break-word;
}
/* 34ch is wider than a quarter cell, so the paragraph was being clipped rather
   than wrapped. The cell is the measure here. */
#diensten .cards.cols-4 > .card p{max-width:none}
#diensten .cards.cols-3{grid-template-columns:repeat(3,1fr)}
#diensten .card{
  /* 300px was tuned for a 3-up. At 4-up each cell is ~25% narrower, so headings
     wrap a line earlier and the shared row track grows anyway; a tall floor on
     top of that only manufactures dead space. */
  min-height:252px;
  /* Own transform so the lift cannot fight the reveal engine's own. */
  transition:background-color .6s var(--ease),box-shadow .5s var(--ease),
             transform .5s var(--ease);
}

/* ---------- One baseline grid across all three cells ----------
   Bottom-anchoring each heading with margin-top:auto aligned NOTHING across the
   row: each heading sat at the foot of its own cell, so where the paragraphs
   differed by a line the headings differed with them. Measured on this row, the
   three h3 tops landed ~75px apart. In a strict 3-up that reads as sloppy
   typesetting, and no amount of hover polish covers it.

   Subgrid is the honest fix: the cards inherit the ROW lines of a track defined
   once on the container, so number, icon, heading, paragraph and the read-on
   line each share a horizontal baseline across all three cells, whatever the
   copy does. Five named rows, and the paragraph row is the only flexible one so
   a two-line paragraph in one card cannot shift its neighbours' headings.

   @supports, not a blind apply: without subgrid the cards keep the flex column
   they already had, which is imperfect but not broken. */
@supports (grid-template-rows: subgrid){
  #diensten .cards.cols-4,
  #diensten .cards.cols-3{
    /* no · icon · heading · paragraph(flex) · read-on */
    grid-template-rows:auto auto auto 1fr auto;
  }
  #diensten .card{
    display:grid;
    grid-template-rows:subgrid;
    grid-row:span 5;
    align-content:start;
  }
  /* The flex-era spacing hacks are all inherited from the row track now. */
  #diensten .card h3{margin-top:0;padding-top:clamp(22px,2.2vw,34px)}
  #diensten .card__more{margin-top:0}
}
#diensten .card:hover,#diensten .card:focus-within{
  transform:translateY(-2px);
  box-shadow:var(--lip-tl),var(--lift-2);
}

/* The ghost numeral. A second, large, very quiet copy of the card's number,
   set in the display face at the top-right of the cell and clipped by the
   card's own overflow:hidden. It is the one purely ornamental element here and
   it is aria-hidden by construction: it is generated content, so it is never in
   the accessibility tree or the copy buffer. */
#diensten .card__no{position:relative;z-index:1;font-weight:400;opacity:.85}
/* Anchored to the CELL, not to the small number. Hanging it off the numeral put
   it at a different distance from the right edge in every card and clipped each
   one by a different amount, which looked like a mistake rather than a
   watermark.

   THE OFFSETS ARE POSITIVE AND MUST STAY POSITIVE. They were negative
   (top:-34px, right:-18px) on the theory that bleeding the numeral off the
   corner was a deliberate crop. It is not, because .card sets overflow:hidden
   (line 430) — so the bleed was not a bleed, it was a slice: every ghost lost
   ~17px off its right side and ~32px off its top. On "04" that amputates the
   right arm of the 4 and the numeral reads as a rendering fault. Measured at
   1440 the card is 283px wide against a ~130px numeral, so the whole figure
   fits inside the cell with room to spare; there was never a reason to hang it
   over the edge. Keep it whole. */
#diensten .card > .card__no::after{
  content:attr(data-ghost);
  position:absolute;
  top:clamp(6px,.8vw,14px);
  right:clamp(6px,.8vw,14px);
  /* --ff-body, NOT --ff-display, and this is the one place on the site where a
     large figure is deliberately not set in Bodoni.

     Bodoni Moda is a Didone: stroke contrast is the whole point of the design.
     At a watermark alpha the hairlines drop below the display's ability to
     resolve them and only the thick stems survive, so "04" loses the 4's
     diagonal and crossbar and renders as "0 1" — a wrong number, not a faint
     one. "01" degrades the same way. Raising alpha does not fix it: measured
     side by side, at .45 the 4 is still ambiguous while the 0 has gone so dark
     it fights the heading. Switzer has near-uniform stroke weight, so every
     part of the glyph fades at the same rate and the numeral stays itself all
     the way down. Weight 700 matches the optical mass Bodoni 400 had. */
  font-family:var(--ff-body);
  font-size:clamp(70px,6.8vw,100px);line-height:.8;font-weight:700;
  letter-spacing:-.02em;
  /* .055 was set against the onyx skin and never re-checked after these cards
     moved to the cream one. On #F3EFE7 a gold at 5.5% is roughly a 1.5% contrast
     step — invisible on any ordinary screen, so the watermark read as a printing
     fault rather than as a numeral. .13 keeps it a watermark (it must stay under
     the heading in the hierarchy) while actually resolving at dpr 1. */
  color:var(--gold-text);opacity:.13;
  pointer-events:none;user-select:none;
  transition:opacity .7s var(--ease),transform .7s var(--ease);
  transform:translate3d(0,0,0);
}
/* The pseudo-element is on .card__no, so it needs the CARD as its containing
   block rather than the numeral. */
#diensten .card{position:relative}
#diensten .card__no{position:static}
#diensten .card:hover .card__no::after,
#diensten .card:focus-within .card__no::after{opacity:.2;transform:translate3d(-7px,0,0)}

#diensten .card__ico{
  margin-bottom:20px;
  transition:transform .5s var(--ease),opacity .5s var(--ease);
  opacity:.78;
}
#diensten .card:hover .card__ico,
#diensten .card:focus-within .card__ico{transform:translateY(-4px);opacity:1}

#diensten .card h3{
  margin-top:auto;padding-top:30px;font-weight:500;
  font-size:var(--fs-h3);line-height:1.12;letter-spacing:-.015em;
  transition:transform .6s var(--ease);
}
#diensten .card:hover h3,#diensten .card:focus-within h3{transform:translateY(-2px)}
#diensten .card p{margin-top:14px}
#diensten .card__more{margin-top:22px;padding-top:0}

/* The foot rule is the card system's own .card__line; it only needs to be a
   hair heavier here, because these cells are larger than the ones on
   diensten.html and a 2px wipe under a 300px plate reads thin. */
#diensten .card__line{height:3px}

/* 4-up needs to break earlier than 3-up did: below ~1180px a quarter-column can
   no longer hold "Webdesign en development" without wrapping to three lines.
   2x2 is the right intermediate — it keeps the numbered reading order (01 02 /
   03 04) where a 3+1 row would orphan the fourth card. */
@media (max-width:1180px){#diensten .cards.cols-4{grid-template-columns:1fr 1fr}}
@media (max-width:960px){#diensten .cards.cols-3{grid-template-columns:1fr 1fr}}
@media (max-width:640px){
  #diensten .cards.cols-4,#diensten .cards.cols-3{grid-template-columns:1fr}
}
@supports (grid-template-rows: subgrid){
  /* Subgrid rows only align cells that SHARE a row. Once the grid wraps to 2x2
     or a single column the cards occupy different rows, so the shared track has
     to be released or every card inherits the tallest cell's spacing. */
  @media (max-width:1180px){
    #diensten .cards.cols-4{grid-template-rows:none}
    #diensten .cards.cols-4 > .card{grid-template-rows:none;grid-row:auto;display:flex;flex-direction:column}
    #diensten .cards.cols-4 > .card h3{margin-top:auto}
    #diensten .cards.cols-4 > .card .card__more{margin-top:auto}
  }
}

@media (prefers-reduced-motion: reduce){
  /* State still CHANGES on hover — the plate darkens and the foot rule is
     already handled by the card system's own reduced-motion rule — but nothing
     travels. */
  #diensten .card,
  #diensten .card__no::after,
  #diensten .card__ico,
  #diensten .card h3{transition-duration:.01ms}
  #diensten .card:hover,#diensten .card:focus-within{transform:none}
  #diensten .card:hover .card__no::after,
  #diensten .card:focus-within .card__no::after{transform:none}
  #diensten .card:hover .card__ico,
  #diensten .card:focus-within .card__ico{transform:none}
  #diensten .card:hover h3,#diensten .card:focus-within h3{transform:none}
}

/* ---------- Pillars: the three principles under the essence heading ----------
   These were transparent divs with `border-top:1px solid var(--rule)` and no
   surface, on the onyx ground of #essentie. On a near-black section that is not
   a quiet card, it is no card at all: three headings and three paragraphs
   floating with a faint line above each. Given the service cards two sections
   down now carry real material, these looked unfinished by comparison.

   They are the same alloy and the same light source as .card, deliberately, so
   the page reads as one system rather than two card designs:
     --houtskool  one step off the onyx ground, which is what lifts them
     --sheen      light on the top edge
     --lip-tl     the milled 1px pocket, so the panel is cut in rather than
                  drawn on
     --gold-grad  a struck 2px rule across the top, drawn left to right as the
                  row arrives

   NO TRANSFORM ON HOVER, and that is a cascade decision rather than taste.
   These are children of a [data-stagger] parent, and css/motion.css declares
   `.is-in[data-stagger]>*{transform:none}` at (0,2,0). A `.pillar:hover{transform}`
   is also (0,2,0), and motion.css loads AFTER this file, so it would win and the
   hover would silently do nothing. Elevation here is therefore carried by shadow
   and surface, which cannot be overridden from there and reads just as raised.
   If a transform is ever wanted, it has to out-specify that rule. */
/* Explicit column counts, not auto-fit. Two reasons, and the second is the real
   one: auto-fit silently changes how many rows exist, which makes the subgrid
   row track below impossible to reason about, and three principles read as a
   deliberate set of three rather than as however many fit. */
.pillars{
  display:grid;
  gap:clamp(18px,2.2vw,28px);
  grid-template-columns:repeat(3,1fr);
}
@media (max-width:900px){.pillars{grid-template-columns:1fr 1fr}}
@media (max-width:600px){.pillars{grid-template-columns:1fr}}

/* Shared baselines across the three panels, the same device the service cards
   use. Without it the paragraph in "Vaste prijzen" started a line higher than
   its neighbours, because that heading sets on one line and the other two wrap
   to two. Only applied while all three actually share a row: below 900px they
   sit on different rows and a shared track would make every panel as tall as
   the tallest. */
@supports (grid-template-rows: subgrid){
  @media (min-width:901px){
    .pillars{grid-template-rows:auto 1fr}
    .pillar{display:grid;grid-template-rows:subgrid;grid-row:span 2;align-content:start}
  }
}
.pillar{
  position:relative;overflow:hidden;
  padding:clamp(26px,2.6vw,38px);
  background-color:var(--houtskool);
  background-image:var(--sheen);
  box-shadow:var(--lip-tl);
  transition:background-color .6s var(--ease),box-shadow .5s var(--ease);
}
.pillar:hover{
  background-color:var(--houtskool-2);
  box-shadow:var(--lip-tl),var(--lift-2);
}
/* The struck top rule. Full width by default so a blocked script leaves three
   finished panels, then parked and drawn only once JS confirms it is running —
   the same arming contract the reveal engine uses. */
.pillar::before{
  content:"";position:absolute;left:0;right:0;top:0;height:2px;
  background:var(--gold-grad);
  transform:scaleX(1);transform-origin:left;
  transition:transform .8s var(--ease);
}
html.pcg-armed .pillar::before{transform:scaleX(0)}
html.pcg-armed .pillars.is-in .pillar::before{transform:scaleX(1)}
/* Struck one after another, left to right, so the row assembles rather than
   flashing on. Matches the 120ms the parent's data-stagger already uses. */
html.pcg-armed .pillars .pillar:nth-child(2)::before{transition-delay:.12s}
html.pcg-armed .pillars .pillar:nth-child(3)::before{transition-delay:.24s}

.pillar h3{padding:0;font-size:var(--fs-h3);line-height:1.16}
.pillar p{margin-top:12px;color:var(--muted);max-width:34ch}

@media (prefers-reduced-motion: reduce){
  .pillar,.pillar::before{transition:none}
  html.pcg-armed .pillar::before{transform:scaleX(1)}
}

/* ---------- Stats ---------- */
/* Breakpoint-free, same reasoning as .cards above. */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:1px;background:var(--rake)}
/* MILLED POCKET, second half of the treatment --sheen started. See the
   longer note at .card. No hover state here, so nothing interpolates. */
.stat{padding:clamp(26px,3vw,40px) clamp(20px,2vw,30px);background-color:inherit;background-image:var(--sheen);box-shadow:var(--lip-tl)}
.skin-onyx .stat,.skin-char .stat{background-color:var(--onyx)}
.skin-creme .stat,.skin-creme-deep .stat{background-color:var(--papier)}
.stat .num{font-family:var(--ff-display);font-weight:800;font-size:clamp(46px,5.6vw,76px);line-height:1;letter-spacing:-.026em;color:var(--gold-text);font-variant-numeric:lining-nums tabular-nums}
/* the label belongs to the number — bind them, don't space them */
.stat .lbl{margin-top:10px;font-size:12px;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--muted)} /* .16em -> the declared label step */

/* ---------- Process / approach ---------- */
/* MACHINED, NOT PRINTED. These rules run the full width of the column, so
   unlike a 90px button border they are long enough to hold a rake: the
   specular peak of --rake sits at 52% of the rule's length, the same point
   the hero canvas rests its light, and each cut is lit ONCE along its run
   instead of being an evenly-lit yellow from end to end.
   A gradient is not a valid <color>, so it cannot live in a `border`
   shorthand — see the warning at pcg.css's --rake, which is why the ~18
   short `border:1px solid var(--rule)` declarations in this file stay flat
   and deliberately so. A full-width rule that wants the treatment has to be
   re-expressed as a 1px FILL, which is what the pseudo-elements below are.
   Purely decorative: the rule carries no information a heading does not. */
.steps{display:grid;gap:0}
.step{display:grid;grid-template-columns:90px 1fr;gap:clamp(24px,4vw,64px);padding:clamp(32px,4vw,52px) 0;align-items:start;position:relative}
.step::before,.step:last-child::after{
  content:"";position:absolute;left:0;right:0;height:1px;
  background:var(--rake);pointer-events:none;
}
.step::before{top:0}
.step:last-child::after{bottom:0}
/* .55 -> .75. Always >=40px, so the large-text threshold (3:1) applies,
   and .55 measured 2.29:1 on --papier / 2.19:1 on --creme. .75 measures
   3.31:1 / 3.07:1 / 7.44:1 on onyx — clears 3:1 on every ground while
   staying visibly quieter than the heading beside it. */
.step__no{font-family:var(--ff-display);font-weight:800;font-size:clamp(40px,4vw,60px);letter-spacing:-.022em;color:var(--gold-text);line-height:1;opacity:.75}
.step h3{margin-bottom:14px}
.step p{color:var(--muted);max-width:56ch}

/* ---------- The homepage process ladder (#aanpak) ----------
   Scoped, deliberately. `.step` is shared by index, aanpak, diensten and
   contact, and the treatment below is tuned against a five-row ladder next to a
   sticky heading. Rolling it out further is a separate decision that wants each
   page looked at, not a global find-and-replace.

   Three moves, all on transform and opacity, all keyed to the class the existing
   reveal engine already sets. js/pcg.js stamps .is-in on each [data-rise] as it
   enters the viewport, so the ladder builds itself as the reader descends
   instead of arriving pre-assembled:

     the rule    draws left to right, so each step is ruled off as it arrives
     the node    a gold diamond is struck onto the rule, a beat later
     the numeral a large ghost twin sits behind the figure, the same device the
                 service cards use, so the two sections read as one system

   The delays are per-row, not uniform: 0ms, 60ms, 120ms on the three parts
   within a row, which is what makes it read as drawn rather than switched on.

   ARMING. Everything degrades to its finished state unless JS has confirmed it
   is running. html.pcg-armed gates every start value, so a blocked script leaves
   the rules full-width and the diamonds struck rather than a ladder of invisible
   hairlines. Same contract as the reveal engine in css/motion.css. */
#aanpak .steps{position:relative}

#aanpak .step::before,
#aanpak .step:last-child::after{
  transform-origin:left;
  transition:transform .85s var(--ease),background-color .85s var(--ease);
}
html.pcg-armed #aanpak .step::before,
html.pcg-armed #aanpak .step:last-child::after{transform:scaleX(0)}
html.pcg-armed #aanpak .step.is-in::before,
html.pcg-armed #aanpak .step.is-in:last-child::after{transform:scaleX(1)}

/* The numeral is the anchor for both the node and the ghost. `isolation` gives
   it its own stacking context, so the ghost's z-index:-1 is contained here and
   cannot slide behind the section's own background layers. */
#aanpak .step__no{position:relative;isolation:isolate;transition:opacity .7s var(--ease)}
html.pcg-armed #aanpak .step.is-in .step__no{opacity:.85}

/* The struck node. A rotated square, the same mark the trust row and the CTA
   note already use, so it is the site's diamond and not a new one. Sits ON the
   row rule above the figure. */
#aanpak .step__no::before{
  content:"";position:absolute;left:2px;top:-.52em;
  width:7px;height:7px;background:var(--gold-grad);
  transform:rotate(45deg) scale(1);
  transition:transform .55s var(--ease) .06s;
}
html.pcg-armed #aanpak .step__no::before{transform:rotate(45deg) scale(0)}
html.pcg-armed #aanpak .step.is-in .step__no::before{transform:rotate(45deg) scale(1)}

/* The ghost twin. Opt-in by construction: attr(data-ghost) resolves to an empty
   string on any step that does not carry the attribute, so no other page picks
   this up even if it later adopts the scoped block. */
#aanpak .step__no::after{
  content:attr(data-ghost);
  position:absolute;
  /* Offset RIGHT and up, so it trails the figure as an echo instead of sitting
     on it. At -.05em it was a near-perfect overlay and the two numerals read as
     a misprint rather than as a watermark. */
  left:.34em;top:-.30em;
  /* --ff-body for the same reason as the service-card watermark above: a Didone
     loses its hairlines before its stems at low alpha, so a Bodoni "04" fades
     into "0 1". At .07 this ghost is even fainter than that one, so it would
     have failed harder. The SOLID figure stays Bodoni — at full opacity and
     weight 800 the contrast reads as intended. Only the faded twin moves. */
  font-family:var(--ff-body);font-weight:700;
  font-size:1.95em;line-height:1;letter-spacing:-.03em;
  color:var(--gold-text);opacity:.07;
  pointer-events:none;user-select:none;
  /* BEHIND the solid figure. A pseudo-element paints above its own parent's
     text by default, so without this the faint twin was drawn over the top of
     the numeral it is supposed to sit behind. .step has no background of its
     own, so a negative z-index lands it on the section ground rather than
     hiding it. */
  z-index:-1;
  transform:translate3d(0,0,0);
  transition:opacity .9s var(--ease) .12s,transform .9s var(--ease) .12s;
}
html.pcg-armed #aanpak .step__no::after{opacity:0;transform:translate3d(-10px,0,0)}
html.pcg-armed #aanpak .step.is-in .step__no::after{opacity:.07;transform:translate3d(0,0,0)}

/* The figure carries more air at 4-up-era sizes, and the heading sits closer to
   its own paragraph than to the row above it. Spacing, not decoration. */
#aanpak .step{padding-block:clamp(36px,4.4vw,60px)}
#aanpak .step h3{margin-bottom:12px}

@media (prefers-reduced-motion: reduce){
  /* Every part arrives in its finished state. Nothing draws, nothing slides. */
  html.pcg-armed #aanpak .step::before,
  html.pcg-armed #aanpak .step:last-child::after{transform:none;transition:none}
  html.pcg-armed #aanpak .step__no::before{transform:rotate(45deg) scale(1);transition:none}
  html.pcg-armed #aanpak .step__no::after{opacity:.075;transform:none;transition:none}
}
.step__tags{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}
/* The label step, like every other tracked micro-label. The border stays a
   flat --rule: a 90px pill edge is far too short to hold a 105deg rake, and
   a gradient is not a valid <color> in a border shorthand anyway. */
.tag{font-size:11px;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--gold-text);border:1px solid var(--rule);padding:6px 13px;border-radius:999px} /* .14em -> the declared label step */

/* ---------- Work / case cards ---------- */
.work-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(20px,2.4vw,40px)}
.work{position:relative;cursor:pointer;display:block}
.work .frame{aspect-ratio:4/3}
.work__media .frame__media{transition:transform 1.4s var(--ease)}
.work:hover .frame__media{transform:scale(1.06)}
.work__meta{display:flex;justify-content:space-between;align-items:baseline;margin-top:20px;gap:20px}
/* h2 on werk.html (the cards sit directly under the page h1) and h3 on the
   homepage (where a section h2 already introduces them). Same size either way. */
.work__meta :is(h2,h3){font-size:clamp(22px,2.2vw,28px)}
.work__cat{font-size:11px;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--gold-text);white-space:nowrap} /* .18em -> the declared label step */
.work__desc{color:var(--muted);margin-top:8px;font-size:15px}

/* ---------- A plate is not a placeholder ----------
   pcg.css:930 puts a film-grain-plus-vignette overlay on EVERY .frame in
   `mix-blend-mode:overlay`. That overlay was written for the empty frames —
   the ones whose only content is a .frame__media gradient standing in for a
   photograph — and on those it still earns its keep: it gives an invented
   rectangle some tooth so it does not read as a flat swatch.
   Applied to a REAL client screenshot it does the opposite. It dirties the
   pixels, drops roughly a stop out of the corners, and makes the one piece
   of actual evidence on the site look like the same placeholder as
   everything around it. A magazine does not vignette a plate.
   `:has(img)` is the exact test, because the img IS the distinction: a
   frame that has one is showing work, a frame that has none is showing a
   gradient. It fires today on the three real shots (index.html's Pierstone
   desktop, werk.html's homepage, werk-pierstone.html's contact page) and it
   will fire automatically on the portfolio screenshots still to be
   supplied, with no page edit. :has() is already relied on in this codebase
   (pcg.css:691, components.css above).
   Nothing else about the frame changes: --frame keeps its ground, the
   stripes overlay, the caption and the parallax are untouched. Where :has()
   is unsupported the declaration is simply dropped and the frame keeps
   today's rendering, so the fallback is the status quo. */
.frame:has(img)::after{display:none}

/* ---------- Testimonials ---------- */
/* The measure has to sit on the blockquote, never on .quote. `ch` on
   the wrapper resolves against the 16px body face (~8.5px per ch), so
   the old `max-width:24ch` made the wrapper ~205px wide while the
   blockquote inside runs at 46px in the display face, which is exactly why this
   pull quote rendered ONE WORD PER LINE. Putting the measure on the
   blockquote makes `ch` resolve in the display face, where 21ch lands
   at roughly 26 characters per line at every step of the clamp. */
.quote{max-width:none}
.quote blockquote{
  /* 700, deliberately NOT 900: a 54px pull quote at the .display weight
     would shout over the h2 that introduces it. */
  font-family:var(--ff-display);font-weight:700;
  /* Now that the measure is right, the quote can carry the size it was
     always meant to have — it is the one display moment in the section. */
  font-size:clamp(28px,4vw,54px);line-height:1.2;letter-spacing:-.018em;
  max-width:21ch;
  text-wrap:balance;
}
/* The mark is punctuation, not an icon: hang it into the left margin so
   the quote's first character stays on the text edge. */
.quote .mark{
  font-family:var(--ff-display);font-weight:700;font-size:90px;line-height:0;
  /* .55 -> .75, same arithmetic as .step__no: 90px is large text, and
     .55 measured 2.19:1 on --creme. */
  color:var(--gold-text);opacity:.75;height:44px;display:block;
  margin-left:-.055em;
}
.quote__by{margin-top:34px;display:flex;align-items:center;gap:16px}
.quote__by .av{width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,#241d12,#0d0c0d);border:1px solid var(--rule);flex:none}
.quote__by .nm{font-weight:500;letter-spacing:.02em}
.quote__by .rl{font-size:12.5px;color:var(--muted);letter-spacing:.04em}

/* testimonial rail
   Fixed at 3 columns, it held 2 cards — so the third cell rendered as
   an empty olive rectangle (the rail's rule-coloured background showing
   through where no card sits). The rail now sizes itself from its
   actual child count: auto-fit drops the empty track, so a pair reads
   as a deliberate pair, and a third card would slot in later without
   anyone touching this file. The `min(100%,…)` guard keeps the track
   from exceeding the container at 390px. */
.t-rail{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,330px),1fr));
  gap:1px;background:var(--rake);border-block:1px solid var(--rule);
}
/* Cards are surfaces, not swatches: lit at the top edge, and tall
   enough that the attributions of a pair sit on one baseline. */
.t-card{
  padding:clamp(30px,3.2vw,46px) clamp(28px,3vw,42px) clamp(30px,3.2vw,44px);
  background-color:var(--onyx);
  background-image:var(--sheen);
  box-shadow:var(--lip-tl);
  display:flex;flex-direction:column;
}
.skin-creme .t-card,.skin-creme-deep .t-card{background-color:var(--papier)}
/* 500 and no higher: this reads as a paragraph over a 26ch measure, and
   600 across 26ch is a wall. It is the one secondary-display rule in this
   file that deliberately stops short. */
.t-card p{font-family:var(--ff-display);font-weight:500;font-size:clamp(19px,1.7vw,23px);line-height:1.4;color:var(--ink);max-width:26ch;text-wrap:pretty}
.t-card .stars{color:var(--gold-text);letter-spacing:.3em;font-size:13px;margin-bottom:20px}
.t-card .who{margin-top:auto;padding-top:34px;font-size:13px;letter-spacing:.04em;color:var(--muted)}
.t-card .who b{color:var(--ink);font-weight:500;display:block;font-family:var(--ff-body);letter-spacing:.02em;margin-bottom:2px}

/* ---------- FAQ accordion ---------- */
/* The container's top edge is the PLATE edge and stays a flat border —
   a gradient cannot live in a border shorthand. The cuts BETWEEN the rows
   are re-expressed as 1px fills so they can take the rake; same division as
   .cards, where the block borders are flat and the 1px gaps are lit. */
.faq{border-top:1px solid var(--rule)}
.faq__item{position:relative}
.faq__item::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  background:var(--rake);pointer-events:none;
}
/* A gold rule grows down the left edge of the row you are pointing at
   or have opened — the row states its own boundary instead of relying
   on a colour shift in the heading alone. */
.faq__item::before{
  content:"";position:absolute;left:calc(var(--sp-2) * -1);top:0;bottom:0;width:2px;
  background:var(--gold-text);
  transform:scaleY(0);transform-origin:top;
  transition:transform .45s var(--ease);
}
.faq__item:hover::before,.faq__item:focus-within::before,.faq__item.open::before{transform:scaleY(1)}
.faq__q{width:100%;display:flex;justify-content:space-between;align-items:center;gap:24px;
  padding:clamp(22px,2.6vw,32px) 0;background:none;border:none;cursor:pointer;text-align:left;color:var(--ink)}
.faq__q h3{font-size:clamp(19px,1.9vw,26px);transition:color .4s var(--ease)}
.faq__item:hover .faq__q h3,.faq__q:focus-visible h3,.faq__item.open .faq__q h3{color:var(--gold-text)}
.faq__q:focus-visible{outline-offset:-2px}
.faq__ic{flex:none;width:22px;height:22px;position:relative}
.faq__ic::before,.faq__ic::after{content:"";position:absolute;background:var(--gold-text);transition:transform .45s var(--ease)}
.faq__ic::before{left:0;top:50%;width:100%;height:1.5px;transform:translateY(-50%)}
.faq__ic::after{left:50%;top:0;height:100%;width:1.5px;transform:translateX(-50%)}
.faq__item.open .faq__ic::after{transform:translateX(-50%) scaleY(0)}
/* Answer panel. Was `transition:height` with the pixel height written
   by JS on every open — a layout animation, and it forced a synchronous
   reflow (offsetHeight read) on open, on resize and on every language
   switch. It is now grid-template-rows 0fr→1fr, which needs no measured
   height at all.
   The JS still writes an inline `height`, and I do not own that file,
   so `height:auto !important` neutralises it. That is the one place an
   !important is load-bearing here rather than lazy.
   `visibility` is transitioned alongside: it is a discrete property, so
   it stays `visible` for the whole collapse and only flips at the end —
   which means the closed panel is genuinely removed from the
   accessibility tree and the tab order, not merely clipped to 0px. */
.faq__a{
  display:grid;
  grid-template-rows:0fr;
  height:auto !important;
  overflow:hidden;
  visibility:hidden;
  /* Closed: stay visible for the whole collapse, then go. Open: appear
     at once. Explicit 0s steps rather than leaning on how `visibility`
     interpolates — the AT state is not something to leave to a
     discrete-interpolation edge case. */
  transition:grid-template-rows .5s var(--ease),visibility 0s linear .5s;
}
.faq__item.open .faq__a{
  grid-template-rows:1fr;visibility:visible;
  transition:grid-template-rows .5s var(--ease),visibility 0s linear 0s;
}
.faq__a>*{overflow:hidden;min-height:0}
/* The bottom air has to live INSIDE the collapsing box. Padding on a
   grid item sits outside its content box, so it survives the automatic
   minimum size and floors the 0fr track — measured: the closed panel
   stayed exactly 32px tall, the padding's own height. As a spacer in
   the content flow it collapses with everything else. */
.faq__a-inner{padding-bottom:0;color:var(--muted);max-width:70ch;font-size:16px}
.faq__a-inner::after{content:"";display:block;height:clamp(22px,2.6vw,32px)}
@media (prefers-reduced-motion:reduce){
  /* The delay goes too, not just the duration — otherwise a collapsed
     panel would stay reachable by a screen reader for half a second
     after it visibly closed. */
  .faq__a,.faq__item.open .faq__a{transition-duration:.01ms;transition-delay:0s}
  .faq__ic::before,.faq__ic::after,.faq__item::before,.faq__q h3{transition-duration:.01ms}
}

/* ---------- The seam ----------
   A lathe-turned band: --seam-ink showing through the --seam-plate mask, both
   published in css/pcg.css.

   CONSOLIDATED, and this was a standing TODO in tools/sync-shell.js: the same
   masked band existed in two places, as an inline style string on the footer and
   as .est__seam in css/pricing.css, and nowhere reusable. It is defined once here
   and both now use it.

   WHERE IT BELONGS, because this was got wrong once. It TERMINATES a block the
   reader has finished — the foot of the page under the colophon, the foot of the
   estimator readout bay. It is not a divider, and it should not be dropped
   between two sections to "break them up": a third element briefly stood between
   the hero and #essentie doing exactly that, and the verdict on it was that it
   read as an empty box.

     - Semantically, css/pcg.css:493 fires the engraved band only at
       dark-to-cream transitions, because on this site the seam is paper meeting
       plate. Hero to manifesto is plate meeting plate. There was no change for
       it to mark.
     - Rhetorically, a struck rule means "this has ended". That is exactly right
       under a colophon and under a price readout. In open space between two
       sections that are one continuous movement, it announces an ending that has
       not happened, and the reader is left looking for the thing that ended.
     - Practically, it had to be given a ground to sit on, which meant a
       skin-char band (#141416) between two skin-onyx (#0B0B0C) sections. That is
       a 4% step: too weak to read as a deliberate material, strong enough to
       read as an accident. Two surfaces became three and the join got harder to
       parse, not easier.

   If a join needs marking, change the ground or close the gap — the shared-skin
   rhythm rule at css/pcg.css:695 is the tool for the second, and it is what fixed
   this one.

   Purely decorative, so every use is aria-hidden and it drops out under
   forced-colors, where a masked gradient cannot render meaningfully. */
.seam{
  display:block;height:3px;width:100%;
  background:var(--seam-ink);
  -webkit-mask-image:var(--seam-plate);
  mask-image:var(--seam-plate);
  -webkit-mask-size:168px var(--seam-h);
  mask-size:168px var(--seam-h);
  -webkit-mask-position:0 calc(var(--seam-h) * -.43);
  mask-position:0 calc(var(--seam-h) * -.43);
  -webkit-mask-repeat:repeat-x;
  mask-repeat:repeat-x;
  opacity:.85;
  pointer-events:none;
}
@media (forced-colors:active){.seam{display:none}}

/* ---------- CTA band ---------- */
.cta-band{position:relative;text-align:center;overflow:hidden}
.cta-band .display{margin-block:24px 40px}
.cta-band__glow{position:absolute;inset:0;z-index:0;background:radial-gradient(60% 80% at 50% 120%,rgba(201,161,74,.16),transparent 70%);pointer-events:none}

/* ---------- Editorial page pagination ---------- */
/* The link to the next chapter is the largest single target on the
   page, and it had no state beyond a colour swap on the heading. Light
   now rises into it from the leading edge as you approach — one warm
   source, same as everywhere else, and a composited opacity fade.
   (Safe to own ::before here: data-spotlight is only ever on .card.) */
.next-page{
  display:block;position:relative;overflow:hidden;isolation:isolate;
  border-top:1px solid var(--rule);
  padding-block:clamp(44px,6vw,86px);
}
.next-page::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:var(--wash);
  opacity:0;transition:opacity .5s var(--ease);
}
.next-page:hover::before,.next-page:focus-visible::before{opacity:1}
.next-page__inner{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}
.next-page__lbl{font-size:11px;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--muted)} /* .28em -> the declared label step */
/* It IS an h3, so it inherits 800 from the ladder correctly — but the
   ladder's h3 tracking (-.012em) is set for 23-30px and is far too loose
   at 62px. Overridden here, not in the ladder. */
.next-page h3{font-size:clamp(30px,5vw,62px);font-family:var(--ff-display);letter-spacing:-.024em;line-height:1;margin-top:14px;transition:color .5s var(--ease)}
.next-page:hover h3{color:var(--gold-text)}
.next-page__arr{
  width:clamp(52px,5vw,74px);height:clamp(52px,5vw,74px);flex:none;
  border:1px solid var(--rule);border-radius:50%;
  display:grid;place-items:center;color:var(--gold-text);font-size:20px;
  transition:background .5s var(--ease),color .5s var(--ease),border-color .5s var(--ease),
             transform .5s var(--ease),box-shadow .5s var(--ease);
}
.next-page:hover .next-page__arr,.next-page:focus-visible .next-page__arr{
  background:var(--gold-grad);color:#15110a;border-color:transparent;
  transform:translateX(6px);box-shadow:var(--cast-gold);
}
.next-page:focus-visible h3{color:var(--gold-text)}
@media (prefers-reduced-motion:reduce){
  .next-page__arr,.next-page h3{transition-duration:.01ms}
  .next-page:hover .next-page__arr{transform:none}
}

/* ---------- Filter chips ---------- */
.filter-bar{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.chip{
  font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;font-weight:500;
  color:var(--muted);border:1px solid var(--rule);background:transparent;
  padding:9px 18px;border-radius:999px;cursor:pointer;
  transition:color .4s var(--ease),border-color .4s var(--ease),
             background-color .4s var(--ease),box-shadow .4s var(--ease),transform .16s var(--ease-soft);
}
.chip:hover,.chip:focus-visible{color:var(--ink);border-color:var(--gold-text);background-color:rgba(201,161,74,.09)}
.chip:active{transform:scale(.96)}
.chip.on{background:var(--gold-grad);border-color:transparent;color:#15110a;box-shadow:var(--cast-gold)}
.chip.on:hover{color:#15110a}
@media (prefers-reduced-motion:reduce){.chip:active{transform:none}}
.is-filtered-out{display:none !important}

/* ---------- Case study furniture ---------- */
/* RULED, NOT BOXED — the same move as .cards, .t-rail and .deliver, and
   this was the last four-sided outline of the family. Rendered at 1440 it
   was the one fact grid on the site still drawing its own left and right
   edges, which read as a widget dropped onto the page rather than as a
   band cut into it. Only the plate edges remain; the three internal cuts
   are raked and the cells are milled pockets, which is what describes the
   object now. Fixed at 4 tracks with exactly 4 items, so the empty-cell
   hazard guarded on .cards cannot arise here. */
.case-facts{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rake);border-block:1px solid var(--rule)}
.case-fact{background-color:var(--onyx);background-image:var(--sheen);box-shadow:var(--lip-tl);padding:clamp(22px,2.4vw,32px)}
.skin-creme .case-fact,.skin-creme-deep .case-fact{background-color:var(--papier)}
.case-fact dt{font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;color:var(--muted)}
.case-fact dd{font-family:var(--ff-display);font-weight:600;font-size:clamp(19px,1.9vw,24px);color:var(--ink);margin-top:9px;line-height:1.2}

/* Same measure pitfall as .quote: the wrapper's `ch` resolved against
   the body face while the paragraph inside runs at up to 30px display.
   The measure now sits on the paragraph, in the face that sets it. */
.pull{
  border-left:2px solid var(--gold-text);padding-left:clamp(22px,3vw,40px);
  margin-block:clamp(34px,4vw,58px);max-width:none;
}
.pull p{font-family:var(--ff-display);font-weight:600;font-size:clamp(21px,2.3vw,30px);letter-spacing:-.01em;line-height:1.3;color:var(--ink);max-width:27ch;text-wrap:pretty}
/* APPARATUS IS SMALLER, NOT PALER. The attribution under a pull quote is
   the one true caption in this sheet, and it was doing the print-typographer's
   inverse: same 13px as everything else, held back by a WEAKER ink. It keeps
   the small size and takes --ink-cap (pcg.css:415/418), the caption ink,
   which is darker on paper and brighter on metal than --muted was:
     --ink-cap #4A4335 on --papier #F8F3E9 = 8.85:1  (was --muted #665c4b, 5.94:1)
     --ink-cap #4A4335 on --creme  #EFE6D3 = 7.89:1
     --ink-cap #B8AE99 on --onyx   #0B0B0C = 8.95:1  (was --steen #918A79, 5.73:1)
   All three were already AA; all three are now AAA at this size. */
.pull cite{display:block;margin-top:18px;font-style:normal;font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-cap)}

.deliver{list-style:none;display:grid;gap:1px;background:var(--rake);border-block:1px solid var(--rule)}
.deliver li{background-color:var(--onyx);background-image:var(--sheen);box-shadow:var(--lip-tl);padding:clamp(16px,1.8vw,22px) 0 clamp(16px,1.8vw,22px) 34px;position:relative;color:var(--ink);font-size:15.5px}
.skin-creme .deliver li,.skin-creme-deep .deliver li{background-color:var(--papier)}
.deliver li::before{
  content:"";position:absolute;left:6px;top:calc(clamp(16px,1.8vw,22px) + .55em);
  width:7px;height:7px;background:var(--gold-grad);transform:rotate(45deg);
}
.deliver li b{font-weight:500}
.deliver li span.n{color:var(--muted);font-size:13px;display:block;margin-top:4px}

/* ---------- Client / trust logo row ---------- */
.logo-row{display:flex;flex-wrap:wrap;align-items:center;gap:clamp(26px,4vw,60px);justify-content:center}
.logo-row span{
  font-family:var(--ff-display);font-weight:600;font-size:clamp(16px,1.6vw,21px);letter-spacing:.06em;
  /* These are CLIENT NAMES, the page's trust row, and at 16-21px they need
     the full 4.5:1. opacity:.7 put them at 3.11:1 on --papier and 2.92:1 on
     --creme. The hover is carried by colour alone now, which is the stronger
     signal anyway. */
  color:var(--muted);transition:color .5s var(--ease);
}
.logo-row span:hover{color:var(--gold-text)}

/* ---------- Timeline ---------- */
/* Plate edge flat, internal cuts raked — same division as .faq and .cards. */
.timeline{list-style:none;border-top:1px solid var(--rule)}
.timeline li{display:grid;grid-template-columns:120px 1fr;gap:clamp(20px,3vw,48px);padding-block:clamp(22px,2.6vw,34px);position:relative}
.timeline li::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  background:var(--rake);pointer-events:none;
}
/* A year is a figure; it should read as one. */
.timeline .yr{font-family:var(--ff-display);font-weight:700;font-size:clamp(20px,2vw,26px);color:var(--gold-text)}
.timeline h3{font-size:clamp(19px,1.8vw,23px)}
.timeline p{color:var(--muted);margin-top:8px;max-width:60ch;font-size:15px}

/* ---------- Footer ---------- */
.footer{background:#070707;color:var(--steen);padding-block:clamp(60px,7vw,100px) 40px;border-top:1px solid rgba(201,161,74,.16)}
.footer__lead{
  display:flex;align-items:flex-end;justify-content:space-between;gap:32px;flex-wrap:wrap;
  padding-bottom:clamp(44px,5vw,72px);margin-bottom:clamp(44px,5vw,72px);
  border-bottom:1px solid rgba(255,255,255,.07);
}
.footer__lead h2{color:var(--creme-d);max-width:16ch}
/* The first track used to be 1.4fr because .footer__brand held a 34ch
   paragraph under the mark. That sentence is gone, so a 1.4fr track would
   leave a quarter of the row empty beside a 37px image. `auto` sizes the
   column to the mark itself and hands the width back to the four link
   columns, which is where the long labels are (measured: 181px -> 234px per
   column at 1136px, so "Webdesign en development" stops wrapping).
   The two-column and one-column steps at 960 and 640 are unaffected: both
   restate grid-template-columns outright. */
.footer__top{display:grid;grid-template-columns:auto repeat(4,1fr);gap:40px}
.footer__brand img{height:54px;width:auto}
.footer h3{font-family:var(--ff-body);font-weight:500;font-size:11px;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--champagne);margin-bottom:20px} /* .2em -> the declared label step */
.footer ul{list-style:none}
.footer li{margin-bottom:12px}
/* min-height:24px for the same reason .crumbs a carries it (css/pcg.css:773): these are
   standalone navigation links, not links inside prose, so they need a real target. At
   15px/1.28 they measured 19px tall — every link in all four footer columns plus the
   address, the phone number and the three legal links, on all 11 pages. The 12px
   li margin already put them outside WCAG 2.5.8's spacing exception, so this was not a
   failure; it was still 19px of target on a phone. */
.footer li a{font-size:15px;color:var(--steen);transition:color .4s;display:inline-flex;align-items:center;min-height:24px}
.footer li a:hover{color:var(--creme-d)}
.footer__bottom{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-top:clamp(50px,6vw,80px);padding-top:30px;border-top:1px solid rgba(255,255,255,.07);font-size:12.5px;letter-spacing:.06em}
.footer__bottom .sig{font-family:var(--ff-display);font-weight:600;letter-spacing:.01em;color:var(--champagne);font-size:15px}

/* ---------- Contact form ---------- */
.field{position:relative;margin-bottom:34px}
.field input,.field textarea{
  width:100%;background:transparent;border:none;border-bottom:1px solid var(--rule);
  color:var(--ink);font-family:var(--ff-body);font-size:17px;font-weight:400;
  padding:12px 0;outline:none;transition:border-color .4s
}
.field textarea{resize:vertical;min-height:90px}
.field label{position:absolute;left:0;top:12px;color:var(--muted);font-size:17px;pointer-events:none;transition:.4s var(--ease)}
.field input:focus,.field textarea:focus{border-color:var(--gold-text)}
.field input:focus+label,.field textarea:focus+label,
.field input:not(:placeholder-shown)+label,.field textarea:not(:placeholder-shown)+label{
  top:-14px;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-text)
}
.field input.is-invalid,.field textarea.is-invalid{border-color:#C4634A}
.field input.is-invalid+label,.field textarea.is-invalid+label{color:#C4634A}
.form-error{
  color:#D9836B;font-size:13.5px;letter-spacing:.02em;line-height:1.5;
  border-left:2px solid #C4634A;padding-left:14px;margin-bottom:20px;
}

/* Topic toggles double as buttons, so give them the pressed state */
.tag[aria-pressed="true"]{background:var(--gold-grad);border-color:transparent;color:#15110a}
button.tag{cursor:pointer;font-family:var(--ff-body);background:transparent;transition:background .4s var(--ease),color .4s var(--ease),border-color .4s var(--ease)}
button.tag:hover{border-color:var(--gold-text)}
/* MEASURED at 390: the four topic toggles render 29px tall (11px type,
   6px padding, 1px border) — well under the 44px touch minimum for what is
   a real pressable control on the contact form, and the only live .tag
   instances on the site are these buttons. Same remedy, and the same
   reasoning, as .lang button, .burger and .drawer__meta a above: grow the
   HIT AREA on coarse pointers, never the type. Widths already measure
   43-85px, so the vertical axis is what needs it; min-width covers the
   narrowest label ("AI", measured 43.1px) for the last 0.9px. Scoped to
   `button.tag` so a decorative .tag label in a .step__tags row is left
   alone. Re-measured with pointer:coarse emulated: 44x44 on all four. */
@media (hover:none),(pointer:coarse){
  button.tag{display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:44px}
}

/* ---------- Trust / risk-reversal bar ---------- */
.trust{border-block:1px solid var(--rule)}
.trust__row{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--rake)}
.trust__cell{background-color:var(--onyx);background-image:var(--sheen);box-shadow:var(--lip-tl);padding:clamp(22px,2.4vw,30px) clamp(18px,2vw,28px);display:flex;flex-direction:column;gap:7px}
.skin-creme .trust__cell,.skin-creme-deep .trust__cell{background-color:var(--papier)}
/* Duplicated verbatim at configurator.css:136 — keep the two in step or
   the dock and the pricing page disagree. */
.trust__cell .t{font-family:var(--ff-display);font-weight:600;font-size:clamp(19px,1.8vw,24px);color:var(--ink);display:flex;align-items:center;gap:.5em}
.trust__cell .t::before{content:"";width:7px;height:7px;background:var(--gold-grad);transform:rotate(45deg);flex:none}
.trust__cell .d{font-size:13.5px;color:var(--muted);line-height:1.5}

/* ---------- Floating CTA dock (site-wide) ---------- */
/* --- Floating scan dock: one ring, then it stops -------------------------
   The dock already announces itself by sliding up, which is enough movement for
   something that arrives unrequested. The ring is here because the dock now
   carries the free-scan offer rather than a generic "book a call", and that
   offer is the reason a first-time visitor would stop scrolling.

   TWICE, NOT INFINITE. The dock is fixed and stays for the rest of the visit;
   an endlessly pulsing element in the corner of the viewport is the exact
   texture of an ad widget, and it repaints every frame for as long as the page
   is open. .show is toggled on every scroll cross, so the animation is keyed to
   it and simply replays if the visitor scrolls back up past the threshold.
   The ring sits OUTSIDE the button so it never touches the label, and reuses
   the @keyframes scan-ring defined with the static band. */
.cta-dock .btn{position:relative}
.cta-dock .btn::after{
  content:"";position:absolute;inset:-5px;border-radius:inherit;
  border:1px solid var(--gold-text,#c9a86a);
  opacity:0;pointer-events:none;
}
.cta-dock.show .btn::after{animation:scan-ring 2.2s var(--ease) .9s 2 both}
@media (prefers-reduced-motion:reduce){
  .cta-dock.show .btn::after{animation:none}
}

.cta-dock{
  position:fixed;right:clamp(16px,2.5vw,30px);bottom:clamp(16px,2.5vw,30px);z-index:85;
  display:flex;align-items:center;gap:14px;
  padding:11px 12px 11px 22px;
  background-color:rgba(11,11,12,.86);background-image:var(--sheen-lit);
  backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);
  border:1px solid rgba(201,161,74,.34);
  box-shadow:var(--lift-3);
  transform:translateY(140%);opacity:0;pointer-events:none;
  transition:transform .6s var(--ease),opacity .6s var(--ease);
}
/* THE DOCK STOPS GUILLOTINING. The dock is opaque to .86 with a 1px edge,
   so at the moment a paragraph or a heading passes behind it the line is
   simply severed — captured at 1440 on index.html, where the dock cut
   clean through the "05 Lancering en groei" heading. A hard edge says the
   render ended; a ramp says the page continues underneath.
   The feather is the dock's OWN material (the same rgba(11,11,12,.86) the
   surface is painted in) carried 14px above its top edge and dying to
   zero, so it is not a second colour and needs no ratio of its own — it is
   the near edge of the shadow --lift-3 is already casting, made explicit.
   Ground-agnostic by construction: on the dark skins it is invisible
   against the ground, on the cream skins it reads as the chip's soft cast,
   which is what a floating object does to paper.
   Measured worst case for text passing THROUGH the ramp: --inkt #1A1610 on
   --papier #F8F3E9 veiled at the ramp's midpoint (.43) = 5.85:1, still
   past 4.5:1; below that the line is occluded by the dock proper exactly
   as it is today, and the dock carries a close button.
   -1px/-1px so the feather covers the 1px border's own width and the two
   top corners do not show a notch. */
.cta-dock::before{
  content:"";position:absolute;left:-1px;right:-1px;bottom:100%;height:14px;
  pointer-events:none;
  background:linear-gradient(to top,rgba(11,11,12,.86),rgba(11,11,12,0));
}
.cta-dock.show{transform:none;opacity:1;pointer-events:auto}
.cta-dock.tuck{transform:translateY(140%);opacity:0;pointer-events:none}
.cta-dock__txt{display:flex;flex-direction:column;line-height:1.35}
.cta-dock__txt b{font-family:var(--ff-display);font-weight:600;font-size:16px;color:var(--creme-d)}
/* `>span`, not a descendant selector. js/pcg.js builds the dock as
   `<b><span data-lang=nl>…</span><span data-lang=en>…</span></b>` plus a
   sibling sub-label span, so a bare `.cta-dock__txt span` also matched the
   two language spans INSIDE the <b> — same specificity as the <b> rule and
   later in source, so it won, and the bold line rendered at 10.5px
   uppercase --steen instead of 16px --creme-d. The child combinator is what
   keeps this rule on the sub-label it was written for. */
.cta-dock__txt>span{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--steen);margin-top:5px}
.cta-dock .btn{padding:.8em 1.3em;font-size:11px;border-color:var(--champagne);color:var(--creme-d)}
.cta-dock .btn::before{background:var(--gold-grad)}
.cta-dock .btn:hover{color:#15110a}
.cta-dock__close{
  background:none;border:none;color:var(--steen);cursor:pointer;font-size:18px;line-height:1;
  padding:9px 11px;align-self:flex-start;transition:color .3s;margin:-5px 0 -5px -11px;
  display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px
}
.cta-dock__close:hover{color:var(--champagne)}

/* ---------- Forced colours: give the machined rules their borders back ----------
   A `border` survives Windows High Contrast — the UA repaints it in the
   system CanvasText colour. A 1px pseudo-element painted with `background`
   does NOT: backgrounds are forced to Canvas and the rule vanishes.
   Three dividers on this site were converted from border to fill above so
   they could take the rake (.step, .faq__item, .timeline li). Under forced
   colours the rake is meaningless anyway, so the fill is dropped and the
   original border is restored — the row separation is structural, not
   decorative, and a list of steps with no rules between them is a real
   loss of legibility for exactly the users this mode exists for.
   `currentColor` rather than var(--rule): in forced-colors the UA has
   already substituted a system colour for the text, so this tracks it.
   The .cta-dock feather is decorative and is simply dropped. */
@media (forced-colors:active){
  .step::before,.step:last-child::after,
  .faq__item::after,.timeline li::after,
  .cta-dock::before{display:none}
  .step{border-top:1px solid currentColor}
  .step:last-child{border-bottom:1px solid currentColor}
  .faq__item{border-bottom:1px solid currentColor}
  .timeline li{border-bottom:1px solid currentColor}
}

/* ---------- Responsive ---------- */
@media (max-width:1100px){
  .nav__links{gap:24px}
  .mega{width:min(620px,92vw)}
}
@media (max-width:960px){
  .trust__row{grid-template-columns:1fr 1fr}
  .nav__links{display:none}
  .burger{display:flex}
  /* .t-rail, .cards and .stats are intentionally absent — all three are
     auto-fit now and size themselves. Re-adding a fixed track here would
     win on source order and silently kill the auto-fit. */
  .work-grid{grid-template-columns:1fr}
  .footer__top{grid-template-columns:1fr 1fr}
  .hero__mono{display:none}
  .hero__veil{background:linear-gradient(0deg,rgba(8,8,9,.95) 0%,rgba(8,8,9,.55) 100%)}
  .nav__brand .bn span{display:none}
}
@media (max-width:760px){
  .nav__right .btn{display:none}
}
@media (max-width:640px){
  .case-facts{grid-template-columns:1fr}
  .step{grid-template-columns:1fr;gap:14px}
  .timeline li{grid-template-columns:1fr;gap:6px}
  .footer__top{grid-template-columns:1fr}
  .footer__bottom{flex-direction:column;align-items:flex-start}
  .quote blockquote{font-size:30px}
  .trust__row{grid-template-columns:1fr}
  .cta-dock{left:14px;right:14px;bottom:14px;justify-content:space-between;padding-left:18px}
  .next-page__inner{gap:18px}
}
/* Below 430 the dock's tracked sub-label wrapped to two lines and pushed
   the bold line into it inside an 86px box — measured collided at 390.
   The 0.14em uppercase tracking is the cause, and the sub-label says
   nothing the button does not already promise, so it goes rather than
   shrinks. One clear line plus the button. */
@media (max-width:430px){
  /* Child combinator again — this must never reach the <b>'s language
     spans, or the dock loses its only line of text. The element hidden
     here is the sub-label WRAPPER, not a [data-lang] element, so per-page
     NL/EN parity is untouched. */
  .cta-dock__txt>span{display:none}
  .cta-dock__txt b{font-size:15px;white-space:nowrap}
}
/* Under ~400 the dock genuinely cannot hold the headline, an 18-character
   tracked button label and a 44px close target on one line: measured at
   360, the button was wrapping to three lines and pushing the box to 83px.
   The button is the thing that must stay intact and tappable, so it gets
   nowrap and the headline is the part that gives. */
@media (max-width:400px){
  .cta-dock{gap:10px;padding:10px 10px 10px 14px}
  .cta-dock .btn{white-space:nowrap;padding:.8em 1em;font-size:10.5px;letter-spacing:.12em}
  .cta-dock__txt b{font-size:14px;white-space:normal}
  .cta-dock__txt{min-width:0}
}

/* ===========================================================================
   SCAN BAND — the free SEO + AEO scan offer
   ---------------------------------------------------------------------------
   An interjection, not a closing address. It deliberately does NOT reuse
   .cta-band: that class is a page's FINAL ask (pcg.css:749 hands it
   --sp-section-loose on the way out, and .cta-band above forces text-align:
   center), so a mid-page band wearing it would read as a second ending and
   the page would appear to finish twice. This one rides .section--tight,
   stays left-aligned, and draws its own two hairlines so it reads as a ruled
   insert BETWEEN two movements rather than as the end of one.

   Ships on .skin-onyx wherever it appears, so the markup stays byte-identical
   across the three pages that carry it and only the placement differs. Onyx is
   ground all three of those pages already stand on, so the band never adds a
   surface a page did not have.

   Every colour is a skin-published token (--ink, --muted, --rule-soft,
   --gold-text via --gold-grad), so if the band is ever moved onto a cream
   section it re-inks itself instead of going invisible.
   =========================================================================== */
.scan-band__inner{
  position:relative;
  display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
  gap:var(--sp-4) var(--sp-6);
  padding-block:var(--sp-5);
  /* --rule-soft, not --rule: two hairlines this close together read twice as
     heavy as one, and the band is an aside, not a headline. Never --rake here,
     that token is a gradient and `1px solid` would silently draw nothing. */
  border-block:1px solid var(--rule-soft);
}
/* The only ornament: a short gold segment struck over the left end of the top
   hairline. Same vocabulary as .pillar's struck top rule, at a fraction of the
   width, which is the whole point of the band being quiet. */
.scan-band__inner::before{
  content:"";position:absolute;left:0;top:-1px;width:56px;height:1px;
  background:var(--gold-grad);
}
/* Basis in ch, not px: the column gives way when the SENTENCE runs out of room,
   which is the thing that actually decides whether two columns still work. */
.scan-band__copy{flex:1 1 42ch;min-width:0;max-width:56ch}
/* Down a step from --fs-h2. At 52px this would out-shout the real section
   heading immediately above or below it, and the band is the smaller idea. */
.scan-band__copy h2{font-size:var(--fs-h3);max-width:22ch}
.scan-band__copy p{margin-top:var(--sp-2);color:var(--muted);max-width:48ch}
.scan-band__action{flex:0 0 auto;max-width:100%}

@media (max-width:640px){
  /* One column. The basis above already forces the wrap well before this, so
     this only tightens the rhythm once the band is stacked — an --sp-6 column
     gap between a heading and a button that now sit on top of each other is
     air nothing earned. */
  .scan-band__inner{gap:var(--sp-3);padding-block:var(--sp-4)}
  .scan-band__copy{flex-basis:100%}
}

/* --- SCAN BAND: motion, and the one reactive gesture ----------------------
   The band now opens the page, so it has to earn attention without shouting.
   Three things, all cheap, none of them looping forever: a gold sweep that
   crosses the top hairline once as the band arrives, a ring that leaves the
   button twice and then stops, and a hairline that answers the pointer.

   WHY :has() AND NOT A CLASS ON THE SECTION. The reveal observer only marks
   elements that carry data-rise, and .scan-band__inner carries none — giving
   it one would animate the band AND its children, which double-fades the same
   content. :has(.is-in) lets the wrapper react to its own revealed child
   instead. Supported everywhere this site already targets (it ships view
   transitions), so this is not the risky part of the file.

   NOTHING HERE LOOPS. An infinite animation on a CTA reads as a banner ad and
   costs main-thread time on every frame for the life of the page. Both
   keyframes are `both`-filled and finite; after ~4s the band is static. */
.scan-band__inner::after{
  content:"";position:absolute;left:0;top:-1px;width:100%;height:1px;
  background:linear-gradient(90deg,transparent,var(--gold-text,#c9a86a),transparent);
  transform:scaleX(0);transform-origin:0 50%;opacity:0;pointer-events:none;
}
.scan-band__inner:has(.is-in)::after{animation:scan-sweep 1.7s var(--ease) .25s 1 both}
@keyframes scan-sweep{
  0%{transform:scaleX(0);opacity:0}
  12%{opacity:1}
  100%{transform:scaleX(1);opacity:0}
}

/* The ring sits OUTSIDE the button (inset:-5px) so it never covers the label,
   and inherits the radius so it tracks whatever .btn is shaped like. */
.scan-band__action .btn{position:relative}
.scan-band__action .btn::after{
  content:"";position:absolute;inset:-5px;border-radius:inherit;
  border:1px solid var(--gold-text,#c9a86a);
  opacity:0;pointer-events:none;
}
.scan-band__action.is-in .btn::after{animation:scan-ring 2.2s var(--ease) .7s 2 both}
@keyframes scan-ring{
  0%{opacity:.5;transform:scale(.97)}
  65%{opacity:0;transform:scale(1.05)}
  100%{opacity:0;transform:scale(1.05)}
}

/* The reactive gesture: the struck gold segment on the top hairline grows when
   the pointer is anywhere on the band, and both hairlines firm up. Hover only —
   it is decoration, so it must not be the thing that tells you the band is
   interactive. The button already does that. */
.scan-band__inner{transition:border-color .45s var(--ease)}
.scan-band__inner::before{transition:width .45s var(--ease)}
.scan-band:hover .scan-band__inner{border-block-color:var(--rule)}
.scan-band:hover .scan-band__inner::before{width:132px}

@media (prefers-reduced-motion:reduce){
  /* Keep the ornament, drop every movement. The sweep is removed rather than
     shortened: a 0s sweep still repaints the hairline for one frame. */
  .scan-band__inner:has(.is-in)::after,
  .scan-band__action.is-in .btn::after{animation:none}
  .scan-band__inner,.scan-band__inner::before{transition:none}
}

/* The hero's third CTA. Gold ink and a gold rule so the free offer is
   distinguishable from "Bereken uw prijs" beside it without becoming a second
   button. Colour only, no weight change: bolding it would make it compete with
   the solid primary, and the whole point of this link is that it is the low
   commitment one. */
.link-underline--gold{color:var(--gold-text,#c9a86a)}
.link-underline--gold::after{background:var(--gold-grad)}

/* A gold lozenge in front of the free offer, borrowed from .cta-note::before so
   it is the site's existing vocabulary rather than a new ornament. It carries
   the attention beat: three slow pulses while the visitor is still reading the
   hero, then it holds. Finite for the same reason the dock's ring is finite --
   a marker that never stops pulsing in a hero reads as a banner, and it would
   repaint for as long as the tab is open.

   The mark is on a ::before so it cannot be selected or read aloud: it is
   decoration, and the words "Gratis SEO + AEO scan" already say everything a
   screen reader needs. */
.link-underline--gold::before{
  content:"";width:6px;height:6px;flex:none;
  background:var(--gold-grad);transform:rotate(45deg);
  animation:scan-mark 2.4s var(--ease) 1.2s 3 both;
}
@keyframes scan-mark{
  0%,100%{opacity:.45;transform:rotate(45deg) scale(1)}
  50%{opacity:1;transform:rotate(45deg) scale(1.45)}
}
@media (prefers-reduced-motion:reduce){
  .link-underline--gold::before{animation:none;opacity:1}
}
