/* ==========================================================================
   SPECIFICATIEBLAD / DE MEETLAT
   robmulder.com homepage stylesheet. Loads AFTER css/main.css and neutralises
   it. opensource.css and apple-light.css are unlinked; everything of theirs
   that still matters is re-implemented here (contrast lifts, OSS rules, app
   strip, contact button, focus ring).

   Rules of the build, enforced below:
   - border-radius is 0 everywhere, no exception.
   - zero blurred box-shadow. The only box-shadow is a drawn 2px rule.
   - no transition: all.
   - no transform that scales a 1px or 2px rule. The single scaling transform
     is the gauge fill (a 2px solid bar, section 7.1 item 3).
   - the finished state is the element's normal CSS. Motion is added on top.
     Nothing starts at opacity 0.
   - prefers-reduced-motion is the LAST block in this file.
   ========================================================================== */


/* ==========================================================================
   1. @FONT-FACE  (self-hosted Barlow, SIL OFL, latin subset)
   Three static cuts. No second family, no latin-ext face: the only non-ASCII
   characters on either homepage are U+00A9, U+00E9, U+00EB, U+00ED.
   ========================================================================== */

@font-face {
  font-family: Barlow;
  src: url(/fonts/barlow-400.woff2) format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Barlow;
  src: url(/fonts/barlow-500.woff2) format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Barlow;
  src: url(/fonts/barlow-800.woff2) format('woff2');
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallback, so the swap does not shift the layout.
   Starting values. Measure CLS and tune these three numbers if it is
   above 0.01. */
@font-face {
  font-family: 'Barlow Fallback';
  src: local('Helvetica Neue'), local('Arial');
  /* Measured, not guessed. Barlow 400 sets the same sample 5.23% narrower
     than Helvetica Neue (4874.3px vs 5143.3px at 100px), and its own ascent
     and descent are 100% and 20% of the em. The overrides compound with
     size-adjust, so they are divided by it: 100/0.9477 and 20/0.9477. */
  size-adjust: 94.77%;
  ascent-override: 105.5%;
  descent-override: 21.1%;
  line-gap-override: 0%;
}


/* ==========================================================================
   2. TOKENS
   Every custom property referenced anywhere in this file is defined here.
   ========================================================================== */

:root {
  /* -- families ------------------------------------------------------ */
  --font-sans: Barlow, 'Barlow Fallback', system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* -- type scale. Every clamp middle term carries a rem component, so
        200% zoom holds. ------------------------------------------------ */
  /* Sized to fit columns 1 to 9 at every desktop width, not just 1440.
     The old curve reached 120px at 1280, where cols 1-9 are only 824px,
     so the argument wrapped to two lines and pushed the button down. */
  --t-display:  clamp(3.5rem, -1.03rem + 9.13vw, 7.5rem);   /* 56 -> 120px, 120 from ~1495 */
  --t-plate:    clamp(2rem, 1.5rem + 2.5vw, 3.5rem);        /* 32 -> 56px  */
  --t-figure:   clamp(2.5rem, 1.5rem + 3.13vw, 4.5rem);     /* 40 -> 72px  */
  --t-h2:       clamp(2rem, 1.833rem + 0.833vw, 2.5rem);    /* 32 -> 40px  */
  --t-h3:       1.75rem;                                    /* 28px        */
  --t-lead:     clamp(1.375rem, 1.25rem + 0.625vw, 1.75rem);/* 22 -> 28px  */
  --t-lead-s:   1.375rem;                                   /* 22px pinned */
  --t-body:     1.125rem;                                   /* 18px        */
  --t-caption:  0.875rem;                                   /* 14px        */
  --t-anno:     0.8125rem;                                  /* 13px mono   */
  --t-wordmark: 1.375rem;                                   /* 22px        */

  /* -- colour -------------------------------------------------------- */
  --surface:     #ffffff;
  --surface-100: #f5f5f5;
  --surface-200: #f1f1f1;
  --ink:         #000000;
  --ink-strong:  #121212;
  --ink-600:     #414141;
  --slate:       #6b6d76;   /* 5.15:1 on white. Every small grey is this. */
  --line:        #d0d0d0;   /* structural 1px rule + gauge rail          */
  --line-soft:   #e4e4e4;   /* rules inside tables, lists, ticks         */
  --graphite:    #252525;
  --signal:      #00c14e;   /* used exactly once, on one 8x8px square    */
  --closer-ground: var(--graphite); /* one-line swap to var(--surface-100)
                                       if the dark band reads as the
                                       rejected direction */

  /* #8d8d8d is NOT USED on any light ground anywhere in this file.
     3.32:1, illegal below 18px, and nothing here needs it. The omission
     is a decision, recorded here on purpose. */

  /* -- grid and space ------------------------------------------------ */
  --page-width: 1400px;
  --gutter: 12px;           /* >=768px: 24px ; >=1280px: 40px */
  --margin-col: 0px;        /* >=768px: 56px ; >=1024px: 96px */
  --gauge-w: 96px;
  --wf-h: 24px;             /* the ruler lying down, <1024px only */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;

  /* -- masthead span. "columns 1 to 9" is end line 10. The calibration
        knob: set to 13 for the full 12 columns if the 120px display line
        wraps on a narrower laptop (it holds on one line at 1440). ------ */
  --masthead-end: 10;

  /* -- ledger (section 04 table) geometry. The three visible column
        rules sit on the three internal column boundaries. ------------- */
  --lw-1: 22%;  /* project name           */
  --lw-2: 12%;  /* organisation           */
  --lw-3: 26%;  /* star figure            */
  --lw-4: 40%;  /* description            */
  --lx-1: 22%;
  --lx-2: 34%;
  --lx-3: 60%;

  /* -- motion. Three easing characters with assigned jobs. ----------- */
  --ease-struct: cubic-bezier(0.215, 0.61, 0.355, 1); /* structure      */
  --ease-load:   cubic-bezier(0.16, 1, 0.3, 1);       /* the load beat  */
  --ease-spring: linear(0, 0.32 8%, 0.79 20%, 1.03 30%, 1.01 46%, 1);

  /* -- the gauge reading. 1:1 with scroll, written by js/spec.js.
        With no JS at all this stays 0, which is the correct picture of
        "you are at the top of the document". ------------------------- */
  --p: 0;
}


/* ==========================================================================
   3. MAIN.CSS NEUTRALISERS
   main.css stays linked (normalize + the landing-page styles) and is never
   reformatted. These rules load later with equal or higher specificity and
   cancel the parts that can reach the new markup.
   ========================================================================== */

/* The max-width cage (728px, 980px above 1069px) on the microdata wrapper. */
.wrapper {
  max-width: none;
  padding: 0;
  margin: 0;
  zoom: normal;
}

/* Its clearfix table boxes. */
.wrapper::before,
.wrapper::after {
  content: none;
  display: none;
}

/* main.css runs a 0.6s translateY(20px) fadeIn on these three names. No
   element in the new markup carries them; this is the belt for the braces,
   because content motion is forbidden on this page. */
.page-header,
.content-section,
.page-footer {
  animation: none;
  padding: 0;
  margin: 0;
  border: 0;
  text-align: left;
}

/* main.css uses !important on these two, so the neutraliser must too. */
.wrapper h2 { font-weight: 500 !important; margin: 0; padding: 0; }
.wrapper h2.plate { font-weight: 800 !important; }
.wrapper h3 { padding: 0 !important; margin: 0; }

/* Legacy hooks that may still ride along on copied markup (social list,
   flag switcher, old CTA, old app strip). Flattened, not relied upon. */
.icon-links,
.icon-links .icon-link-item {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 0;
  float: none;
  top: auto;
  text-align: left;
}
.icon-links .icon-link { min-width: 0; min-height: 0; transition: none; }
.icon-links .icon { height: auto; }
.icon-links .icon-link:hover { transform: none; opacity: 1; }
.top-right { position: static; margin: 0; right: auto; }
.contact-button,
.contact-button.not-looking {
  border-radius: 0;
  width: auto;
  min-width: 0;
  margin: 0;
  line-height: normal;
  transition: none;
}
.contact-button:hover { transform: none; }
.app-strip,
.app-strip-item,
a.app-strip-item:hover {
  border-radius: 0;
  box-shadow: none;
  background: none;
  transform: none;
  margin: 0;
  gap: 0;
  width: auto;
  height: auto;
  overflow: visible;
  justify-content: flex-start;
}

/* The three itemprop="worksFor" divs wrap ordinary body copy. Keep them for
   the structured data, take them out of the layout. */
.wrapper [itemprop="worksFor"] { display: contents; }


/* ==========================================================================
   4. RESET / BASE
   ========================================================================== */

html {
  overflow-x: clip;
  scrollbar-gutter: stable;
  background: var(--surface);
  text-size-adjust: 100%;
}

body {
  overflow-x: clip;
  margin: 0;
  max-width: none;
  background: var(--surface);
  color: var(--ink-strong);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
}

/* Radius is 0 on every element, no carve-out. */
*,
*::before,
*::after {
  border-radius: 0;
}

img,
svg {
  display: block;
  max-width: 100%;
}

p,
ol,
ul,
figure,
table {
  margin: 0;
  padding: 0;
}

li { list-style: none; }

table {
  border-collapse: collapse;
  border-spacing: 0;
}

#main,
#open-source {
  scroll-margin-block-start: 96px;
}

