/* ==========================================================================
   SiBo Marketing — dark cinematic design system
   Rewritten 2026-08-17 for the Schmidhuber-inspired redesign.
   Hand-coded, no framework, no external font or asset requests.

   CONCEPT — "cinematic, image-free"
   The reference (schmidhuber.de) gets its power from full-bleed photography.
   SiBo has no photos and no completed portfolio work, and using stock would
   imply project work that isn't Siem's. So the visual weight comes instead
   from three things: type at extreme scale, full-bleed colour fields, and
   motion. One idea per screen, generous darkness, nothing decorative that
   pretends to be content.

   PALETTE — the brand colours are unchanged, but on a dark base their roles
   have to change, and one of them is hard-constrained:
     Peacock  #053229 — the page base. White on it = 14.05:1.
     Sapphire #72B0AB — THE accent. Links, indicators, arrows, rules,
       index numerals. On Peacock = 5.70:1, clears AA for text.
     Spruce   #355E58 — on Peacock it is only ~2.5:1, so it can NEVER be
       text or an interactive colour here. It is used exclusively as a
       SURFACE and gradient colour (raised panels, hero wash). This is the
       single most important rule in this file.
   --bg-deep / --surface are derived shades of Peacock (same hue, different
   value) — value steps, not new brand colours.

   MOTION — every animation is wrapped in a prefers-reduced-motion guard and
   every scroll-reveal starts from a visible state if JS never runs, so the
   page is fully readable with JS disabled.
   ========================================================================== */

:root {
  /* ---- Brand ---- */
  --peacock:  #053229;
  --spruce:   #355E58;
  --sapphire: #72B0AB;

  /* ---- Derived value steps (same hue as Peacock) ---- */
  --bg-deep:   #02100D;
  --bg:        #053229;
  --surface:   #0A3F34;
  --surface-2: #104C40;

  /* ---- Text on dark ---- */
  --text:       #FFFFFF;                    /* 14.05:1 on --bg */
  --text-muted: rgba(255, 255, 255, 0.72);  /* ~7.9:1 composited on --bg */
  --text-faint: rgba(255, 255, 255, 0.52);  /* decorative/meta only, never body */
  /* Instructional small text (form hints). 0.52 alpha measured 4.40:1 on
     --surface — a fail. 0.66 clears 4.5:1 on both --bg and --surface. */
  --text-hint: rgba(255, 255, 255, 0.66);
  --line:       rgba(255, 255, 255, 0.14);
  --line-strong:rgba(255, 255, 255, 0.42);  /* form borders — ~5.9:1 composited */

  --accent:     var(--sapphire);
  --on-accent:  #04241D;                    /* dark text ON a sapphire fill, ~7.4:1 */
  --error:      #FF9A92;                    /* light red — dark-mode safe, ~7.4:1 on --bg */
  --error-bg:   rgba(255, 154, 146, 0.10);

  /* ---- Type ---- */
  --font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
          "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* The display size is deliberately enormous — it is the whole visual.
     Capped at 8rem: above that the Home claim's second line ("gevonden
     worden") no longer fits the container at 1280px and wraps to a third
     line, breaking the deliberate two-line composition. */
  --fs-display: clamp(2.75rem, 10.5vw, 8rem);
  --fs-claim:   clamp(2.5rem, 8.5vw, 6.5rem);
  --fs-h2:      clamp(1.9rem, 5.5vw, 4rem);
  --fs-h3:      clamp(1.25rem, 2.2vw, 1.75rem);
  --fs-lede:    clamp(1.0625rem, 1.6vw, 1.375rem);
  --fs-eyebrow: 0.75rem;

  /* ---- Layout ---- */
  --container: 78rem;
  --measure:   54ch;   /* statements are short — a tighter measure than prose */
  --gutter:    clamp(1.5rem, 5vw, 4rem);
  --section-y: clamp(5rem, 14vh, 10rem);

  --r-sm: 4px;
  --r-md: 10px;
  --r-pill: 999px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --transition: 240ms var(--ease);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
body {
  margin: 0;
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.7;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img { max-width: 100%; height: auto; display: block; }
ul, ol { padding-left: 1.2rem; }

h1, h2, h3 {
  margin: 0 0 0.4em;
  color: var(--text);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 0.95;
  text-wrap: balance;
}
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); letter-spacing: -0.02em; line-height: 1.2; }

p { margin: 0 0 1.1rem; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-underline-offset: 0.2em; }
a:hover { color: var(--text); }

/* Focus is restyled, never removed. Sapphire reads clearly on every
   surface in this file. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Skip link ---------- */
.skip-link {
  position: absolute;
  left: 1rem;
  top: -5rem;
  z-index: 300; /* above the header (200), which is above the nav overlay (150) */
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.8rem 1.2rem;
  border-radius: var(--r-sm);
  font-weight: 700;
  text-decoration: none;
  transition: top var(--transition);
}
.skip-link:focus { top: 1rem; color: var(--on-accent); }

/* ---------- Layout primitives ----------
   .container owns the HORIZONTAL axis only; sections own the VERTICAL axis.
   Keeping them on separate axes is what stops a padding shorthand on one
   from silently wiping the gutter of the other. */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ---------- Header ---------- */
/* The mobile nav overlay is a CHILD of this header, so raising the header's
   own z-index does not lift the close button above it — the overlay comes
   along. The logo and toggle therefore each carry a z-index above the
   overlay's, inside this header's stacking context (see .logo / .nav-toggle).
   Without that the menu could only be dismissed with Escape or by picking a
   link, leaving a visitor who opened it by accident stuck. */
.site-header {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 200;
  padding-block: clamp(1.25rem, 3vw, 2rem);
}
/* Inner pages don't have a full-bleed hero behind the header, so it needs
   its own ground there. Applied via a body class. */
body.has-solid-header .site-header {
  position: sticky;
  top: 0;
  background: rgba(2, 16, 13, 0.86);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  padding-block: 0.85rem;
}
.header-inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* Stacked wordmark, echoing the reference's boxed logo lockup. */
.logo {
  position: relative;
  z-index: 160; /* above the .primary-nav overlay (150), which is a sibling */
  display: inline-block;
  font-weight: 800;
  font-size: clamp(0.95rem, 1.5vw, 1.15rem);
  line-height: 1.05;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
}
.logo span { display: block; }
.logo:hover, .logo:focus-visible { color: var(--accent); }

/* ---------- Desktop nav ----------
   Siem chose to keep a visible desktop nav rather than the reference's
   hamburger-only approach — his audience is small local business owners,
   for whom a hidden nav is a real cost. */
.primary-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.35rem;
}
.primary-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.4rem 0.9rem;
  border-radius: var(--r-pill);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  transition: color var(--transition), background var(--transition);
}
.primary-nav a:hover,
.primary-nav a:focus-visible { color: var(--accent); }
.primary-nav a[aria-current="page"] {
  color: var(--accent);
  background: rgba(114, 176, 171, 0.14);
}

/* ---------- Mobile nav toggle + fullscreen overlay ---------- */
.nav-toggle {
  position: relative;
  z-index: 160; /* keeps the close (X) button clickable above the open overlay */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 48px;
  height: 48px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
}
/* Scoped to the decorative bars only. A bare `.nav-toggle span` also matches
   the visually-hidden "Menu" label and — being more specific than
   .visually-hidden — repaints it as a fourth visible bar. It also shifts every
   nth-child index by one, which left the open state rendering as a malformed
   X instead of a clean close button. */
.nav-toggle span[aria-hidden="true"] {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--text);
  transition: transform var(--transition), opacity var(--transition);
}
/* Indices are 2/3/4 because the visually-hidden label is child 1. */
.nav-toggle[aria-expanded="true"] span:nth-child(2) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(4) { transform: translateY(-7px) rotate(-45deg); }