::selection {
  background: var(--ink);
  color: var(--surface);
  text-shadow: none;
}

/* Focus, once, for everything focusable. Never bare :focus, never
   outline: none without a replacement. */
:where(a, button, [role="button"], summary, [tabindex]):focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}


/* ==========================================================================
   5. TYPE ROLES
   ========================================================================== */

/* display, the argument */
.claim { margin: 0; }
.claim__setup,
.claim__payoff { display: block; }

.claim__setup {
  font-size: var(--t-lead);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--ink-600);
}

.claim__payoff {
  font-size: var(--t-display);
  font-weight: 500;
  line-height: 0.98;
  letter-spacing: -0.035em;
  color: var(--ink);
  overflow-wrap: anywhere;
  min-width: 0;
}

/* plate, the job title */
.plate {
  font-size: var(--t-plate);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink-strong);
  overflow-wrap: anywhere;
  min-width: 0;
}

/* figure, the star counts and the CoronaMelder commit count */
.fig {
  font-size: var(--t-figure);
  font-weight: 800;
  line-height: 0.9;
  letter-spacing: -0.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* U+2605 is outside the font subset, so the glyph comes from the platform.
   Wrapping it keeps a 72px fallback glyph from setting the line. */
.fig__star {
  font-size: 0.5em;
  font-weight: 400;
  vertical-align: 0.28em;
  letter-spacing: 0;
}

/* section headings */
.h2 {
  font-size: var(--t-h2);
  font-weight: 500;
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
}

/* h3, lead phrases, OSS names */
.h3 {
  font-size: var(--t-h3);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
}

/* lead, fluid 22 -> 28px */
.lead {
  font-size: var(--t-lead);
  font-weight: 500;
  line-height: 1.28;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
}

/* lead, pinned at 22px where the spec prints 22px */
.lead--22 { font-size: var(--t-lead-s); }

/* body */
.prose {
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0;
  color: var(--ink-strong);
}

.prose + .prose { margin-block-start: var(--space-4); }

/* caption */
.caption {
  font-size: var(--t-caption);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
}

/* anno, the mono annotations */
.anno {
  font-family: var(--font-mono);
  font-size: var(--t-anno);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--slate);
}

/* wordmark, the only other uppercase on the page */
.wordmark {
  margin: 0;
  font-size: var(--t-wordmark);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}


/* ==========================================================================
   6. BAND GRID
   Full-bleed shell, 12 columns capped at --page-width, margin column added
   as inner padding so the gauge never overlaps a track.
   ========================================================================== */

.band {
  position: relative;
  width: 100%;
}

.band__inner {
  max-width: calc(var(--page-width) + var(--margin-col));
  margin-inline: auto;
  padding-inline: calc(var(--gutter) + var(--margin-col)) var(--gutter);
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-4);
  row-gap: var(--space-4);
}

/* Two full-bleed ground switches. Exactly two, never three. */
.band--tint { background: var(--surface-100); }
.band--invert { background: var(--closer-ground); }
.band--invert,
.band--invert .h2,
.band--invert .prose,
.band--invert .lead { color: var(--surface); }
.band--invert .band__num,
.band--invert.is-current .band__num { color: var(--surface); }
.band--invert :where(a, button, [role="button"], [tabindex]):focus-visible {
  outline-color: var(--surface);
}

/* The band rule is a real element. It reports the reading position by
   weight: 1px hairline at rest, 2px ink when current. Integer pixels,
   never scaleY. */
.band__rule {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 1px;
  background: var(--line);
  transition:
    height 200ms var(--ease-struct),
    background-color 200ms linear,
    clip-path 520ms var(--ease-struct);
}

.band.is-current > .band__rule {
  height: 2px;
  background: var(--ink);
}

/* BEAT B, band rule draw. From-state only behind .js, which the script adds
   after confirming prefers-reduced-motion is false. Without .js the rule is
   simply full width. */
.js .band:not(.is-ruled) > .band__rule { clip-path: inset(0 100% 0 0); }

/* The section numeral. Real text, in the margin column from 1024px and
   inline above the h2 below it. */
.band__num {
  grid-column: 1 / -1;
  align-self: start;
  transition: color 200ms linear;
}

.band.is-current .band__num { color: var(--ink-strong); }

/* Section rhythm. Five distinct values, deliberately uneven. Build-time
   non-negotiable: do not normalise these. */
.band--00 { margin-block-start: 0; }
.band--01 { margin-block-start: 128px; }
.band--02 { margin-block-start: 96px; }
.band--03 { margin-block-start: 160px; }
.band--04 { margin-block-start: 96px; }
.band--05 { margin-block-start: 64px; }
.band--06 { margin-block-start: 128px; }
.band--07 { margin-block-start: 192px; }
.footer   { margin-block-start: 0; }


/* ==========================================================================
   7. THE GAUGE  (de meetlat)
   One fixed element, four children, nothing focusable, aria-hidden.
   ========================================================================== */

.gauge {
  display: none;
  position: fixed;
  inset-block: 0;
  left: 0;
  width: var(--gauge-w);
  pointer-events: none;
  z-index: 2;
}

/* The gauge is fixed, so it runs over the one inverted band (07). The rail
   and the ticks need nothing: #d0d0d0 and #e4e4e4 read on white and on
   #252525 alike. The two solid marks do need a flip, and it cannot be
   mix-blend-mode: a position:fixed element establishes its own stacking
   context, so a child blends against that context and never against the
   page. Measured: the white fill painted 255,255,255 on white, i.e. the
   whole mechanic was invisible. spec.js sets html.on-invert while the last
   band is current, and this one token flips.

   Below 1024px the gauge lies along the bottom edge on its own opaque
   ground, so that ground has to flip with the marks: a white strip across
   the #252525 closer is a visible cut in a sheet whose whole structure is
   two full-bleed ground switches. One more token, the same class, no second
   mechanism. The ground is unused above 1024px, where the rail is
   transparent and only the two solid marks need the flip. */