@media (max-width: 819px) {
  .primary-nav {
    position: fixed;
    inset: 0;
    z-index: 150;
    background: var(--bg-deep);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition), visibility var(--transition);
  }
  .primary-nav.is-open { opacity: 1; visibility: visible; }
  .primary-nav ul {
    flex-direction: column;
    gap: 0.5rem;
    text-align: center;
  }
  .primary-nav a {
    justify-content: center;
    font-size: clamp(1.75rem, 7vw, 2.75rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    padding: 0.5rem 1.5rem;
  }
  /* Scroll lock. `body { overflow: hidden }` alone does NOT lock page scroll —
     the viewport propagates overflow from the ROOT element, not from body, so
     the page still scrolled behind the open overlay (verified: scrollTo(0,900)
     moved to 900 with the menu open). Because .site-header is position:absolute,
     that carried the close (X) button out of the viewport and left touch users
     with no way out of a fullscreen overlay — there is no Escape key on a phone.
     Locking the root fixes the cause. */
  html:has(body.nav-open) { overflow: hidden; }
  body.nav-open { overflow: hidden; }
  /* Belt and braces: even if the lock above fails on an engine without :has(),
     pinning the header keeps the X on screen at any scroll position. */
  body.nav-open .site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
  }
}
@media (min-width: 820px) {
  .nav-toggle { display: none; }
  .primary-nav { display: block !important; opacity: 1; visibility: visible; }
}

/* ---------- Hero ----------
   Full viewport. `svh` keeps mobile browsers' collapsing toolbars from
   pushing the claim off-screen; `vh` is the fallback for older engines. */
.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--bg-deep);
}

/* ---------- Hero atmosphere texture (2026-08-17) ----------
   Generated abstract textures. Deliberately abstract: SiBo has no photography
   and no finished portfolio work, so anything depicting people or "projects"
   would imply work that does not exist. These claim nothing — they are pure
   atmosphere.

   Applied to .hero's OWN background layer, not a pseudo-element: both
   ::before and ::after are already taken by the drifting Spruce/Sapphire
   washes below, and the element background paints beneath both, so the
   washes still layer on top exactly as designed.

   No <img>: the texture carries no information, so it needs no alt text and
   should never enter the accessibility tree.
   image-set() serves WebP with a JPEG fallback. The -sm files (960px) go to
   small viewports so phones never pull the 1920px asset. ~2KB mobile /
   ~7KB desktop as WebP — it is a smooth gradient, hence the tiny files.
   The assets are pre-darkened to 42% brightness because a background image
   cannot take opacity; that keeps them quiet under the washes and keeps the
   white hero type well clear of AA. */
.hero {
  background-color: var(--bg-deep);
  background-image: image-set(
    url("../img/hero-atmos-sm.webp") type("image/webp"),
    url("../img/hero-atmos-sm.jpg") type("image/jpeg"));
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.hero--inner {
  background-image: image-set(
    url("../img/hero-inner-sm.webp") type("image/webp"),
    url("../img/hero-inner-sm.jpg") type("image/jpeg"));
}
@media (min-width: 720px) {
  .hero {
    background-image: image-set(
      url("../img/hero-atmos.webp") type("image/webp"),
      url("../img/hero-atmos.jpg") type("image/jpeg"));
  }
  .hero--inner {
    background-image: image-set(
      url("../img/hero-inner.webp") type("image/webp"),
      url("../img/hero-inner.jpg") type("image/jpeg"));
  }
}
.hero-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(7rem, 16vh, 10rem) clamp(4rem, 10vh, 7rem);
  text-align: center;
}
.hero--left .hero-inner { text-align: left; }

/* The colour field that replaces photography. Two slow-drifting radial
   washes in Spruce and Sapphire over the deep base. Spruce is safe here
   precisely because it is a surface, never text. */
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  pointer-events: none;
  z-index: 0;
}
.hero::before {
  width: 78vmax; height: 78vmax;
  top: -32vmax; left: -22vmax;
  background: radial-gradient(circle, rgba(53, 94, 88, 0.85), transparent 62%);
  animation: drift-a 26s var(--ease) infinite alternate;
}
.hero::after {
  width: 62vmax; height: 62vmax;
  bottom: -30vmax; right: -18vmax;
  background: radial-gradient(circle, rgba(114, 176, 171, 0.42), transparent 62%);
  animation: drift-b 32s var(--ease) infinite alternate;
}
@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(8vw, 6vh, 0) scale(1.12); }
}
@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.08); }
  to   { transform: translate3d(-7vw, -5vh, 0) scale(1); }
}

/* A fine grid overlay — structure without imagery. */
.hero-grid {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line) 1px, transparent 1px);
  background-size: clamp(60px, 8vw, 120px) clamp(60px, 8vw, 120px);
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 78%);
          mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 78%);
  opacity: 0.5;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 clamp(1.25rem, 3vw, 2rem);
  font-size: var(--fs-eyebrow);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}