:root          { --gauge-ink: var(--ink); --gauge-ground: var(--surface); }
html.on-invert { --gauge-ink: #ffffff; --gauge-ground: var(--closer-ground); }
.gauge__rail,
.gauge__ticks,
.gauge__fill,
.gauge__cursor { position: absolute; }

.gauge__rail {
  left: 40px;
  width: 1px;
  inset-block: 0;
  background: var(--line);
}

.gauge__ticks {
  left: 41px;
  width: 4px;
  inset-block: 0;
  background: repeating-linear-gradient(
    to bottom,
    var(--line-soft) 0 1px,
    transparent 1px 8px
  );
}

/* The one scaling transform in the build: a 2px solid black bar whose only
   edge is horizontal, so there is no hairline to resample. */
.gauge__fill {
  left: 39px;
  width: 2px;
  inset-block: 0;
  background: var(--gauge-ink);
  transform-origin: top;
  transform: scaleY(var(--p));
}

.gauge__cursor {
  left: 32px;
  top: 0;
  width: 16px;
  height: 16px;
  background: var(--gauge-ink);
  translate: 0 calc((100dvh - 16px) * var(--p));
}


/* ==========================================================================
   8. NAMEPLATE ROW
   44px, full bleed, 1px bottom rule. No motion.
   ========================================================================== */

.np {
  border-block-end: 1px solid var(--line);
}

.np > .band__inner {
  align-items: center;
  row-gap: var(--space-2);
  /* Pinned, not min-height. The row is 44px by design, and leaving it free
     let the font swap change its height and push the whole document down:
     0.017 of layout shift, all of it from one row. Above 768px the content
     is a 32px photo and one line of 14px text, so 44px is never tight. */
  height: 44px;
}

.np__left {
  grid-column: 1 / 8;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  min-width: 0;
}

.np__right {
  grid-column: 8 / -1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  min-width: 0;
}

.np__photo {
  width: 32px;
  height: 32px;
  object-fit: cover;
  flex: 0 0 auto;
}

/* The 20px hairline between the wordmark and the role subtitle. */
.np__hair {
  width: 1px;
  height: 20px;
  background: var(--line);
  flex: 0 0 auto;
}

.np__role {
  font-size: var(--t-caption);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
  min-width: 0;
}

.np__social {
  display: flex;
  align-items: center;
  gap: 0;
}

.np__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--ink-strong);
  text-decoration: none;
}

/* Monochrome, in currentColor. The inline SVGs carry a fill attribute; CSS
   beats a presentation attribute. */
.np__icon svg { width: 18px; height: 18px; }
.np__icon svg path,
.np__icon svg rect,
.np__icon svg circle,
.np__icon svg polygon { fill: currentColor; }


/* ==========================================================================
   9. AVAILABILITY STRIP
   44px, #f1f1f1, 1px bottom rule. The only green on the page, once, never
   on text. No motion.
   ========================================================================== */

.avail {
  background: var(--surface-200);
  border-block-end: 1px solid var(--line);
}

.avail > .band__inner {
  align-items: center;
  min-height: 44px;
}

.avail__line {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--ink-strong);
}

.avail__dot {
  width: 8px;
  height: 8px;
  background: var(--signal);
  flex: 0 0 auto;
}


/* ==========================================================================
   10. TITLE BLOCK  (section 00)
   ========================================================================== */

.band--00 > .band__inner { padding-block-start: var(--space-12); }

.title__head { grid-column: 1 / var(--masthead-end); }

/* The two 2px ink rules that open and close the masthead. */
.plate__rule {
  height: 2px;
  background: var(--ink);
}

.title__head .plate { margin-block-start: var(--space-2); }
.title__head .claim { margin-block-start: var(--space-3); }
/* 12px, not 8px: at line-height 0.98 the 120px descenders sit below the
   line box and would otherwise touch the closing rule. */
.title__head .plate__rule--close { margin-block-start: var(--space-3); }

/* BEAT A, the single load beat. CSS only, no JS, no from-state held: the
   keyframe has a from and no to, so the finished state is the element's own
   CSS. Top rule opens from the left, bottom rule from the right. */
@media (prefers-reduced-motion: no-preference) {
  .plate__rule { animation: rule-draw-lr 620ms var(--ease-load); }
  .plate__rule--close { animation: rule-draw-rl 620ms var(--ease-load) 120ms; }
}

@keyframes rule-draw-lr { from { clip-path: inset(0 100% 0 0); } }
@keyframes rule-draw-rl { from { clip-path: inset(0 0 0 100%); } }

.title__left {
  grid-column: 1 / 6;
  align-self: start;
}

.title__right {
  grid-column: 8 / 13;
  align-self: start;
}

.title__subline { margin: 0; }
.title__left .btn { margin-block-start: var(--space-6); }
.title__note { margin-block-start: var(--space-3); max-width: 44ch; }
.title__note + .title__note { margin-block-start: var(--space-2); }

.title__right .prose { max-width: 57ch; }
.title__right .audience { margin-block-start: var(--space-6); max-width: 50ch; }


/* ==========================================================================
   11. APP INVENTORY  (three ruled rows, still inside section 00)
   ========================================================================== */

.inv__row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: start;
  border-block-start: 1px solid var(--line-soft);
  padding-block: var(--space-4);
}

.title__right + .inv__row { margin-block-start: var(--space-8); }

.inv__label {
  grid-column: 1 / 3;
  padding-block-start: var(--space-4);
}

.inv__cells {
  grid-column: 3 / -1;
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-4);
  row-gap: 0;
}

/* One anchor per cell: icon, name, fact. Hit area 64px from the padding. */
.inv__cell {
  flex: 0 1 200px;
  min-width: 0;
  padding-block: var(--space-4);
  padding-inline-end: var(--space-4);
  text-decoration: none;
  color: var(--ink-strong);
}

.inv__icon {
  width: 48px;
  height: 48px;
  /* nos.png is a transparent logo on white and vanished without an edge.
     Drawn as an inset outline so the box stays exactly 48px. */
  outline: 1px solid var(--line-soft);
  outline-offset: -1px;
}

.inv__name {
  display: block;
  margin-block-start: var(--space-3);
  font-size: var(--t-body);
  line-height: 1.3;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line);
  text-underline-offset: 0.18em;
  transition:
    text-decoration-color 150ms linear,
    text-decoration-thickness 150ms linear;
}

.inv__fact {
  display: block;
  margin-block-start: var(--space-1);
  max-width: 22ch;
  font-size: var(--t-caption);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
}


/* ==========================================================================
   12. BAND 01, CORONAMELDER  (full bleed #f5f5f5)
   ========================================================================== */

.band--01 > .band__inner { padding-block: var(--space-12); }
.band--01 .h2 { grid-column: 1 / 6; }
.band--01 .prose { grid-column: 6 / -1; max-width: 62ch; }

/* The verified figure. Prose keeps the verb "schreef" / "wrote". */
.stat {
  grid-column: 1 / 6;
  margin: 0;
}

.stat__cap {
  display: block;
  margin-block-start: var(--space-3);
  max-width: 28ch;
  font-size: var(--t-caption);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
}


/* ==========================================================================
   13. BAND 02, WAT U KRIJGT  (sticky 320px label + numbered ledger)
   ========================================================================== */

.band--02 > .band__inner {
  grid-template-columns: 320px minmax(0, 1fr);
  column-gap: var(--space-12);
  padding-block-start: var(--space-6);
}

.band--02 .band__num { grid-column: 1 / -1; }

.wg__label {
  grid-column: 1 / 2;
  position: sticky;
  top: var(--space-6);
  align-self: start;
}

.band--02 .wg__label .h2 { font-size: 2rem; }
.wg__label .lead { margin-block-start: var(--space-4); }

.wg__list {
  grid-column: 2 / -1;
  counter-reset: wg;
}

/* Block, not grid: the text after Rob's <b> is a bare text node, and in a
   grid container a text node becomes an anonymous item in the next cell.
   The 40px counter track is padding plus an absolute marker. */
.wg__item {
  counter-increment: wg;
  position: relative;
  padding-inline-start: 56px;
  padding-block: var(--space-5);
  border-block-start: 1px solid var(--line-soft);
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--ink-600);
}

.wg__item::before {
  content: counter(wg, decimal-leading-zero);
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(var(--space-5) + var(--space-2));
  width: 40px;
  font-family: var(--font-mono);
  font-size: var(--t-anno);
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--slate);
}

/* Rob's own <b> is the lead phrase. No extra class needed in the markup. */
.wg__item > b,
.wg__lead {
  display: block;
  margin-block-end: var(--space-2);
  font-size: var(--t-h3);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
}

.wg__extra {
  grid-column: 2 / -1;
  border-block-start: 1px solid var(--line);
  margin-block-start: var(--space-6);
}

.wg__extra > li {
  padding-block: var(--space-5);
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--ink-600);
}

.wg__extra > li + li { border-block-start: 1px solid var(--line-soft); }
.wg__extra > li > b { color: var(--ink-strong); }


/* ==========================================================================
   14. BAND 03, SPECIALISATIES & PROJECTEN  (prose + ruled link index)
   ========================================================================== */

.band--03 > .band__inner { padding-block-start: var(--space-6); }
.band--03 .h2 { grid-column: 1 / 9; }
.band--03 .prose { grid-column: 1 / 9; max-width: 68ch; }