.eyebrow::before {
  content: "";
  width: clamp(1.5rem, 4vw, 3rem);
  height: 1px;
  background: var(--accent);
}
.hero-inner .eyebrow::before { display: none; }
.hero--left .hero-inner .eyebrow::before { display: block; }

/* The claim. Each line is a block so the break is deliberate, not a
   wrapping accident. */
.claim {
  margin: 0;
  font-size: var(--fs-claim);
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.04em;
}
.hero--home .claim { font-size: var(--fs-display); }
.claim .line { display: block; }

/* On Diensten this element is the <h1> (it carries the page's exact-match
   primary keyword, which the short claim above it cannot), so it has to
   explicitly reset the heading defaults rather than inherit them. Visual
   hierarchy and document hierarchy differ here on purpose. */
.hero-sub {
  margin: clamp(1.5rem, 3.5vw, 2.5rem) auto 0;
  max-width: 46ch;
  font-size: var(--fs-lede);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: normal;
  color: var(--text-muted);
}
/* The claim rendered as a non-heading (a <p>) for the same reason. */
p.claim { margin: 0; }
.hero--left .hero-sub { margin-inline: 0; }

.hero-cta { margin-top: clamp(1.75rem, 4vw, 2.75rem); }

/* Only Home gets the full-viewport drama. Inner pages get a substantial
   but shorter hero, so the content below starts within reach and the
   sticky header on those pages doesn't push the section past the fold. */
.hero--inner {
  min-height: 0;
  padding-block: clamp(4rem, 12vh, 7rem) clamp(3rem, 8vh, 5rem);
}
.hero--inner .hero-inner { padding-block: 0; }

/* Carousel sits inside the services statement rather than in a section of
   its own — the statement's own H2 introduces it, so it needs no invented
   heading of its own. */
.showcase-wrap { margin-top: clamp(2rem, 5vw, 3.5rem); }

/* Reserve room at the foot of the Home hero so the scroll cue can never
   collide with the CTA above it on short viewports. */
.hero--home .hero-inner { padding-bottom: clamp(6rem, 15vh, 9rem); }

/* Scroll cue at the bottom of the home hero. */
.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: clamp(1.5rem, 4vh, 3rem);
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-decoration: none;
}
.scroll-cue:hover, .scroll-cue:focus-visible { color: var(--accent); }
.scroll-cue::after {
  content: "";
  width: 1px;
  height: clamp(28px, 5vh, 46px);
  background: linear-gradient(to bottom, var(--accent), transparent);
  animation: cue 2.4s ease-in-out infinite;
}
@keyframes cue {
  0%, 100% { opacity: 0.3; transform: scaleY(0.6); transform-origin: top; }
  50%      { opacity: 1;   transform: scaleY(1);   transform-origin: top; }
}

/* ---------- Buttons & text CTAs ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 52px;
  padding: 0.85rem 1.75rem;
  border: 2px solid var(--accent);
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition), color var(--transition),
              transform var(--transition);
}
.btn:hover, .btn:focus-visible {
  background: transparent;
  color: var(--accent);
  transform: translateY(-2px);
}
.btn-secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--line-strong);
}
.btn-secondary:hover, .btn-secondary:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

/* The statement CTA — a text link with a rule that extends on hover. */
.cta-link {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: clamp(1.25rem, 3vw, 2rem);
  font-size: 1rem;
  font-weight: 700;
  color: var(--accent);
  text-decoration: none;
}
.cta-link::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: currentColor;
  transition: width var(--transition);
}
.cta-link:hover::after,
.cta-link:focus-visible::after { width: 4rem; }
.cta-link:hover, .cta-link:focus-visible { color: var(--text); }

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: clamp(1.5rem, 3.5vw, 2.25rem);
}
.hero-inner .cta-row { justify-content: center; }
.hero--left .hero-inner .cta-row { justify-content: flex-start; }

/* ---------- Statement sections ----------
   The core rhythm of the site: index numeral, big headline, one short
   paragraph, one CTA. One idea per screen. */