.idx {
  grid-column: 1 / -1;
  margin-block-start: var(--space-8);
  border-block-end: 1px solid var(--line);
}


/* ==========================================================================
   15. BAND 04, OPEN SOURCE BIJDRAGES  (ruled table, 72px numerals)
   ========================================================================== */

.band--04 > .band__inner { padding-block-start: var(--space-6); }
.band--04 .h2 { grid-column: 1 / 9; }
.band--04 .prose { grid-column: 1 / 9; max-width: 68ch; }

.ledger-wrap {
  grid-column: 1 / -1;
  position: relative;
  margin-block-start: var(--space-8);
}

.ledger { width: 100%; table-layout: fixed; }

.ledger td,
.ledger th {
  vertical-align: top;
  text-align: left;
  padding: var(--space-5) var(--space-4) var(--space-5) 0;
  border-block-start: 1px solid var(--line-soft);
  overflow-wrap: anywhere;
}

/* Column widths come from the colgroup, not from a row. The header row is
   visually hidden (out of flow), so a fixed-layout table has no first row
   to measure and would split the width into four equal columns. Four bare
   <col> elements are the only reliable carrier. The three drawn column
   rules read the same four numbers, so they cannot drift. */
.ledger col:nth-child(1) { width: var(--lw-1); }
.ledger col:nth-child(2) { width: var(--lw-2); }
.ledger col:nth-child(3) { width: var(--lw-3); }
.ledger col:nth-child(4) { width: var(--lw-4); }

.ledger__name {
  font-size: var(--t-h3);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
}

.ledger__org {
  font-size: var(--t-caption);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
}

.ledger__desc {
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0;
  color: var(--ink-strong);
}

/* The five compact rows sit in the same table: smaller name, 40px numeral. */
.ledger__row--compact .ledger__name { font-size: var(--t-body); font-weight: 800; }
.ledger__row--compact .fig { font-size: 2.5rem; }

.ledger__outro { grid-column: 1 / 9; margin-block-start: var(--space-6); }

/* The three visible column rules, on the three internal boundaries.
   BEAT C, the only cascading beat on the page: they draw downward when the
   band is ruled, with authored delays. No sibling-index(), so Chromium,
   Safari and Firefox behave identically. */
.ledger__col {
  position: absolute;
  inset-block: 0;
  width: 1px;
  background: var(--line);
  transition: clip-path 480ms var(--ease-struct);
}

.ledger__col.col-1 { left: var(--lx-1); transition-delay: 0ms; }
.ledger__col.col-2 { left: var(--lx-2); transition-delay: 60ms; }
.ledger__col.col-3 { left: var(--lx-3); transition-delay: 120ms; }

.js .band--04:not(.is-ruled) .ledger__col { clip-path: inset(0 0 100% 0); }


/* ==========================================================================
   16. BAND 05, SECURITY & BUG BOUNTY  (inset Notice, columns 4 to 9)
   ========================================================================== */

.band--05 > .band__inner { padding-block-start: var(--space-6); }

.notice {
  grid-column: 4 / 10;
  background: var(--surface-100);
  padding: 16px 16px 20px;
  border: 0;
}

.notice__label { display: block; }
.notice .h2 { font-size: 2rem; margin-block-start: var(--space-3); }
.notice .prose { margin-block-start: var(--space-3); max-width: 62ch; }


/* ==========================================================================
   17. BAND 06, PERSOONLIJK  (photograph beside prose)
   ========================================================================== */

.band--06 > .band__inner { padding-block-start: var(--space-6); }

.portrait {
  grid-column: 1 / 5;
  margin: 0;
}

.portrait img {
  width: 100%;
  max-width: 456px;
  height: auto;
}

.band--06 .h2 { grid-column: 6 / -1; }
.band--06 .prose { grid-column: 6 / -1; max-width: 58ch; }

/* The heading and its paragraph are ONE grid item beside the photograph.
   As two separate items they auto-place into two rows, the 411px image
   stretches row 1, and a 350px hole opens between the h2 and its own text
   (measured). Wrapping them also keeps the itemprop="worksFor" wrapper,
   which is display: contents, from leaking a grid-positioned child into
   the band grid: at <=767px `.band__inner > *` cannot reach through
   display: contents, so the paragraph kept grid-column 6/-1 on a
   one-column grid, created four implicit tracks and collapsed the photo
   and the h2 to zero width (measured at 320 and 375). Inside .pers both
   grid-column declarations above are inert. */
.pers {
  grid-column: 6 / -1;
  align-self: start;
}

.pers .prose { margin-block-start: var(--space-4); }


/* ==========================================================================
   18. BAND 07, CTA CLOSER  (full bleed #252525, zero entrance motion)
   ========================================================================== */

.band--07 > .band__inner { padding-block: var(--space-12); }

.closer__line {
  grid-column: 1 / 9;
  margin: 0;
  font-size: var(--t-plate);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--surface);
}

.closer__sub {
  grid-column: 1 / 9;
  margin: 0;
  font-size: var(--t-lead-s);
  font-weight: 500;
  line-height: 1.28;
  letter-spacing: -0.02em;
  color: var(--surface);
}

.band--07 .btn { grid-column: 1 / 4; }


/* ==========================================================================
   19. FOOTER
   ========================================================================== */

.footer {
  border-block-start: 1px solid var(--line);
}

.footer > .band__inner {
  padding-block: var(--space-5) var(--space-6);
  align-items: baseline;
}

.footer__left { grid-column: 1 / 7; }
.footer__right { grid-column: 7 / -1; text-align: end; }

.footer p {
  margin: 0;
  font-size: var(--t-caption);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--slate);
}

.footer p + p { margin-block-start: var(--space-2); }


/* ==========================================================================
   20. COMPONENTS AND STATE MATRICES
   Nine states exist. loading/pending, error, empty and success are not
   applicable on a static page with no fetch and no form, so they are
   deliberately not built. Recorded here rather than silently skipped.
   ========================================================================== */

/* -- 20.1 Prose link, BEAT F -----------------------------------------------
   rest: 1px #d0d0d0 underline. hover or focus: 2px #000000, 150ms linear.
   A non-colour affordance on every link.
   :where() keeps this at one-type-selector specificity, so every control
   rule below wins over it without an !important anywhere. */
:where(.wrapper) a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line);
  text-underline-offset: 0.18em;
  transition:
    text-decoration-color 150ms linear,
    text-decoration-thickness 150ms linear;
}

:where(.wrapper) a:focus-visible,
:where(.wrapper) a:active {
  text-decoration-thickness: 2px;
  text-decoration-color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  :where(.wrapper) a:hover {
    text-decoration-thickness: 2px;
    text-decoration-color: var(--ink);
  }
}

:where(.band--invert) a { text-decoration-color: var(--slate); }

@media (hover: hover) and (pointer: fine) {
  :where(.band--invert) a:hover { text-decoration-color: var(--surface); }
}

/* Controls are not prose links. */
.skip,
.btn,
.lang,
.np__icon,
.idx__row,
.inv__cell { text-decoration: none; }


/* -- 20.2 Button, primary (.btn), 2 instances, BEAT E ---------------------
   rest: #000000 ground, #ffffff label, Barlow 500 18px, radius 0,
   min-height 44px, padding 0 24px, no border, no shadow.
   hover (fine pointers only): #252525, 140ms.
   active: scale(0.98) at 90ms; release on the linear() spring over 200ms.
   focus-visible: 2px ink outline, offset 2px.
   disabled: not applicable, a mailto: is never disabled.
   Label width is identical in every state. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--space-6);
  border: 0;
  background: var(--ink);
  color: var(--surface);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition:
    background-color 140ms var(--ease-struct),
    color 140ms linear,
    transform 200ms var(--ease-spring);
}

@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--graphite); color: var(--surface); }
}

.btn:active {
  transform: scale(0.98);
  transition:
    background-color 140ms var(--ease-struct),
    color 140ms linear,
    transform 90ms var(--ease-struct);
}

.btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* -- Button, inverted (.btn--invert), 1 instance, section 07 only -------- */
.btn--invert {
  background: var(--surface);
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .btn--invert:hover { background: var(--surface-200); color: var(--ink); }
}

.btn--invert:focus-visible {
  outline: 2px solid var(--surface);
  outline-offset: 2px;
}


/* -- 20.3 Language link (#language_nl, #language_en) ---------------------
   rest: 16px #121212, no underline, 44x44 hit area from padding.
   active page: inset 0 -2px 0 #000000 plus aria-current="page". That is the
   only box-shadow on the page and it is a rule, not a blur.
   hover: 2px ink underline. focus-visible: 2px ink outline, offset 2px. */
.lang {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--ink-strong);
  transition: box-shadow 200ms linear;
}

.lang.active,
.lang[aria-current="page"] {
  box-shadow: inset 0 -2px 0 var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .lang:hover {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-decoration-color: var(--ink);
    text-underline-offset: 0.18em;
  }
}

.lang:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}


/* -- 20.4 Index row (.idx__row), 8 instances, BEAT D ---------------------
   The whole row is one real <a>. rest: 1px #d0d0d0 rule, 32px Barlow 500
   link text, mono 13px route right-aligned. hover or focus-within: the
   row's own rule 1px -> 2px and #d0d0d0 -> #000000, plus the label's own
   underline to 2px. No translate, no lift, no shadow. */
.idx__row {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 56px;
  padding-block: var(--space-4);
  color: var(--ink-strong);
}

.idx__row::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 1px;
  background: var(--line);
  transition:
    height 200ms var(--ease-struct),
    background-color 200ms linear;
}

.idx__label {
  font-size: 2rem;
  font-weight: 500;
  line-height: 1.06;
  letter-spacing: -0.02em;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line);
  text-underline-offset: 0.18em;
  transition:
    text-decoration-color 150ms linear,
    text-decoration-thickness 150ms linear;
  min-width: 0;
}

.idx__route {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--t-anno);
  line-height: 1.2;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--slate);
  text-align: right;
}

.idx__row:focus-within::before,
.idx__row:focus-visible::before {
  height: 2px;
  background: var(--ink);
}

.idx__row:focus-within .idx__label,
.idx__row:focus-visible .idx__label {
  text-decoration-thickness: 2px;
  text-decoration-color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .idx__row:hover::before { height: 2px; background: var(--ink); }
  .idx__row:hover .idx__label {
    text-decoration-thickness: 2px;
    text-decoration-color: var(--ink);
  }
}

.idx__row:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}


/* -- 20.5 App icon cell (.inv__cell), 11 instances -----------------------
   rest: 48px icon, 18px name with a 1px underline, 14px fact.
   hover: the name's underline to 2px ink. No scale, no lift, no ground
   change. focus-visible: 2px ink outline, offset 2px. */
.inv__cell:focus-visible .inv__name,
.inv__cell:active .inv__name {
  text-decoration-thickness: 2px;
  text-decoration-color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .inv__cell:hover .inv__name {
    text-decoration-thickness: 2px;
    text-decoration-color: var(--ink);
  }
}

.inv__cell:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}


/* -- 20.6 Social icon link ---------------------------------------------- */
@media (hover: hover) and (pointer: fine) {
  .np__icon:hover { color: var(--ink); }
}


/* -- 20.7 Skip link ----------------------------------------------------- */
.skip {
  position: absolute;
  inset-block-start: var(--space-4);
  inset-inline-start: var(--space-4);
  z-index: 3;
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: var(--t-body);
  clip-path: inset(50%);
}

.skip:focus,
.skip:focus-visible {
  clip-path: none;
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}


/* ==========================================================================
   21. UTILITIES
   ========================================================================== */

/* Visually hidden but available to assistive tech. Used on the table
   caption and the table head. Never display: none. */
.u-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

.u-measure { max-width: 57ch; }
.u-tabular { font-variant-numeric: tabular-nums; }
.u-nowrap { white-space: nowrap; }


/* ==========================================================================
   22. RESPONSIVE
   ========================================================================== */

@media (min-width: 768px) {
  :root {
    --gutter: 24px;
    --margin-col: 56px;
  }
}

@media (min-width: 1024px) {
  :root { --margin-col: 96px; }

  /* The instrument, and the numeral moved into the margin column it
     measures: right of the rail at x=40, clear of the 16px cursor. */
  .gauge { display: block; }

  .band__num {
    position: absolute;
    inset-block-start: var(--space-6);
    left: 56px;
    width: 40px;
  }

  .band--00 > .band__inner > .band__num { inset-block-start: var(--space-12); }
}

@media (min-width: 1280px) {
  :root { --gutter: 40px; }
}

/* --------------------------------------------------------------------------
   22.1 DE DUIMLAT  (the thumb rule)

   There is no left margin column on a phone, so the margin column lies down.
   The ruler runs along the bottom edge, where the thumb already rests, and
   the section numerals move into each band's own top padding. Same element,
   same four parts, same three border widths, same --p. No new markup, and
   the only JavaScript is the ground flip over the one dark band.

   Every rule from here to the end of section 22 sits inside a max-width
   query, so nothing here can reach a 1024px viewport.
   -------------------------------------------------------------------------- */

/* Below 1024px the role subtitle wraps onto a second line, so the 44px pin
   would clip it. The pin only has to hold where the swap can move the page,
   which is the desktop layout. */
@media (max-width: 1023px) {
  .np > .band__inner { height: auto; min-height: 44px; }

  /* ---- SC 2.5.8 on the one standalone link that misses it ---------------
     "Connect op LinkedIn" is the only non-prose link on the page under 24px
     that is not part of a sentence run. Vertical padding on an inline box
     paints and hit-tests but does not enter line layout, so this is a 45px
     target for 0px of document height and 0 layout shift. The eight
     .idx__row anchors already measure 75px at 390 and 66px at 768, and the
     icon and language links already measure 44px, so they need nothing.
     The nine open source project names are the same case: a standalone
     link filling a table cell, 22px to 39px at 768 and 19px to 34px on a
     phone, 47px to 67px after. The four footer links are NOT: they sit
     inside running sentences ("Bel, WhatsApp of iMessage me."), are exempt
     under SC 2.5.8, and padding them only makes their two line boxes
     overlap by 9px, so the later link would steal the earlier one's taps.
     They are left alone on purpose. */
  .title__note a,
  .ledger__name a { padding-block: 14px; }

  /* ---- THE RULER, TURNED ------------------------------------------------
     A 24px strip on its own white ground, with the 1px rail as its top
     edge, so content that scrolls under it ends on a hairline of the
     sheet's own vocabulary and not on a cut. It is read, never touched:
     pointer-events stays none, nothing in it is focusable, and there is no
     tap-to-seek, because that is a scrollbar and the platform has one.
     With no JS --p stays 0: rail, ticks and the square parked at the left,
     no fill, which is the true picture of "top of the document". */
  .gauge {
    display: block;
    inset-block: auto 0;
    inset-inline: 0;
    width: auto;
    /* The home indicator sits under the bar on an iPhone. The ground grows
       to cover it; the marks stay in the top 24px, inside the thumb arc.
       env() resolves to 0px on every other device. */
    height: calc(var(--wf-h) + env(safe-area-inset-bottom, 0px));
    background: var(--gauge-ground);
  }

  /* the 1px #d0d0d0 datum, and the bar's own top edge */
  .gauge__rail {
    inset-block: 0 auto;
    inset-inline: 0;
    width: auto;
    height: 1px;
  }

  /* the same 8px pitch, 4px deep, turned through 90 degrees */
  .gauge__ticks {
    inset-block: 1px auto;
    inset-inline: 0;
    width: auto;
    height: 4px;
    background: repeating-linear-gradient(
      to right,
      var(--line-soft) 0 1px,
      transparent 1px 8px
    );
  }

  /* the 2px active rule growing along the ruler from the left */
  .gauge__fill {
    inset-block: 0 auto;
    inset-inline: 0;
    width: auto;
    height: 2px;
    transform-origin: left center;
    transform: scaleX(var(--p));
  }

  /* The same 16px square as the desktop cursor, walking right. It moves by
     translate, not by left: animating left dirties layout on every changed
     frame, which the measurement caught as 0.0003 of layout shift after a
     scroll. A raw percentage on translate cannot be used (it resolves
     against the square's own 16px box) and 100dvw counts a classic
     scrollbar it cannot see, so the bar declares itself a size container
     and the square reads the bar's own inline size with cqi. Exact at both
     ends: 0 at --p 0, flush right at --p 1, and compositor only. */
  .gauge { container-type: inline-size; }

  .gauge__cursor {
    inset-block: 0 auto;
    right: auto;
    left: 0;
    translate: calc((100cqi - 16px) * var(--p)) 0;
  }

  /* IT NEVER COVERS CONTENT. The reservation is in the first layout, so it
     costs 24px of document height and 0.00000 of layout shift. Nothing is
     ever parked under the bar at the document end. */
  body { padding-block-end: calc(var(--wf-h) + env(safe-area-inset-bottom, 0px)); }
}