.statement {
  padding-block: var(--section-y);
  border-top: 1px solid var(--line);
}
.statement-inner {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
}
.statement-index {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.statement h2 .line { display: block; }
.statement-body {
  max-width: var(--measure);
  font-size: var(--fs-lede);
  color: var(--text-muted);
}
@media (min-width: 900px) {
  .statement-inner {
    grid-template-columns: 6rem minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: clamp(2rem, 4vw, 4rem);
  }
  .statement-index { padding-top: 0.8rem; }
}

/* A section that inverts to a lighter surface, for rhythm. */
.statement--surface { background: var(--surface); border-top-color: transparent; }

/* ---------- Services carousel (Home) ----------
   The dot-indicator carousel from the reference, repurposed for a services
   showcase. Native CSS scroll-snap — no library, keyboard-scrollable, and
   every card stays in the DOM for search engines.
   Deliberately NOT used on Diensten: hiding seven long service descriptions
   behind horizontal scrolling would damage both readability and the SEO
   Siem asked to protect. */
.showcase { padding-block: var(--section-y); border-top: 1px solid var(--line); }
.showcase-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: clamp(2rem, 5vw, 3.5rem);
}
.showcase-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(min(78vw, 22rem), 1fr);
  gap: 1.25rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-bottom: 0.5rem;
}
.showcase-track::-webkit-scrollbar { display: none; }
.showcase-card {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--transition), transform var(--transition);
}
.showcase-card:hover { border-color: var(--accent); transform: translateY(-4px); }
.showcase-card h3 { margin: 0; }

/* Card heading IS the link — one descriptive link per card instead of seven
   identical "Lees meer" links. That was both a WCAG 2.4.4 (Link Purpose)
   weakness and, per keyword-strategist, seven wasted internal anchors:
   descriptive anchor text attributes each service term to the Diensten
   anchor that owns it. The stretched ::after makes the whole card clickable
   while keeping exactly one link in the accessibility tree. */
.showcase-card { position: relative; }
/* Safety net for any future long Dutch compound in a card title: without this
   an unbreakable word runs past the card's padding (measured 46px past on
   "Zoekwoordenonderzoek"). The &shy; hints in the markup do the pretty
   breaking; this stops anything without a hint from escaping the card. */
.showcase-card h3 { overflow-wrap: break-word; hyphens: auto; -webkit-hyphens: auto; }
.showcase-link {
  color: var(--text);
  text-decoration: none;
}
.showcase-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-md);
}
.showcase-card:hover .showcase-link,
.showcase-link:focus-visible { color: var(--accent); }
/* Focus ring has to sit on the card, since the <a> itself is inline text
   while its hit area is the whole card. */
.showcase-link:focus-visible { outline: none; }
.showcase-card:has(.showcase-link:focus-visible) {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-color: var(--accent);
}
.showcase-card p { color: var(--text-muted); font-size: 0.9375rem; }
.showcase-price {
  margin-top: auto;
  padding-top: 0.5rem;
  font-weight: 700;
  color: var(--accent);
}
.showcase-controls {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-top: clamp(1.5rem, 3vw, 2rem);
}
.showcase-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: transparent;
  color: var(--accent);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.showcase-arrow:hover:not([aria-disabled="true"]) { border-color: var(--accent); background: rgba(114,176,171,0.12); }
/* aria-disabled, not the `disabled` attribute: a disabled button is removed from
   the tab order, so disabling the one that currently HAS focus (pressing Next to
   the end of the track) dropped focus to <body> and lost the user's place. These
   stay focusable and the click handler no-ops instead. */