/* 768 to 1023px: the 56px margin column exists here, so the numeral goes
   into it, exactly as it does at 1024px. Same two declarations, one number
   different. */
@media (min-width: 768px) and (max-width: 1023px) {
  .band__num {
    position: absolute;
    inset-block-start: var(--space-6);
    left: var(--gutter);
    width: 40px;
  }

  .band--00 > .band__inner > .band__num { inset-block-start: var(--space-12); }
}

@media (max-width: 767px) {
  /* The wordmark and the role subtitle stack here, so the 20px vertical
     hairline between them reads as a stray mark. */
  .np__hair { display: none; }

  /* Every band collapses to one column. These two declarations have to win
     against the per-band layout rules above, which are more specific. */
  .band__inner { grid-template-columns: minmax(0, 1fr) !important; }
  .band__inner > *,
  .inv__row > * { grid-column: 1 / -1 !important; }

  .band--01 { margin-block-start: 80px; }
  .band--02 { margin-block-start: 64px; }
  .band--03 { margin-block-start: 96px; }
  .band--04 { margin-block-start: 64px; }
  .band--05 { margin-block-start: 40px; }
  .band--06 { margin-block-start: 80px; }
  .band--07 { margin-block-start: 112px; }

  .band--00 > .band__inner { padding-block-start: var(--space-5); }
  .band--01 > .band__inner,
  .band--07 > .band__inner { padding-block: var(--space-8); }
  .band--02 > .band__inner { column-gap: var(--space-4); }

  .wg__label { position: static; }
  .np__right { justify-content: flex-start; }
  .footer__right { text-align: left; }
  .inv__label { padding-block-start: 0; }

  /* ---- the numeral, in the band's top padding ---------------------------
     The margin column rotated: the numeral leaves the content flow and sits
     in the band's own top padding, still real text, still coloured by
     .is-current, still directly above its own h2 on the left datum. The
     positioned ancestor is .band, which is position: relative already.
     Every band's padding-block-start here is >= 20px, so a 15.6px line at
     4px never touches the first content box, and band 00 has no band rule
     to sit on. Recovers the 16px row plus its 16px row-gap in eight bands:
     256px of document height for zero loss of information. */
  .band__num {
    position: absolute;
    inset-block-start: var(--space-1);
    inset-inline-start: var(--gutter);
    width: 40px;
  }

  /* ---- nameplate: thin rows, every target still a full 44 x 44 ----------
     display: contents promotes the photo, the wordmark and the role line to
     items of the nameplate's own flex line, so the role sentence takes a
     line of its own instead of wrapping three times inside a narrow track.

     Nothing is shrunk to make it fit. The five hit boxes keep 44 x 44, and
     220px of them plus the photo plus the wordmark need 352px of line: that
     fits from about 380px of viewport upward and does not at 375 and below,
     so flex-wrap decides it per device with no breakpoint to pick. Measured
     57.2px from 380 up, 81.2px below it, against 117 and 136 today, and
     0.00000 of layout shift on both sides of the boundary.

     The negative block margin lets the boxes overflow their own 24px row
     instead of setting it. They overhang into the role line, which holds no
     link, and never past the nameplate rule: elementFromPoint 2px, 6px and
     12px under the rule returns the strip, never a social link. */
  .np > .band__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    height: auto;
    min-height: 0;
    row-gap: 2px;
    column-gap: var(--space-2);
    /* 10px at the top, because the hit boxes reach 10px above their own
       row and the top row is the top of the document: at 8px the first
       four pixels of all five targets fell off the canvas. */
    padding-block: 10px 6px;
  }

  .np__left { display: contents; }
  .np__photo { width: 24px; height: 24px; }
  .np .wordmark { font-size: 1rem; }

  .np__role {
    order: 1;
    flex: 1 0 100%;
    font-size: 0.8125rem;
    line-height: 1.4;
  }

  .np__right {
    margin-inline-start: auto;
    gap: 0;
  }

  .np__icon,
  .np .lang { margin-block: -10px; }

  /* Each social anchor is an inline-flex box inside its own list item, so
     the item's line box carries the inherited 1.55 strut and sets 24.8px
     where the anchor's own margin box is 24px. A flex item has no strut. */
  .np__social > li { display: flex; }

  /* The current-language mark is an inset 2px rule at the bottom of the
     44px box. That box now reaches 10px past its own row, so the rule
     landed in the middle of the role sentence and read as a strikethrough
     (seen in a screenshot, not in a computed style). Same 2px of ink, put
     on the glyphs instead, where it cannot move when the box does. */
  .np .lang.active,
  .np .lang[aria-current="page"] {
    box-shadow: none;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-decoration-color: var(--ink);
    text-underline-offset: 0.18em;
  }

  /* ---- availability strip ----------------------------------------------
     44px is a desktop figure: it matches the 44px nameplate row, and on a
     phone the nameplate is not 44px any more. The strip holds one 16px
     line and no control, so it is sized by its own content here. */
  .avail > .band__inner {
    min-height: 0;
    padding-block: var(--space-2);
  }

  /* ---- masthead: the ask above the fold --------------------------------
     Four spacing trims and nothing else. No flex order, no reordering: the
     DOM order stays subline, button, note, note, so a screen reader hears
     what the eye sees. */
  .band--00 > .band__inner { row-gap: var(--space-1); }
  .title__head .plate { margin-block-start: var(--space-1); }
  .title__head .claim { margin-block-start: var(--space-1); }
  .title__left .btn { margin-block-start: var(--space-2); }
  /* plate__rule--close keeps its 12px: at 56px the descender of "draaien."
     still hangs about 8px below the line box. */

  /* ---- the numbered ledger, full width ---------------------------------
     56px of padding-inline-start was 14% of a 390px screen and pushed the
     body copy further right than the h2 above it. The counter goes into the
     row's own 20px top padding, the same move the band numerals make. */
  .wg__item { padding-inline-start: 0; }
  .wg__item::before { inset-block-start: var(--space-1); }

  /* ---- app inventory: 11 ruled rows, not a ragged 2-column grid ---------
     Icon in a 48px column, name and fact beside it: the ledger shape the
     sheet already owns. Every row is one 64px hit area and every row is the
     same height, so the block stops looking like a broken masonry. */
  .inv__row { padding-block: var(--space-3) 0; row-gap: var(--space-2); }
  .inv__cells { display: block; }
  .inv__cell {
    display: grid;
    grid-template-columns: 48px minmax(0, 1fr);
    column-gap: var(--space-4);
    align-items: start;
    padding-block: var(--space-2);
    padding-inline-end: 0;
    border-block-start: 1px solid var(--line-soft);
  }
  .inv__cell:first-child { border-block-start: 0; }
  .inv__icon { grid-area: 1 / 1 / 3 / 2; }
  .inv__name { grid-area: 1 / 2 / 2 / 3; margin-block-start: 0; }
  .inv__fact { grid-area: 2 / 2 / 3 / 3; max-width: none; }

  /* ---- the open source ledger, regridded --------------------------------
     Four stacked blocks per row put the 40px star figure on a line of its
     own under the project name, which is the one number a visitor came for.
     Two columns instead: name and organisation left, figure right, the
     description across both. The table keeps its markup and its semantics,
     the three drawn column rules are hidden below 768px already. */
  .ledger,
  .ledger tbody,
  .ledger tr,
  .ledger td {
    display: block;
    width: auto;
  }

  .ledger tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-4);
    border-block-start: 1px solid var(--line-soft);
    padding-block: var(--space-3);
  }

  .ledger td {
    border-block-start: 0;
    padding: 0;
  }

  .ledger td + td { margin-block-start: 0; }
  .ledger td:empty { display: none; }
  .ledger__name { grid-area: 1 / 1 / 2 / 2; }
  .ledger__org { grid-area: 2 / 1 / 3 / 2; }
  .ledger__stars {
    grid-area: 1 / 2 / 3 / 3;
    justify-self: end;
    align-self: start;
  }
  .ledger__desc { grid-area: 3 / 1 / 4 / 3; margin-block-start: var(--space-2); }
  .ledger .fig,
  .ledger__row--compact .fig { font-size: 2.5rem; }
  .ledger__col { display: none; }

  .notice { padding: var(--space-4) var(--space-4) var(--space-5); }
  .idx__label { font-size: var(--t-lead-s); }
  .idx__row { flex-wrap: wrap; gap: var(--space-1); }
  .idx__route { flex: 1 0 100%; text-align: left; }

  /* The portrait is a 366px square on a 390px phone, which makes section 06
     a full-bleed face. One line, and it reverts by deleting it. */
  .portrait img { max-width: 288px; }
}

@media (max-width: 479px) {
  :root { --t-body: 1rem; }

  /* 22px sets three lines inside 296px and pushes the ask under the fold.
     18px sets two and is still a step above the 16px body here. */
  .title__subline { font-size: 1.125rem; }

  /* 296px of inner width is the tightest case on the page: the five 44px
     targets cannot share a line with the photograph and the wordmark, so
     the masthead is three rows here and every row pays for itself. Type
     sizes and hit areas are untouched; this is air only, and it is what
     puts the whole mail button inside the ~470px a 320x568 phone shows. */
  .np > .band__inner {
    row-gap: 0;
    padding-block: 10px 4px;
  }

  .avail > .band__inner { padding-block: 6px; }
}


/* ==========================================================================
   22b. VERTICAL AIR (proportional)

   Added after Rob asked for "wat meer verticale margins". Placed last so
   it wins on source order at equal specificity, exactly as it was tested.
   No !important anywhere.

   THE DERIVATION
   --------------
   A gap is not a number, it is a ratio. One coefficient, k = 0.6, applied to
   the font-size of the type at the boundary, rounded to the 4px scale:

     display  120px  x 0.6 = 72.0 -> 72     --air-display
     figure    72px  x 0.6 = 43.2 -> 44     --air-figure
     plate     56px  x 0.6 = 33.6 -> 32     --air-plate
     h2        40px  x 0.6 = 24.0 -> 24     --air-h2
     h2 (2rem) 32px  x 0.6 = 19.2 -> 20     --air-h2s
     lead      28px  x 0.6 = 16.8 -> 16     --air-lead
     lead-22   22px  x 0.6 = 13.2 -> 12     --air-lead-s
     body      18px  x 0.6 = 10.8 -> 12     --air-body
     caption   14px  x 0.6 =  8.4 ->  8     --air-caption

   FOUR RULES, NOTHING ELSE
   R1  between two blocks: the gap takes the LARGER type at the boundary.
   R2  inside one utterance (setup/payoff of one claim, a caption under its
       own figure): the gap takes the SMALLER type.
   R3  optical: a line set at line-height < 1 already carries a descent
       reserve of 0.2em under its baseline, so the drawn gap is not the seen
       gap. Subtract it. 72 - 0.2*120 = 48  ->  --air-display-o.
   R4  floor: this is an air pass, so no gap shrinks. Where the formula lands
       under the shipped value the shipped value stands (noted per rule).

   A drawn 2px rule is transparent: it takes the type of the block it caps,
   it is not a unit of its own.

   ALREADY CORRECT, SO NOT TOUCHED (the proof the coefficient is the right
   one, not a number picked to fit):
     .band--0n > .band__inner padding-block-start = 24 = 0.6 x the 40px h2.
     band rhythm 64/96/128/160/192 = 2,3,4,5,6 x --air-plate (32). Five
     distinct values, uneven, untouched.

   BELOW 768px the masthead has five pixels of fold clearance at 320, so
   every gap above the mail button is frozen at its shipped value. The same
   tokens run below the button, restated for the smaller type that lives
   there (display 56, h2 32, lead 22, body 16).
   ========================================================================== */

/* Six tokens, not nine: --air-figure (44), --air-lead-s (12) and
   --air-caption (8) all land under the value already shipped at their one
   boundary each (.stat__cap 12, .title__right .audience 24, the second
   .title__note 8), so R4 keeps the shipped number and the token would be a
   declaration nothing reads. Derived above, not declared here. */
:root {
  --air-display:   72px;
  --air-display-o: 48px;
  --air-plate:     32px;
  --air-h2:        24px;
  --air-h2s:       20px;
  --air-lead:      16px;
  --air-body:      12px;
}


/* --------------------------------------------------------------------------
   THE GRID ROW. Stacked grid items are block-to-block, R1, and the type that
   opens a band is the h2: row-gap = --air-h2. This one declaration carries
   "every section h2 -> its prose" and "band 01 h2 -> the 557 figure".
   -------------------------------------------------------------------------- */
.band__inner { row-gap: var(--air-h2); }


/* --------------------------------------------------------------------------
   TITLE BLOCK, >= 768px only.
   -------------------------------------------------------------------------- */
@media (min-width: 768px) {

  /* strip -> top rule. The rule is transparent and caps the 56px plate, so
     the derived gap is --air-plate, 32. Shipped is 48, R4 floor holds it.
     UNCHANGED, and deliberately: rendered at 72 this was the one gap on the
     page that read as a hole rather than as air (looked at, not assumed). */

  /* top rule -> plate. Rule is transparent, caps the 56px plate. R1. 8 -> 32 */
  .title__head .plate { margin-block-start: var(--air-plate); }

  /* plate -> setup line. 56 vs 28, larger wins. R1. 12 -> 32 */
  .title__head .claim { margin-block-start: var(--air-plate); }

  /* setup -> argument. One sentence in two parts, so the SMALLER type sets
     it. R2. 0.6 x 28 = 16. 0 -> 16 */
  .claim__payoff { margin-block-start: var(--air-lead); }

  /* argument -> closing rule. R1 gives 72, R3 takes back the 24px descent
     reserve the 115-120px line already holds. 12 -> 48 */
  .title__head .plate__rule--close { margin-block-start: var(--air-display-o); }

  /* closing rule -> subline. Same boundary as the line above the rule, so
     the same 48: the frame then reads as one closed unit, because the gap
     that leaves it is not smaller than the biggest gap inside it.
     24 here + the 24px row gap of .band__inner = 48. 16 -> 48 */
  .title__head { margin-block-end: var(--air-h2); }
}


/* --------------------------------------------------------------------------
   THE OTHER BANDS. Same coefficient, smaller type, smaller gaps.
   -------------------------------------------------------------------------- */

/* band 02 sticky label: that h2 is pinned at 2rem, so it gets 20, not 24.
   Same ratio, different type. 16 -> 20 */
.wg__label .lead { margin-block-start: var(--air-h2s); }

/* band 05 notice: also a 2rem h2. 12 -> 20 on both. */
.notice .h2 { margin-block-start: var(--air-h2s); }
.notice .prose { margin-block-start: var(--air-h2s); }

/* band 06: a full 40px h2 over its own prose. 16 -> 24 */
.pers .prose { margin-block-start: var(--air-h2); }

/* band 07 closer: that line is the 56px plate, not a 40px h2, so it may not
   share the h2's row gap. calc + the row gap = --air-plate: 32 on desktop,
   20 on a phone, where both tokens step together. 16 -> 32 */
.closer__line { margin-block-end: calc(var(--air-plate) - var(--air-h2)); }

/* band 02 list: the 28px lead phrase over its own 18px sentence is one
   utterance, so R2 takes the smaller side. 0.6 x 18 = 12. 8 -> 12 */
.wg__item > b,
.wg__lead { margin-block-end: var(--air-body); }