.showcase-arrow[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.showcase-arrow svg { width: 18px; height: 18px; }
.showcase-dots { display: flex; gap: 0.25rem; margin-left: auto; }
/* WCAG 2.2 AA 2.5.8 (Target Size, Minimum) wants 24x24 CSS px. The visual dot
   stays small — the button is a 24x24 transparent hit area with the dot drawn
   by ::before, so the design doesn't change but the target is compliant. */
.showcase-dots button {
  width: 24px; height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.showcase-dots button::before {
  content: "";
  width: 10px; height: 10px;
  border: 1px solid var(--accent);
  border-radius: 50%;
  background: transparent;
  transition: background var(--transition), transform var(--transition);
}
.showcase-dots button[aria-current="true"]::before { background: var(--accent); transform: scale(1.25); }

/* ---------- Diensten: vertical service sections ---------- */
.service {
  padding-block: clamp(3rem, 7vh, 5.5rem);
  border-top: 1px solid var(--line);
  scroll-margin-top: 6rem;
}
.service-inner { display: grid; gap: clamp(1.25rem, 3vw, 2.5rem); }
.service-num {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
/* DESKTOP OVERLAP FIX (2026-08-17).
   Long Dutch compounds — "Zoekwoordenonderzoek", "Concurrentieonderzoek" —
   have no natural break opportunity. This heading sits in a fixed 20rem grid
   track declared `minmax(0, 20rem)`, and that `0` minimum lets the track
   ignore the word's min-content width: instead of widening the column, the
   text simply overflowed it and ran straight across the description in the
   next column. Measured at 1280px: 723px of text in a 320px track, intruding
   352px into the body. Four of the seven headings did it.
   Two-part fix:
     - hyphens:auto breaks them at real syllable points (html lang="nl"
       supplies the Dutch hyphenation dictionary), with overflow-wrap as the
       hard fallback for anything the dictionary can't hyphenate.
     - a column-appropriate max size: the global h2 clamp tops out at a width
       meant for full-bleed headings, which is simply too large for a 20rem
       track. */
.service h2 {
  font-size: clamp(1.5rem, 1.15rem + 1.5vw, 2.5rem);
  hyphens: auto;
  -webkit-hyphens: auto;
  overflow-wrap: break-word;
}
.service-body { max-width: 62ch; color: var(--text-muted); }
.service-body p { margin-bottom: 1rem; }
.service-price {
  display: inline-flex;
  align-items: center;
  margin: 0.5rem 0 0;
  padding: 0.45rem 1.1rem;
  border: 1px solid var(--accent);
  border-radius: var(--r-pill);
  font-weight: 700;
  color: var(--accent);
}
@media (min-width: 900px) {
  .service-inner {
    grid-template-columns: 5rem minmax(0, 20rem) minmax(0, 1fr);
    align-items: start;
    gap: clamp(2rem, 4vw, 3.5rem);
  }
  .service-num { padding-top: 0.6rem; }
}

/* ---------- Closing contact block ----------
   The oversized human sign-off, the emotional counterweight to all the
   darkness above it. */
.signoff {
  padding-block: clamp(6rem, 16vh, 11rem);
  border-top: 1px solid var(--line);
  background:
    radial-gradient(70% 120% at 50% 100%, rgba(53, 94, 88, 0.55), transparent 70%),
    var(--bg-deep);
  text-align: center;
}
.signoff-claim {
  margin: 0;
  font-size: var(--fs-claim);
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.04em;
}
.signoff-claim .line { display: block; }
.signoff-sub {
  margin: clamp(1.5rem, 3vw, 2.25rem) auto 0;
  max-width: 44ch;
  font-size: var(--fs-lede);
  color: var(--text-muted);
}
.signoff-mail {
  display: inline-block;
  margin-top: clamp(1.5rem, 3.5vw, 2.5rem);
  font-size: clamp(1.125rem, 3.2vw, 2rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: border-color var(--transition), color var(--transition);
  word-break: break-word;
}
.signoff-mail:hover, .signoff-mail:focus-visible {
  color: var(--text);
  border-bottom-color: var(--accent);
}
.signoff-region {
  margin-top: 1.25rem;
  font-size: 0.875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ---------- Forms ---------- */
.form-wrap { max-width: 40rem; }
.form-field { margin-bottom: 1.5rem; }
label, legend {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--text);
}
fieldset { border: 0; padding: 0; margin: 0 0 1.5rem; }
input[type="text"],
input[type="email"],
input[type="tel"],
select,
textarea {
  width: 100%;
  min-height: 52px;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font: inherit;
  transition: border-color var(--transition), background var(--transition);
}
/* Inputs are --surface, so the same 4.40:1 fail applied here. Placeholders
   carry instructions, so they get the passing token too. */
input::placeholder, textarea::placeholder { color: var(--text-hint); }
input[type="text"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.07);
}
select option { background: var(--bg); color: var(--text); }
textarea { min-height: 160px; resize: vertical; }

/* Not --text-faint (0.52 alpha): measured 4.40:1 on --surface, a flat fail of
   the 4.5:1 minimum for normal text. Form hints are real instructions, not
   decoration, so they get their own token that clears AA on both surfaces. */
.hint { margin-top: 0.4rem; font-size: 0.875rem; color: var(--text-hint); }
.field-error {
  display: none;
  margin-top: 0.4rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--error);
}
.form-field.has-error input,
.form-field.has-error select,
.form-field.has-error textarea { border-color: var(--error); }
.form-field.has-error .field-error { display: block; }
/* The consent checkbox's error paragraph sits OUTSIDE #consentField (it can't
   live inside, since .checkbox-field is a flex row), so the descendant rule
   above never matched it and the consent error could never appear. Sibling
   rule covers that case. */
.form-field.has-error + .field-error { display: block; }

.form-summary,
.form-success {
  display: none;
  margin-bottom: 1.5rem;
  padding: 1.1rem 1.25rem;
  border-radius: var(--r-md);
  border-left: 3px solid;
}
.form-summary {
  border-color: var(--error);
  background: var(--error-bg);
  color: var(--error);
}
.form-success {
  border-color: var(--accent);
  background: rgba(114, 176, 171, 0.12);
  color: var(--text);
}
.form-summary.is-visible, .form-success.is-visible { display: block; }
/* No-JS path. contact.php redirects to ...#formSuccess / ...#formSummary, and
   :target reveals the right block with no JavaScript at all — without this,
   a visitor with JS disabled submitted the form and landed on a page that
   looked completely unchanged, with no confirmation either way. */
.form-summary:target, .form-success:target { display: block; }

.checkbox-field {
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  min-height: 44px;
}
.checkbox-field input {
  width: 24px; height: 24px;
  flex: none;
  margin-top: 0.15rem;
  accent-color: var(--accent);
}
.checkbox-field label { font-weight: 400; color: var(--text-muted); margin: 0; }

/* Honeypot — hidden from sighted users and assistive tech alike. */
.hp-field {
  position: absolute;
  left: -9999px; top: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* ---------- Blog ---------- */
.post-card {
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}
/* Same failure as .hint and found the same way: .post-card's background is
   --surface, where 0.52 alpha is 4.40:1. 14px is normal-size text for WCAG,
   so it needs 4.5:1, not the 3:1 large-text allowance. */
.post-meta { font-size: 0.875rem; color: var(--text-hint); }
.article-body {
  max-width: 62ch;
  font-size: 1.0625rem;
  line-height: 1.8;
  color: var(--text-muted);
}
.article-body h2 { margin-top: 2em; color: var(--text); }
.article-body h3 { margin-top: 1.5em; color: var(--text); }
.article-body > *:first-child { margin-top: 0; }

/* ---------- Footer ---------- */
.site-footer {
  padding-block: clamp(3rem, 7vw, 4.5rem);
  border-top: 1px solid var(--line);
  background: var(--bg-deep);
}
.footer-inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  gap: 2rem;
}
.footer-brand {
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.1;
}
.footer-inner nav ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
}
.footer-inner nav a {
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--text-muted);
  text-decoration: none;
}
.footer-inner nav a:hover, .footer-inner nav a:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}
.footer-copy {
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--text-faint);
}
@media (min-width: 780px) {
  .footer-inner { grid-template-columns: 1fr auto; align-items: start; }
  .footer-copy { grid-column: 1 / -1; }
}