/* --------------------------------------------------------------------------
   <= 767px. The tokens restated for the type that actually renders here:
   display 56, plate 32, h2 32, lead 22, body 16. Nothing above the mail
   button moves: the shipped masthead rules keep winning on specificity and
   nothing in this block reaches them.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  :root {
    --air-display:   32px;  /* 0.6 x 56 */
    --air-display-o: 20px;  /* 32 - 0.2 x 56 */
    --air-plate:     20px;  /* 0.6 x 32 */
    --air-h2:        20px;  /* 0.6 x 32 */
    --air-h2s:       20px;
    --air-lead:      12px;  /* 0.6 x 22 */
    --air-body:       8px;  /* 0.6 x 16 */
  }

  /* Air below the button, and only below it: the contact column is the
     first thing after the two notes. R1 against the 32px h2 tier. 4 -> 24 */
  .title__right { margin-block-start: var(--air-h2); }
}


/* ==========================================================================
   22c. MOBILE VERTICAL AIR  —  variant O, "recompose the masthead"

   Rob: "onderzoek dit ook op mobiel."

   THE PREMISE THAT WAS WRONG
   --------------------------
   Every gap above the mail button was frozen so the button would clear
   "about 470px" on a 320x568 phone. That figure was an estimate. Measured:

     iPhone SE 2/3  375x667   first paint 549   <- the real floor
     iPhone 13/14   390x844   first paint 699
     iPhone 15/16   393x852   first paint 695
     Chrome Android 360x800   first paint 696
     iPhone Pro Max 430x932   first paint 775
     320x568 iPhone           cannot be measured: iOS 27 refuses the device
                              type. Rob's own 320-wide visitors are all
                              Chrome on Android at 320x658.

   So the page was sized for a device that does not exist in the traffic,
   and every real phone paid for it. The design floor is 549, not 470.

   WHAT THIS VARIANT DOES ABOUT IT
   -------------------------------
   Not a fold query. A step at (min-height: N) is a second composition to
   draw, to look at and to keep true, and it hands the small phone the
   cramped one for ever. One composition, on every phone, funded twice:

     1. the furniture pays first. The masthead spends pixels on chrome
        before the argument appears: the nameplate bar, the availability
        bar, and the band's own top padding. 10px of that is phantom, and
        it is taken back here (see BUY, below).
     2. the surplus the measurement proves pays the rest. 549 - 530 = 19px
        of clearance is kept on the floor device, so the whole button is
        still above first paint there, and 186px of it is handed back on
        the median phone (393x695) where it was never needed.

   Every gap below is the token section 22b already derived for phone type
   (0.6 x the largest font-size at the boundary, 4px scale, minus the 0.2em
   descent reserve where line-height < 1). Not one new number is invented:
   the tokens were restated for the phone in 22b and then not applied above
   the button. This block applies them.

   Nothing outside (max-width: 767px). Desktop is byte-identical, proved by
   dumping every element box at 1440 with and without this file.
   ========================================================================== */


/* --------------------------------------------------------------------------
   BUY 1 — the availability bar, 6px of padding to 4.
   It holds one 16px line and no control. It is a status field, not prose,
   and it is the one piece of furniture between the nameplate and the
   argument that is sized by nothing but habit. 35.4 -> 31.4 on every phone.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .avail > .band__inner { padding-block: 4px; }
}

/* --------------------------------------------------------------------------
   BUY 2 — the nameplate's 10px top padding, on the phones where it guards
   a row that is not there.

   The 10px exists because the five 44px hit boxes carry margin-block: -10px
   and, when they share the top row, the top 10px of all five would fall off
   the canvas. Below 377px of viewport they do NOT share the top row: the
   line wraps and the boxes sit in row two, where they overhang into the
   wordmark row instead. Measured, not assumed: .np__right lands at y=10 from
   377px up and at y=34 at 376 and below (binary search, fonts loaded).

   So this pair of declarations makes that wrap explicit at exactly the
   widths where it already happens, rather than trusting a font metric to
   keep deciding it, and then takes the 6px:
     flex-basis 100%  -> the row is guaranteed, whatever the font does
     justify-content  -> flex-end reproduces today's right-aligned group to
                         the pixel (the auto inline margin did it before,
                         and on a full-width line it has nothing to push).
   The bar becomes a symmetric 4px field. 81.2 -> 75.2 at 320 and 375.
   Hit boxes then span y=18..62 inside a 75px bar: nothing clipped at the
   top, nothing past the nameplate rule at the bottom.
   -------------------------------------------------------------------------- */
@media (max-width: 376px) {
  .np > .band__inner { padding-block: 4px; }
  .np__right { flex: 0 0 100%; justify-content: flex-end; }
}

/* --------------------------------------------------------------------------
   SPEND — the six gaps above the button, at the phone tier of the 22b
   tokens. Type on this tier: display 56, plate 32, h2 32, lead 22, body 16.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {

  /* band 00 kept a 4px row gap while every other band on the phone runs on
     --air-h2 (20). That single exception is what pressed the closing rule
     onto the subline. R1 at that boundary is the 32px h2 tier: 20.
     It also sets "closing rule -> subline", and 20 is exactly right there:
     the desktop pass says the gap that leaves the frame must not be smaller
     than the biggest gap inside it, and the biggest gap inside it here is
     --air-display-o, 20. So the frame closes on its own largest interval,
     with no second declaration. 4 -> 20 */
  .band--00 > .band__inner { row-gap: var(--air-h2); }

  /* top rule -> plate. The rule is transparent, it caps the 32px plate. R1.
     0.6 x 32 = 19.2 -> 20.  4 -> 20 */
  .title__head .plate { margin-block-start: var(--air-plate); }

  /* plate -> setup line. 32 against 22, the larger type sets it. R1. 4 -> 20 */
  .title__head .claim { margin-block-start: var(--air-plate); }

  /* setup -> argument. One sentence in two parts, so R2 takes the smaller
     side, the 22px setup. 0.6 x 22 = 13.2 -> 12.  0 -> 12
     This is the gap that was doing the most damage: the payoff was welded
     to its own setup, so the masthead read as one block of type instead of
     as a claim. */
  .claim__payoff { margin-block-start: var(--air-lead); }

  /* argument -> closing rule. R1 gives 0.6 x 56 = 32, R3 takes back the
     0.2em descent reserve the 54.88px line box already holds under its
     baseline: 32 - 11.2 = 20.8 -> 20.  12 -> 20
     The shipped 12 was the same optical correction applied twice. */
  .title__head .plate__rule--close { margin-block-start: var(--air-display-o); }

  /* the second note -> the argument column. 22b gives .title__right a 20px
     top margin to buy air "below the button", because the row gap under it
     was 4. The row gap is 20 now, so that margin is counted twice and the
     boundary opened to 40: a hole, seen in a screenshot, not computed.
     The row gap carries 20 of it now, so the margin keeps only the last 4:
     R4 says an air pass never shrinks a gap, and 24 is what shipped.
     40 -> 24, which is the shipped value, exactly. */
  .title__right { margin-block-start: var(--space-1); }

  /* subline -> the ask. The subline is 18px here (the <=479 block sets it),
     the button label 16: R1 on 18 gives 10.8. The button is a 44px control,
     not a line of type, so it takes the lead step rather than the body one,
     the same relation the desktop pass gives it. 8 -> 12 */
  .title__left .btn { margin-block-start: var(--air-lead); }
}


/* ==========================================================================
   23. REDUCED MOTION  (last block in the file, on purpose)

   Two gates, never folded together: consent (this block) and capability
   (the .js class the script adds only when consent is false). The block is
   explicit rather than a blanket wildcard, because a wildcard would also
   kill the gauge and would say nothing about the rAF loop.

   THE GAUGE IS KEPT UNDER REDUCE. This is a judgement call, stated out loud
   so the next person does not delete it as a bug. The fill and the square
   are 1:1 with the user's own scroll, linear, no easing, no overshoot, no
   autonomous movement: direct manipulation feedback, the same class of
   thing as a scrollbar. It is the only wayfinding device across 4.5
   viewports and the margin numerals alone do not replace it. Only the
   200ms weight transitions are dropped, so the rules snap. If Rob
   disagrees, .gauge { display: none } inside this block is the whole
   change, and the numerals still carry the position because they are real
   text.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .plate__rule,
  .band__rule,
  .ledger__col {
    animation: none;
    clip-path: none;
  }

  .band__rule { height: 1px; }
  .band.is-current > .band__rule { height: 2px; }

  *,
  *::before,
  *::after {
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }

  .btn:active { transform: none; }
}