/* ---------- Scroll reveal ----------
   Elements start visible and are only hidden once JS confirms it can
   observe them (`js-reveal` is set on <html> by the script). Without JS,
   nothing is ever hidden. */
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 700ms var(--ease), transform 700ms var(--ease);
}
.js-reveal .reveal.is-visible { opacity: 1; transform: none; }

/* ---------- Reduced motion ----------
   Kills the drifting washes, the scroll cue pulse, and the reveal
   transforms outright rather than merely shortening them. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .hero::before, .hero::after, .scroll-cue::after { animation: none; }
  .js-reveal .reveal { opacity: 1; transform: none; }
}

/* ==========================================================================
   Hero video (2026-08-17, moved here from the standalone scroll stage)
   The clip sits behind the Home claim and is scrubbed by scroll position.

   LAYER ORDER inside .hero (all explicit, because .hero's ::before/::after
   washes and its children compete in the same stacking context):
     0  .hero-media  — the video, plus the generated texture layered ON TOP
                       of it so the footage stays part of the background
     1  .hero::before / ::after — the drifting Spruce/Sapphire washes
     2  .hero-grid   — the faint grid
     3  .hero-inner  — the claim and CTA
   Decorative throughout: aria-hidden wrapper, tabindex="-1" video, no
   controls, nothing focusable, so it never enters the reading or tab order.
   ========================================================================== */
.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.hero-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The texture stays ON TOP of the footage — this is what stops the clip
   popping out of the design. It is the same generated atmosphere asset used
   as the hero background elsewhere, plus a dark veil to sink the footage
   back and a Spruce cast to pull it onto the palette. */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to bottom, rgba(2,16,13,0.72) 0%, rgba(2,16,13,0.42) 38%, rgba(2,16,13,0.66) 100%),
    linear-gradient(rgba(53, 94, 88, 0.20), rgba(53, 94, 88, 0.20)),
    image-set(
      url("../img/hero-atmos-sm.webp") type("image/webp"),
      url("../img/hero-atmos-sm.jpg") type("image/jpeg"));
  background-size: cover, cover, cover;
  background-position: center;
  opacity: 0.94;
}
@media (min-width: 720px) {
  .hero-media::after {
    background-image:
      linear-gradient(to bottom, rgba(2,16,13,0.72) 0%, rgba(2,16,13,0.42) 38%, rgba(2,16,13,0.66) 100%),
      linear-gradient(rgba(53, 94, 88, 0.20), rgba(53, 94, 88, 0.20)),
      image-set(
        url("../img/hero-atmos.webp") type("image/webp"),
        url("../img/hero-atmos.jpg") type("image/jpeg"));
  }
}

/* Explicit layer order — see the block comment above. */
.hero--home .hero-grid  { z-index: 2; }
.hero--home .hero-inner { position: relative; z-index: 3; }
.hero--home .scroll-cue { z-index: 3; }

/* Reduced motion: no clip at all (JS never attaches a src), so the poster
   underneath is what shows. Nothing else needs to change — the hero keeps
   its normal height, so there is no dead scroll to collapse. */
@media (prefers-reduced-motion: reduce) {
  .hero-video { display: none; }
}

/* ==========================================================================
   Pakketten (2026-08-18)
   Four indicative packages sitting above the seven service anchors on
   Diensten. A conversion device, not an SEO one — the anchors below keep the
   keywords, so nothing here competes for a heading.
   The fourth card ("Maatwerk") is deliberately IDENTICAL in weight to the
   priced three: it is the route for anyone the €399/5-page package would
   otherwise turn away, and styling it as a footnote would defeat its purpose.
   ========================================================================== */
.packages {
  padding-block: var(--section-y);
  background: var(--surface);
  border-block: 1px solid var(--line);
}
.packages-title {
  font-size: var(--fs-h2);
  margin-bottom: 0.6em;
}
.packages-intro {
  max-width: var(--measure);
  color: var(--text-muted);
  font-size: var(--fs-lede);
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.package-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
}
@media (min-width: 700px)  { .package-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1180px) { .package-grid { grid-template-columns: repeat(4, 1fr); } }

.package-card {
  display: flex;
  flex-direction: column;
  padding: clamp(1.4rem, 2.5vw, 2rem);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.package-card h3 {
  margin-bottom: 0.5em;
  font-size: var(--fs-h3);
}
.package-card p { color: var(--text-muted); }

/* Price sits at the foot of the card, pushed down so all four line up
   regardless of how much copy each one carries. */
.package-price {
  margin-top: auto;
  padding-top: 1.25rem;
  color: var(--text) !important;
  font-weight: 700;
  font-size: 1.125rem;
}
/* Hosting / secondary line under the headline figure. */
.package-price span {
  display: block;
  margin-top: 0.2rem;
  font-weight: 400;
  font-size: 0.9rem;
  color: var(--text-muted);
}

/* Basis vs deluxe inside the SEO card. */
.package-tiers {
  list-style: none;
  padding: 0;
  margin: 0 0 0.75rem;
  display: grid;
  gap: 0.75rem;
}
.package-tiers li {
  padding-left: 0.9rem;
  border-left: 2px solid var(--accent);
  color: var(--text-muted);
  font-size: 0.95rem;
}
.package-tiers strong { color: var(--text); }
.package-note {
  font-size: 0.9rem;
  margin-bottom: 0;
}
.package-card .cta-link { margin-top: 1rem; }
