/*
  Inkoop Service Nederland — the Dutch pages.

  ── Where it loads, and it is no longer one page ─────────────────────────────

  /nl/, /nl/besparingsscan/ and /nl/mijnisn/ load it as their *whole* visual identity: the
  ISN sections exist nowhere else and no other stylesheet is on those three pages at all.

  Since 2026-09-17 the Dutch Learn tree loads it too — /nl/blog/, /nl/guides/ and every
  article and guide under them — and there it is on the page **beside** learn.css, for its
  chrome and nothing else. `learnChrome` in build.mjs argues why: /nl/blog/ is where the
  ISN strip's `Kenniscentrum` shortcut and the bar's `Kennis` menu both point, and it was
  answering them in Buyers & Co. chrome. So the two loading patterns are: alone on the
  three ISN pages, and after learn.css on the Dutch Learn pages.

  That second pattern is safe because of one property of this file, worth keeping: **every
  selector in it is `body.isn …` or `.isn-…`.** Nothing here is a bare `.section`, `.shell`
  or `h2`, so a page that loads it beside another stylesheet gets the bar re-dressed and
  the `--i-*` palette declared, and not one Learn rule shadowed. The nearest thing to an
  exception is the accent block below — `body.isn [class*='--blue']` and its five siblings
  — which matches on a substring of the class attribute; no Learn class contains one
  (`learn-grid--blog` and `learn-callout--note` are the only `--` names over there). Adding
  an unscoped rule to this file is what would break the pattern, and there is no reason to.

  It is *not* an override layer over styles/home.css: that stylesheet and the nine beside
  it are not loaded on any of these pages. What is loaded before this file is always
  tokens.css and base.css — the reset, the type baseline, the focus rings,
  `[data-reveal]`'s hidden state, the reduced-motion blanket rule and the burger panel's
  behaviour — and everything in here is either new or a re-dressing of one of those under
  `body.isn`.

  The specificity arithmetic that makes the re-dressing work: base.css's rules are single
  class selectors (0,1,0) and every rule here that competes with one is `body.isn` plus
  that class (0,2,0) or more. So nothing in base.css had to be edited to put a white 68px
  bar where a 92px glass one was, and every page that does not carry `class="isn"` on its
  `<body>` is untouched. Twenty-one of the 41 built Dutch pages carry it — the three above
  plus the eleven blog and seven guide pages — and the twenty that do not are still
  Buyers & Co.: /nl/manifesto/, /nl/suppliers/, /nl/product-cost-estimator/, /nl/faq/,
  /nl/contact/, /nl/briefing/, /nl/plan-een-gesprek/, and the industries hub with its
  twelve sector pages. The whole English tree is untouched too.

  ── The palette is the brief's, exactly ─────────────────────────────────────

  Eleven colours, and the ratio matters as much as the values: 65–75% white and warm
  off-white, 15–20% black and grey, 10–15% accent. Which is why the *saturated* accents are
  concentrated — the hero's step discs, the ticks, the links, the figure, the final block —
  and the long middle of the page is white with black type on it. A page where every section
  has an accent ground spends the whole allowance in the first three screens and has nothing
  left to make the closing call to action loud.

  The category rail used to be the largest single item in that budget: twenty saturated blocks
  filling the second screen. It is now twenty tints of the same five accents, which is what
  gave the allowance back to the things that have to be loud. The argument is at `.isn-cat`.

  Red is the one with a rule attached: limited use, never dominant. It carries four of the
  twenty rail cards — as a tint — one benefit's icon, and nowhere else.

  ── Tints, saturated grounds, and the two inks ──────────────────────────────

  Every accent has a saturated value and exactly **one** tint, and the pair does two different
  jobs. A tint is a ground you set type and drawings straight onto: the process cards
  (`.isn-svc` and `.isn-step-art`), the mega-menu marks, the category rail. Ink on all five
  tints computes between 17.8:1 (blue) and 18.6:1 (yellow), so a tint needs no per-accent
  decision at all — the type is `--i-ink` and that is the end of it. One tint per accent is also
  what makes the rule the site is held to: the same brand colour is the same light background
  everywhere it appears, so a blue category card and the Analyse card are one measured value.

  A saturated ground is the exception, and it is where a second ink comes from: white on
  #2463FF is 4.9:1 and clears AA for body text, where white on #FF6A13 is 2.9:1 and fails even
  the large-text threshold — and a name nobody can read is not a brand colour, it is a bug. So
  `--card-fg` is per accent for that case. Since the rail stopped being saturated its only
  reader is `.isn-on-accent .isn-ic`, which inverts the two-tone icons on the same grounds; the
  token stays because the case it answers is real and the next saturated surface should not
  have to re-derive it.
*/

body.isn {
  /* ── The brief's palette, verbatim ─────────────────────────────────────── */
  --i-ink: #0a0a0a;
  --i-white: #ffffff;
  --i-paper: #f7f7f4;
  --i-grey: #f1f1ef;
  --i-line: #e2e2de;
  --i-muted: #6f706c;

  --i-blue: #2463ff;
  --i-blue-soft: #eef3ff;
  --i-orange: #ff6a13;
  --i-orange-soft: #fff1e7;
  --i-yellow: #ffc928;
  --i-yellow-soft: #fff8d9;
  --i-green: #13a866;
  --i-green-soft: #eaf8f1;
  --i-red: #ff4d4f;
  --i-red-soft: #fff0f0;

  /*
    ── Two colours from a second brief, and they are deliberately not aliases ──

    The top strip arrived after the palette above and named its own two values: #E8F0FF for the
    ground and #4B4B4B for the text. Neither is in the list above and neither is a rounding of
    something that is — the ground is a full point bluer than `--i-blue-soft`'s #EEF3FF, and the
    ink is between `--i-muted`'s #6F706C and `--i-ink`, warm grey against neutral grey.

    So they are declared rather than mapped onto the nearest existing token. Mapping them would
    be a quiet redesign of a value the brief gave in hex, and the difference is visible: the
    strip on #EEF3FF reads as the same paper as the scan card and the FAQ tint, where #E8F0FF
    reads as a band of its own above white — which is the whole job of the row.

    Named for the role and not the colour, because the role is what a later change would follow:
    anything on this strip takes these two and nothing else does.
  */
  --i-strip: #e8f0ff;
  --i-strip-fg: #4b4b4b;

  /*
    ── The type scale ───────────────────────────────────────────────────────

    This page adds **no font family and no type value of its own.** Both the faces and the
    hierarchy they are set in are the ones the site already had — `styles/tokens.css` and
    `styles/base.css` — aliased here so that every rule below names an `--i-*` token and the
    whole identity is one system rather than a per-component decision:

      · `--font-display`  Figtree 400–800 — headlines *and* body copy, exactly as the rest of
                          the site sets it. `--font-body` is defined as `--font-display` in
                          tokens.css; the two aliases below keep the two roles nameable
                          without implying two faces.
      · `--font-mono`     IBM Plex Mono 400/500 — uppercase metadata only: the section
                          eyebrows, the small labels and the bar's own links. Never a
                          headline, never body copy.
      · `--i-wordmark`    the logotype's stack, and the one place a fourth face would not be
                          an inconsistency — see the paragraph on it below.

    Two faces and six weights, which is exactly what the stylesheet request in build.mjs
    asks for. An earlier pass on this page introduced Inter Tight for the headlines at
    750–900: it is gone, and so is every number that arrived with it. It was never part of
    this design system, and the weights it was set at do not exist in Figtree's request
    (400;500;600;700;800), so every 850 and 900 in this file was being synthesised or
    rounded. Display type is 800, tightened by tracking rather than by weight, which is what
    the rest of the site does and the reason it reads as type rather than as a template.

    ── Every number below is the site's own, not this page's ─────────────────

    The sizes are tokens.css's `--t-*` scale and the tracking and leading are the values
    base.css sets on `h1`, `h2`, `h3` and `.eyebrow`. That is the point of aliasing rather
    than redeclaring: a headline here sets identically to a headline on the English site, so
    the colour, the product photography and the motion carry the new identity and the
    typography carries the old quality instead of competing with it.

    Exactly one number is not copied — the hero's floor, and the reason is under `--i-hero`.
  */
  --i-display: var(--font-display);
  --i-body: var(--font-body);
  --i-mono: var(--font-mono);

  /*
    ── The logotype's stack is the site's own, alias and all ──────────────────

    `var(--font-wordmark)`, unchanged and unwrapped: Neue Haas Grotesk Display first,
    Helvetica Neue behind it. Exactly what `.topbar .brand-text` sets for `Library of Trade`,
    so the two lockups are the same letter on the same machine.

    This token used to splice `var(--font-display)` into the middle of that chain — Figtree —
    on the reasoning that a webfont that actually loaded beats the OS's grotesk beside copy
    set in a designed face. The instruction is the opposite and it is the instruction that
    decides a logotype: the wordmark uses the same font as the Library of Trade site. Figtree
    at 800 and Helvetica Neue at 700 are visibly two different logos, and this is one brand
    family.

    The licensed face is still absent from this repo — no .woff2, .otf or .ttf anywhere in
    the tree — so on a Mac this resolves to Helvetica Neue and on Windows to Arial Bold, and
    both sites drift together rather than apart. Drop the woff2 into `assets/fonts/`, add its
    `@font-face`, and both lockups start resolving to the real face with no change here.
  */
  --i-wordmark: var(--font-wordmark);

  /*
    base.css sets one tracking for `h1`, `h2` and `h4` — -0.021em — and one for `h3`, and
    that is the whole heading system: the hero is not a special case with a tighter setting
    of its own. So `--i-track-hero` and `--i-track-display` carry the same number. Two role
    names for one decision, which means a later change to only one of them reads as a
    deliberate departure rather than as drift nobody noticed.

    0.1em on the label is `.eyebrow`'s, and it is not only the eyebrow's: twelve rules in this
    file spend `--i-track-label`, so it is the site's setting for any small capitalised label
    and not one element's preference.

    It was 0.16em once, on the reasoning that `INKOOPCOLLECTIEF VOOR BEDRIJVEN` is longer than
    any of the site's own eyebrows and needs the air. **Both halves of that were wrong.** No
    eyebrow on this site has ever read that — the seven of them are inventoried above the
    `.isn-eyebrow` rule, and the longest is /nl/'s `Hét inkoopcollectief voor MKB Nederland` at
    39 characters counted, against 31 for the string quoted here. So the sentence argued for
    extra air from a string that was both absent and eight characters shorter than the case it
    was supposed to be the extreme of. 0.1em is the site's value and stays; what is deleted is
    a justification, not a number.

    One consumer has since dropped out. `.isn-hero--scan .isn-hero-copy .isn-eyebrow` sets
    `--i-track-body` instead, because 0.1em exists to give capitals the rhythm their
    letterforms withhold and that eyebrow is no longer set in capitals. That is a departure
    from this token, argued where it happens rather than here.
  */
  --i-track-hero: -0.021em;
  --i-track-display: -0.021em;
  --i-track-h3: -0.014em;
  --i-track-body: 0;
  --i-track-label: 0.1em;

  /* base.css's `h1`, `h2`, `h3` and body leadings, in that order and nothing between them. */
  --i-lead-hero: 0.95;
  --i-lead-display: 0.96;
  --i-lead-h3: 1.12;
  --i-lead-body: 1.55;

  /*
    ── The one number on this page that is measured rather than copied ────────

    `--t-display` is `clamp(40px, 6.2vw, 76px)` and this keeps its 76px cap and raises its
    floor, but the middle term is not a `vw` at all — it is `cqw`, a fraction of the hero's
    **copy column**, and `.isn-hero-copy` carries `container-type: inline-size` to make that
    resolvable.

    The reason is arithmetic that a viewport unit cannot see. The headline is two fixed lines
    and the longer of the two, `Slimme bedrijven`, measures 7.86em in Figtree 800 at this
    tracking — measured, not estimated. So a two-line headline needs `7.86 × size` of column,
    and the column is a *fraction* of the viewport minus a gap minus a 520px card. At 1440 that
    is 564px, and 6.2vw would ask for 89px in it: the headline broke into four lines, which is
    the one thing the brief rules out about it by name.

    12.4cqw is 7.86em with a little slack, which means the two lines fit at every width the
    two-column hero exists at — 65px at 1201, 70px at 1240 and up — and the size is correct by
    construction rather than by a breakpoint per viewport. Below 1200 the hero is one column,
    the column is the shell, and the cap takes over at 76px.

    The floor is 44px rather than `--t-display`'s 40, and 44 exactly: it is the bottom of the
    brief's 44–54 mobile band *and* the largest size at which `Slimme bedrijven` still sets on
    one line in a 375px phone's 335px column — with the tighter tracking the 460 media query
    puts on it, without which the crossover is 42.6 and the band cannot be met at all. Under
    about 370px of viewport the first line does break in two; the arithmetic is in that query.
  */
  --i-hero: clamp(44px, 12.4cqw, 76px);
  --i-h2: var(--t-h2);
  --i-h3: var(--t-h3);
  --i-lead: var(--t-lead);
  --i-body-size: var(--t-body);
  --i-small: var(--t-small);
  /*
    The smallest size on the site, which is mono's. The eyebrows and the labels take it in
    Plex Mono, where 12px is the size the face is drawn for; the page's own small print takes
    it in the body face, and that is the only place a 12px Figtree line appears.
  */
  --i-micro: var(--t-mono);
  /*
    Form fields take the body size, and the token exists rather than the field rules naming
    `--i-body-size` directly because of one fact about it: 16.5px is above the 16px below
    which iOS zooms the page on focus. A later change to body copy must not silently take
    the fields under that line.
  */
  --i-field: var(--i-body-size);

  /* ── Rhythm ───────────────────────────────────────────────────────────── */
  /*
    ── The page's own width, and it is a backstop rather than a measure ───────

    1800px, and it was 1240. 1240 was a *content* width: every section was 1240px centred, the
    bar and the hero ran on the gutter, and on a 1440 window that put the wordmark at 56 and
    every section headline at 100 — two vertical lines 44px apart, which is exactly the amount
    that reads as a mistake rather than as a decision. One container now, `.isn-shell` below,
    and this is its cap.

    So the page is on the gutter at every width up to 1800 — 1280, 1440, 1512, 1728 all are —
    and past that it centres as one block, bar included, so the line holds there too instead of
    the sections stopping and the bar carrying on. 1800 rather than nothing at all because a
    fourteen-card rail and a two-column compare table on a 2560 monitor would otherwise set
    2448px wide, and reading across that is a neck movement.

    Reading measures are *not* this: they belong on the text — `--i-lead`'s 46ch, the leads'
    62ch, the step bodies' 30em — so a block starts on the line and stops where the sentence
    should. `docs/setup/13-visual-conventions.md` calls that the difference between a container
    and a measure.
  */
  --i-shell: 1800px;
  --i-gutter: clamp(20px, 5vw, 56px);
  --i-y: clamp(64px, 7.4vw, 118px);
  --i-bar: 68px;

  /*
    The utility strip above the bar — three benefit claims and three shortcuts.

    38px, from the brief's `36–40px` band and explicitly not higher. A height and not padding,
    because the row holds a 19px image beside a 12px line and the two would otherwise set the
    height between them: `min-height` plus `align-items: center` means the strip is 38px whether
    the images have arrived or not, which is what stops the nav under it from jumping once per
    icon on a cold load — and what keeps the fold arithmetic below honest at first paint.
  */
  --i-strip-h: 38px;

  /*
    Everything above the hero, hairline included: the strip's 38px, `body.isn .nav`'s 68px
    `min-height`, and the bar's 1px `border-bottom`. Declared rather than measured, because the
    hero's fold rule subtracts it in CSS and `--bar-h` — js/nav.js's — does not exist at first
    paint.

    One token, and one place to change it: anything added above the nav is added here too, or the
    hero overruns the fold by exactly the new row's height. The strip is the proof that the
    warning was worth writing — it arrived after the fold was pinned, and this sum is the only
    edit the pin needed.
  */
  --i-chrome: calc(var(--i-strip-h) + var(--i-bar) + 1px);
  --i-r: 20px;
  --i-r-sm: 12px;
  --i-r-lg: 26px;

  background: var(--i-white);
  color: var(--i-ink);
  font-family: var(--i-body);
  font-size: var(--i-body-size);
  line-height: var(--i-lead-body);
  letter-spacing: var(--i-track-body);
  /* Figtree at 800 over a saturated ground gets crunchy without this. */
  -webkit-font-smoothing: antialiased;
}

/*
  Every accent resolves through the same handful of variables, so a component says
  `isn-x--orange` once and never names a hex value. `--a` is the saturated colour, `--a-soft`
  its tint, `--card-fg` the ink that is legible on `--a`, and `--a-ink` the accent at a weight
  that survives *on* `--a-soft`.

  `--a-ink` is the newest of the four and has two readers, both of them a stroke of the accent
  on the accent's own tint: the outline drawings on the three process cards, which were the
  first thing on this site to do that, and the rail's drawn stand-in now that `.isn-cat` is a
  tint too. Blue carries it as itself — #2463FF on #EEF3FF measures 4.39:1. Green and red are
  the two that only nearly do, 2.81:1 and 2.95:1 on their own tints, which is under the 3:1 a
  non-text stroke wants and is why they are worth writing down rather than rounding up: both are
  a drawing's outline and not type, both are read at 3–4 units of stroke, and darkening either
  produces a bottle green and a brick that stop being this palette's colours. Orange is close
  and gets there by being mixed a sixth toward the ink, landing at #D75B12 and 3.52:1 on its own
  tint.

  Yellow cannot get there at all, and that is worth stating plainly rather than fudging:
  #FFC928 on #FFF8D9 is 1.4:1, a line that reads as a smudge, and every darker yellow is a
  brown. Mixing it toward the ink until a 4-unit line holds produces #937518 — olive, dull,
  and the one drawing in the row that looked like it came from another palette. So yellow's
  ink is not a yellow at all: it borrows orange's, the same #D75B12, which is 3.6:1 on the
  yellow tint and stays a warm saturated line instead of a dead one. The surface is still
  `--a-soft`, so the card is still the yellow one at a glance across the row — what changed
  is the hue of the stroke on it, not the accent. The two accents sharing one ink is the
  point rather than a shortcut: there is no third warm dark in this palette to invent.

  The mix is always on the colour and never on the tint: the tint is what makes the three
  cards legible as three steps from across the page, and darkening it to rescue a stroke
  would trade the thing that works for the thing that doesn't.
*/
body.isn [class*='--blue'] {
  --a: var(--i-blue);
  --a-soft: var(--i-blue-soft);
  --card-fg: var(--i-white);
  --a-ink: var(--i-blue);
}

body.isn [class*='--orange'] {
  --a: var(--i-orange);
  --a-soft: var(--i-orange-soft);
  --card-fg: var(--i-ink);
  --a-ink: color-mix(in srgb, var(--i-orange) 84%, var(--i-ink));
}

body.isn [class*='--yellow'] {
  --a: var(--i-yellow);
  --a-soft: var(--i-yellow-soft);
  --card-fg: var(--i-ink);
  --a-ink: color-mix(in srgb, var(--i-orange) 84%, var(--i-ink));
}

body.isn [class*='--green'] {
  --a: var(--i-green);
  --a-soft: var(--i-green-soft);
  --card-fg: var(--i-ink);
  --a-ink: var(--i-green);
}

body.isn [class*='--red'] {
  --a: var(--i-red);
  --a-soft: var(--i-red-soft);
  --card-fg: var(--i-ink);
  --a-ink: var(--i-red);
}

body.isn [class*='--grey'] {
  --a: var(--i-grey);
  --a-soft: var(--i-grey);
  --card-fg: var(--i-ink);
}

/* ── Shared furniture ─────────────────────────────────────────────────────── */

/*
  ── The page container. One rule, and the bar is in it ───────────────────────

  Every left edge on this page is this declaration: the wordmark's, the hero headline's, every
  section headline's, the FAQ's, the mission's. That is why `body.isn .nav` is in the selector
  list rather than carrying its own copy of the same three values — the bar and the content
  cannot drift apart if there is only one place to change.

  The `padding` shorthand, not `padding-inline`: `.nav` in styles/base.css is
  `padding: 0 var(--gutter) 14px`, and that 14px bottom belongs to a bar that sits over a black
  hero with a utility strip in it. The shorthand is what removes it.

  A block that needs a narrower measure caps *itself* and stays on this line — see `--i-shell`
  above. A block that wants to run past the page edge does it by leaving the container: the
  rail's `.cats-viewport` is the one that does, and it reproduces this rule's left edge with
  `max(var(--i-gutter), calc((100vw - var(--i-shell)) / 2 + var(--i-gutter)))` so its first card
  still starts on the line while the row bleeds off both sides.
*/
.isn-shell,
body.isn .nav {
  width: 100%;
  max-width: var(--i-shell);
  margin: 0 auto;
  padding: 0 var(--i-gutter);
}

.isn-section {
  padding: var(--i-y) 0;
}

.isn-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
  base.css's `.eyebrow` declaration for declaration: uppercase IBM Plex Mono at 12px/500/0.1em,
  leading 1.4, so it reads as metadata rather than as the first line of the copy. Only the
  colour is this page's, because `--i-muted` is.

  It is written out rather than reached by adding `.eyebrow` to the markup because the class
  name is this page's; the values are the site's either way, and 500 is Plex Mono's heaviest
  requested weight. A label at 700 in the body face — which is what this was two passes ago —
  is a small bold sentence, not a label.

  ── Seven instances, and not one of them a section label ─────────────────────

  It was the section label: every `isnHead()` printed one, and the founder and mission
  sections printed their own. All nine of those are out on request — "haal de eyebrows weg bij
  de secties. behalve bij de hero section." What is left is a hero label or a result label and
  never a chapter heading. There are seven because the site grew pages, not because the section
  label came back. Read out of build.mjs's seven `class="isn-eyebrow"` sites:

    · /nl/ hero               `ISN_HERO.eyebrow`                  Hét inkoopcollectief voor MKB Nederland
    · /nl/besparingsscan/     `ISN_SCAN_PAGE.hero.eyebrow`        Gratis inkoopcollectief voor MKB
    · the scan outcome panel  `ISN_SCAN.three.resultEyebrow`      Wat we zien
    · category index          `ISN_CATEGORY_INDEX.eyebrow`        Categorieën
    · a category hero         `ISN_CATEGORY_PAGES.energie.hero`   Energie
    · branche index           `ISN_BRANCHE_INDEX.eyebrow`         Voor wie?
    · a branche hero          `ISN_BRANCHE_PAGES['bouw-installatie'].hero`  Bouw & installatie

  This block used to name the hero's string as `INKOOPCOLLECTIEF VOOR BEDRIJVEN` and claim that
  /nl/ and /nl/besparingsscan/ printed *the same* one. Neither survived: the copy was rewritten
  and the two heroes have never matched. The strings are listed with their sources so the next
  reader re-derives them from the content files instead of trusting this comment.

  ── All seven are sentence case in the source; the caps are this rule's ──────

  Not one content file types capitals, and `ISN_HERO.eyebrow`'s docblock gives the reason: typing
  `HÉT INKOOPCOLLECTIEF VOOR MKB NEDERLAND` there changes nothing on screen and introduces a second
  way of writing the same field. So `text-transform` below is the only thing making an eyebrow
  uppercase anywhere on this site.

  Which is what makes it removable per instance, and one instance has removed it:
  `.isn-hero--scan .isn-hero-copy .isn-eyebrow` sets `text-transform: none` on instruction, and
  takes `--i-track-label` off with it, because 0.1em is tracking that only capitals need. For that
  field alone the "typing capitals changes nothing" argument no longer holds — `ISN_SCAN_PAGE.hero
  .eyebrow`'s docblock says so in place. Sentence case is still right there; it is now load-bearing
  rather than a filing convention.

  The colour is overridden twice, and both overrides are argued where they sit: green by
  `.isn-outcome .isn-eyebrow`, blue by the scan hero's rule.

  There was briefly one instance more than the seven above — `GRATIS BESPARINGSSCAN` over the
  besparingsscan section's headline, asked for by name in that brief and then withdrawn: "haal deze
  ook weg en zorg dat die titel daar staat dus top aligned." Its removal is the whole of that
  alignment, since `.isn-h` carries no top margin; see `isnScanSection()` in build.mjs.

  The 18px below is therefore hero spacing only. `.isn-head`'s own bottom margin is untouched, and
  every section that prints a head still starts with its headline, which is what makes them read
  as statements instead of as chapters in a template.
*/
.isn-eyebrow {
  margin: 0 0 18px;
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--i-muted);
}

/*
  Every headline on the page, one rule, and it is base.css's `h1, h2, h3, h4` rule: Figtree
  800, tracked -0.021em, leading 0.96. The tracking does the work the weight is not allowed
  to — 800 tightened sets denser than a synthesised 900 and keeps the counters open, which is
  the difference between type and a bold template.

  0.96 on a display line, tighter than any body leading and deliberately so: these headlines
  are three or four words, and a three-word headline set at 1.2 reads as three separate
  lines rather than as one block.
*/
.isn-h,
.isn-hero-h,
.isn-mission-h,
.isn-final-h,
.isn-outcome-h,
.isn-calc-figure {
  font-family: var(--i-display);
  font-weight: 800;
  letter-spacing: var(--i-track-display);
  line-height: var(--i-lead-display);
  text-wrap: balance;
}

.isn-h {
  margin: 0;
  font-size: var(--i-h2);
}

.isn-lead {
  max-width: 62ch;
  margin: 20px 0 0;
  font-size: var(--i-lead);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

.isn-head {
  margin-bottom: clamp(38px, 4.4vw, 66px);
}

.isn-micro {
  margin: 0;
  font-size: var(--i-micro);
  font-weight: 600;
  letter-spacing: var(--i-track-body);
  color: var(--i-muted);
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */

/*
  Two variants on the page, plus two inversions of them for the blue block. The brief
  allows exactly two actions and names them; `Start nu`, `Word lid` and `Ontdek meer` are
  forbidden by name, which is a copy rule this file cannot enforce and a reason not to
  build a third button style that would invite one.
*/
.isn-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 52px;
  padding: 0 26px;
  border: 1.5px solid transparent;
  border-radius: var(--i-r-sm);
  font-family: var(--i-body);
  font-size: var(--i-small);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.isn-btn:hover {
  /* One pixel. Enough to answer the pointer, not enough to be motion. */
  transform: translateY(-1px);
}

.isn-btn[disabled],
.isn-btn:disabled {
  cursor: not-allowed;
  opacity: 0.45;
  transform: none;
}

.isn-btn--solid {
  background: var(--i-blue);
  border-color: var(--i-blue);
  color: var(--i-white);
}

.isn-btn--solid:hover {
  background: #1b4fd8;
  border-color: #1b4fd8;
}

.isn-btn--quiet {
  background: var(--i-white);
  border-color: var(--i-line);
  color: var(--i-ink);
}

.isn-btn--quiet:hover {
  border-color: var(--i-ink);
}

/* The two on the blue block: white solid, and an outline of the same white. */
.isn-btn--white {
  background: var(--i-white);
  border-color: var(--i-white);
  color: var(--i-blue);
}

.isn-btn--white:hover {
  background: var(--i-blue-soft);
  border-color: var(--i-blue-soft);
}

.isn-btn--ghost {
  background: transparent;
  border-color: color-mix(in srgb, var(--i-white) 55%, transparent);
  color: var(--i-white);
}

.isn-btn--ghost:hover {
  border-color: var(--i-white);
  background: color-mix(in srgb, var(--i-white) 12%, transparent);
}

.isn-btn--bar {
  min-height: 44px;
  padding: 0 18px;
}

.isn-btn--panel {
  width: 100%;
  justify-content: center;
  margin-top: 18px;
}

.isn-btn-arrow {
  flex: none;
  /* Optically centred against a 700-weight cap height, which sits above the x-height. */
  margin-top: 1px;
  transition: transform var(--dur-fast) var(--ease);
}

.isn-btn:hover .isn-btn-arrow {
  transform: translateX(3px);
}

/*
  `.isn-btn--to-scan` used to live here — it hid the arrow on a button that descended the page
  instead of going anywhere. Every link it was on pointed at `#besparingsscan`, and all three now
  point at `./besparingsscan`, which is a route: /nl/besparingsscan/ is its own page, so the arrow
  is correct on all three and the modifier has no users left. Deleted rather than left standing,
  the same treatment the hand-drawn underline's classes got.
*/

/* ── Icons ────────────────────────────────────────────────────────────────── */

/*
  One drawing, two colours, five accents — see content/isn-icons.js. The badge sets `color`
  for the edge and `--ic-soft` for the interior plane, and never draws a plate of its own:
  a rounded square behind every glyph is the icon-pack look the brief rejects by name.
*/
.isn-ic {
  display: inline-flex;
  flex: none;
  color: var(--a, var(--i-blue));
  --ic-soft: var(--a-soft, var(--i-blue-soft));
}

.isn-ic > svg {
  width: 26px;
  height: 26px;
}

.isn-ic--lg > svg {
  width: 38px;
  height: 38px;
}

.isn-ic--xl > svg {
  width: 62px;
  height: 62px;
}

.isn-ic--hub > svg {
  width: 30px;
  height: 30px;
}

/*
  On a saturated card the two-tone system inverts: the edge becomes the card's own ink and
  the plane a translucent film of it, because the accent it would otherwise be drawn in is
  the ground it is sitting on.
*/
.isn-on-accent .isn-ic {
  color: var(--card-fg);
  --ic-soft: color-mix(in srgb, var(--card-fg) 22%, transparent);
}

/* ── The category visual ──────────────────────────────────────────────────── */

/*
  One image zone, one set of rules, every category.

  The visuals are cut out on transparency and the card's accent tint is the background, so
  there is no white plate, no inner card and no second frame here — the wrapper is a fixed
  box and the image is contained inside it. The tint is near-white, which is the one thing
  about this zone that changed with `.isn-cat`: a cut-out is now a dark object on a light
  ground rather than a light one on a saturated ground, and it is still a ground and not a
  plate because it is the whole card and not a box inside it. `--visual-h` is the only thing a section
  changes, and it changes it once for all of its cards: the objects arrive one at a time
  and must not each set their own scale, or fourteen cards read as fourteen photographers.

  `object-fit: contain` plus both maxima and `width/height: auto` is what keeps a portrait
  object and a landscape one at the same *visual* weight rather than at the same width.
  Nothing is ever cropped and nothing is ever stretched.

  The single track is written out as `minmax(0, 1fr)` rather than left implicit, and that is
  what makes the maxima work at all. An implicit track is auto-sized, so it grows to the
  image's intrinsic 700px, and `max-height: 100%` then resolves against *that* — a percentage
  measured against a box the percentage's own subject just defined. The image came out 236px
  tall in a 164px zone and lay over the card's name. A `minmax(0, 1fr)` track takes its size
  from this element's definite height instead, so the percentage has something real to
  resolve against and the object scales down to fit. It still never scales up: `max-*` only
  ever shrinks.
*/
.isn-visual {
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  width: 100%;
  height: var(--visual-h, 132px);
  padding: var(--visual-pad, 8px);
  box-sizing: border-box;
}

.isn-visual-img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/*
  The stand-in, for a category whose photograph is missing: its own icon, scaled up. Not a grey
  box and not a spinner — the card is complete, it simply has a drawing where it will later have
  a photograph, and nothing about it reads as missing. Every one of the twenty rail entries keeps
  an `icon` for exactly this reason (see `bouwmaterialen` in content/isn.js), so this path is a
  live contract even on the day no card is taking it.

  ── `--a-ink`, because the ground under it changed ───────────────────────────

  This used to be `--card-fg` and a 20% film of it, which was right while the only card printing
  a drawing was a *saturated* one: the accent was the ground, so the drawing had to be the card's
  ink. `.isn-cat` is now `--a-soft`, and `--card-fg` on a tint is the one combination that fails
  outright — blue's is `--i-white`, so a missing `energie.png` would have drawn a white bulb on
  #EEF3FF and the card would have looked empty rather than provisional.

  `--a-ink` is the token that exists for precisely this: the accent at a weight that survives on
  its own tint, blue and green and red as themselves and the two warm accents mixed a sixth
  toward the ink. It is the same stroke the process cards' drawings are set in, on the same
  ground, which is what makes a stand-in read as the accent's own object.

  The interior plane needs no rule at all now, and that is why the second declaration is gone:
  `.isn-ic`'s own `--ic-soft` is already `--a-soft`, so a filled shape is painted in the ground
  it sits on and occludes what is behind it instead of crossing it — exactly what
  `.isn-una-plane` does on the process cards, and argued there. `--grey` has no `--a-ink`, so a
  grey card's drawing falls back through an invalid `color` to the ink it inherits from the card,
  which is the value that rule sets anyway.
*/
.isn-visual--drawn .isn-ic > svg {
  width: min(72px, 52%);
  height: auto;
  aspect-ratio: 1;
}

.isn-visual--drawn .isn-ic {
  width: 100%;
  justify-content: center;
  color: var(--a-ink);
}

/* ── The strip above the bar ───────────────────────────────────────────────── */

/*
  A 38px band of #E8F0FF over the whole width: three benefit claims on the left, three
  shortcuts on the right, and nothing else in it.

  ── Flow, and that is the whole mechanism ────────────────────────────────────

  base.css's strip lives *inside* a `fixed` header and rides up on a transform js/nav.js
  drives, because `--topbar-h` is reserved as padding by a dozen rules across five
  stylesheets. This one is the element *before* a `sticky` header, so it scrolls away once,
  the nav lands at y=0 by itself, and there is no script, no `--bar-shift`, no measured
  height and no keyboard trap. The full argument is over `isnUtilityStrip` in build.mjs; the
  only thing this file has to do about it is add `--i-strip-h` to `--i-chrome`, which it
  does at the top.

  Nothing here has a `z-index`. The bar above it does, and it never needs to win: while any
  of the strip is on screen the bar has not reached `top: 0`, so the two are never in the
  same place.

  ── What the brief rules out is the whole reason this is four flat rules ──────

  No gradient, no shadow, no radius, no card. So there is no border either — the strip and
  the white bar under it separate on tone, and a hairline between two different colours is
  a third edge in a 38px row that is meant to read as calm. The one visual decision left is
  the ground, and it is a hex from the brief.
*/
.isn-utility {
  background: var(--i-strip);
}

/* `.isn-shell` in the markup supplies the width, the cap and the gutters. This is the row. */
.isn-utility-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  min-height: var(--i-strip-h);
}

.isn-utility-claims {
  display: flex;
  align-items: center;
  gap: 26px;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Tighter than the claims: three items that are each one word, against three that are three. */
.isn-utility-nav {
  display: flex;
  align-items: center;
  gap: 22px;
}

/*
  ── One rule for a claim and a shortcut, because they are one voice ──────────

  Both are a mark beside a short line at the same size, weight and colour, and the row's
  hierarchy comes from *side* rather than from styling: benefits left, navigation right. Two
  rules would be two chances for them to drift.

  ── 12px, which is the site's smallest and not a new number ──────────────────

  The brief asks for benefit text "duidelijk kleiner dan de main-nav tekst". `.nav-links a`
  in base.css is the body face at 14px/500 untracked, so `--i-micro` — the 12px that the
  page's own small print already takes in this face — is two pixels under it and the
  instruction is met with a token that already existed.

  Not smaller than that. Below 12 there is only `--t-mono-sm`'s 10.5px, which is drawn for
  tracked-out uppercase metadata and is not a size to set a sentence-case claim a visitor is
  meant to read once and believe; and a fourth type value would contradict the header of this
  file, which states that this page adds no type value of its own. So the difference in size
  is the two pixels, and the rest of the hierarchy is carried by weight against the nav's
  neighbours — the 700 CTA and the 21px lockup — and by this row being 38px tall.
*/
.isn-utility-claim,
.isn-utility-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--i-body);
  font-size: var(--i-micro);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0;
  color: var(--i-strip-fg);
  /*
    The row is one line and stays one line. A claim that wrapped would take the strip past
    the 40px the brief caps it at, which is the failure this prevents rather than styles.
  */
  white-space: nowrap;
}

.isn-utility-link {
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}

/*
  Colour, and only colour. No background, no underline, no card — the brief rules all three
  out by name, and on a 38px band any of them would make three shortcuts into three buttons.
*/
.isn-utility-link:hover,
.isn-utility-link:focus-visible {
  color: var(--i-ink);
}

/*
  ── There was a third rule here, singling `MijnISN` out ──────────────────────

  `.isn-utility-link[data-shortcut='mijnisn'] { font-weight: 600 }` — half a weight heavier
  than its two neighbours, which an earlier brief permitted and which cost nothing while the
  link was absent from the built HTML.

  It renders now, and the instruction that put it there is explicit about the consequence:
  "Alle drie moeten exact dezelfde visuele stijl hebben — donkergrijs, zelfde icon height,
  zelfde tekstgrootte, zelfde verticale alignment, consistente spacing." So the rule is gone
  and so is the `data-shortcut` attribute it was keyed on, which existed for this one rule and
  for nothing else. What is left is the pair of rules above: one for the row's voice, one for
  the link's hover.
*/

/*
  The four supplied marks. `width` and `height` are on the elements — 15 for the check, 19
  for the three nav marks, both exact, since each file is a square canvas at 4× its box — so
  this rule only has to stop a flex row from squeezing them. Nothing recolours them: the
  check arrived blue and the three arrived near-black, which is what the strip wants. See
  the module docstring in icons.py.
*/
.isn-utility-mark {
  flex: none;
}

/* ── The bar ──────────────────────────────────────────────────────────────── */

/*
  White, sticky, 68px, one hairline — and the same element `js/nav.js` already drives. The
  glass, the blur, the theme table and base.css's *own* utility strip — the one inside the
  header, riding up on a transform — all belong to a black hero this page does not have.
  This page's strip is the sibling above, and js/nav.js cannot see it. What is kept is the
  behaviour: the burger panel, its focus and Escape handling, `aria-current` on the in-page
  links, and the `--bar-h` publication the panel's own height cap reads.

  `data-theme="light"` is printed on the bar and nothing changes it, because no ISN section
  carries `data-theme` — so nav.js's section list is empty and the bar cannot flip.
*/
body.isn .topbar {
  position: sticky;
  transform: none;
  background: var(--i-white);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid var(--i-line);
  color: var(--i-ink);
}

/* Width and padding come from the container rule this shares with `.isn-shell`. */
body.isn .nav {
  gap: 30px;
  min-height: var(--i-bar);
  border-top: 0;
}

body.isn .nav-end {
  gap: 18px;
}

.isn-brand {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  margin-right: auto;
  color: inherit;
  text-decoration: none;
}

/*
  This brand's own mark: five petals pinwheeling around a centre, in the five accent colours.
  It is the proposition as one drawing — separate parties, one rotation — and it is why the
  borrowed marks that stood here before are gone.

  ── Placed, not masked, and that is the mechanism changing ───────────────────

  Every mark this bar has carried until now was a black PNG masked in `currentColor`, so one
  file served the bar, the footer and a dark ground. A five-colour drawing cannot be: a mask
  reads alpha only and would flatten all five petals to one ink. So this is
  `background-image`, the colours are in the pixels, and a ground this mark cannot sit on is
  now possible rather than impossible — see `isnMark()` in build.mjs.

  The five inks are the site's five accents as good as exactly: the yellow samples #ffc813
  against `--i-yellow`'s #ffc928, and the blue #447afd against `--i-blue`'s #2463ff, which is
  the one that is visibly lighter. Nothing in the stylesheet depends on them matching — no
  rule reads a colour out of this file — but it is why the mark sits in this palette without
  being tuned to it.

  ── Cut out, trimmed, and 16 colours ────────────────────────────────────────

  The supplied file is 1254 × 1254 of RGB with the drawing floating in white. This asset is
  that drawing lifted off its ground by alpha, trimmed to itself at 512 × 464 — so the box
  and the mark are the same rectangle and the left edge lands on the gutter the wordmark and
  the headline below it already sit on — and quantised to sixteen colours, which five flat
  inks do not notice and which took it from 95 KB to 44 KB. The white lines between the
  petals went transparent with the ground, since they run out to the edge; on this page's
  white that is the same picture, and on a colour the petals separate in that colour.

  34px is the globe's height, kept: the ratio is 1.10 rather than 0.99, so the mark is ~4px
  wider than before and reads as the same weight beside a wordmark whose block measures
  ~36px.
*/
.isn-mark {
  display: inline-block;
  flex: none;
  height: 34px;
  aspect-ratio: 512 / 464;
  background-image: url('../assets/logo-mark-isn.png');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/*
  The wordmark, and the one place on this page that is not set in the display face.

  ── Four numbers copied from `Library of Trade`, not re-derived ──────────────

  `.topbar .brand-text` in base.css sets that lockup at 21px / 700 / 0.86 / -0.025em, and
  this is those four values. The instruction was the font of the Library of Trade site, and a
  logotype is the face *and* its setting: Helvetica Neue at 700 tracked -0.025em is a
  different object from the same face at 800 tracked -0.03em, and side by side in one brand
  family the difference reads as a mistake rather than as a variant.

  So the only thing this rule still decides for itself is that the name is two lines, which
  is `ISN_META.wordmark`'s decision and not a typographic one. At 21px and 0.86 the cap of
  `Inkoop` and the baseline of `Nederland` measure ~36px against the 34px mark, so the text
  block and the mark are one object of the same visual size — which is what makes it a logo
  and not a caption beside an icon.

  `Inkoop Service` is a third wider than `Library of`, and 21px clears the two-item nav and
  the CTA in a 68px bar with room to spare because Helvetica Neue Bold sets narrower than
  Figtree 800 did.

  No colour. It inherits the bar's ink, the same way the mark takes its blue from a rule of
  its own; nothing here has to know which ground it is on.
*/
.isn-brand-name {
  font-family: var(--i-wordmark);
  font-size: 21px;
  font-weight: 700;
  line-height: 0.86;
  letter-spacing: -0.025em;
  white-space: nowrap;
}

.isn-brand-name > span {
  display: block;
}

/*
  ── The bar takes the site's setting, and only its colour is this page's ────

  base.css sets the nav links, the panel links and the toggle in uppercase IBM Plex Mono at
  12px/500/0.08em — the control-room voice — and these three rules used to override all of
  it with the body face at 15–17px, sentence case, on the reasoning that Dutch labels like
  `Hoe het werkt` set a third wider in uppercase mono and would crowd a two-line lockup.

  They do set wider, and at base.css's own 12px it does not matter: `HOE HET WERKT` measures
  ~105px, the two links and their gap ~230px, and the bar is 1240px wide with a 190px lockup
  and one CTA in it. The earlier objection was to uppercase mono at *15px*, which nothing on
  the site asks for.

  So all that is left here is colour — `--fg-sub` and `--fg` are dark-theme values and this
  page is paper — and the panel's hairline, which is the palette's and not the type's.
*/
body.isn .nav-links a {
  color: var(--i-muted);
}

body.isn .nav-links a:hover,
body.isn .nav-links a[aria-current='page'] {
  color: var(--i-ink);
}

/* Beside the two nav links, so it is set as one of them. */
.isn-nav-login {
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--i-muted);
  text-decoration: none;
}

.isn-nav-login:hover {
  color: var(--i-ink);
}

body.isn .nav-toggle {
  color: var(--i-ink);
}

body.isn .nav-panel {
  padding: 6px var(--i-gutter) 24px;
  background: var(--i-white);
  border-top: 1px solid var(--i-line);
}

body.isn .nav-panel a {
  color: var(--i-ink);
  border-bottom: 1px solid var(--i-line);
}

/*
  The panel's one call to action, and `display` here is a fix rather than a tidy — the same
  fix base.css makes for `.nav-panel .btn`, which the ISN button did not inherit because it
  is a different class.

  `body.isn .nav-panel a` above is (0,2,1) and outranks `.isn-btn`'s own `inline-flex` at
  (0,1,0), so the button was a block box: no flex centring, which is what `.isn-btn--panel`'s
  `justify-content: center` was written to do, and no `gap`, so the arrow sat straight against
  the last letter and then dropped to a second line. This selector is (0,3,1) and wins.

  `border-bottom-width` is the older half of the rule and stays: that same hairline sets every
  row's bottom border to 1px, and on the button it lands on a border the other three sides draw
  at 1.5px, so without it the blue box is a pixel shallower along the bottom than at the top.
*/
body.isn .nav-panel .isn-btn {
  display: inline-flex;
  border-bottom-width: 1.5px;
}

/*
  ── The bar's two dropdowns ─────────────────────────────────────────────────

  `Categorieën` opens the fourteen categories in four named groups; `Kennis` opens Blogs,
  Guides and Besparingsscan. Both are base.css's disclosure — `.nav-drop`, `.nav-drop-toggle`,
  `.nav-drop-menu`, `.nav-drop-list`, `.nav-drop-all` — which was written for a Buyers & Co.
  menu that renders on no page, and is complete down to the panel variant and the caret's
  rotation. So this section restates colour and adds one layout, exactly as the three rules
  above it do for the nav row: reuse the class, override the palette.

  Two overrides are corrections rather than palette, and both are the light theme's doing.
  `--bg-raised` *is* `--bg` on light (tokens.css says so on purpose: the page separates with
  hairlines, not elevation), so base's row hover paints white on white and there is no hover
  at all; and `--fg-soft` under the `Alle categorieën` rule is a dark-theme grey. A menu row
  that does not answer the pointer reads as a heading, so the hover is a plane here — soft
  blue, the same one the scan's chips take when they are chosen, which is the page's existing
  "you have hold of this" colour.
*/
body.isn .nav-drop-toggle {
  color: var(--i-muted);
}

body.isn .nav-drop-toggle:hover,
body.isn .nav-drop[open] > .nav-drop-toggle {
  color: var(--i-ink);
}

body.isn .nav-drop-menu {
  border-color: var(--i-line);
  border-radius: var(--i-r-sm);
  background: var(--i-white);
}

body.isn .nav-drop-list a {
  color: var(--i-ink);
}

body.isn .nav-drop-list a:hover {
  background: var(--i-blue-soft);
}

body.isn .nav-drop-all {
  border-top-color: var(--i-line);
  letter-spacing: var(--i-track-label);
  color: var(--i-muted);
}

body.isn .nav-drop-all:hover {
  color: var(--i-ink);
}

/*
  ── Capped to the window, and scrolling inside it ───────────────────────────

  It has to be capped, because nothing else can scroll it. The menu is `position: absolute`
  inside a `position: sticky` bar, so whatever runs past the bottom of the window is not
  merely ugly — it is unreachable. Measured at 1440 × 900 with the page at the top and the
  groups still in a grid: the panel was 878.5px tall and its top edge is at 96.8, so it
  ended at 975.3, and the two rows that ended below 900 — `Notariële & juridische diensten`
  and the `Alle categorieën` footer — could not be brought back by scrolling. The bar rides
  up by the strip's 38px and then pins, which recovers 38 of the 75.3 and still leaves the
  footer off screen. At 1440 × 740 five rows were gone; at 1280 × 640, eight. This is
  base.css's `.nav-panel` argument one element in: "the overflow is simply not on screen and
  scrolling the page cannot bring it back — the bar does not move".

  Packing the groups below took the panel to 710.2 and is what actually fixes a 900px
  window — this cap is the net under it. It acts wherever 710.2 does not fit, which is any
  window shorter than 835.2, and it is the only thing standing between a 640px laptop and
  eight lost categories.

  ── Why the subtrahend is `--i-chrome`, and why the literal is 18 and not 32 ─

  28px of daylight under the panel is the target, and `--i-chrome` overshoots the panel's
  top by 10.2px, so the literal is 28 − 10.2 rounded to 18. Measured gap where the cap
  binds: 28.2px at 740 and at 640 alike. Above 835.2 the panel is shorter than the cap and
  the gap is whatever the content leaves — 92.9 at 900, 272.9 at 1080 — which is the
  requirement read the only way it can be, as a floor on the clearance and not a target for
  a menu that has stopped needing the room.

  `--i-chrome` is 107 — the strip's 38, the bar's 68 and its hairline. The panel's top is
  96.8, because it hangs from the toggle's own box and not from the bar's bottom edge: the
  toggle is 21.7px tall and centred in the 68px row, so it ends at 82.8, which is 23.2
  above the bar's bottom at 106, and base.css's `top: calc(100% + 14px)` hands 14 of that
  back. Those 10.2px are geometry rather than a fudge, and they are the same in both bar
  states — pinned, the panel's top is 58.8 against a bar that measures 69.

  Which is why the cap subtracts the unpinned 107 and not js/nav.js's `--bar-h`. On this
  page the bar is sticky and the strip is a flow sibling nav.js cannot see, so `--bar-h`
  reads 69 whether the strip is on screen or not; a cap measured from 69 is 813 tall and
  overruns a 900px window by 9.8px at the top of the page, which is the one place every
  reader starts. Measuring from the taller number costs a gap that opens to 66px once the
  strip has gone, and a panel with too much room under it is the side to be wrong on.

  Two heights, oldest first, so an engine without `dvh` still gets a cap. `dvh` is not
  dead weight on a rule that only acts at ≥1200px, where `.nav-links` is still on screen:
  that includes an iPad in landscape at 1366, and a retracting toolbar there is exactly
  the difference between the two units — the same reason `.isn-intake-sheet` gives.

  ── The panel scrolls; the page gains nothing ───────────────────────────────

  `document.documentElement.scrollHeight` is 9730 with the menu shut and 9730 with it open,
  before this rule and after it: an absolutely positioned child of the bar was never part
  of the document's height. `overscroll-behavior` buys the other half of that — without it
  a wheel that reaches the end of the list carries on into the page, which slides the bar
  up and takes the open panel with it.

  The scrollbar is the one cost, and after the repack below it is rarely even drawn: at
  1440 × 900 the panel is 710.2 against 775 of room and does not scroll. Where it does, on
  this machine the scrollbar is an overlay and `clientWidth` stays 530. A classic 15px one
  would take each column to 241.5 and each name's room to 183.5px, and these names cannot
  respond by wrapping — `.nav-links a` in base.css sets `white-space: nowrap` on every link
  in the bar, this menu's rows included, so `Notariële & juridische diensten` at a measured
  191.7px would run into the column gap instead. It is 0.7px over the 191 it has today, so
  that edge is already touched; 8.2px over is the platform's worst case and it is not this
  rule's to fix.

  ── `.nav-links` in the selector, not `body.isn` ────────────────────────────

  It keeps this off the burger panel by construction. In the stack the menu is
  `position: static` inside `.nav-panel`, which carries its own `dvh` cap and its own
  `overflow-y: auto`; a cap here would win at (0,1,1), put a second scroller inside the
  first, and then need undoing in a `.nav-panel` rule — the trap the width below declines
  to walk into. Both of the bar's dropdowns are in `.nav-links`, so `Kennis` is capped too,
  which is free: three rows never reach it.
*/
body.isn .nav-links .nav-drop-menu {
  max-height: calc(100vh - var(--i-chrome) - 18px);
  max-height: calc(100dvh - var(--i-chrome) - 18px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/*
  Five groups in two columns, and since the repack below they no longer have to divide evenly.
  An earlier version of this comment counted four and argued that an even number was the whole
  point, because an odd one leaves a hole in the second column. Both halves went stale at once:
  the fifth group arrived, and the columns stopped sharing rows. They now pack independently,
  so two groups stand on the left and three on the right, and the hole the even number was
  protecting is what the repack removed.

  ── The panel is wider than base.css's, by arithmetic rather than by taste ──

  452px was measured for twelve category names in two columns, and for rows with nothing in
  front of the name. Every row here now opens with a 30px tile and a 10px gap, so the width
  is 452 + 2 x 40 = 532px: the tile added once per column and nothing else. Each group measures
  249px of that (532 less the panel's 24px of padding, less the 8px column gap, halved — 250 on
  paper, 249 live), the row's own 9px either side takes 18, the tile and its gap take 40, and
  191px is left for the name.

  191 is the number worth keeping, because it is what the 209px column leaves a name today, to
  the pixel. Deriving the width from the tile instead of rounding up to a comfortable-looking
  520 or 560 means the label column comes out of this change untouched: the longest name,
  `Technische benodigdheden` at 172.7px at 14px in this face, keeps exactly the 18.3px of slack
  it already had, and every measurement taken against the old panel still holds.

  Leaving the panel at 452 and letting the tile eat into the label column is the version that
  does not work, and it fails by 22px: 151px of name room breaks `Technische benodigdheden`
  onto two lines, and leaves `Schoonmaakproducten` (146.4px) 4.6px short of breaking too.

  That name took the title from `Schoonmaakproducten` when it replaced MRO; the order behind
  it is Schoonmaakproducten, `Schoonmaakdiensten` (135px), `Transport & logistiek` (127.8px)
  and `Pakketverzending` (110.8px). All five measured on the live panel with a Range over the
  text, not from the grid track, which reports the column and not the ink.

  ── Why the slack is load-bearing ──

  These links are `nowrap`, and this paragraph has now had it both ways. It is true that
  base.css sets no `white-space` on `.nav-drop-list a` — that is the correction an earlier
  version made — but `.nav-links a` does, and every row in this list is inside `.nav-links`.
  Measured over CDP on all twenty-five rows, `white-space` computes to `nowrap` on every one.
  So the correction was right about its selector and wrong about the outcome, which is the
  more expensive way to be wrong.

  That inverts what the slack defends against. The U+00AD two of the names carry at the
  compound seam, put there for the carousel card — `Schoonmaak­producten` and
  `Schoonmaak­diensten`, whose min-content would be `Schoonmaak-` at 93px — cannot fire under
  `nowrap`, so there is no two-line fallback to run short into. A column that comes out too
  narrow overflows instead, silently and to the right. `Notariële & juridische diensten` draws
  191.7px of ink in the 191px it has, so that edge is not hypothetical: it is 0.7px past
  already, and invisible only because the panel's own padding is what it bleeds into. The
  measured room is the whole defence precisely because nothing downstream catches it.

  `grid-template-columns` is restated rather than inherited inside a group: `.nav-drop-list` is
  two columns, which is right for one list of twelve names and wrong five times over here —
  three names in two columns is a row and a half, and the group titles would stop lining up
  with anything.

  ── One class, and no `body.isn`, on purpose ──

  532px is a bar-only number: in the burger panel the menu is the stack's width and a fixed
  532 would put a horizontal scrollbar on a 390px phone. The selector is left at (0,1,0) so
  that `.nav-panel .nav-drop-menu`'s `width: auto` at (0,2,0) still outranks it, while it
  still beats base.css's own `.nav-drop-menu` at equal specificity because isn.css loads
  second. Adding `body.isn` would take it to (0,2,1), win in the panel, and need a second
  rule restating a width base.css already sets — the same trap `--compact`'s `min-width` is
  undone for four rules above.
*/
.isn-nav-menu--cats {
  width: 532px;
}

.isn-nav-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 15px 8px;
}

/*
  ── In the bar the two columns pack independently, and grid cannot do that ───

  A grid shares its row heights across the columns, and with these five group sizes that
  is where the panel's height goes. The groups measure 288.3 (Werkplek, 6), 198.3
  (Techniek, 4), 333.3 (Energie, 7), 243.3 (Diensten, 5) and 153.3 (Financieel, 3) — a
  title block of 19.3 plus 44 per row and base.css's 1px between them, so a six-row group
  is 19.3 + 6 × 44 + 5 = 288.3 exactly, and every one of the five checks out that way.

  In two grid rows plus a third, that is 288.3 + 15 + 333.3 + 15 + 153.3 = 804.9, because
  row 1 is as tall as Werkplek even though Techniek only fills 198.3 of it, row 2 is as
  tall as Energie against Diensten's 243.3, and Financieel is pushed onto a row of its own
  where it sits alone in the left column with 249px of nothing beside it. 168.3px of that
  is air.

  Packed as two independent columns it is 636.6 — Werkplek + Energie on the left, Techniek
  + Diensten + Financieel on the right, 13 item rows against 12, which is the closest these
  five sizes come to even. The panel goes from 878.5 to 710.2 and fits a 900px window with
  no scrolling at all, which is the point: the cap above becomes the safety net rather than
  the mechanism.

  Flex column-wrap is the only way there without touching the data. Multi-column packs just
  as tightly and needs no cap, but it fills in source order — Werkplek, Techniek, Energie,
  Diensten, Financieel — which balances to {Werkplek, Techniek} against {Energie, Diensten,
  Financieel}, 18 rows against 19 and no gain worth the change; multicol has no `order`, so
  it can only be fixed in `content/isn.js`. `grid-template-rows: masonry` is not shipped.

  ── The wrap cap, and why it is `max-height` ────────────────────────────────

  `max-height` and not `height`: Chrome wraps a column flex container at either, but with
  `height` the container *is* that tall, and 740px of box around 636.6px of groups would
  hand back more air than the repack just recovered. Measured with `max-height`, the
  container is 636.5 — its content, to a rounding error.

  740 is a wrap point, not a height, and any value in [636.6, 849.9) produces this
  arrangement: 636.6 is the left column with Energie in it, and 849.9 is the left column
  with Techniek added as well. 740 sits 103.4 above the floor and 109.9 below the ceiling,
  so at 45px a row the left column can take two more categories before Energie is pushed
  right, and Techniek would have to fall from four categories to one before it was pulled
  left. Neither is a change anyone makes without looking at this menu.

  ── The order, and the two columns ──────────────────────────────────────────

  Flow order has to be Werkplek, Energie, Techniek, Diensten, Financieel; source order is
  Werkplek, Techniek, Energie, Diensten, Financieel. Three `order` values fix that, and
  they are on `nth-child` rather than on names because the groups carry no class of their
  own. They are inert everywhere `display` is not flex, which is the whole of the burger
  panel.

  `width: calc(50% - 4px)` is the grid's own column, arrived at differently: the container's
  content box is 506, the 8px `column-gap` survives from the rule above as the gap between
  the two flex lines, and (506 − 8) / 2 = 249 — the same 249 the grid reported, so the 191px
  the longest name is measured against is untouched.

  `.nav-links` in the selector for the same reason as the cap: it keeps every line of this
  off the burger panel by construction, where `body.isn .nav-panel .isn-nav-groups` stacks
  the five groups in one column and must go on doing so with no undo rule to keep in step.
*/
body.isn .nav-links .isn-nav-groups {
  display: flex;
  flex-flow: column wrap;
  align-content: flex-start;
  max-height: 740px;
}

body.isn .nav-links .isn-nav-group {
  flex: none;
  width: calc(50% - 4px);
}

body.isn .nav-links .isn-nav-group:nth-child(2) {
  order: 1;
}

body.isn .nav-links .isn-nav-group:nth-child(4) {
  order: 2;
}

body.isn .nav-links .isn-nav-group:nth-child(5) {
  order: 3;
}

.isn-nav-group .nav-drop-list {
  grid-template-columns: minmax(0, 1fr);
}

/*
  The row becomes a tile and a name.

  `flex` replaces base.css's `block` on the anchor — and `.nav-panel a`'s `block` too, at equal
  specificity and later in the file, which is wanted: the stacked panel prints the same marks.
  `align-items: center` is what lets a 30px tile and a text line of a different height sit
  against each other without either being told the other's size, and `flex: none` on the mark
  is what stops a long name from squeezing the photograph — with 18.3px of measured slack it
  should not come to that, but a tile 2px narrower in one row of twenty is exactly the kind of
  fault that surfaces once a name changes and nobody re-measures.

  No padding of its own, deliberately. base.css's `7px 9px` stays and the tile sets the row
  height instead: 44px in the bar, and 55px in the stacked panel, where `.nav-panel
  .nav-drop-list a` gives the same tile 12px and `.nav-panel a`'s hairline adds the odd pixel.
  A vertical padding here would have won in the
  panel at equal specificity and had to be undone there in a second rule, to arrive at a
  number the tile already implies. The tile is the tallest thing in the row, so it should be
  what sets the row.

  `.isn-nav-name` has no rule at all and is still worth existing: it makes the label one flex
  item, so `min-width: auto` applies to it and the row shrinks to the name's min-content — the
  U+00AD break — instead of overflowing the panel. That is the failure mode the width above is
  measured to avoid, and this is what it degrades to if a future name defeats the measurement.
*/
.isn-nav-group .nav-drop-list a {
  display: flex;
  align-items: center;
  gap: 10px;
}

/*
  A tinted plate, which the icon system refuses by name — see `.isn-ic` above: "a rounded
  square behind every glyph is the icon-pack look the brief rejects". This is not that, and the
  distinction is which set is being quoted. `.isn-visual` gives the hero card's photograph a
  tinted object zone in the category's accent, because a cut-out on white with no ground floats;
  this is that zone at 30px, in a list where twenty-five of them carry the five-colour rhythm the
  bar otherwise has no room to show. All twenty-five rows print a photograph into it.

  It has not always been all of them. Three printed a drawn icon instead — `ai-cloud`, `marketing`
  and `product-sourcing`, the three with no photograph of their own — genuinely sitting on the plate
  the brief rejects, and that was the right way round: three glyphs on a ground that the other
  photographs establish reads as a set with three stand-ins in it, where three glyphs floating in a
  column of tinted tiles reads as three rows that look broken. `marketing` has its own cut-out now
  and the other two were removed as categories, so the plate carries nothing but photographs; the
  argument is kept because the drawn branch in build.mjs is kept.

  9px is a literal and not `--i-r-sm`, because a radius token does not survive a change of
  scale. 12px is the panel's own corner and reads as a corner there at 452px wide; on a 30px
  box it is 40% of the side and reads as a blob. 9px is 30%, which is the roundest this box
  gets while still being a square with its corners taken off. 24px of image inside the 30px
  leaves 3px of tint on the long side — enough to read as a ground rather than as a border. The
  glyph is held to 20px against the photograph's 24: the cut-outs run from 0.60 to 1.58 in
  aspect, so on every one of them the short side is well inside the box, while the drawn icon
  fills its viewBox square. At a matched 24px the glyph covers 576px² against the narrowest
  photograph's 360, and reads a weight heavier than the photographs it stands in for; 20px puts
  it at 400 and in the middle of their range.
*/
.isn-nav-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  background: var(--a-soft);
}

.isn-nav-mark img {
  width: 24px;
  height: 24px;
  object-fit: contain;
}

.isn-nav-mark .isn-ic > svg {
  width: 20px;
  height: 20px;
}

/*
  The group's name, in the page's label voice — `.isn-eyebrow`'s four values at the site's
  smallest size, which is the one drawn for tracked-out uppercase metadata and is what
  `.nav-drop-all` at the foot of the same panel already uses. The 9px indent is `.nav-drop-list
  a`'s own padding, so the heading sits on the same left edge as the names under it rather than
  9px out from them.
*/
.isn-nav-group-title {
  margin: 0 0 3px;
  padding: 0 9px;
  font-family: var(--i-mono);
  font-size: var(--t-mono-sm);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-muted);
}

/*
  The two summaries take the panel's ink, not the bar's grey.

  Muted is right in the bar, where `body.isn .nav-links a` sets every label grey and ink is
  what hover and the current section mean. It is wrong here: `body.isn .nav-panel a` sets the
  panel's rows in ink, a `summary` is not an `a` so it kept the grey, and two grey rows in a
  stack of four ink ones read as unavailable rather than as the two that open.
*/
body.isn .nav-panel .nav-drop-toggle {
  color: var(--i-ink);
}

/*
  In the burger panel there is no room for two columns of anything: the panel is the viewport
  wide, minus the gutter, at every width that has it. So the four groups become four stacked
  blocks, and `body.isn .nav-panel a`'s hairline above already turns their rows into the same
  kind of row as the four links they sit under.
*/
body.isn .nav-panel .isn-nav-groups {
  grid-template-columns: minmax(0, 1fr);
  gap: 4px;
}

body.isn .nav-panel .isn-nav-group-title {
  padding: 12px 0 0;
}

/*
  ── `Voor wie?`: sixteen branches, and only one of them a link ───────────────

  The same panel, the same list and the same type as `Categorieën` beside it, and deliberately
  not the same *markup*. This one is base.css's flat `.nav-drop-list` — the two-column grid it
  already is — where the categories menu puts `.isn-nav-groups` inside it.

  That was a decision, not a convenience. The three `order` rules above are `nth-child(2)`,
  `(4)` and `(5)` of `.isn-nav-group`, written for the categories menu's five groups in a known
  sequence; a branche list reusing the class would have its second, fourth and fifth rows
  repacked into a column of their own. Scoping those three to the categories panel would have
  worked too, and would have left the categories menu depending on a selector qualified for a
  panel that does not use it. Not sharing the class means the categories dropdown cannot change
  at all, which is the guarantee that was wanted rather than a near miss at it.

  Everything else *is* shared, and by inheritance rather than by copying: `body.isn
  .nav-drop-menu`'s white plate and hairline, `body.isn .nav-drop-list a`'s ink and its soft
  blue hover, `body.isn .nav-drop-all`'s mono footer, and the viewport cap on
  `body.isn .nav-links .nav-drop-menu` — which this panel needs least and inherits anyway.

  ── No width rule at all, and that is the measurement ───────────────────────

  base.css's `.nav-drop-menu` is 452px, measured for twelve names in two columns with nothing in
  front of them. That is this list's shape exactly, four rows deeper. 452 less its two 1px
  borders and its 24px of padding is 426 of content; less base's 6px column gap, halved, is 210px
  a column — the grid reports `210px 210px` live. The row's own 9px either side takes 18, so
  192px is left for the name.

  Measured over CDP on all sixteen rows with the type below applied, `Sport, cultuur &
  verenigingen` draws 182.8px of ink — the widest of the sixteen, ahead of `Onderwijs &
  kinderopvang` at 164.6, `Webshops & e-commerce` at 162.5, `Zakelijke dienstverlening` at 154.2
  and `Automotive & mobiliteit` at 149.1, with `Vastgoed` at 60.2 and `Horeca` at 45.7 the
  narrowest. So the longest name has 9.2px of slack, and the panel keeps base's width because the
  arithmetic says it can — not because nobody checked. The categories panel next door had to go
  to 532 for a reason this one does not have: a 30px tile and a 10px gap in front of every name.

  The whole panel measures 452 × 396.6 at 1440 × 900, top edge at 96.8 and bottom at 493.5:
  275.8 of list, a 37.2 note, the 37.7 footer and 24 of padding. It is 378.4px clear of the
  bottom of that window and does not scroll — `scrollHeight` equals `clientHeight` — so the cap
  it inherits is pure insurance here rather than the mechanism it is next door.

  The class exists anyway, for the two rules that must name this panel and not that one — the
  reading direction below, and the intake pill's `:has()` in section 12.
*/

/*
  Down the left column, then down the right. Not across.

  base.css's grid is row-major, which is right for twelve unordered names and wrong for these:
  `ISN_BRANCHES` in content/isn.js lists the sixteen in a deliberate sequence, and row-major
  prints it 1 2 / 3 4 / 5 6, so reading it in order means the eye crossing the panel eight
  times. Eight explicit rows with `grid-auto-flow: column` prints 1–8 down the left and 9–16
  down the right, which is how the categories panel beside it reads its own two columns.

  Eight and not `auto-fill`: there is no CSS that counts rows of a grid — the same reason
  `--compact` is decided by `menu()` in build.mjs. Sixteen is `ISN_BRANCHES.length`, so a
  seventeenth branche wants a ninth row here; the panel grows by one 33.6px row and nothing else
  moves, and the cap above is what catches it if the window is short.

  `.nav-links` in the selector, for the same reason the groups' repack carries it, and here it
  is load-bearing rather than tidy: `.nav-panel .nav-drop-list` at (0,2,0) takes the stack to
  one column, and eight explicit rows plus `grid-auto-flow: column` in one column would put the
  other eight into an *implicit* second column — reintroducing two columns in a 390px stack,
  from a rule whose whole purpose is the bar.
*/
body.isn .nav-links .isn-nav-branches {
  grid-template-rows: repeat(8, auto);
  grid-auto-flow: column;
}

/*
  Fifteen of the sixteen rows, which are `<span>` and not `<a>`.

  Only `/nl/branche/bouw-installatie/` exists. The other fifteen are in the panel because the
  reader is picking what kind of company they run and a list of one answers nobody, and they are
  not anchors because `emit()`'s link gate in build.mjs checks an href's *shape* and never its
  target: fifteen `<a>` elements would pass every gate and ship fifteen 404s.
  content/isn-branche-pages.js argues the choice; this rule is what makes it read as deliberate
  instead of as a list with fifteen broken rows in it.

  Every value here is `.nav-drop-list a`'s, restated because a class selector cannot reach a
  `<span>` — and one value is not: the colour. `body.isn .nav-drop-list a` is ink and these are
  muted. That is the whole affordance. Same face, same size, same box, same 33.6px row, one step
  quieter, and no hover plane, because base's `a:hover` cannot match them either.

  `font-family` and `font-weight` are *not* restated, and that is measured rather than assumed:
  with only the six declarations below applied, all fifteen spans compute `Figtree` at
  `font-weight: 400`, which is what `.nav-drop-list a` asks for. They inherit it from the page —
  the anchor needs the two because it is overriding `.nav-links a`, and a span has nothing to
  override.

  `white-space` is restated for exactly that reason, and it is the one that bit. `.nav-links a`
  sets `nowrap` on every link in the bar and a `<span>` is not one, so without this line these
  names wrap where the anchor beside them cannot. The first measurement of this panel was taken
  before this rule existed: at the inherited 16.5px with wrapping allowed, `Sport, cultuur &
  verenigingen` reported 115.7px of ink — the width of its longest *line* — and came out tenth
  widest of the sixteen instead of first. The width argued above is measured after.
*/
.isn-nav-soon {
  display: block;
  padding: 7px 9px;
  font-size: 14px;
  line-height: 1.4;
  white-space: nowrap;
  color: var(--i-muted);
}

/*
  In the burger panel the same fifteen rows take the stack's row, not the bar's.

  `.nav-panel a` is 13px 0 at body size in 500 with a hairline under it, `.nav-panel
  .nav-drop-list a` trims the padding to 12px, and `body.isn .nav-panel a` puts the hairline in
  `--i-line` and the text in ink. None of those four selectors reaches a `<span>`, so without
  this the fifteen would keep the bar's 14px and its 7px/9px in a stack of 16.5px rows with
  rules between them — the one place where "same typography as the link beside it" means a
  different set of numbers.

  `nowrap` is dropped rather than inherited-and-overridden: the stack is one column of the
  viewport's width, `Sport, cultuur & verenigingen` needs 182.8px of it at the bar's size and
  more at this one, and a 390px phone minus the gutter and the panel's 16px indent has the room
  — but a longer name added later has nowhere to overflow to, and wrapping is the right answer
  in a stack where it was the wrong one in a fixed-width panel.
*/
body.isn .nav-panel .isn-nav-soon {
  padding: 12px 0;
  border-bottom: 1px solid var(--i-line);
  font-size: var(--i-body-size);
  font-weight: 500;
  white-space: normal;
}

/*
  The one sentence in either panel, under the list and above the `Alle branches` footer.

  It is there because fifteen quiet rows are a fact and not an explanation: a reader who tries
  `Horeca` and gets nothing is owed a reason in the place they tried it. Mono-small would have
  made it a second `.nav-drop-all`; this is a sentence, so it takes the body face at the page's
  smallest size, muted, which is `.isn-micro` without its 600 weight — small print reads as
  small print here, and a 12px line at 600 in a panel of 400s reads as a label.

  The 9px indent is `.nav-drop-list a`'s own padding, so the sentence starts on the same left
  edge as the sixteen names above it, exactly as `.isn-nav-group-title` does next door. In the
  burger panel that indent goes: `.nav-panel .nav-drop-menu` already pads the whole menu 16px
  in, and rows there sit flush against it.
*/
.isn-nav-note {
  margin: 10px 9px 0;
  font-size: var(--i-micro);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

body.isn .nav-panel .isn-nav-note {
  margin: 12px 0 0;
}

/* ── 01 · Hero ────────────────────────────────────────────────────────────── */

/*
  ── The height budget, and why these are vh and not vw ───────────────────────

  One thing has to be true on a 1440 × 900 *window*: the bar, the headline, the scan card *and*
  the three-step strip are all visible without scrolling.

  A window is not a viewport, and that distinction is the whole of this pass. A 900px-tall
  Chrome window on macOS gives the page around 780px once the tab strip, the address bar and a
  bookmarks bar are out of it — so a fold measured at `innerHeight: 900` is a fold nobody
  actually has, and a strip whose last line lands at 789 is a strip the reader sees the top half
  of. Measured that way and then measured again in the window, the first pass fitted and the
  second did not.

  So the budget is ~780, and it is spent like this at 1440 — bar 69, this padding 17, the taller
  of the two columns 512, the row gap 16, the strip 122, this padding 19. 755, which leaves the
  strip's last sentence ending near 720 with the fold at 780 and at 900 alike.

  The strip is 122 and not the 109 it reads as, because it now carries its own bottom padding —
  see `.isn-hero-steps`. That padding is inside the pin and this one is outside it, which is the
  distinction the two paragraphs below turn on.

  ── The budget is now a floor, and the fold is pinned ────────────────────────

  A hero that fitted inside 900 was the whole of the earlier pass, and it left 158px of the
  *next* section — `INKOOPCATEGORIEËN` and the rail's two arrows — above the fold. Fitting is not
  the same as ending: the first screen has to end at the strip.

  So `.isn-hero-grid` below carries a `min-height` that resolves to exactly the space between
  the bar and the fold, its first row takes what is left over, and the strip — the second row —
  sits on the bottom edge of that box. At 1440 × 900 the strip's last pixel is the fold's, and
  `.isn-cats` starts 19px below it — this padding, which is the next paragraph.

  This padding stays, and lands *below* the fold at 900. That is deliberate twice over: it is the
  space the strip needs when the window is too short for the pin to bind, and above the fold it
  is the margin that keeps a sub-pixel rounding from showing a hairline of the next section,
  which is the failure this whole block is here to prevent. The `min-height` subtracts the top
  padding and not this one, which is what puts the strip's own bottom edge — not the section's —
  on the fold.

  The 755 above is therefore no longer the arithmetic of the fold; it is the floor under it. On a
  window shorter than 755 + 19 the pin cannot help — a `min-height` only ever adds space — and
  the strip crosses the fold by whatever is missing, which is why the numbers in this block are
  still the ones that matter and are still spent this carefully. Measured, that floor binds well
  below the windows anyone has: at 1440 × 700 the card column gives up 112px of its own and the
  strip still lands exactly on the fold.


  Where the 72px came from: 26 out of this padding, the row gap and the strip's own top space,
  and 46 out of the card — its padding, the rhythm between its blocks, 2px off each chip's
  vertical padding and 1px off each grid gap. No type got smaller anywhere; every number in this
  pass is space between things.

  The two paddings are `vh`, which is the whole point: the budget is the *window's* height, so
  a shorter window has to buy the same four things with fewer pixels, and a rule in `vw` cannot
  see that — it reads a 1440 × 800 laptop as identical to a 1440 × 1117 desktop and overruns the
  first by exactly what it wasted on the second. At 800 these two come to 32 rather than 36.

  Floors 14/16 rather than lower: below that the bar's hairline reads as the top edge of the
  eyebrow. This was 40/44 in `vw` two passes ago, then 18/20 in `vh`.
*/
.isn-hero {
  /* Named because the fold rule on `.isn-hero-grid` has to subtract this exact value, and a
     second copy of the clamp would drift the moment either is tuned. */
  --hero-pad-t: clamp(14px, 1.9vh, 24px);

  /* Named one pass later, for a stronger version of the same reason. `.isn-hero--scan`'s fold
     rule has to make its hero *end* at the bottom edge of the viewport rather than merely reach
     down to it, so it subtracts both paddings from the height it claims. The home hero's rule
     below subtracts `--hero-pad-t` alone and therefore overruns the fold by this value; that is
     its bottom row's arrangement and deliberately not changed here. */
  --hero-pad-b: clamp(16px, 2.1vh, 26px);

  padding: var(--hero-pad-t) 0 var(--hero-pad-b);
}

/*
  ── The container is `.isn-shell`, the same one every section has ────────────

  The alignment is not solved here any more — it is solved once, by the rule `.isn-shell` and
  `body.isn .nav` share at the top of this file, and this row is one of its users. So the
  wordmark, the eyebrow, the headline, the lead, the claims, the strip's first column and every
  section headline below the fold are all on one x, and there is no second expression of the
  page's left edge to keep in step with the first.

  What is left here is the split, and the two `minmax(0, …)`.

  ── The split ────────────────────────────────────────────────────────────────

  A 45–48 / 52–55 split and a gap cannot all three hold: they sum to 97–103% before the gap,
  which is 3.3% at this width. So the card track takes 47vw and the copy takes what is left,
  which at 1440 is 676px of card and 608px of copy — 45.8% / 50.9% with 3.3% between them.
  Both stated bands are within a percent of that, and the one that gives is the card's.

  `47vw` rather than a fixed 520px because the container is nearly the window: a fixed track
  would pour every pixel a wider screen adds into the text column, and the headline is sized off
  that column (`--i-hero` is `12.4cqw`). Floor 520 — the brief's own minimum, and the width at
  which the card still holds three chip columns. Ceiling 700, past which a form stops reading as
  a card; beyond that the copy column keeps the growth and the shell's own cap stops both.

  `minmax(0, …)` on both because the right one holds a `<select>`, whose intrinsic minimum
  would otherwise out-vote the track and take the width out of the copy column.

  ── Two gaps, because they are two different measurements ─────────────────────

  `column-gap` is the 3.3% the split above is calculated against, in `vw`. `row-gap` is the
  space above the strip's hairline, and it is `vh` for the same reason the hero's padding is:
  it comes out of the height budget, and it is the one gap in the fold that a shorter window
  should be allowed to close a little. One `gap` for both made the strip's hairline sit 43px
  down on every screen, which is 21px of the 900px fold spent on nothing.

  16px at 1440 rather than the 22 it was, which is the brief's "direct aansluiten onder de hero":
  the hairline belongs to the strip below it, not to the row above it, and the 24px of space the
  strip keeps on its own side is what says so.

  ── The two rows, and where the leftover height goes ─────────────────────────

  Both rows were implicit until the fold had to *end* at the strip rather than merely contain it.
  Now the box is as tall as the space under the bar and the two rows divide it: `1fr` takes
  everything left over after the strip has measured itself, and the strip is `auto`, so it sits
  on the bottom edge whatever the window's height is.

  The leftover — 134px at 1440 × 900, against a copy column of 488 and a strip of 138 — goes into
  that first row and `align-items: center` splits it evenly above and below the two columns,
  which is the reason that line is not `start`. (It reads as leftover only from the copy column's
  side: `.isn-cgrid` stretches, so the cards take the whole row and it is they who give the space
  back when the strip grows.) Poured
  into the `row-gap` instead it would read as a hole between the cards and the hairline; spent
  above the headline alone it would push the eyebrow into the middle of the screen. Half and half
  is the only distribution that leaves the composition looking chosen at 900 *and* at 1117.

  `minmax(0, 1fr)` and not `1fr`: the copy column's headline and the card grid both have an
  intrinsic minimum, and `1fr` is `minmax(auto, 1fr)` — it would refuse to go below their
  content height on a short window and push the strip down instead of letting it overrun,
  which is the one case where overrunning is the honest outcome.

  `svh` with a `vh` line under it, in that order: the fallback runs where `svh` is unknown, and
  on a phone toolbar-collapse it is `svh` that keeps the strip from being hidden behind chrome
  that is about to appear. Neither binds on the stacked layout — below 1200 the content is
  always taller than the window — so this rule is a desktop rule that costs nothing elsewhere.
*/
.isn-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(520px, 47vw, 700px));
  grid-template-rows: minmax(0, 1fr) auto;
  column-gap: clamp(28px, 3vw, 44px);
  row-gap: clamp(14px, 1.8vh, 22px);
  align-items: center;
}

/*
  The pin itself, on the one hero that has a second row.

  Selected by the strip's presence and not by a page class, because the strip is the whole
  reason: this rule exists to pin `Kennismaking / Analyse / Voorstel` to the bottom of the first
  screen, and a hero with no such row has nothing to pin.

  /nl/besparingsscan/'s hero is the same `.isn-hero-grid` and has never matched this selector — it
  has three children (copy, card, category row) and no steps strip. That used to be the end of the
  story, because that hero was deliberately *shorter* than the fold. It is not any more: it owns the
  whole first viewport now, by a rule of its own at `.isn-hero--scan .isn-hero-grid` that subtracts
  both hero paddings rather than just the top one. So there are two fold rules in this file with
  different arithmetic, and this selector is the reason they can coexist: widening it to cover both
  heroes would have changed the home hero's geometry, which no instruction asked for.

  Where `:has()` is missing the rule drops and the hero is what it was before this pass: 742px
  that fits inside the fold without ending at it. A worse first screen, not a broken one.
*/
.isn-hero-grid:has(> .isn-hero-steps) {
  min-height: calc(100vh - var(--i-chrome) - var(--hero-pad-t));
  min-height: calc(100svh - var(--i-chrome) - var(--hero-pad-t));
}

/*
  The query container `--i-hero` is a fraction of. `min-width: 0` for the same reason the
  tracks carry it, and `container-type: inline-size` is what makes `cqw` resolvable inside it —
  without it, `cqw` silently falls back to the viewport and the headline is 89px again.

  No `max-width`. There was a 720px one here, as an ultra-wide guard while this row was the only
  thing on the page running on the gutter; the shell's cap replaced it, and the copy column now
  tops out at ~840px on the widest screen the page ever centres at. `--i-lead`'s own 46ch is
  what keeps the lead readable inside that, which is where a measure belongs.
*/
.isn-hero-copy {
  min-width: 0;
  container-type: inline-size;
}

/*
  Heavy, and not thick. 800 is Figtree's top requested weight and the site's own display
  weight; the density comes from the tracking and a 0.95 leading, which closes the gap
  between the two lines without touching the letterforms. Both numbers are base.css's `h1`,
  which is the point — this is the site's headline at the site's setting, and only its size
  floor is the brief's.

  900 here was synthesised at one point — there is no Figtree 900 in the stylesheet request —
  and a synthesised weight at this size is exactly the cartoon-thick the brief refuses.
*/
.isn-hero-h {
  margin: 0;
  font-size: var(--i-hero);
  font-weight: 800;
  letter-spacing: var(--i-track-hero);
  line-height: var(--i-lead-hero);
}

/*
  Two fixed lines. The break is the markup's, not the container's — a headline this size that
  rewraps at some intermediate width lands the break inside one of the two halves, and then
  it reads as broken rather than as built. `--i-hero` above is what makes that safe at every
  width; this rule only has to stop the two spans sharing a line.

  Nothing else. Two accents have now come off this headline: a `--blue` line modifier on
  `kopen samen in.`, removed by the brief by name, and the drawn yellow underline under
  `samen in.`, removed on request — with it went `.isn-hero-word`, the `inline-block` span
  that existed to measure the stroke against its own text, and `.isn-underline`. So the
  largest type on the site is one weight in one colour, and the colour on this screen is the
  blue CTA and the category cards, which are both things you can act on.
*/
.isn-hero-line {
  display: block;
}

.isn-hero-lead {
  max-width: 46ch;
  margin: clamp(24px, 2.4vw, 34px) 0 0;
  font-size: var(--i-lead);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/* The hand-drawn arrow and its one line, pointing back up at the thing above it. `.isn-compare-note`
   used to share this rule and no longer does: it is a `.isn-hero-note` now, blue and italic like
   the other two cues on the site, and section 06 argues why. What is left here is the final CTA's
   own note, which is muted small type rather than an annotation. */
.isn-final-note {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 18px 0 0;
  font-size: var(--i-small);
  font-weight: 600;
  color: var(--i-muted);
}

.isn-hand-arrow {
  flex: none;
  width: 46px;
  height: 32px;
  color: var(--i-yellow);
  /* Leaning, and rotated a little further: a symmetric curve reads as an icon. */
  transform: rotate(-8deg) scaleX(-1);
}

.isn-hand-arrow--up {
  color: var(--i-blue);
  transform: rotate(6deg);
}

.isn-final-note > span {
  max-width: 34ch;
  padding-top: 6px;
}

/* ── The three claims ─────────────────────────────────────────────────────── */

/*
  Three claims in one row, each an object with a line under it rather than a bullet with a
  tick in front of it. The brief asks for larger black icons and for the row to be
  horizontal, so: three equal columns, the drawing above its own label. That is also what
  lets a claim of thirty characters take two lines without moving the other two.

  Icon-beside-label was the other reading of "horizontaal" and does not fit. The copy column
  measures ~500px at the 1240px shell, so a third of it is ~160px, and 38px of drawing plus
  its gap leaves ~110px for a thirty-character sentence.

  The category and branche heroes are why these rules still exist unchanged: their claims are the
  long kind — `Gratis en vrijblijvend`, `Geen verplichtingen`, `Binnen enkele minuten inzicht` at
  133, 130 and 192px set bold — and equal tracks are what keeps three of those on one row. The
  homepage's three are half that long and want the opposite treatment; it takes `--row` below.

  /nl/besparingsscan/ used to be the third page here and is not any more: its three promises are a
  title over a subline now, which is a different object with different measurements, and it has
  `.isn-hero-perks` in the scan-hero block below. Nothing was generalised to cover both — see the
  argument at `isnScanPerk` in build.mjs for why a shared component would have printed half of one.
*/
.isn-hero-claims {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 24px);
  margin: clamp(26px, 2.6vw, 36px) 0 0;
  padding: 0;
  list-style: none;
}

.isn-hero-claim {
  display: grid;
  gap: 10px;
  font-size: var(--i-small);
  font-weight: 700;
  line-height: 1.3;
}

/*
  ── `--row`: the homepage's compact value row ────────────────────────────────

  One horizontal band of proof directly above the two actions, and a modifier rather than a
  rewrite of the rules above, because the two pages that print this component disagree about
  what the row should do and only one of them is in the brief.

  ── Content-sized, not column-sized ─────────────────────────────────────────

  Equal tracks divide *the column*, not the copy. On the homepage that put the three claims at
  x≈56, 266 and 476 with ~100px of air inside each track, and the row read as three separate
  content blocks spread across the hero instead of one line of proof. Worse, a 160px track is
  narrower than `Samen sterker inkopen.` set bold, so the third claim broke onto two lines
  while the other two kept one, and the row was two lines tall and visibly uneven.

  Flex fixes both at once, because flex items are content-sized: each claim is exactly as wide
  as its own sentence and the gap is a real gap rather than the remainder of a division.
  `flex-start`, never `space-between` — `space-between` is the same defect by a different
  mechanism, pushing the outer two to the column's edges and making the interval depend on the
  column width instead of on the design.

  No `max-width` here and there should not be one: capping the row would reintroduce wrapping
  the moment a sentence got longer, and a claim that no longer fits should wrap as a whole
  item, which is what `flex-wrap` does.

  ── One flat 32px, not a clamp ──────────────────────────────────────────────

  The interval was `clamp(26px, 3.8vw, 64px)` — 55px at 1440 — under an earlier brief that
  named a 44–64 band and asked for a taper through tablet. The follow-up brief replaces the
  band with a single number, `gap: 32px`, on the ground that 55px of air still read as three
  separate blocks rather than one trust row. So the clamp goes: at three sentences this short
  the gap is the only lever, and a viewport-scaled lever is what made the interval a function
  of the window instead of a decision.

  Flat is also why no tablet rule is needed any more. 32px is already inside what the taper
  reached at 1024, so the row simply keeps its spacing down to the phone block, which sets its
  own 26 — tighter still, and therefore left alone.

  Measured at 1440 with `Altijd gratis. / Bespaar tot 25%. / Samen sterker inkopen.`: the three
  claims start at x=56, 167 and 307.5, both gaps exactly 32.0, and the row's ink runs 56 → 457.9
  — 401.9px, against a lead that breaks at 607.7 and CTAs whose left edge is the same 56. Same
  at 1200 and, at the 51.2px gutter, at 1024. Vertical spacing is untouched: the 18px is the
  wrap gap, the mark boxes are still 34px, and the row's own height is 65.2px at every desktop
  width, the same as it was before this change.

  ── The vertical build survives at every width ──────────────────────────────

  Each claim keeps its mark above its own label, phones included; see the 430px block for what
  that replaced and why the wrap is enough there.
*/
.isn-hero-claims--row {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: start;
  gap: 18px 32px;
  margin: clamp(36px, 3vw, 44px) 0 0;
}

/*
  `white-space: nowrap` is the whole of the brief's "voorkom dat `Samen sterker inkopen.` op
  twee regels afbreekt". It is safe against overflow *because* of `flex-wrap` above: an item
  that cannot fit the rest of the line moves to the next line whole rather than being
  squeezed, and the widest of the three needs ~165px against 280 of usable width at 320px —
  measured, not assumed.

  13px is the brief's 12–14 between mark and label. `justify-items: start` so the mark sits on
  the label's left edge: without it the grid stretches its items, and a stretched mark box
  would place its own contents against a full-width track rather than against the label.
*/
.isn-hero-claims--row .isn-hero-claim {
  justify-items: start;
  gap: 13px;
  white-space: nowrap;
}

/*
  Black edge, coloured plane. `.isn-ic` takes `color` from `--a` and `--ic-soft` from
  `--a-soft`, so overriding `color` alone is the whole of it: the stroked outline becomes ink
  and the filled interior keeps the category's accent. "Zwart als hoofdiconkleur, kleine
  merkaccenten", out of the icon set that already exists.
*/
.isn-hero-claim .isn-ic {
  color: var(--i-ink);
}

/*
  The compact row's mark box, both kinds of mark in one pair of declarations — the drawn ones
  and the supplied files are the same object at this point and the brief sizes them together.

  34 and not 38 because the brief asks for 32–36 across the three, and it is the *box* that is
  set, never the ink: `icons.py` writes each supplied file as its ink at 80% of a canvas 4x
  this number, so one number here moves all three marks at once. That is what makes "optisch
  dezelfde grootte" checkable rather than per-file — and it needed checking, because the
  arithmetic and the eye disagreed. Rendered ink, measured on the page at 1440: 28px of tag,
  27 of chart, 26 of circle. A monotone one-pixel ladder with the densest mark at the bottom
  of it, which is the side of "mag NIET groter lijken" to err on. `icons.py`'s `CLAIM` docstring
  carries the rest of that argument, including the first cut that put the circle at 24 and was
  plainly wrong at three times zoom.

  Not on `.isn-ic--lg`, which four other components share, and not on `.isn-hero-claim`, which
  `/nl/besparingsscan/` also renders. Both selectors here are (0,2,1), above `.isn-ic--lg >
  svg` at (0,1,1) and `.isn-claim-img` at (0,1,0), so neither depends on source order.
*/
.isn-hero-claims--row .isn-claim-img,
.isn-hero-claims--row .isn-ic > svg {
  width: 34px;
  height: 34px;
}

/*
  The supplied mark, in the same zone the drawing occupies — see `isnClaimMark()` in build.mjs
  for which of the three claims gets which. 38px matches `.isn-ic--lg`, which is what a claim
  row without `--row` sizes its drawings at; the compact row overrides both to 34 together.

  One rule for the whole set and no per-file exception, because the sizing is not done here:
  icons.py writes each file as its ink centred on a square canvas 4x the box, so the mark's
  optical size and its centreline are already correct inside the PNG. That is what lets a
  file arrive without a stylesheet edit, and it is why `object-fit: contain` is insurance
  rather than layout — a square file in a square box has nothing to contain.

  No `display` here, and that is worth one line rather than a declaration: `.isn-hero-claim`
  is a grid, so both kinds of mark are grid items and both are blockified — the drawing's
  `inline-flex` becomes `flex`, the image's `inline` becomes `block`. Neither sits in a line
  box, so neither carries the strut's descender under it, and the labels below them line up
  across the row without anything being told to. Measured on the compact row: all three marks
  34px tall, all three labels at the same `y`.

  No background, no plate, no radius. The files are cut out on transparency and the hero is
  white, so the mark is ink on the page — the same treatment the drawings get.
*/
.isn-claim-img {
  width: 38px;
  height: 38px;
  object-fit: contain;
}

/* ── The two actions ──────────────────────────────────────────────────────── */

/*
  `Plan kennismaking →` and `Doe gratis besparingsscan`, under the claims.

  A flex row and not a grid: the two labels are 21 and 25 characters, so two equal tracks would
  set the shorter button 40px wider than its text needs and the pair would read as a segmented
  control rather than as a primary and a secondary. `flex-wrap` because there is a band around
  1240px where the copy column is ~500px and the two buttons want 510 — they stack there rather
  than shrinking, and `.isn-btn`'s `white-space: nowrap` means shrinking is not on offer anyway.

  ── The margin, and where it is paid from ────────────────────────────────────

  28px at 1440 and 24 at 1200, which is the brief's 24–30 band, against the claims' own 43 above
  them. The asymmetry is the grouping and it is the point of both numbers: the three claims belong
  to the lead, and these two are a block of their own under the set. Equal intervals would make
  five loose rows out of two groups, and the previous pair — 36 above, 22 below — read as one long
  ladder because 36 is not far enough from 22 to say "different kind of break".

  Both grew (36→43 and 22→28) and the fold budget is fixed, so the 13px comes from the claims row
  itself, which lost a line: it was two lines tall at 1240 because `Samen sterker inkopen.` broke
  inside a 160px grid track, and content-sized flex items plus `nowrap` make it one. The mark also
  came down from 38px to 34. Net, the copy column is *shorter* than before these two margins grew
  — measured at 1440x900, not budgeted. The copy column was never the tall one anyway; see
  `.isn-cgrid`, which takes a height budget of its own rather than coming to whatever four cards
  happen to add up to.
*/
.isn-hero-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: clamp(24px, 2vw, 28px) 0 0;
}

/*
  How the process runs, in three steps, along the whole foot of the hero.

  This is `.hero-steps` from apps/landing-page/styles/home.css, ported: the grid whose rows are
  declared once and taken by each item with `subgrid`, the column gap the hairlines float in,
  the dividers as pseudo-elements, and one rule above the set. That file carries the full
  argument; the three parts worth repeating are the ones a later simplification would remove.

  ── Why the rows are declared on the list and not on the items ───────────────

  Three columns each holding a name and a sentence produces a wobble the moment one name wraps
  and the others do not: between 720 and 1200px a column is ~250px wide and
  `Onderhandelen & besparen` takes two lines while `Intakegesprek` takes one, so its sentence
  would start a line lower than its neighbours' and the strip would read as three loose blocks
  instead of one row. The rows live on the `<ol>` and each `<li>` takes them with `subgrid`, so
  all three names sit in one band and all three sentences start on the same baseline whatever
  the names do. Where subgrid is missing the items fall back to their own rows and the wobble
  returns — that is the layout without this rule, not a broken one.

  ── Why the dividers are pseudo-elements ────────────────────────────────────

  A `border-left` with padding beside it would make the first column wider than the other two
  by exactly that padding and shift the second and third off the line the eye reads down.
  Centred in the gutter instead, all three keep the full track width and the first stays flush
  with the headline above it.

  ── What is not ported ──────────────────────────────────────────────────────

  The `margin-top`, because there is nothing to set it against: the strip is a row of
  `.isn-hero-grid`, so the grid's own `row-gap` — 16px at 1440 — is the space above the rule, and
  the brief asks for little space there.

  ── The space below the rule, and which edge it is measured to ───────────────

  It is what makes the hairline read as the top of a block rather than as the bottom of what
  precedes it. The ported 32 became 24 at one point, because eight of them were the cheapest
  eight pixels in the fold; 24 is what the brief then asked to be re-measured, and it turned out
  to fail the band it asks for — 22–28px between the rule and the title/number row — from
  underneath.

  The reason is that the row has two top edges. `.isn-hero-step-n`'s negative margin lifts the
  disc 3.5px above the title's line box so it sits optically centred on the first line, so a
  padding of 24 put the title at 24 and the *row* at 20.5. Padding that satisfies both readings
  is therefore 25.5–28, and this clamp is that intersection: 28 from ~903px of window up, 26 at
  the floor. Measured at 1440 × 900 — rule to disc 24.4, rule to title 27.9; at 1366 × 768 —
  21.8 and 26.0.

  ── The bottom padding is what makes the pin read as an ending ────────────────

  The pin above puts this element's bottom edge on the fold. With `padding: 24px 0 0` that edge
  was also the last line box's, so at 1440 × 900 the sentences ended 2.8px from the screen and
  the two dividers ran into it — a strip that satisfies the fold arithmetic and still reads as a
  strip the window cut in half, which is the exact impression the pin exists to remove. The last
  line's *ink* stopped around 890, so this is a fix for what the edge says, not for clipping.

  So the element keeps a floor of its own, and the dividers stop with the items rather than at
  the box. It was 12.6px at 900; it is 25.2 now, and the number comes out of two clauses of the
  brief that measure the same distance and disagree by a hair. The bottom of the steps is to end
  24–36px above the bottom of the viewport, and 18–24px is to stay under each step's body copy
  before the viewport ends — and since the pin makes this box's bottom edge the viewport's, both
  are this one padding. The single value in both bands is 24, which is what a 900px window gets:
  measured, the third sentence's line box ends 24.6px above the fold.

  Not 24 flat, because the padding and the distance are not the same number: `--i-chrome` is 1.1px
  short of the real bar-plus-header, so the pinned box overhangs the fold by that much at every
  height. The cap is 26 and 2.8vh reaches 25.2 at 900, which is the 24.6 above. Fixing the
  variable is a change to the two elements it measures, and both are out of bounds.

  `vh` and not a fixed number, for the reason the hero's two paddings are: on a short window this
  is the first space that should give. Floors at 18, which is the lower clause's floor and the
  point below which the sentence is against the edge again.

  Where the 15px both paddings gained is paid from: `.isn-cgrid` is `align-self: stretch` in the
  row that measures itself second, so the strip took it out of the cards — 305.8px each at
  1440 × 900, down from 313.9 — and nothing else on the screen moved. The next section still
  starts at 918.9, below the fold.

  It survives the 720px stack, where only `padding-top` is overridden: there the strip is the
  last thing in the hero, its bottom is not the viewport's, and 23.6px under the third step at
  390 × 844 is the same statement the pinned row makes, so there is nothing to reset.
*/
.isn-hero-steps {
  /* Both tracks of the hero grid, which is where the width comes from: the grid is on the page
     gutter with no shell, so column one starts on the wordmark's line and column three ends on
     the right gutter — the same edges the scan card has. */
  grid-column: 1 / -1;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto auto;
  column-gap: clamp(30px, 3.6vw, 56px);
  row-gap: 8px;
  margin: 0;
  padding: clamp(26px, 3.1vh, 28px) 0 clamp(18px, 2.8vh, 26px);
  border-top: 1px solid var(--i-line);
}

.isn-hero-step {
  position: relative;
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
}

/* Half the column gap: on the centre line of the gutter, whatever the gutter currently is. */
.isn-hero-step + .isn-hero-step::before {
  content: '';
  position: absolute;
  left: calc(-0.5 * clamp(30px, 3.6vw, 56px));
  top: 1px;
  bottom: 2px;
  width: 1px;
  background: var(--i-line);
}

/*
  The step name, with its number beside it, and the only two levels in the column.

  The loudest thing in the strip and the quietest thing in the hero, which is the whole of its
  job: findable at a glance by someone scanning for how this works, invisible to someone
  reading the headline. 800 and `--i-track-h3` because that is this page's setting at this size
  — the Library of Trade original is 600, which is what a title over a dark globe needs. Normal
  capitalisation, on request and by the site's own rule: labels here are uppercased by CSS, and
  this is a title now, not a label.

  A flex row rather than the disc floated into the text, so a name that wraps aligns its second
  line under its first instead of under the disc. `align-items: start` with the disc's own
  negative margin doing the vertical centring — see below — because centring the *row* would
  hang the disc halfway down a two-line name.

  Not an `<h2>` — see build.mjs.
*/
.isn-hero-step-title {
  --step-name: clamp(19px, 1.55vw, 23px);
  display: flex;
  align-items: start;
  gap: 12px;
  margin: 0;
  font-family: var(--i-display);
  font-size: var(--step-name);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
  color: var(--i-ink);
}

/*
  The number, as a filled disc in the accent — the one place colour enters the strip now that
  the three coloured ticks on the hairline are gone. 30px, white digit, no shadow.

  ── The negative margin ──────────────────────────────────────────────────────

  The disc is taller than the line box it sits beside: the name sets at up to 23px with a 1.12
  leading, so a line is ~26px and the disc is 32. Half the difference, lifted — the disc ends up
  optically centred on the *first* line of the name whether or not there is a second, which
  `align-items: center` on the row cannot do.

  The calc reads the name's size out of `--step-name` rather than out of `1em`, and that is not
  a preference: `em` in a declaration resolves against the element's *own* font-size, which this
  rule sets to 15px, so `1em` here would be 15 and the disc would sit 4px too high. The variable
  is declared on the paragraph, where the clamp already lives, and holds whatever the name is
  currently set at.

  `flex: none` because a flex item with a `width` is still shrinkable, and the third disc would
  lose a pixel to `Onderhandelen & besparen` at the narrow end of the desktop range.
*/
.isn-hero-step-n {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-top: calc((var(--i-lead-h3) * var(--step-name) - 32px) / 2);
  border-radius: 50%;
  background: var(--a);
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  color: #fff;
}

/*
  Body size and at 1.5 rather than the hero paragraph's 1.55: a third of the window sets these
  as two lines, which is short enough to read as a caption to the name above it and not as a
  second paragraph. In `--i-muted`, one step down from the names, so the strip has its own
  internal order without borrowing contrast from the hero.

  The `max-width` is ported from the Library of Trade rule and now earns its keep, which it did
  not while this strip was in the shell: the hero runs on the page gutter, so past ~1700px a
  third of the window is wider than a line of 16px type should be, and without the cap the three
  sentences straighten into three long single lines.
*/
.isn-hero-step-body {
  max-width: 30em;
  margin: 0;
  font-size: clamp(14.5px, 1.1vw, 16px);
  line-height: 1.5;
  color: var(--i-muted);
}

/* ── 02b · Het categorieraster, rechts in de hero ─────────────────────────── */

/*
  Four category cards where the scan card used to be, drawn from a pool of thirteen: separate
  companies with separate spend, bundled into one collective volume, and the discount that comes
  out of it. `isnCollectiveGrid()` in build.mjs carries the argument for what is in the markup and
  what is deliberately not; this file is the choreography.

  ── The whole animation is one attribute per phase ──────────────────────────

  `js/isn-cgrid.js` sets four attributes on one card and never touches a style, a class or a
  string. Everything below is the reaction:

    `data-grow` 0–3   how many of the four tiles are still hidden. Absent means all four —
                      the state build.mjs prints, so a card at rest is a complete company row.
    `data-state`      absent: the tiles, apart. `bundle`: merged into the collective panel.
                      `deal`: the same panel, transformed into the discount.
    `data-vol` 1–6    which state of the bundled amount shows. Absent means the total.
    `data-swap`       the card is faded out for a category change.

  Two consequences worth stating, because they are what keeps this readable at all. Every phase
  boundary is a single attribute write, so the sequencing inside a phase is expressed as
  `transition-delay` on the elements that move — the merge is one write and five staggered
  reactions, not five writes. And every state a card can rest in is reachable with the script
  removed, which is how the reduced-motion composition (bottom of this file) is a still of this
  same markup rather than a second design.

  ── Why `data-state` and not two panels ────────────────────────────────────

  The brief's most important transition is that the four tiles must *become* the surface: "de
  losse cards vormen uiteindelijk ÉÉN groter vlak … NIET: tiles verdwijnen → dan verschijnt er een
  random nieuw vlak. WEL: de losse tiles ZELF transformeren in het gezamenlijke vlak." Two things
  make that read, and both are geometry rather than effect.

  The tiles converge for real — `translateX` toward the row's centre by the gap plus a small
  overlap, so the outer two travel about 25 pixels at desktop size — and while they travel their
  shadows and borders fade out and their *inner* corners go square while the outer two keep their
  radius. Four rounded tiles with a gap become one slab with rounded ends. That is the melting the
  brief asks for, and it is four `border-radius` transitions.

  And `.isn-cc-plate` arrives *as* that slab: it is white, unshadowed, the width of the four
  merged tiles and the height of one, so the frame it appears in is the frame the tiles are
  already in. Only then does it grow to the panel, take its mint and light up. So there is no
  moment where something new appears somewhere else — the surface is born at the tiles' own
  outline. It is a separate element from `.isn-cc-panel` for the same reason: a background that
  grows takes its text with it, and the lockup and the amount have to sit still inside a surface
  that is arriving.
*/

.isn-cgrid {
  /*
    The gap is the only number here, and the square is derived.

    `container-type: size` plus `align-self: stretch` is what gets the cards their real height.
    The hero's first row is `minmax(0, 1fr)` of a section pinned to the fold, so its height is
    definite but not a number anyone can write down — at 1440×900 it is 638px, and it grows
    linearly with the viewport (roughly `100vh - 262px`). A `60vh` guess stood here and left 98
    pixels unused. Sizing the list from `min(100cqw, 100cqh)` is the inscribed square of whatever
    that row turns out to be, with no magic number and nothing left over.

    `align-self: stretch` is not optional: size containment on an element the parent centres has
    no height to contain, and the whole grid collapses. The ≤1200px override drops both together
    for the same reason, in the other direction — there the row is `auto`, so a size container
    would resolve to zero.

    ── Why the square is 88% of the row it could have ─────────────────────────

    "Maak het hele 2×2 visual ongeveer 10–15% kleiner … de hero moet het niet visueel opnemen
    tegen de headline." Filling the inscribed square exactly was the right answer while the
    question was "how much room is there"; the question now is how much of it the visual should
    take, and those are different. 0.88 of the row and a gap that roughly doubled turn 621px of
    inscribed square at 1440×900 into a 547px block of four 262px cards — the visual 12% smaller
    on both axes, the cards 14% smaller, and the whitespace between them the thing that grew.

    The gap is `vh` and not `cqw` for the same reason it always was: it is space in the hero's
    column, not a proportion of a card. Both numbers stay above the stacked breakpoint's floors,
    where the ≤1200px rules replace the height term and the square comes from `aspect-ratio`.

    And the 74px the square no longer uses goes to the *left*, not half to each side: `place-items`
    is `center end`. While the list filled its column its right edge was the page's right gridline
    by arithmetic, and the header's button and the strip's rule both end on that line. Centring the
    smaller square would have moved it 37px inside, which is close enough to the gridline to read as
    a miss rather than as a margin. Justified to the end it keeps the alignment it had, and all of
    the new space lands between the headline and the cards — which is the space the brief is asking
    for when it says the visual must not compete with the headline.
  */
  --cc-gap: clamp(11px, 2.4vh, 22px);
  container-type: size;
  align-self: stretch;
  display: grid;
  place-items: center end;
  min-width: 0;
}

.isn-cgrid-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: var(--cc-gap);
  width: calc(min(100cqw, 100cqh) * 0.88);
  height: calc(min(100cqw, 100cqh) * 0.88);
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
  One card, and the eleven numbers its interior is built from.

  `container-type: inline-size` and `cqw` throughout: a card is 312px in the hero at 1440×900,
  172px stacked on a phone, and every size inside it is a percentage of its own width rather than
  of the viewport. That is what lets one set of rules cover both without a second breakpoint —
  the tiles, the amount, the percentage and the padding all shrink together and keep their
  proportions, which is the thing that stops the small card looking like the big card with the
  text left large.

  The clamps' floors are legibility, not taste: 12px is the smallest a category name may set,
  24px the smallest the percentage may, and both are reached at about a 155px card.

  `--cc-slab-h` is the tile height *and* the plate's opening height, from one declaration, because
  the merge only works if those two are equal. Deriving it from `--cc-tile` rather than letting
  the tiles size to their content is what makes it a number the plate can also use.

  `--cc-vis` is 43% of the card and not 40 because the card itself got smaller: "de echte
  product-images mogen groter en prominenter". 43% of 262 is 113px against 40% of 306's 122 — three
  pixels of absolute object for eleven of card, so the object's *share* of the card goes up while
  the card comes down, which is what prominence is here. The low module keeps 97px, and the merged
  slab is 62 of it.
*/
.isn-cc {
  --cc-pad: clamp(10px, 5.4cqw, 18px);
  --cc-vis: clamp(46px, 43cqw, 128px);
  --visual-h: var(--cc-vis);
  --visual-pad: 0px;
  --cc-name: clamp(12px, 5.4cqw, 17px);
  --cc-tile: clamp(28px, 17.6cqw, 55px);
  --cc-slab-h: calc(var(--cc-tile) * 1.34);
  --cc-tgap: clamp(4px, 3.8cqw, 12px);
  --cc-lap: calc(var(--cc-tile) * 0.09);
  --cc-merge: calc(var(--cc-tgap) + var(--cc-lap));
  --cc-slab-w: calc(var(--cc-tile) * 4 - var(--cc-lap) * 3);
  --cc-tr: calc(var(--cc-tile) * 0.26);
  --cc-r: clamp(14px, 7.7cqw, 24px);
  --cc-pr: clamp(10px, 6cqw, 19px);
  container-type: inline-size;
  display: grid;
  min-width: 0;
  min-height: 0;
  transition: opacity 420ms var(--ease);
}

/*
  The category change: "de card fadet subtiel uit … nieuwe card fadet in". 420ms each way, within
  the 350–500ms the brief allows, and the module holds the card at zero for one fade before it
  clones the new markup in — so the swap itself is never on screen.
*/
.isn-cc[data-swap] {
  opacity: 0;
}

/*
  The card surface, and the two numbers in it this file does not take literally.

  Radius 20–24px, a soft shadow, one hairline, an off-white ground: all as specified. The border
  is `--i-line` (#e2e2de) rather than the brief's `rgba(20,20,20,0.08)`, which over white is about
  #ebebeb — two percent lighter and, more to the point, neutral grey where this page's hairline is
  warm. The scan card, the strip and the comparison table all draw the same line; a hero card that
  draws a cooler one is the only cool edge on the page.

  The shadow is `0 10px 28px rgb(10 10 10 / 5%)`, which is the brief's `rgba(0,0,0,0.05)` in this
  file's warm ink — the same substitution and the same reason. It replaces a 12/35/6%: one point of
  alpha and seven pixels of blur, which is the difference between a card sitting on the page and a
  card floating above it. "Geen zware schaduw, geen dashboard card look."

  The ground is warm: `--i-paper` at a third over white, about #fcfcfb. The page is white, so an
  off-white card is a surface the reader can see the edge of without the edge being drawn — which
  is what lets the shadow come down rather than up. It is the page's own paper colour diluted, not
  a new one, so nothing here introduces a second warm white to keep in step.

  `overflow: hidden` is load-bearing twice: it clips the mint light to the card, so nothing bleeds
  onto the page ground, and it keeps the plate inside the radius as it grows.
*/
.isn-cc-body {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(5px, 3cqw, 12px);
  min-height: 0;
  padding: var(--cc-pad);
  overflow: hidden;
  border: 1px solid var(--i-line);
  border-radius: var(--cc-r);
  background: color-mix(in srgb, var(--i-paper) 34%, var(--i-white));
  box-shadow: 0 10px 28px rgb(10 10 10 / 5%);
}

/*
  The half that never moves: "Het bovenste deel van de card verandert NIET tijdens de animatie."

  The object is 40% of the card's width, which is section 2's "ongeveer 35–40% van de card" and
  about as large as it can be with the name and the panel below it inside the fold's height
  budget. Nothing here is `aria-hidden`; the name is the card's accessible text.
*/
.isn-cc-top {
  display: grid;
  justify-items: center;
  gap: clamp(3px, 2.2cqw, 8px);
  min-width: 0;
}

.isn-cc-name {
  font-size: var(--cc-name);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: var(--i-track-h3);
  color: var(--i-ink);
  text-align: center;
}

/*
  The half that does all the work. Both states live in this one cell, so they occupy exactly the
  same space and nothing shifts vertically at any point in the sequence.
*/
.isn-cc-low {
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  align-items: center;
  min-height: 0;
}

.isn-cc-crowd {
  grid-area: 1 / 1;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: var(--cc-tgap);
}

/*
  One company. White, hairline, soft shadow, the premises large inside it, the amount bold
  underneath — section 4, and the amount is the only text in it.

  All four tiles are one height (`--cc-slab-h`) rather than the height of their contents, so the
  `+22` tile lines up with the three that hold an image and the plate has one number to open at.

  The transition list is the merge, and the order in it is the choreography. `transform` and
  `border-radius` run long (420ms) because they are the movement the reader is meant to follow.
  `box-shadow` and `border-color` are what makes that movement *visible at all*, and they are the
  one thing here that had to be measured rather than reasoned about: a tile is white on a white
  card, so its hairline and its 2px shadow are the only reason four boxes read as four boxes. Fade
  them at the start of the merge — which is what "de individuele tile shadows verdwijnen" invites —
  and the row turns into one white field before it has moved anywhere, so the reader sees tiles
  disappear and a surface appear, which is exactly the failure section 6 names. So they hold at
  full strength for 260ms of travel and then dissolve over 300ms, landing as the plate arrives.

  `background-color` is in the list for a reason that only appeared once the discount stopped being
  a mint panel. The four tiles stay merged behind the plate for the rest of the sequence, and they
  are white on a card that is off-white — so with nothing painted over them, the discount state had
  a 171 × 62px white slab behind its percentage. Invisible for a year under the mint; a background
  shape the moment the mint left. They go transparent 560ms into the merge, which is where the plate
  is already opaque: the one window in the sequence where a colour change on the tiles cannot be
  seen at all.
*/
.isn-cc-tile {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: calc(var(--cc-tile) * 0.03);
  width: var(--cc-tile);
  height: var(--cc-slab-h);
  padding: calc(var(--cc-tile) * 0.08) calc(var(--cc-tile) * 0.05);
  border: 1px solid var(--i-line);
  border-radius: var(--cc-tr);
  background: var(--i-white);
  box-shadow: 0 2px 6px rgb(10 10 10 / 6%);
  transition:
    opacity 300ms var(--ease),
    transform 420ms var(--ease),
    border-radius 420ms var(--ease),
    box-shadow 300ms var(--ease) 260ms,
    border-color 300ms var(--ease) 260ms,
    background-color 260ms var(--ease) 560ms;
}

.isn-cc-bm {
  width: calc(var(--cc-tile) * 0.8);
  height: auto;
  object-fit: contain;
  transition: opacity 300ms var(--ease);
}

.isn-cc-spend {
  font-size: clamp(8px, 4.1cqw, 12px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--i-ink);
  font-variant-numeric: tabular-nums;
  transition: opacity 220ms var(--ease);
}

/*
  The fourth tile. Same box, same radius, same height, one shade quieter — the overlapping-avatar
  pattern, not a data cell. `isnMoreTile()` in build.mjs argues why it is back.
*/
.isn-cc-tile--more {
  background: var(--i-grey);
  box-shadow: none;
}

.isn-cc-more {
  font-size: clamp(9px, 4.7cqw, 14px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--i-muted);
  font-variant-numeric: tabular-nums;
  transition: opacity 300ms var(--ease);
}

/*
  Sections 3 and 5: the four tiles arrive one at a time, and the arrival accelerates.

  Opacity, a 7px lift and a 0.92 scale — the three the brief names, and nothing else. The tiles
  keep their layout space while hidden, so the row never reflows: the first tile appears where it
  will still be standing four beats later, which is what lets the merge start from stable
  positions instead of from a row that has been growing sideways.

  The acceleration is not here. It is in `BEATS` in js/isn-cgrid.js, because it is timing.
*/
.isn-cc[data-grow='0'] .isn-cc-tile,
.isn-cc[data-grow='1'] .isn-cc-tile:nth-child(n + 2),
.isn-cc[data-grow='2'] .isn-cc-tile:nth-child(n + 3),
.isn-cc[data-grow='3'] .isn-cc-tile:nth-child(n + 4) {
  opacity: 0;
  transform: translateY(7px) scale(0.92);
}

/*
  THE MERGE — section 6, and the reason this stylesheet exists in this shape.

  Both `bundle` and `deal` match `[data-state]`, so the tiles stay merged for the whole of the
  rest of the sequence; they are behind the plate by then, and a card that resets to its company
  row does it while faded out.

  The travel: each tile moves toward the row's centre by `(1.5 - i) × (gap + overlap)`, which
  closes all three gaps and then keeps going by a ninth of a tile so they overlap slightly —
  "de horizontale afstand wordt kleiner, de overlap mag heel subtiel toenemen". At desktop size
  the outer tiles cross 25 pixels and the row narrows from 256 to 205.

  The radii: outer corners keep `--cc-tr`, every inner corner goes to 0. This is the single most
  effective line in the file — four rounded rectangles with gaps read as four cards, and one
  rounded-ended slab reads as one surface, and the transition between them is legible at 420ms.

  The order things leave in is the sentence the merge is making. The amounts go first (220ms, no
  delay): they are the *individual* fact, and the tiles are already closing by the time they are
  gone. The premises and the `+22` hold 240ms longer, so what the reader watches travelling is
  four buildings, and they fade under the arriving plate rather than before it. The hairlines and
  shadows dissolve last of all — see `.isn-cc-tile` for why that ordering is not a preference.
*/
.isn-cc[data-state] .isn-cc-tile {
  border-color: transparent;
  background-color: transparent;
  box-shadow: none;
}

.isn-cc[data-state] .isn-cc-tile:nth-child(1) {
  transform: translateX(calc(var(--cc-merge) * 1.5));
  border-radius: var(--cc-tr) 0 0 var(--cc-tr);
}

.isn-cc[data-state] .isn-cc-tile:nth-child(2) {
  transform: translateX(calc(var(--cc-merge) * 0.5));
  border-radius: 0;
}

.isn-cc[data-state] .isn-cc-tile:nth-child(3) {
  transform: translateX(calc(var(--cc-merge) * -0.5));
  border-radius: 0;
}

.isn-cc[data-state] .isn-cc-tile:nth-child(4) {
  transform: translateX(calc(var(--cc-merge) * -1.5));
  border-radius: 0 var(--cc-tr) var(--cc-tr) 0;
}

.isn-cc[data-state] .isn-cc-spend {
  opacity: 0;
}

.isn-cc[data-state] .isn-cc-bm,
.isn-cc[data-state] .isn-cc-more {
  opacity: 0;
  transition-delay: 240ms;
}

/*
  The panel: the surface the tiles become, and both things that surface says. It comes after
  `.isn-cc-crowd` in the same grid cell, so it paints above the tiles — that is the whole of why
  the tiles vanish *into* the collective and not before it.
*/
.isn-cc-panel {
  grid-area: 1 / 1;
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  width: 100%;
  height: 100%;
  min-height: 0;
  pointer-events: none;
}

/*
  The unlock, and the one element allowed outside the panel's box.

  A single diffuse radial field behind the plate: "zachte mint gloed achter het nieuwe vlak,
  diffuus radiaal licht … licht-groene bloom", and against the four things it must not be —
  "GEEN neon / GEEN particles / GEEN confetti / GEEN gaming-effect" — the defence is that it is
  one gradient with no hard stop and a 22% mint at its centre, scaled and faded rather than
  flashed. The reference the brief gives is "Apple / Stripe / high-end product reveal".

  ── Why this is an animation and not two transitions ────────────────────────

  "De groene light/glow mag absoluut NIET permanent zichtbaar zijn. Alleen tijdens het samensmelten
  en de unlock." It used to open on `bundle` and then *stay*: four fifths of it held all the way
  through the discount, on three resting cards at once, which is most of what made the grid read as
  green widgets. The light has to come and go inside one state, and a transition cannot do that —
  two values, one direction. So it is 760ms of keyframes on `bundle` alone.

  0 → 1 → 0 across them, peaking at 42%: invisible at 300ms when the tiles start travelling,
  brightest at about 620 as the plate takes its mint and grows into the panel, and gone by 1060 —
  before the total lands at 1180. Inside the brief's "ongeveer 500–800ms", and its order is the
  order: light while it merges, then the amount, in a card that is no longer lit.

  Nothing has to switch it off on the transformation, which is the point of putting it here. The
  animation belongs to `bundle`; when the attribute becomes `deal` the name is gone and the base
  rule's `opacity: 0` is simply what is true. There is no permanent state to remove, because none
  is ever reached. The card's `overflow: hidden` keeps this light on a surface rather than letting
  it become a green cloud on the page.
*/
.isn-cc-glow {
  grid-area: 1 / 1;
  place-self: center;
  width: 155%;
  height: 200%;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--i-green) 22%, transparent),
    color-mix(in srgb, var(--i-green) 8%, transparent) 55%,
    transparent 78%
  );
  opacity: 0;
  transform: scale(0.74);
}

.isn-cc[data-state='bundle'] .isn-cc-glow {
  animation: isn-cc-unlock 760ms var(--ease) 300ms 1 both;
}

@keyframes isn-cc-unlock {
  0% {
    opacity: 0;
    transform: scale(0.74);
  }

  42% {
    opacity: 1;
    transform: scale(1);
  }

  100% {
    opacity: 0;
    transform: scale(1.05);
  }
}

/*
  The surface itself, and the two-stage arrival that makes it the tiles rather than a new shape.

  Stage one, 240ms after a 320ms delay: it fades in white, unshadowed, `--cc-slab-w` ×
  `--cc-slab-h` — the exact box the four merged tiles occupy, with the tiles' own radius. There is
  nothing to see in this stage, and that is the point: it lands invisibly on top of them.

  The 320ms delay is the whole trick, and it was 0 in the first cut, which killed the merge dead.
  The tiles need 420ms to travel; a plate that starts fading in at 0 is opaque at 220 and the
  reader watches a white rectangle appear over four tiles that have moved a third of the way. So
  the plate waits until the travel is two thirds done and reaches full opacity as it completes —
  arriving as the slab the tiles have just become, which is the only reading of section 6 that is
  not a content swap.

  Stage two, 400ms after a 480ms delay: it grows to the full panel, its radius opens from the
  tile's to the panel's, and it takes its mint. `width` and `height` rather than a `scale`,
  because a scaled plate would carry a distorted radius and a smeared highlight into the state
  the reader looks at longest. It finishes at 880ms, which is section 13's "samensmelten
  ±700–900 ms" measured from the first pixel of movement to the last.

  Two backgrounds, and only the colour transitions. The white radial highlight is present in both
  states — "subtiel gradient / radiaal licht" is allowed and it is what stops a flat mint rectangle
  looking like a coloured div — so there is no popping layer at any point.

  ── Stage three: the surface leaves, and the number stays ───────────────────

  "De huidige grote mintgroene discount panels vullen bijna de hele onderkant van de card en zien
  er goedkoop uit … de card blijft grotendeels wit." The mint used to *deepen* on `deal` and stand
  there, which put a full-width green field on three of four cards at all times. So `deal` is now
  where the plate goes away: it contracts to 56% × 46% and fades out over 420ms, and what is left
  is `COLLECTIEVE KORTING` and the percentage on the card's own off-white.

  Contracting *and* fading rather than only fading, because the plate is the thing the tiles became
  and it has to be seen to leave rather than to blink: the mint collapses inward toward the number
  that is opening from the centre at the same moment. 56% is also the brief's own ceiling for a mint
  accent behind the percentage — "maximaal ongeveer 50–60% van de beschikbare breedte" — which is
  what this passes through on the way to nothing.

  The mint is therefore only ever on screen while the bundling is: the collective surface is mint,
  the collective discount is type. `--i-green-soft` and the inset white highlight both belong to the
  one state, and the second declaration below is the only place a deeper mint would have gone.
*/
.isn-cc-plate {
  grid-area: 1 / 1;
  position: relative;
  place-self: center;
  width: var(--cc-slab-w);
  height: var(--cc-slab-h);
  overflow: hidden;
  border-radius: var(--cc-tr);
  background-color: var(--i-white);
  background-image: radial-gradient(120% 96% at 50% 12%, rgb(255 255 255 / 72%), transparent 62%);
  box-shadow: none;
  opacity: 0;
  transition:
    opacity 240ms var(--ease) 320ms,
    width 400ms var(--ease) 480ms,
    height 400ms var(--ease) 480ms,
    border-radius 400ms var(--ease) 480ms,
    background-color 400ms var(--ease) 480ms,
    box-shadow 400ms var(--ease) 480ms;
}

.isn-cc[data-state] .isn-cc-plate {
  width: 100%;
  height: 100%;
  border-radius: var(--cc-pr);
  background-color: var(--i-green-soft);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 70%),
    inset 0 0 0 1px color-mix(in srgb, var(--i-green) 14%, transparent),
    0 10px 26px -14px color-mix(in srgb, var(--i-green) 45%, transparent);
  opacity: 1;
}

.isn-cc[data-state='deal'] .isn-cc-plate {
  width: 56%;
  height: 46%;
  box-shadow: none;
  opacity: 0;
  transition:
    width 420ms var(--ease),
    height 420ms var(--ease),
    border-radius 420ms var(--ease),
    opacity 380ms var(--ease) 60ms,
    box-shadow 300ms var(--ease);
}

/*
  The single pass of light: "een zachte highlight veegt één keer over het vlak."

  On the unlock and nowhere else, with the glow, because they are one event — "korte premium mint
  light reveal" is a bloom behind the surface and a sheen across it, and splitting them across two
  states was what gave the discount a second light of its own. It starts at 620ms rather than 140:
  the plate is only opaque at 560, and a sheen before that sweeps something the reader cannot see.

  Once, and `both` so it holds at zero afterwards rather than snapping back to the start. A sheen at
  105° and 62% white across 760ms is a reveal; the same thing on a loop is a shimmer, which is the
  gaming effect the brief refuses.
*/
.isn-cc-plate::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(105deg, transparent 32%, rgb(255 255 255 / 62%) 50%, transparent 68%);
  opacity: 0;
}

.isn-cc[data-state='bundle'] .isn-cc-plate::after {
  animation: isn-cc-sweep 760ms var(--ease) 620ms 1 both;
}

@keyframes isn-cc-sweep {
  0% {
    opacity: 0;
    transform: translateX(-64%);
  }

  28% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    transform: translateX(64%);
  }
}

/*
  Section 8: the ISN lockup, the bundled amount, `gebundeld`. Three elements in the middle of the
  surface and nothing else — "GEEN `gebundeld volume p.j.`".

  The three fade in staggered behind the plate's leading edge, and the order is the sentence: the
  amount first at 460ms because it is what the merge was adding up, then who bundled it at 760ms,
  then the one word that says what the number is at 880ms. On the way out they leave together and
  upward in 300ms, which is section 11's "€1,2 mln fadet/schuift subtiel weg".

  Not one of the three may be earlier than 460ms, and the reason is the plate rather than taste:
  the plate is only opaque at 560ms, so anything before that fades in over four buildings still
  travelling. The amount at 460 crosses the plate's own leading edge, which reads as the number
  arriving *with* the surface; at 300 it read as a number floating over the tiles.

  `position: relative` is not for placing anything. All four children of the panel sit in the same
  grid cell, and painting order decides which is on top: static boxes paint in one layer and
  positioned ones in a later layer, whatever the DOM order says. `.isn-cc-plate` has to be
  positioned so its `::after` can inset the sweep, so an opaque mint plate was painting over the
  logo, the amount and the percentage — all three present, at full opacity, invisible. Positioning
  these two puts them in the plate's layer, where source order is what settles it, and the glow
  stays behind because it is the one that stayed static. `.isn-cc-deal` carries the same line for
  the same reason.
*/
.isn-cc-bundle {
  grid-area: 1 / 1;
  position: relative;
  place-self: center;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: clamp(1px, 1.1cqw, 4px);
  min-width: 0;
  padding: 0 calc(var(--cc-pad) * 0.4);
}

.isn-cc-lockup,
.isn-cc-vol,
.isn-cc-cap {
  opacity: 0;
  transform: translateY(5px);
  transition:
    opacity 300ms var(--ease),
    transform 300ms var(--ease);
}

.isn-cc[data-state='bundle'] :is(.isn-cc-lockup, .isn-cc-vol, .isn-cc-cap) {
  opacity: 1;
  transform: none;
}

.isn-cc[data-state='bundle'] .isn-cc-vol {
  transition-delay: 460ms;
}

.isn-cc[data-state='bundle'] .isn-cc-lockup {
  transition-delay: 760ms;
}

.isn-cc[data-state='bundle'] .isn-cc-cap {
  transition-delay: 880ms;
}

.isn-cc[data-state='deal'] :is(.isn-cc-lockup, .isn-cc-vol, .isn-cc-cap) {
  opacity: 0;
  transform: translateY(-6px);
}

/*
  The supplied logo at card size: the multicolour symbol as a `background-image` from
  `assets/logo-mark-isn.png`, and `Inkoop Service Nederland` in black beside it, from
  `ISN_META.wordmark`. Nothing here recolours, redraws or re-proportions either half — the mark
  keeps its own `aspect-ratio: 512 / 464` and this rule sets a height, and the text is the same
  two lines in the same face as the header's lockup.

  Two lines rather than one, at this size, for the reason `isnWordmarkLines()` gives: 24
  characters on one line beside a 22px mark either sets too small to be a lockup or is wider than
  the panel it sits in. Stacked, the whole thing is about 90px and the panel is 278px.
*/
.isn-cc-lockup {
  display: flex;
  align-items: center;
  gap: clamp(3px, 1.9cqw, 6px);
  max-width: 100%;
}

.isn-cc-lockup .isn-mark {
  height: clamp(13px, 7cqw, 22px);
}

.isn-cc-wm {
  display: grid;
  font-family: var(--i-wordmark);
  font-size: clamp(6px, 2.95cqw, 9.5px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--i-ink);
  text-align: left;
  white-space: nowrap;
}

/*
  Below a 210px card the text half of the lockup comes off and the mark stays.

  A container query and not a media query, because the condition is the card's own width and
  nothing else: 210px is where `2.95cqw` reaches 6.2px, and 6px type is a grey smudge rather than
  a name. What is left is the supplied symbol at 15px, which is still the whole logo the brief
  requires — unredrawn, unrecoloured, in its own proportions — just without a second half that
  would only be legible to someone who already knew what it said. It is the same reduction the
  header makes at its own small end, and the panel it sits in has `Inkoop Service Nederland`
  spelled out at every width above this one.
*/
@container (max-width: 210px) {
  .isn-cc-wm {
    display: none;
  }
}

/*
  Section 9's amount, as six states stacked in one cell — so the number changes without the block
  it sits in changing size, and the widest state (`€1,2 mln`) sets the width for all of them.

  The last state is set larger because it is the one the reader is meant to leave with: "Het
  eindbedrag moet dominant zijn." The 200ms crossfade per step is deliberately short — a slow
  fade between two numbers is two numbers on screen at once, and the brief wants "een paar
  snelle, betekenisvolle sprongen".
*/
.isn-cc-vol {
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
}

.isn-cc-vt {
  grid-area: 1 / 1;
  font-family: var(--i-display);
  font-size: clamp(15px, 8.8cqw, 28px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.025em;
  color: var(--i-ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 200ms var(--ease);
}

.isn-cc-vt--total {
  font-size: clamp(18px, 11.2cqw, 36px);
}

.isn-cc:not([data-vol]) .isn-cc-vt--total,
.isn-cc[data-vol='1'] .isn-cc-vt:nth-child(1),
.isn-cc[data-vol='2'] .isn-cc-vt:nth-child(2),
.isn-cc[data-vol='3'] .isn-cc-vt:nth-child(3),
.isn-cc[data-vol='4'] .isn-cc-vt:nth-child(4),
.isn-cc[data-vol='5'] .isn-cc-vt:nth-child(5),
.isn-cc[data-vol='6'] .isn-cc-vt:nth-child(6) {
  opacity: 1;
}

.isn-cc-cap {
  font-size: clamp(7px, 3.7cqw, 11px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.03em;
  color: color-mix(in srgb, var(--i-green) 52%, #4a4a46);
}

/*
  The end state, and it is now two pieces of type on an off-white card and nothing else.

  `COLLECTIEVE KORTING` small above, `−18%` large below: "bijna puur typografisch … veel witruimte,
  groot percentage, geen overbodige achtergrondvormen, geen extra uitleg, geen subtitle onder het
  percentage. Het percentage moet de focus zijn." There is nothing to remove for that here — the
  markup was always these two elements — so what makes it true is the plate leaving on `deal` and
  the two numbers below.

  17cqw is 45px on a 262px card against 15.4's 40, and the extra five pixels are the mint field's:
  a percentage that had to hold its own against a full-width panel can afford to be the largest
  thing in the low module now that the panel is gone. The kicker takes the same treatment in the
  other direction — 3.4cqw, so it stays a small caption at 9px while the number grows.

  The percentage reveals from the centre rather than fading in flat: 0.94 → 1 over 340ms at 300ms,
  which is the mint contracting and the number opening in the same breath. The kicker follows at
  480ms, because a label that arrives with the number competes with it and a label that arrives
  after it reads as its caption.

  The gap between them is `1.6cqw` and not `1cqw` for the reason the whole state exists: on a mint
  panel the two read as one block because the panel bound them, and on white the space between them
  is the only thing that says which is the caption.
*/
.isn-cc-deal {
  grid-area: 1 / 1;
  /* In the plate's painting layer — see `.isn-cc-bundle` for what happens without this. */
  position: relative;
  place-self: center;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: clamp(2px, 1.6cqw, 6px);
  min-width: 0;
}

.isn-cc-kicker {
  font-size: clamp(6.5px, 3.4cqw, 10.5px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--i-green) 66%, #000);
  text-align: center;
  opacity: 0;
  transform: translateY(4px);
  transition:
    opacity 260ms var(--ease),
    transform 260ms var(--ease);
}

.isn-cc-pct {
  font-family: var(--i-display);
  font-size: clamp(24px, 17cqw, 52px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;
  color: color-mix(in srgb, var(--i-green) 88%, #000);
  font-variant-numeric: tabular-nums;
  opacity: 0;
  transform: scale(0.94);
  transition:
    opacity 300ms var(--ease),
    transform 340ms var(--ease);
}

.isn-cc[data-state='deal'] .isn-cc-pct {
  opacity: 1;
  transform: none;
  transition-delay: 300ms;
}

.isn-cc[data-state='deal'] .isn-cc-kicker {
  opacity: 1;
  transform: none;
  transition-delay: 480ms;
}

/*
  ── The two frames before the animation exists ────────────────────────────────

  build.mjs prints four company rows, because that is what is true with no script. The mix the
  reader arrives on — two vakken on their collective discount, one on a full company row, the
  first one's row emptied so it has somewhere to build from — is written by `js/isn-cgrid.js` at
  mount, and a deferred module can run on either side of first paint. On the side where the cards
  have already been painted, every one of those writes is a *transition*: two panels growing and a
  row of tiles vanishing, a quarter of a second after the page settled. Which is the one thing an
  animation about a considered collective cannot open with.

  So the module holds `data-cc-init` for two frames and this turns the choreography off for the
  duration. It has to be the last rule in the block: it ties on specificity with the
  `[data-state]` rules above — three compound selectors each — and a tie is decided by source
  order. Two frames rather than one because one `requestAnimationFrame` only guarantees *a* frame
  boundary; the second is what guarantees a paint happened in between, after which these values are
  simply where the cards started.

  Transitions stay off for good once their values match; an animation does not, because a keyframe
  animation restarts the moment its name comes back. That used to mean a pass of light across the
  seeded vak two frames after load, and it no longer happens for a reason worth writing down: both
  animations in this block are keyed on `bundle`, and `bundle` is not a state the module seeds. The
  mount composition is discounts and company rows — states made of transitions only — so there is
  nothing to restart. `animation: none` stays here anyway. It costs nothing, and the seed it depends
  on is a list in another file.
*/
.isn-cgrid[data-cc-init] .isn-cc-body,
.isn-cgrid[data-cc-init] .isn-cc-body *,
.isn-cgrid[data-cc-init] .isn-cc-body *::after {
  transition: none;
  animation: none;
}

/*
  The drawn objects, for the nine categories that have no photograph yet — and the one place in this
  block where two kinds of visual have to be balanced against each other rather than sized.

  "De zwarte line icons zijn nu te groot en te hard … maak ze kleiner, subtieler, optisch lichter,
  beter in verhouding met de echte product-assets." They were at 86% of the visual box in full
  `--i-ink`, which put a 105px black drawing beside a 122px photograph — and a line drawing at the
  same size as a photograph is heavier than it, because a photograph has soft edges and light in it
  and a drawing is all edge.

  Two changes, and the first is the one that does the work: 64% of the box instead of 86%, so 72px
  against the photograph's 113. Strokes are geometry in these SVGs and scale with the box, so a
  smaller icon is also a thinner one — the size reduction *is* most of the "optisch lichter".

  The second is the ink: `--i-ink` diluted to about #404040 against white. Not grey — a grey icon
  beside a colour photograph reads as disabled — but far enough off black that the drawing stops
  being the darkest thing on the card. `--ic-soft` stays the category's own tint, so the object is
  still the accent and the ink is still ink — and the thing that lets it be both is what is behind
  it: `.isn-cc-body` is #FCFCFB, so the tint has a near-white ground to read as a plane against.

  The rail used to be quoted here as the other half of that pair, and it is not one any more.
  `.isn-cat` is a tint now, so a plane in `--a-soft` there would be the same value as the ground,
  and a plane the same value as its ground is not a plane. Its stand-in draws as an outline in
  `--a-ink` instead; see `.isn-visual--drawn`. Two treatments, and which applies is decided by
  what is behind the drawing rather than by which component it is in.
*/
.isn-cc .isn-visual--drawn .isn-ic {
  color: color-mix(in srgb, var(--i-ink) 78%, var(--i-white));
  --ic-soft: var(--a-soft);
}

.isn-cc .isn-visual--drawn .isn-ic > svg {
  width: calc(var(--visual-h) * 0.64);
  height: calc(var(--visual-h) * 0.64);
}

/* ── 02 · De besparingsscan, nu in een eigen sectie ───────────────────────── */

/*
  A white card, and what separates it from what is behind it is a hairline plus one very soft
  shadow rather than a ground of its own.

  This rule has been all three of the card's homes without changing: its own section, then the
  hero's right column, and now its own section again. What came back with the section is
  `.isn-section--scan` further down this file — the paper band the card sits on — which had been
  deleted when the card moved into the hero, because a paper band behind it *there* would have
  cut the hero in half at exactly the point where the headline and the scan were meant to read
  as one thing. Under the category rail that is no longer true, and the tone change is what says
  the scan is a place rather than more hero.

  There is no `max-width` here, and there is no longer one anywhere for the home instance either.
  A `.isn-section--scan .isn-scan` rule used to cap it at 860 and push it to the end of an `fr`
  track; both are gone, because `.isn-scan-grid`'s second track is now a clamped pixel width and a
  cap inside a capped track is a number to keep in step with another number. Either card is as
  wide as the track it is in, on both pages.

  Radius `--i-r`, 20px — the card radius the rest of the page already uses, and inside the
  18–20 the brief asks for. Not `--i-r-lg`'s 26: the larger corner belongs to the full-width
  panels further down, and this is a form.

  Padding is 13–16, down from 22–46 in three passes. The card had to fit beside a headline *and*
  leave the three-step strip inside the fold, and 46px of padding on four sides was 92px of that
  budget. It is no longer in the fold, and the number stays anyway: it is what the compact card
  on /nl/besparingsscan/ is now measured against, and 16 is the smallest value at which the chips
  still read as being inside something — the chips carry a 1.5px border of their own, so the
  visible gutter between a chip and the card's edge is a shade wider than the number says.
*/
.isn-scan {
  padding: clamp(13px, 1.4vw, 16px);
  border: 1px solid var(--i-line);
  border-radius: var(--i-r);
  background: var(--i-white);
  box-shadow:
    0 1px 2px rgb(10 10 10 / 5%),
    0 20px 44px -26px rgb(10 10 10 / 18%);
}

/*
  The band the card sits in on /nl/.

  `--i-paper`, which is the boundary: the hero and the category rail above this are both white,
  and a third white block in a row reads as one very long hero. It is the same paper the compare,
  founder and mission sections use, so this adds a band to an existing rhythm rather than a tone.
  It is also what makes the card's shadow exist — a white card on white is a hairline.
*/
.isn-section--scan {
  background: var(--i-paper);
}

/*
  ── Two columns: the pitch, then the card ────────────────────────────────────

  This section was a full-width head with the card capped at 720px underneath it, left-aligned,
  and 608px of nothing to the right of the form at 1440 — the emptiest rectangle on the page, and
  directly beside the one thing on it that is supposed to be filled in. So the space became a
  column and the column got the argument for filling the form in, on a `0.72fr / 1fr` split that
  gave the card 58% of the row.

  That split is now reversed, and the reason is the shape it produced: 740 × 483 at 1440. A form
  that is half again as wide as it is tall is a panel, not a tool — the eye reads it as a strip of
  page furniture rather than as something with a first field. The instruction is explicit: narrower,
  more vertical, more of a widget, better balanced against the copy.

  So the card gets a measured track and the copy gets what is left. `clamp(460px, 36vw, 540px)`
  resolves to 460 / 461 / 518 / 540 at viewports of 1201 / 1280 / 1440 / 1920, against copy
  columns of 573 / 651 / 754 / 1092. Three numbers, each with a job:

  * **460 floor.** Inside the card that is 426px of content. Step 2's two-up fields have a 200px
    floor, so 426 is the narrowest width at which they stay two-up — one pixel less and the step
    collapses to a single column and grows about 100px, which is the fold moving for the sake of
    the card's silhouette.
  * **36vw** keeps the seam moving with the viewport instead of pinning it, which is what an `fr`
    did well and is the only thing lost by leaving `fr` behind.
  * **540 ceiling.** Past it the chips go back to looking like page furniture, and the copy column
    is the element that should absorb a 1920px screen: a lead can be 1092px wide, a text input
    cannot.

  A clamped pixel track rather than a ratio because the card's width is now a designed number, not
  a leftover — the same device `.isn-hero-grid` uses for the same reason one section up. It also
  retires the `justify-self: end` / `max-width: 860px` pair that used to sit under here: with the
  track itself capped there is no slack inside it to push the card around in.

  `align-items: start`. Measured at 1440 the tracks are 753.602 × 518.398, the copy column runs
  536 tall and the card 422, and it is their top line that has to agree; `stretch` would make the
  card pretend to the copy's height. The 114px the copy runs on for below the card is not a
  mismatch to fix — it is the band the blue cue crosses to reach the card's lower-left corner.

  `--i-gutter` for the column gap: the same clamp the page's left and right margins use, so the
  distance between the two columns is the distance from the page edge to the headline. A gap
  invented for this one grid is the thing that makes a two-column section look drawn by hand.
*/
.isn-scan-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(460px, 36vw, 540px));
  gap: var(--i-gutter);
  align-items: start;
}

/*
  ── The left column ─────────────────────────────────────────────────────────

  `.isn-lead`'s own `62ch` is not the cap that matters here — the column is ~754px at 1440, so
  the track is what measures the text. What the column does need is a ceiling on the type it
  inherits: `.isn-h` is `--i-h2`, sized for a headline spanning the whole shell, and this one has
  well under half of it. `--i-h2` is a clamp on `vw`, so it already comes down with the viewport;
  what it cannot know is that this instance is in a column. Hence the one override, and it is a
  `min()` rather than a second clamp: it takes `--i-h2` where `--i-h2` is the smaller, so the
  phone case is untouched.

  The column stays a block container. It was tempting to make it a flex column for the sake of the
  cue at its foot, but `.isn-scan-how`'s hairline is a full-column rule and in a flex column with
  `align-items: flex-start` it would shrink to the width of the longest step — the cue can be
  placed with one property on itself instead.
*/
.isn-scan-pitch .isn-h {
  font-size: min(var(--i-h2), 46px);
}

.isn-scan-pitch .isn-lead {
  margin-top: 16px;
}

/*
  ── `Zo werkt het` ───────────────────────────────────────────────────────────

  Three steps, and what the brief says about them is that they were tucked away: 12px, wrapping
  across one line, under the promises. They are now the first thing after the lead, one per line,
  at body size — because the objection they answer comes first. Nobody weighs what a scan is worth
  until they know it will not cost them an afternoon, and `Kies …`, `Vul …`, `Bekijk …` is the
  whole answer to that.

  The hairline above them is kept from the earlier pass, and so is the reason: this is a second
  kind of information — what it takes, not what it is worth — and 30px of air would have said that
  less clearly than 1px does.

  A grid, not the wrapping flex row it was. The row existed to be allowed to break between steps
  rather than inside them, which is a constraint a one-per-line list does not have; and it is the
  vertical form that turns the numerals into a column instead of three decorations. 10px between
  steps, the same rhythm the promises below use, because the two lists are two halves of one
  argument and a different gap in each would make them two sections.
*/
.isn-scan-how {
  margin-top: clamp(22px, 2.4vw, 30px);
  padding-top: clamp(18px, 2vw, 24px);
  border-top: 1px solid var(--i-line);
}

.isn-scan-how-label {
  margin: 0 0 12px;
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-muted);
}

.isn-scan-how-steps {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--i-body-size);
  font-weight: 600;
  letter-spacing: var(--i-track-body);
  color: var(--i-ink);
}

/*
  24px for the marker column, and the promises below use the same 24 — the two lists sit directly
  on top of each other in one column, and two text edges 6px apart down the left side of a column
  is the kind of thing that makes a section look assembled rather than drawn.
*/
/* Started rather than centred, for the reason the promises below are: a numeral that stays beside
   the first line survives a step wrapping on a narrow phone, and at 24px against a 25.575px line
   the two arrangements are otherwise the same picture. */
.isn-scan-how-step {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
}

/*
  The number is a numeral in a tinted square, not a counter in a circle: the steps bar inside the
  card one column to the right already uses a circle for `1 2 3`, and these are the same three
  steps described rather than a second copy of that control. Blue — the accent the card's own
  active step uses, so the same three numbers are the same colour in both places.

  24px, up from the 18 the micro-sized single line used. A marker much smaller than the line it
  labels reads as a bullet that happens to contain a digit; at 24 against a 16.5px line it is a
  numbered step. Radius 7 keeps the corner in proportion to the box — 6 on 24 is visibly tighter
  than 6 on 18 was.
*/
.isn-scan-how-n {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  background: var(--i-blue-soft);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--i-blue);
}

/*
  Three promises, and the gap is the point of them.

  10px, against the 18–24 the hero's claims sit on. They are one argument in three parts and they
  have to read as a block: `Ontdek hoeveel je mogelijk kunt besparen / Zie snel waar de grootste
  besparingskansen liggen / Profiteer van collectieve inkoopkracht` on a 24px rhythm is three
  separate statements with air between them, which is what this looked like before and what the
  brief calls out.

  600 and `--i-body-size`, so they are heavier than the lead above them and lighter than a
  heading — the weight is what makes three one-line sentences a list rather than three leads.
*/
.isn-scan-benefits {
  display: grid;
  gap: 10px;
  margin: clamp(22px, 2.4vw, 30px) 0 0;
  padding: 0;
  list-style: none;
}

/*
  `align-items: start` and not `center`, which is what this said until a 390px phone showed why:
  two of the three promises wrap there, and a tick centred on a two-line block sits in the gap
  between the lines, marking neither. Started, it marks the line it belongs to. Nothing moves on a
  desktop, where none of the three wraps.
*/
.isn-scan-benefit {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  font-size: var(--i-body-size);
  font-weight: 600;
  letter-spacing: var(--i-track-body);
}

/*
  The tick on its own soft disc, green. 20px, which is the smallest a filled disc can be and
  still hold a 12px mark with a visible ring of tint around it; below that it reads as a green
  dot. `--i-green-soft` and not the accent at full strength — three saturated 20px discs down the
  left edge of a column would be the loudest thing in the section, and the section's loudest
  thing is the button.

  20px inside the 24px column the steps above set, centred: a disc the same nominal size as a
  square looks smaller than it, so the 2px of inset on each side is what makes the two markers
  read as the same object rather than making the circle look overgrown.

  3px of top margin, which is `(25.575 - 20) / 2` for the body line-height this list sets: the row
  starts its items now, so the disc has to be pushed onto the middle of the first line itself. The
  numeral above needs none — 24 in a 25.575 line is 0.8px out, and 0.8px of a 7px-radius square is
  not a thing anyone can see.
*/
.isn-scan-tick {
  display: grid;
  margin-top: 3px;
  place-items: center;
  justify-self: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--i-green-soft);
  color: var(--i-green);
}

/*
  ── The foot of the column: one row, and now one thing in it ──────────────────

  This row held two unrelated sentences — a line of reassurance at the left, the blue cue at the
  right — and the reassurance line is out on request. What is left is a row with one child, and it
  is kept rather than folded away because the cue is placed entirely by margins that only a flex
  parent resolves: `margin-left: auto` to the seam, and `-30px` on top to lift the arrow. Give
  `.isn-scan-note` those margins as a block-level box and the auto resolves to 0 — the cue lands at
  the left edge, under a headline, pointing at nothing.

  The geometry is unchanged by the removal, because the row's height was never the paragraph's. The
  cue's margin box is 48px (78 tall, less the 30 the lift takes back) against the line's 17, so the
  row measured 48 with both in it and measures 48 with one. `align-items: flex-end` is what used to
  put the two sentences on one bottom edge and is now a no-op with a single item; it stays as the
  row's stated intent, at no cost, so a second item added here lands on the edge the first one sits
  on. `flex-wrap` and the `gap` are gone with the second item — there is no pair left to wrap.

  Why the row exists at all: the copy column is the taller of the two — 536 against the card's 422
  at 1440 — so a cue on a line of its own put its arrow 135px below the card and 76px to its left,
  which is an arrow pointing into the band's white space, and the brief asks for a cue *between the
  columns*. This row's top edge is fixed by the benefits above it, and the lift off that edge is
  what brings the tip up to the card's lower corner.
*/
.isn-scan-foot {
  display: flex;
  align-items: flex-end;
  margin-top: clamp(20px, 2.2vw, 26px);
}

/*
  ── The blue cue ─────────────────────────────────────────────────────────────

  `.isn-hero-note` already draws this exact object on /nl/besparingsscan/ — blue italic phrase,
  drawn arrow curving out of its end, `align-items: flex-end` so the arrow's tail meets the
  baseline. Everything about its appearance is inherited; these two rules only say where it goes in
  this one grid and how far into the gutter it is allowed to reach.

  `margin-left: auto` for the seam. The other two numbers each close one half of the distance to the
  card's lower-left corner, and both were arrived at by measuring, because the obvious move does
  nothing: the tip sits at 76% of the arrow's own width and at the very top of its box, so a *bigger*
  arrow moves the tip left, and a flex item aligned to `flex-end` can never begin above the row's own
  top edge — that edge is fixed by the benefits above it. Trying 112×78 and 128×88 left the tip at
  y 593 both times and only pushed the column's bottom further down.

  So: `-30px` on the top to lift the box, and a negative right margin to slide it across. The lift
  spends white space the column has and the row does not have to grow into — a negative top margin
  shrinks the item's margin box, so the row is 48px rather than 78 and the whole column ends 18px
  higher than before. 30 and not 60: it is the italic line, not the arrow, that runs out of
  room first — at 30 its top sits 43px below the last benefit's box, and every further pixel is spent
  from that gap until the cue is reading level with `Profiteer van collectieve inkoopkracht`.

  0.95 of the gutter, so the box stops 3px short of the card: the copy column's right edge is exactly
  one gutter from the card's left edge, and the arrow has to read as leaving one column and arriving
  at the other, which it cannot do if it starts inside the card's own margin — nor if it touches it.

  Together they put the tip at (837, 563) against a corner at (866, 529): 29px to its left and 34px
  under it, aimed into the corner. Before the lift it was 76px under, which is an arrow pointing at
  the white space beside the card rather than at the card.

  No rule here hides it below 1200px: `.isn-hero-note { display: none }` in the stacked block
  already covers both instances, and the reason is the same in both — once the card is underneath
  the copy, an arrow aimed up and to the right is worse than no arrow at all. That rule now takes
  `.isn-scan-foot` with it, because this cue is the row's only child and an empty row is still its
  own 26px of margin.

  The selector carries the parent only for weight. `.isn-hero-note` sets `margin` as a shorthand
  and is declared further down this file, so a bare `.isn-scan-note` would tie on specificity and
  lose on order — with the visible result being the wrong gap, not a missing rule, which is the
  kind of override that survives review.
*/
.isn-scan-pitch .isn-scan-note {
  margin: -30px calc(var(--i-gutter) * -0.95) 0 auto;
}

/* A third larger than the scan hero's 84×58, and it is the only instance with room for it: there
   the arrow rises out of a note that sits under three claims with the card immediately beside it,
   here it has a band of white to cross diagonally, and a small arrow crossing a large gap reads as
   decoration rather than as a direction. */
.isn-scan-note .isn-hand-arrow--scan {
  width: 112px;
  height: 78px;
}

/*
  Three steps in one line above the form, and it has to stay one line: `1 Categorieën
  2 Je bedrijf 3 Uitkomst` sets ~300px at these numbers, which fits a 540px card with room
  left. `flex-wrap` is kept anyway for a 320px phone, where it breaks to two rows rather than
  overflowing the card.
*/
.isn-steps-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin: 0 0 clamp(8px, 0.8vw, 10px);
  padding: 0;
  list-style: none;
  font-size: var(--i-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--i-muted);
}

.isn-stepdot {
  display: flex;
  align-items: center;
  gap: 7px;
}

.isn-stepdot > span {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--i-grey);
  color: var(--i-muted);
  font-size: 11px;
  transition:
    background-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.isn-stepdot[data-on='true'] {
  color: var(--i-ink);
}

.isn-stepdot[data-on='true'] > span {
  background: var(--i-blue);
  color: var(--i-white);
}

.isn-stepdot[data-done='true'] > span {
  background: var(--i-green);
  color: var(--i-white);
}

.isn-step {
  margin: 0;
  padding: 0;
  border: 0;
}

/*
  `--i-h3`, which is the site's own third level, and not the 23–32px this used to be. The card
  sits beside a 76px headline: a 32px title inside it is a second headline in the same field of
  view, and the reader has to decide which of the two the page is about. At h3 it reads as the
  card's own label, which is what it is.
*/
.isn-step-title {
  display: block;
  margin: 0;
  padding: 0;
  font-family: var(--i-display);
  font-size: var(--i-h3);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
}

/* One level below it again — step 3 has the outcome above this and the fields under it. */
.isn-step-title--sub {
  margin-top: clamp(16px, 1.8vw, 22px);
  font-size: var(--i-lead);
}

.isn-step-hint {
  max-width: 58ch;
  margin: 6px 0 0;
  font-size: var(--i-small);
  color: var(--i-muted);
}

/* ── The fourteen category chips ──────────────────────────────────────────── */

/*
  A real checkbox, visually replaced. The input keeps the semantics, the keyboard and the
  form value; the label is the surface. `:has()` is what lets the checked state style the
  whole chip without a class — and the fallback if a browser lacks it is a chip that does
  not visibly change, which is why the tick mark is also driven off `:checked` directly.
*/
/*
  ── There was a `.isn-chips` grid above this, and both cards scroll a row now ─

  It was three fixed columns and five rows — 219px of the home card's height — and it is gone
  along with the one problem three columns had: a chip measured ~165px, of which the mark took 24,
  its gap 8 and the padding 20, leaving `Kantoorartikelen` ~113px for 118px of label, so labels
  were allowed to wrap and every chip was sized for two lines. The home card is now 460–540px
  wide, where three columns put a ~150px box around a 16px word. Both cards use `.isn-chiprow`
  further down this file instead, one row deep on /nl/besparingsscan/ and two on /nl/, and no chip
  in it wraps.

  A chip is therefore 39px here, and the number that sets it is the vertical padding: the mark
  inside is a fixed 28px square and the borders are 1.5 either side, so the content is 28 + 3 +
  two paddings.

  ── There is no `min-height` here, and that is the honest version ─────────────

  There was one, at 42, and it had already stopped doing anything: at 4px of padding the content
  sets 43 and the 42 never applied. A declared `min-height` under the padding is a second number
  to keep in step with it for no effect. `.isn-chiprow .isn-chip` does declare one, because there
  the row's own height is the thing being designed rather than a consequence.
*/
.isn-chip {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  border: 1.5px solid var(--i-line);
  border-radius: var(--i-r-sm);
  background: var(--i-white);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease),
    background-color var(--dur-fast) var(--ease);
}

.isn-chip:hover {
  border-color: var(--i-muted);
}

.isn-chip input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}

.isn-chip:has(input:checked) {
  border-color: var(--i-blue);
  background: var(--i-blue-soft);
}

.isn-chip:has(input:focus-visible) {
  outline: 2px solid var(--i-ink);
  outline-offset: 3px;
}

.isn-chip[hidden] {
  display: none;
}

/*
  No `hyphens: auto`, deliberately, and the break lives in the content instead.

  Three columns in a 520px card is a ~155px chip, of which the mark and the padding take 52 —
  so a label has about 100px, and `Kantoorartikelen` sets 118px at 14px as one unbreakable
  word: with no break opportunity it does not wrap, it *overflows*, and it ran into the chip's
  right border. `hyphens: auto` fixes the overflow and gets the break wrong — the browser's
  Dutch dictionary takes the last opportunity that fits and sets `Kantoorartike-len`, when the
  compound's seam is `Kantoor-artikelen`.

  So the label in content/isn.js carries one U+00AD at that seam and `hyphens` stays at its
  `manual` default: the only place the word may break is the place it should. Nothing else in
  the grid needs it — every other label either fits or has a space in it.
*/
.isn-chip-name {
  font-size: var(--i-small);
  font-weight: 600;
  line-height: 1.2;
}

/*
  ── The object and the tick share one 24px cell ────────────────────────────

  They used to be two boxes: the drawing on the left and a tick absolutely positioned at the
  chip's right edge, which reserved 22px of every chip permanently whether anything was
  selected or not. Stacked in one cell they cost that 22px once — and a selected chip whose
  object *becomes* a tick is also a clearer state than a mark appearing somewhere else in the
  box while the object stays put.

  One grid area, both children in it, opacity between them. Not `display: none`, which would
  let the cell collapse on the frame the swap happens.
*/
.isn-chip-mark {
  position: relative;
  display: grid;
  flex: none;
  width: 28px;
  height: 28px;
  place-items: center;
}

.isn-chip-mark > * {
  grid-area: 1 / 1;
  transition: opacity var(--dur-fast) var(--ease);
}

.isn-chip .isn-ic > svg {
  width: 24px;
  height: 24px;
}

/*
  The object in a chip, at 28px. Larger than the 24px drawing beside it because a rendered
  object at this size is mostly its own silhouette — a drawing is a stroke on nothing and reads
  at 24, a photograph of a box needs the extra 4px before the box is recognisably a box.

  `contain`, so a tall object and a wide one are the same height here rather than the same
  width, and neither is cropped. The zone is square and fixed for the same reason the rail's
  is: fourteen visuals arriving one at a time must not each set their own scale.
*/
.isn-chip-img {
  width: 28px;
  height: 28px;
  object-fit: contain;
}

.isn-chip-tick {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--i-blue);
  color: var(--i-white);
  opacity: 0;
}

/* Whichever of the two is in the cell fades out under the tick. */
.isn-chip input:checked ~ .isn-chip-mark .isn-ic,
.isn-chip input:checked ~ .isn-chip-mark .isn-chip-img {
  opacity: 0;
}

.isn-chip input:checked ~ .isn-chip-mark .isn-chip-tick {
  opacity: 1;
}

.isn-chip-tick > svg {
  width: 14px;
  height: 14px;
}

.isn-chips-empty {
  margin: 12px 0 0;
  font-size: var(--i-small);
  color: var(--i-muted);
}

/* ── Fields ───────────────────────────────────────────────────────────────── */

/*
  `minmax(200px, 1fr)`, down from 260 and then from 220. The floor decides how narrow the card is
  allowed to be, and it is the tighter of two constraints at every step:

  * Two columns need `2 × floor + 12` of track. At the 460px floor of the card's own clamp the
    track is 426, so a 220 floor needs 452 and collapses; 200 needs 412 and holds.
  * Collapsing is not a graceful degradation here. Step 2 has five fields; one column makes it
    five stacked rows and about 100px taller than step 1, so the card — and everything under it —
    would move as the reader crosses from the chips to their details.

  What 200 costs is text room inside a select, and one field could not pay it: see
  `.isn-field--wide` below.
*/
.isn-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-top: clamp(14px, 1.4vw, 18px);
}

.isn-field {
  display: block;
}

/*
  One field takes the whole row: `Indicatieve jaarlijkse uitgaven`.

  A `select.isn-input` is `padding: 0 14px` with `padding-right: 40px` for the chevron, so its
  text room is its width minus 54. In a half row of a 460px card that is ~150px, and the longest
  option — `€ 1 miljoen – € 5 miljoen` — needs about 190. A `<select>` does not wrap or ellipsise
  its selected option; it clips it, so the reader's own answer would read `€ 1 miljoen – € 5 m`.

  `grid-column: 1 / -1` rather than a wider floor on the grid, because the constraint belongs to
  this one field's content. Its hint fits one line across the full row, which is the second reason
  it is the right field to spend the row on.
*/
.isn-field--wide {
  grid-column: 1 / -1;
}

.isn-field--search {
  max-width: 440px;
  margin-top: 8px;
}

.isn-field-label {
  display: block;
  font-size: var(--i-small);
  font-weight: 700;
}

.isn-field-opt {
  font-weight: 500;
  color: var(--i-muted);
}

.isn-field-hint {
  display: block;
  margin-top: 3px;
  font-size: var(--i-micro);
  color: var(--i-muted);
}

/*
  48px, not 52. Four of these stack on step 2, so the four pixels are sixteen, and 48 is still
  well over the 44px a field has to be to be a target. The `--i-field` size does not move with
  it — it is pinned at 16px because iOS zooms the page on a smaller one.
*/
.isn-input {
  display: block;
  width: 100%;
  min-height: 48px;
  margin-top: 6px;
  padding: 0 14px;
  border: 1.5px solid var(--i-line);
  border-radius: var(--i-r-sm);
  background: var(--i-white);
  font-family: var(--i-body);
  font-size: var(--i-field);
  color: var(--i-ink);
}

select.isn-input {
  /* Room for the native arrow on every platform, and no custom one drawn over it. */
  padding-right: 40px;
}

.isn-input:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 1px;
  border-color: var(--i-blue);
}

.isn-input[aria-invalid='true'] {
  border-color: var(--i-red);
}

.isn-field-problem {
  display: block;
  margin-top: 6px;
  font-size: var(--i-micro);
  font-weight: 600;
  color: #d43b3d;
}

.isn-consent {
  max-width: 62ch;
  margin-top: 12px;
}

/* Steps 2 and 3: a quiet `Terug` and the forward action, in a row. */
.isn-step-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: clamp(14px, 1.5vw, 20px);
}

.isn-step-actions .isn-micro {
  margin-right: auto;
}

/*
  Step 1 has no `Terug`, so its action is not a row of two — it is the primary call to action
  of the page, full width, with the terms under it. `Gratis · Vrijblijvend · ±1 minuut` sits
  *below* the button and not beside it, because everything it says is a reason to press and a
  reader who has already pressed does not need it.
*/
.isn-step-cta {
  display: grid;
  gap: 6px;
  margin-top: clamp(8px, 0.9vw, 10px);
}

/*
  **There are three block-button heights in this file now, and this rule owns the middle one.**
  It used to own two and the comment here said "46px here and 52 everywhere else"; that reading is
  false since the scan card's step 1 was sized against a reference screenshot, so all three are
  named here rather than left to be discovered:

      52px  `.isn-btn`'s own height — every button on the page that is not full width
      46px  this rule — a full-width button inside a card, on /nl/, in step 2 and 3 of both cards,
            and in the gate, receipt and calculator panels
      58px  `.isn-step--compact .isn-btn--block` — step 1 of /nl/besparingsscan/'s card only,
            where the reference's 80px button in an 800px card is both ×0.72 → 57.6 and 10% of the
            card's width → 57.6. The argument is on that rule.

  Why 46 survives as the middle value rather than being folded into either neighbour: it is the
  height a full-width button in a card was argued down to when the home page's fold was rebudgeted —
  the two pixels under `.isn-btn`'s 52 came out of the same pass as the chips' four, and 46 is still
  over the 44 a press target has to be. Nothing about that pass is undone by one step of one card
  growing, and widening the exception to every card would move /nl/'s fold for a reason that page
  never asked for.
*/
.isn-btn--block {
  width: 100%;
  min-height: 46px;
  justify-content: center;
}

.isn-micro--under {
  text-align: center;
}

/* ── Step 3: the outcome, before the ask ──────────────────────────────────── */

/*
  Value first, and the step reads that way on purpose: what we see, and which categories we
  will look at — and only then the three contact fields. There is not a percentage anywhere in
  it, because we have no data about this company yet and any number here would be invented.

  A panel inside a panel, so its padding is a step below the card's own: 16–22 against the
  card's 20–30. The list of "wat we vervolgens doen" that used to sit between the chips and
  the fields is gone with the step-3 rewrite — three numbered lines of process inside a card
  that has to fit beside a headline, describing what happens after a form the reader has not
  filled in yet.
*/
.isn-outcome {
  padding: clamp(14px, 1.6vw, 18px);
  border-radius: var(--i-r);
  background: var(--i-green-soft);
  --a: var(--i-green);
  --a-soft: #d5f0e3;
}

.isn-outcome .isn-eyebrow {
  margin-bottom: 10px;
  color: #0d7a4a;
}

.isn-outcome-h {
  margin: 0;
  font-size: var(--i-h3);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
}

.isn-outcome-body {
  max-width: 62ch;
  margin: 10px 0 0;
  font-size: var(--i-small);
  color: #3f5c4e;
}

.isn-outcome-label {
  margin: 14px 0 8px;
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: #3f5c4e;
}

.isn-outcome-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-outcome-chips li {
  padding: 7px 13px;
  border-radius: var(--r-pill);
  background: var(--i-white);
  font-size: var(--i-micro);
  font-weight: 700;
}

.isn-scan-report {
  margin: 16px 0 0;
  font-size: var(--i-small);
  font-weight: 600;
}

.isn-scan-report[data-tone='error'] {
  color: #d43b3d;
}

/* A rule with a word in it, separating the form's own submit from the other way in. */
.isn-scan-or {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: clamp(20px, 2vw, 26px) 0 14px;
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-muted);
}

.isn-scan-or::before,
.isn-scan-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--i-line);
}

.isn-scan-book {
  margin: 0;
  font-size: var(--i-small);
  font-weight: 700;
}

.isn-scan-book a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--i-blue);
  text-decoration: none;
}

.isn-scan-book a:hover {
  text-decoration: underline;
}

.isn-scan-done {
  display: grid;
  justify-items: start;
  gap: 16px;
  --a: var(--i-green);
  --a-soft: var(--i-green-soft);
}

/* ── 02b · /nl/besparingsscan/ — the same card, one step re-presented ─────── */

/*
  ── What is different here, and it is no longer "how little of it there is" ──

  It used to be. This block said that `.isn-step-title`, `.isn-step-hint`, `.isn-step-cta`,
  `.isn-btn--block` and `.isn-micro--under` were the home page's rules on the home page's markup,
  and that the delta was a pill and a taller field. **That is superseded by "het formaat van de
  form moet zoals hier",** which is an instruction about this card's *size* — so every one of those
  five now carries an override scoped to `.isn-step--compact`, and the card's own padding carries
  one scoped to `.isn-scan--compact`. The method, the reference figures and the ×0.72 that produced
  each number are in the block above the padding rule below; nothing here restates them.

  What is still shared, unchanged, and should stay that way: `.isn-steps-bar`, `.isn-step`,
  `.isn-chip` and all seven of its parts, `.isn-field`, `.isn-input`, `.isn-fields`, steps 2 and 3
  entire and the receipt. The scoping is deliberate on both counts — the sizes are step 1 of the
  card on /nl/besparingsscan/, not the card, and certainly not the form.

  ── The height, and it is no longer the whole point ──────────────────────────

  This block existed to make the card *short*: a brief asked for 430–500px against the ~700 the
  full card set, and it recorded 429 at 1440. Two things retired that number. Step 1 lost its chip
  row to step 2 — so 120px of the 429 (a `Waar wil je op besparen?` row and one row of chips)
  describes markup that is not in this step any more, and the 100px it counted for one field is now
  counted twice, for the URL field and the branche field. And then the size instruction above asked
  for the reference's proportions, which are larger than ours at every internal measure except the
  field height. The card is **575px** at 1440 by intent.

  The arithmetic, so the next change to any one line can be checked against the whole. Every figure
  is this file's own clamps and tokens resolved at a 1440 window — **computed, not measured**, since
  the pass that set them had no browser:

    74.0  the card's own padding, 36 top and 36 bottom, plus its 2px of hairline
     0    the steps bar, hidden while step 1 is showing — see the `:has()` rule below
    59.6  the pill: 13.97px of mono at 1.4, 8px of padding either side, 24.05 of margin under it
    34.7  the title at 30.96px on `--i-lead-h3`'s 1.12
    33.0  the hint at 17.42px on the body's 1.55, and the 6px above it
   125.7  the URL field: 38.02 above it, a 16.56px label at 1.55, the 56px input 6px below it
   125.7  the branche field — the same three parts at the same three sizes
   122.8  the button at 58, its terms line at 15.98, the 6 between them and the 33.98 above it
    ─────
   575.4

  The same arithmetic at 1201, the narrowest two-column window, comes to 516.2 — the clamps are all
  `vw` and the card's own track is `40vw`, so the card's internals hold their proportion of it
  across the whole two-column band rather than only at 1440. Above 1440 every clamp is at its
  ceiling and the figure stays 575.4 while the card keeps widening to its own 700px cap, which is
  the same "the fill only falls above the cap" behaviour `.isn-hero--scan .isn-hero-h` has and the
  reason the fold survives a short, wide window.

  The card's rendered height is **not** this figure, though, and that is new in the same pass: the
  card is a stretched grid item now, so its box is `max(this content height, the left column)`. See
  `.isn-hero--scan .isn-scan` further down, where both edges are pinned on instruction.
*/

/*
  The steps bar is in the markup on both pages and hidden on this one while step 1 shows.

  Not removed, because it is the same generator and the same three steps — a reader who presses
  `Start gratis scan →` lands on step 2 and the bar is there, telling them where they are, as it
  is on /nl/. What it cannot do is take 30px off the *first* step of a page whose job is to be
  short: `1 Categorieën 2 Je bedrijf 3 Uitkomst` above a card that has not been started yet
  announces a three-step form to somebody who has been offered a one-minute scan.

  `:has()` reads the state off the DOM the module already maintains — step 1 `hidden` means the
  reader has moved on — so there is no class to add and no second source of truth about which
  step is showing. It is load-bearing elsewhere in this file (`.isn-chip:has(input:checked)`),
  and the fallback in a browser without it is a visible steps bar, which is the honest
  degradation: the card is 30px taller and everything works.
*/
.isn-scan--compact .isn-steps-bar {
  display: none;
}

.isn-scan--compact .isn-scan-form:has([data-step='1'][hidden]) .isn-steps-bar {
  display: flex;
}

/*
  ── "het formaat van de form moet zoals hier" — the method, once ─────────────

  A reference screenshot was supplied and every number in this section and the six rules under it
  is derived from it the same way, so the derivation is written once here and each rule below only
  states its own two figures.

  **The reference image is ~2000px wide and the card in it is 800px.** Our card's track is
  `minmax(0, clamp(440px, 40vw, 700px))` at `.isn-hero--scan .isn-hero-grid`, so at a 1440 window it
  is **576px**. **The scale factor is 576 / 800 = 0.72**, and it is applied to nothing that is not
  a proportion of the card — which is the one distinction the whole method rests on:

  * **Fixed-size UI chrome compares in absolutes.** An input's height is a decision about touch
    targets and does not scale with a viewport. The reference's fields measure ~57px and
    `.isn-input--lg` below is 56. **They already match, and nothing here changes the field height.**
  * **Type, padding and gaps scale with the card,** because they *are* proportions of the object.
    Reference → ×0.72 → the value each rule below sets.

  ── The 56-against-57 is also what dates the capture, and that matters ───────

  The 800px card figure is only usable if the image is a 1× capture; at 2× the card would be 400
  CSS pixels and the factor would be 1.44, i.e. every number below would be wrong by a factor of
  two. An earlier attempt to settle it from character widths gave 1×, 1.5× **and** 2× depending on
  which string was measured, so it settled nothing. **The field height is the reading that settles
  it:** a form field is one of the few objects on a web page whose size is a fixed convention rather
  than a proportion, ours is 56 and the reference's measures 57, and a 2× reading would make the
  reference's fields 28.5 CSS pixels — which is not a field. So 1×, and the 800 is trustworthy.

  ── Two sizes are smaller than ×0.72, and the reason is a fit, not taste ─────

  The two long strings in this step were measured against the card's own content width before the
  scaled sizes were accepted. At 1440 the card is 576 with 36px of padding and 1px of hairline a
  side, so **502px** of content. Both strings were read out of `content/isn.js` (`ISN_SCAN_PAGE.card`)
  and counted, and both are set with **0.48em per character at Figtree regular / 0.52em at 800** —
  this file's own two established constants, at `.isn-hero-perk-sub` and `.isn-hero--scan .isn-hero-h`
  respectively, and not the 0.40/0.505 an earlier pass guessed. The guess is worth naming because it
  is falsifiable and false: at 0.40em `.isn-hero-lead`'s 128 characters would set two lines in its
  46ch measure, and the fold docblock records three.

      title  `Doe de gratis besparingsscan`      28 chars, 1 cap, 3 spaces, Figtree 800
             ≈ 14.98em, less 0.014em × 28 of `--i-track-h3` = 14.59em
             at ×0.72's 34.34px → 500px of 502  → one line by 2px, i.e. no line at all
             at 30.96px         → 452px of 502  → 90.0% fill, and one line at every window

      hint   `Ontdek binnen 1 minuut waar jouw…` 54 chars, 8 spaces, Figtree regular
             ≈ 25.92em, no tracking
             at ×0.72's 23.62px → 612px of 502  → two lines, over by 110px
             at 17.42px         → 452px of 502  → 90.0% fill, one line

  So the **title is 30.96px and not 34.34 (×0.649 of the reference, a 10% step down)** and the
  **hint is 17.42px and not 23.62 (×0.531, a 26% step down)**. Everything else in the table below
  is the ×0.72 figure unreduced. 90% was chosen as the fill both step down to because the
  per-character constants are ±5% at best and a wrap in a card whose whole purpose is to be scanned
  in one look is a worse outcome than two points of size. Neither reduction was verifiable in a
  browser — see the note at the foot of this block.

  ── Every clamp is `vw` and every ceiling is its own 1440 value ──────────────

  The card's track is `40vw` between 1100 and 1750 and its padding below is `2.5vw`, so a `vw` clamp
  on an internal size holds a constant fraction of the card across the whole two-column band —
  502/576 at 1440 and 418/480 at 1201 are both 87.1% of the card, so both fit checks above hold at
  1201 unchanged. Above 1440 the ceilings engage: the card keeps widening to its 700px cap while its
  internals stop, so the fill only falls. That is deliberate and it is the same device
  `.isn-hero--scan .isn-hero-h`'s 60px ceiling uses. It is also what keeps the hero inside the fold
  on a wide, short window — uncapped, the card's content reached 647px at 1750 × 800 against a 661px
  budget, which is 34px of overrun bought for nothing anybody asked for.

  Every floor is today's value, so **no narrow window changes**: `13px` of padding, `20px` of title
  (`--i-h3`'s own floor), `14px` of hint and label (`--i-small`), `12px` of pill and terms line
  (`--i-micro`), `16px` of field rhythm, `10px` above the CTA.

  ── What is NOT here ────────────────────────────────────────────────────────

  The reference's own `GRATIS BESPARINGSSCAN` badge carries a small leaf glyph. **Ours does not get
  one.** `.isn-scan-pill` is printed as `<p class="isn-scan-pill">${esc(P.pill)}</p>` in build.mjs —
  text and nothing else — and the instruction is "maar dan zonder dit rare icon", i.e. *do not adopt
  the reference's icon*, not *remove ours*. There is no `::before` here and none is wanted.

  The carousel's card design is likewise not from the reference: "gebruik niet deze carousel
  vormgeving maar die huidige die je al hebt." `.isn-hero-cat` and its parts are untouched by this
  pass.

  **No visual confirmation was possible.** The pass that wrote these rules had no browser, so every
  figure above is arithmetic on the reference's measurements and on this file's own tokens. A
  screenshot pass should check the two fit checks first — they are the only two places where being
  wrong by 10% is visible as a wrap rather than as a size.
*/

/*
  36px at 1440, up from 16, and it is the single largest change in the card.

  Reference 50px in an 800px card → ×0.72 → 36. `2.5vw` is that number written so it travels: 30.02
  at 1201, 36.0 at 1440, and a 36px ceiling that engages at exactly 1440 for the reason the block
  above gives. 6.25% of the card either way, which is the reference's own proportion.

  The floor is 13px — the base `.isn-scan` rule's own floor, unchanged — and it engages below a
  520px window. That is not tidiness: `.isn-step-title`'s 20px floor sets 291.8px of `Doe de gratis
  besparingsscan`, and a 360px phone leaves the card 296px of content at 13px of padding against
  288 at 16. The title fits on one line on the narrowest phone only at the padding this rule already
  had, so the floor is load-bearing and must not be raised to match the ceiling.

  What the extra 20px a side costs is step 2, which is a `.isn-fields` grid of
  `repeat(auto-fit, minmax(200px, 1fr))` with a 12px gap and therefore needs 412px of content to
  stay two-up. At 1201 — the narrowest two-column window and so the binding one — the card is 480.4
  and this padding leaves **418.4px, which clears 412 by 6.4px.** It was 444.8 before, so this pass
  spends 26 of that 32.8px margin. That is the one number in this rule worth re-deriving before
  anybody widens the padding again: at `2.7vw` the step collapses to one column at 1201 and grows
  about 100px, which is the fold moving for the sake of a gutter.

  Scoped to `.isn-scan--compact` and not to `.isn-scan`: the home page's card and the two
  `.isn-scan--calc` cards on the category pages keep the 13–16px their own fold budgets were argued
  against. Same specificity as `.isn-scan`, so this wins on source order, which is how
  `.isn-scan--calc` already overrides the same property further down this file.
*/
.isn-scan--compact {
  padding: clamp(13px, 2.5vw, 36px);
}

/*
  `Gratis besparingsscan` again, as a green pill above the title.

  Green and not blue: blue on this page is the thing you press, and there is exactly one of
  those in the card. The pill states a fact about the offer — it costs nothing — which is the
  same thing `--i-green` says on the outcome panel in step 3 and on the `Geen verplichtingen`
  claim in the hero.

  It repeats the hero's eyebrow, one column across, and that is deliberate on a lead page: the
  card is the only thing a reader who scrolled past the copy is looking at, and it has to say
  what it is without the sentence beside it.

  ── Three of its numbers are the reference's, and one is not ─────────────────

  **The three declarations below carried `--i-micro`, `5px 11px` and `0 0 10px` and no longer do.**
  The comment that stood here described a badge sized to disappear into the card; the size
  instruction asks for the reference's badge, which is a third larger in every direction. Method,
  factor and the 1× argument are in the block above the padding rule; the figures are:

      font-size        reference ~19px  → ×0.72 → 13.68  → `clamp(12px, 0.97vw, 14px)`, 13.97 at 1440
      padding          reference 12×24  → ×0.72 → 8.6×17.3 → `8px 17px`
      margin-bottom    reference 34px   → ×0.72 → 24.48  → `clamp(10px, 1.67vw, 24px)`, 24.05 at 1440

  The font-size and the margin travel on `vw` for the reason every other internal size here does,
  with today's `--i-micro` and 10px as their floors, so nothing below a ~1240px window moves.
  **The padding is the one that stays absolute,** and the distinction is the method's own: a pill's
  box is set by its own text's cap-height and its own radius, not by the card it happens to sit in —
  the same reading that keeps `.isn-input--lg` at 56px. `8px 17px` is therefore the ×0.72 figure at
  the reference width and it is not scaled off it.

  `--i-track-label`'s 0.1em is untouched, which is what keeps `GRATIS BESPARINGSSCAN` reading as a
  label at 14px rather than as a very small sentence: 21 characters of IBM Plex Mono at ~0.6em plus
  the tracking sets ~206px, and with 34px of padding the pill is ~240px inside 502px of card.
*/
.isn-scan-pill {
  display: inline-flex;
  align-items: center;
  margin: 0 0 clamp(10px, 1.67vw, 24px);
  padding: 8px 17px;
  border-radius: 999px;
  background: var(--i-green-soft);
  font-family: var(--i-mono);
  font-size: clamp(12px, 0.97vw, 14px);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  line-height: 1.4;
  color: #0d7a4a;
}

/*
  ── Step 1's type, at the reference's proportions ────────────────────────────

  Five overrides, all scoped to `.isn-step--compact` — the `<fieldset>` class build.mjs writes on
  step 1 of this one card and nowhere else. Two things follow from that scope and both are the
  reason it is not `.isn-scan--compact`:

  * **Steps 2 and 3 of this same card keep the shared sizes.** They print `.isn-step-title`,
    `.isn-step-hint`, `.isn-field-label`, `.isn-step-cta` and `.isn-btn--block` too, and step 2 is
    five fields and step 3 an outcome panel plus three more — a 31px legend and a 38px field rhythm
    there would add roughly 200px to a step that already has the most in it. The instruction points
    at the first screen, so the override does.
  * /nl/'s card, the two `.isn-scan--calc` calculators and the gate/receipt panels are untouched, on
    the same reasoning as the padding rule above.

  Each `font-size` floor is the token the rule replaces, so every one of these is a no-op below
  ~1210px and the phone layout is bit-for-bit what it was. The two step-downs from ×0.72 — the title
  and the hint — are argued with their fit arithmetic in the method block above; the other three are
  the scaled figure unreduced.

      title       reference 47.7 → ×0.72 → 34.34 → 30.96 at 1440   (`--i-h3` was 26)
      hint        reference 32.8 → ×0.72 → 23.62 → 17.42 at 1440   (`--i-small` was 14)
      label       reference 23   → ×0.72 → 16.56 → 16.56 at 1440   (`--i-small` was 14)
      terms line  reference 22   → ×0.72 → 15.84 → 15.98 at 1440   (`--i-micro` was 12)
      CTA gap     reference 47   → ×0.72 → 33.84 → 33.98 at 1440   (was 10)

  `--i-h3`, `--i-small` and `--i-micro` themselves are not touched, and must not be: they are the
  page's shared steps, and the whole rest of this stylesheet is measured against them. What this
  card asked for is a size, not a scale.
*/
.isn-step--compact .isn-step-title {
  font-size: clamp(20px, 2.15vw, 31px);
}

/*
  The hint's `max-width: 58ch` in the base rule is now the looser of two constraints and is left
  alone: at 17.42px, 58ch is ~575px and the card gives 502, so the card measures it. Worth knowing
  before anybody reads the 58 as the thing keeping this on one line — it is not.
*/
.isn-step--compact .isn-step-hint {
  font-size: clamp(14px, 1.21vw, 17.5px);
}

/*
  Both labels are short — `Website URL` at 11 characters and `In welke branche ben je actief?` at 30,
  the longest of the two — so 16.56px sets ~248px of the card's 502 and there is no fit to check
  here. It is the one figure in this block that could have gone straight to the ×0.72 number.
*/
.isn-step--compact .isn-field-label {
  font-size: clamp(14px, 1.15vw, 16.5px);
}

/*
  33.98 at 1440 against the 10 this was, and it is the largest proportional jump in the card — the
  reference puts a third of a field's height between the last field and the button. `.isn-step-cta`'s
  own 6px `gap` between the button and the terms line under it is untouched: that pair is one object
  and the reference shows it the same way.
*/
.isn-step--compact .isn-step-cta {
  margin-top: clamp(10px, 2.36vw, 34px);
}

/*
  58px, and it is the one number in this pass that two independent readings agree on.

  As an absolute: the reference's button is 80px and ×0.72 is 57.6. As a proportion: 80px in an 800px
  card is 10% of the card's width, and 10% of our 576 is 57.6. The absolute and the proportional
  readings converge, which is what makes 58 defensible where `.isn-btn--block`'s own 46 was argued
  down to the minimum a target may be. Flat and not a clamp, for the reason `.isn-input--lg` is flat:
  a press target is a fixed convention, and this is now the largest one in the card by a wide margin.

  It does not reach step 2's or step 3's forward buttons, or the bar's `Plan kennismaking`, or /nl/'s
  card — see the scope argument at the head of this block. So there are now three block-button
  heights in the file and the comment at `.isn-btn--block` names all three.
*/
.isn-step--compact .isn-btn--block {
  min-height: 58px;
}

.isn-step--compact .isn-micro--under {
  font-size: clamp(12px, 1.11vw, 16px);
}

/*
  56px against `.isn-input`'s 48. Step 1 of the compact card is two fields and a button, and both
  fields take this height — the four on step 2 keep the 48, where four of them stack and the eight
  pixels would be thirty-two.

  Not larger than this. A 64px input inside a 20px-radius card reads as a search bar on a
  landing page rather than as a field in a form, and the difference between the two is whether
  the reader expects an answer or expects to be asked something next.

  **The size instruction that grew everything else in this step deliberately did not touch this
  number, and 56 is now load-bearing twice.** The reference screenshot's own fields measure ~57px,
  so ours already matched and there was nothing to scale — a field's height is a touch-target
  convention rather than a proportion of the card. And because it is a convention, the 56-against-57
  is what dates the capture as 1× and therefore what makes the reference's 800px card width usable
  at all; the full argument is in the method block above `.isn-scan--compact`'s padding. Changing
  this number would take that evidence with it.
*/
.isn-input--lg {
  min-height: 56px;
  font-size: var(--i-lead);
}

/*
  38.02 at 1440, up from 16, and this is the field rhythm of the whole step.

  Reference 53px between the two fields → ×0.72 → 38.16, written as `2.64vw` with today's 16px as
  the floor so nothing below ~1210px moves. It is `.isn-field--branche`'s number too — the two are
  one rhythm and the comment on that rule says why — and it is *also* the gap between the hint and
  the first field, because the same declaration on the first of the two siblings is what that gap is.
  The reference shows the two distances equal, so one number is the honest reading rather than a
  convenience.

  Neither of these two classes exists anywhere but step 1 of this card (`stepOneCompact` in
  build.mjs), which is why they are edited in place instead of being wrapped in
  `.isn-step--compact`. That is not true of `.isn-fields`' own 12px `gap` one block up — step 2 uses
  it — so the two-up rhythm on step 2 is deliberately not part of this change.
*/
.isn-field--url {
  margin-top: clamp(16px, 2.64vw, 38px);
}

/*
  The branche field, directly under the URL field and matching it.

  `.isn-input--lg` cannot be reached from here: `select()` in build.mjs writes one class list on its
  control and it is shared with step 2's four selects, which keep the 48px. So the two declarations
  are repeated against a modifier on the label instead of the class being extended — three lines
  duplicated is cheaper than a second parameter threaded through a helper five fields use.

  The same top margin as `.isn-field--url` and not the `.isn-fields` grid's gap: these two are
  siblings in a fieldset, not cells in a grid, and the step reads as URL-then-branche-then-button on
  one rhythm. That clamp is now `clamp(16px, 2.64vw, 38px)` in both places rather than
  `clamp(12px, 1.4vw, 16px)`, on the size instruction — the argument, and the reference's 53px it
  comes from, is on `.isn-field--url` above. **The two must stay identical**; a rhythm expressed as
  two numbers is a rhythm that will drift.
*/
.isn-field--branche {
  margin-top: clamp(16px, 2.64vw, 38px);
}

.isn-field--branche .isn-input {
  min-height: 56px;
  font-size: var(--i-lead);
}

/*
  The category label and the row's two arrows on one line, because the arrows belong to the row
  and there is no room above it for a line of their own. Same arrangement as the category band's
  `.isn-cats-top` further down, at a smaller size.
*/
.isn-chiprow-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: clamp(12px, 1.4vw, 16px);
}

/*
  30px rather than the band's 38. These sit beside a 14px label inside a form card, not beside
  a section caption, and at 38 they set the height of the whole row.
*/
.isn-chiprow-arrows .isn-rail-arrow {
  width: 30px;
  height: 30px;
}

.isn-chiprow-arrows .isn-rail-arrow > svg {
  width: 13px;
  height: 13px;
}

/*
  `.isn-step` is a `<fieldset>`, and every browser's UA sheet gives a fieldset
  `min-inline-size: min-content`.

  That is the one rule in this file's way. The min-content width of a step containing fourteen
  no-wrap chips in a nowrap flex row is the sum of all fourteen — 2140px — so the fieldset
  refuses to be 686px wide, the row never becomes a scroller, and everything inside the card
  (the input, the button, the row) runs off the right edge of the page. Re-measured with the
  rule suspended, at a 1440 viewport: the fieldset lays out 2140 wide and
  `document.documentElement.scrollWidth` reads 2821. It was 1847 and 2529 while the row held
  thirteen chips, and it grew because splitting `facilitair` in two added the widest label in
  the row — `Schoonmaakproducten`, a 211px chip — as well as a fourteenth.

  `min-width` beats the UA's `min-inline-size` here because it is an author declaration, and the
  two are the same property in a horizontal writing mode.

  It was scoped to `.isn-scan--compact` while /nl/ still had a three-column chip grid, whose
  min-content is one chip wide and which therefore needed nothing. Now that both cards scroll a
  row, both fieldsets have a min-content width in the thousands, and the rule belongs to
  `.isn-scan .isn-step`. Left scoped to the compact card, the home card would not have been
  slightly wrong — it would have pushed the whole page 1000px wide.
*/
.isn-scan .isn-step {
  min-width: 0;
}

/*
  ── One scrolled row on both cards, and it works before any script runs ───────

  An ordinary `overflow-x: auto` flex row: fourteen chips, every one of them a real checkbox
  with a real label, reachable by trackpad, by finger, and by keyboard — `Tab` moves through
  them and the browser scrolls the row to whichever has focus, which is the one navigation this
  cannot break. `js/isn-scan.js` adds the two arrows and a mouse drag on top of that, and it
  mounts on `[data-chip-rail-viewport]` rather than on either page, so the home card reusing this
  row needed no line of script.

  ── The 5px of vertical padding is not spacing ───────────────────────────────

  `.isn-chip:has(input:focus-visible)` draws a 2px outline 3px outside the chip, and an outline
  *is* clipped by a scroll container. Without those five pixels the keyboard focus ring on a
  chip is sliced off top and bottom, which is the accessibility defect this layout would
  otherwise introduce over the grid it replaces.

  ── No scroll snapping ──────────────────────────────────────────────────────

  The category band snaps, because its cards are 180px wide and a half-shown card there is a
  card you cannot read. A chip is 40-odd pixels and a half-shown one is a visible promise that
  the row continues — which is exactly the affordance a scroller with hidden scrollbars needs.
  Snapping would also fight the drag: the row would settle somewhere other than where it was
  let go.

  `user-select: none` because the only text in here is fourteen category names on labels, and
  a mouse drag across them would otherwise select them instead of moving the row.
*/
.isn-chiprow {
  display: flex;
  gap: 8px;
  margin: clamp(8px, 0.9vw, 10px) 0 0;
  padding: 5px 1px;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  user-select: none;
}

.isn-chiprow::-webkit-scrollbar {
  display: none;
}

/*
  `flex: none` so fourteen chips size to their labels and overflow, rather than sharing the
  row's width and setting three characters each. 54px tall — comfortably a touch target, and the
  height the single-row card can afford because there is one row of it instead of five.

  `nowrap` on the name, which is what makes the row a row: a chip that wraps is two lines tall
  and takes the other twenty-five with it. Three of the twenty-six labels carry a soft hyphen:
  `Kantoor⟨AD⟩artikelen`, written into `ISN_SCAN.categories`, and the two cleaning names, written
  into `ISN_CATEGORY_VISUALS` because at 390 they ran 21px and 8px past the 148px carousel card they
  also sit on. None of the three fires in this row, and that is this rule rather than the text:
  `nowrap` refuses every break opportunity there is, a soft hyphen and the space in
  `Zakelijke verzekeringen` alike.
*/
.isn-chiprow .isn-chip {
  flex: none;
  min-height: 54px;
  padding: 6px 12px;
}

/*
  ── Two rows deep, on the home card ──────────────────────────────────────────

  The scan page's card is 540–720px wide and its row is the page's first move, so one row of
  fourteen is right there: three or four chips visible, and the arrows say the rest exist. The
  home card is 460–540, where one row shows three of fourteen — which reads less as a carousel
  than as a list that has been cropped.

  So this variant turns the flex row into a two-row grid that still scrolls sideways:
  `grid-auto-flow: column` with two auto rows makes seven columns of two, about six chips visible
  at 518px, and a seventh column half-shown to say there are more. It has to be a grid — a
  `flex-wrap: wrap` inside an `overflow-x: auto` box wraps against the *visible* width, so it
  would quietly become seven stacked rows and scroll nothing.

  46px per chip rather than the 54 above: two rows plus the 8px gap is 100px of card, and 54 would
  make it 116. 46 is still a thumb-sized target — it is the number the chips used on a phone while
  they were a grid — and the mark inside is 28, so nothing is being squeezed.

  `align-items: start` because a column whose two chips have labels of different lengths is not a
  reason for one of them to be taller than the other; without it the odd column at the end
  stretches its single chip over both rows.
*/
.isn-chiprow--rows {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, auto);
  align-items: start;
}

.isn-chiprow--rows .isn-chip {
  min-height: 46px;
}

.isn-chiprow .isn-chip-name {
  white-space: nowrap;
}

/* While the row is being pushed by a mouse. `auto`, so the drag is not smoothed into lag. */
.isn-chiprow[data-dragging] {
  cursor: grabbing;
  scroll-behavior: auto;
}

.isn-chiprow[data-dragging] .isn-chip {
  cursor: grabbing;
}

/* ── The scan page's hero ─────────────────────────────────────────────────── */

/*
  ── The split turned back over, and this time a headline is what turned it ───

  On /nl/ the copy column is the wider of the two and the card is 47vw: the page is making an
  argument and the scan is the first thing you can do about it. Here the card was the page, so the
  split turned over — 720px of card against 564.8 of copy at 1440.

  Three sizes have been asked for since, in one instruction, and they do not all fit in that
  arrangement. **`de titel moet groter`** is the one that decides the track, because a headline this
  file breaks in the markup (`headline: ['Koop samen in.', 'Check je besparing.']`) can only grow
  until its longer line reaches the column's edge:

      `Check je besparing.`   19 characters, ~0.52em each at Figtree 800
      in a 564.8px column     564.8 / (19 × 0.52) = 57.2px before it touches both edges

  48px was already 87% of that. So "bigger" inside a 564.8px column is a handful of pixels and then
  a wrap that puts `besparing.` on a third line — which is the one thing the two fixed lines exist
  to prevent. The column had to widen, and the only place the width is available is the card.

  The reference layout the instruction points at has it the same way round, which is the check that
  this is not a reading of convenience. Measured off that image (2000px wide, so ratios rather than
  pixels): copy 1016 / card 800 of the 1816px the two tracks share = **55.9% / 44.1%.** So:

      1440 window   shell 1328, column gap 43.2   card 576.0   copy 708.8   → 55.15% / 44.85%
      1201 window   shell 1089, column gap 36.0   card 480.4   copy 572.6   → 54.39% / 45.61%
      1750 window   shell 1638, column gap 44.0   card 700.0   copy 894.0   → 56.09% / 43.91%

  **This supersedes the band an earlier instruction put on this rule — left 43–45%, right 55–57%.**
  That band is where 50vw/720px came from, it is recorded here because it was explicit, and it is
  the mirror image of the one the reference shows. Both cannot hold. The later instruction wins and
  the earlier number is written down rather than quietly dropped, so that reversing this is one
  edit: `clamp(560px, 50vw, 720px)` in this rule and a 48px ceiling at `.isn-hero--scan .isn-hero-h`.

  700px and not 600 as the ceiling, which is the one number in the clamp that is neither the
  reference's nor the headline's: above about 1750 the card stops growing, and every pixel past that
  goes to the copy column alone. At 600 the copy column reaches 1044px at 1912 — a 52ch lead in a
  1044px track, with nothing capping it above 1200 — and the composition stops being two columns and
  becomes a page with a form pinned to it. 700 keeps the wide case at 944px, which is within 20px of
  what this hero already did at that window, so the widest layout is unchanged and only the laptop
  widths move.

  1201 stays the binding case for everything measured inside the left column — it is the narrowest
  window that still has two columns (the grid stacks at 1200), and there the gutter is
  `--i-gutter`'s 56px ceiling on each side, so the shell holds 1089. The copy column is 120px wider
  there than it was, which is what pays for the headline and for the category cards at
  `.isn-hero-cats`.

  ── And this hero ends at the fold, which is new in this pass ────────────────

  `min-height: calc(100svh - var(--i-chrome) - var(--hero-pad-t) - var(--hero-pad-b))`, so the
  section's own box ends on the bottom edge of the first viewport and whatever follows it starts *at*
  that edge. Without the rule the hero was content-sized — 545.7px at 1440 × 900 — and the next
  section began at y = 652.7, 247px above the fold, which is the defect this rule fixes. What
  follows is now `.isn-section--collective`: the category rail that used to sit here left the page
  with `isnCategoriesSection('scan')` (see `.isn-hero-cats` below), and measured after both changes
  that section's top edge is at y = 900 at 1440 × 900, y = 900 at 1201 × 900 and y = 800 at
  1440 × 800 — the fold exactly, at all three. Three things make it that expression and not another:

  * `--i-chrome` is the utility strip's 38px, the bar's 68px and the hairline — declared at `:root`
    precisely so a fold rule can subtract it in CSS, where js/nav.js's measured `--bar-h` does not
    exist at first paint. Anything added above the nav is added to that token, not to this rule.
  * Both hero paddings, because this rule has to make the hero *end* at the edge. The home hero's
    `:has(> .isn-hero-steps)` rule subtracts `--hero-pad-t` alone and overruns by its bottom
    padding; that is its business, and its selector is why this one could not simply be widened to
    cover both — the scan hero has three children and no steps strip, which is exactly why it had
    no fold rule at all before this pass.
  * `svh` with a `vh` line above it, in that order, for the reason the home hero's rule gives: the
    fallback runs where `svh` is unknown, and on a phone toolbar-collapse `svh` is the one that
    does not leave a strip of the next section showing.

  It is a floor and not a height, which is what makes it safe below 1200: there the three areas
  stack into one column — copy, then card, then category row, with the block's own gaps — and a
  column that holds all three is taller than `100svh - 107px` on any phone-shaped window, so the
  floor is slack and adds nothing. On the one shape where it is not slack (a narrow *and* tall
  window: stacked layout, ~1400px of viewport height) it hands the surplus to `align-content: center`
  below, which centres the stack instead of leaving a hole under it. The fold requirement is about
  what the *first* viewport contains and is unaffected either way.

  ── Three areas, because the category row is a third child ──────────────────

  `.isn-hero-grid`'s own two rows are for /nl/'s three-step strip, which spans both columns. Here
  the second row is the row of category cards, which spans neither: it belongs under the copy and
  the card stands beside both. So this names the areas rather than counting children —

      copy scan
      cats scan

  — and the three children take one each. Two things follow that no `order` property could give:

  * The card's area is both rows plus the row gap — measured 512.9px at 1440 (373.6 of copy row, a
    34px gap, 105.3 of category row) against its own content height of 409 plus its 47.5px
    `margin-top` — so the row of categories cannot push it anywhere and cannot be pushed by it. That is the
    brief's "de categoriekaarten moeten compact genoeg zijn om de formulierkaart niet weg te
    drukken", enforced by the geometry rather than by keeping the row short enough to hope. (It sits
    at the top of that area rather than filling it; `align-items: start` below, and the reason is in
    the same paragraph.)
  * At ≤1200 the areas become one column in the order `copy` / `scan` / `cats` — which is the
    brief's mobile order, headline and subtext, then the promises, then the form, then the
    categories. That is also the DOM order, so nothing is reordered visually away from the
    reading order and a keyboard user reaches the form before the six decorations.

  ── Where the leftover height goes: `align-content`, not a row that grows ────

  Owning the fold and filling it are two different problems, and the second one has an honest
  answer and a dishonest one. Three windows, measured in the browser and not reasoned about —
  `getComputedStyle(grid).gridTemplateRows` for the two tracks, `.rowGap` for the gap between them,
  `getBoundingClientRect()` for everything else:

      1440 × 900   grid 757   rows 373.6 + 105.3 over a 34.0 gap = 512.9   leftover 244.1 → 122.0 a side
      1440 × 800   grid 661   rows 373.6 + 105.3 over a 34.0 gap = 512.9   leftover 148.2 →  74.1 a side
      1201 × 900   grid 757   rows 424.8 together over a 28.8 gap = 453.6   leftover 303.4 → 151.7 a side

  The card is 409 tall at all three and shorter than the two rows it spans, so it decides nothing
  here. **The leftover is real and no amount of tuning removes it:** the headline's ceiling was 48px
  because that is where `Check je besparing.` stopped fitting the column (measured at
  `.isn-hero--scan .isn-hero-h`; it resolved to 47.4px at 1440 and 38.0px at 1201), the lead is three
  rendered lines of a sentence the instruction dictates, and a row of 87.5px-wide cards cannot be
  250px tall. A composition that genuinely needed 757px at 900 would need 661 at 800 and not have it.

  **The three rows of that table are from before this pass and are kept as the record of why the
  rule is `center`, not as a description of the current page.** Four things moved since: the track
  split (720/564.8 → 576/708.8 at 1440), the headline's ceiling (48 → 60), the card's internal type
  and padding, and the category row becoming a drifting rail with a card width in `cqi`. Every one
  of those makes the composition taller or wider, so the leftover shrinks — the direction the
  paragraph above wants — and none of them can make it negative, because `min-height` is a floor and
  the grid grows past it. The arithmetic that says the growth fits is at the top of the type block:
  ~503px of left column against ~753 available at 1440 × 900. **What none of it is, is measured.**
  This pass had no browser available, so the numbers in the table are the last real readings taken
  of this hero and the new ones are owed. A screenshot pass replaces this paragraph with a fourth
  table; until it does, treat 373.6 / 105.3 / 244.1 as history.

  ── The same windows after the card grew — computed, not measured ─────────────

  Arithmetic on this file's own tokens and on the copy in `content/isn.js`, with no browser involved.
  It does not replace the table above and it is not a re-measure: it is what the tokens now say, and
  the first screenshot pass should believe neither figure until it has read both off a real render.

  The left column at 1440, where the copy track is 708.8px wide:

      34.8    the eyebrow — 12px of `--t-mono` at 1.4, plus its 18px bottom margin
     113.13   the headline — two lines of `clamp(34px, 8.4cqw, 60px)` on `--i-lead-hero`'s 0.95;
               8.4cqw of 708.8 is 59.54px, just under the 60 ceiling, so 59.54 × 0.95 × 2
      34.0    the gap above the lead
      87.0    the lead — three rendered lines of 18.72px (`--i-lead`'s 1.3vw) on the body's 1.55
      36.0    the gap above the promises
      97.61   the promise row
      ──────
     402.54   the copy area
    + 34.0    this rule's own `row-gap` at 1440 (2.4vw)
    + 105.3   the category row, carried over from the table above as the last real reading
      ──────
     541.8

  The card at the same width is **575.4** — the table in the 02b compact-card block, and 166.4px more
  than the 409 the paragraph above records, because the size instruction scaled its type, its padding
  and its field rhythm to the reference's proportions. So `max(541.8, 575.4)` is the card, and **the
  card now drives the grid.** That is a reversal of the sentence above it: through every reading in
  the table the two rows decided the height and the card "decided nothing here". It is also the reason
  `.isn-hero--scan .isn-scan` below carries `align-self: stretch` and no top offset, and why the rail
  carries `align-self: end` — with the card the taller box, the surplus lands in the *rows*, and the
  rail has to be pushed to the bottom of a row grown taller than it is.

      1440 × 900   floor 757     content 575.4   leftover 181.6 → 90.8 a side
      1440 × 800   floor 661     content 575.4   leftover  85.6 → 42.8 a side
      1440 × 768   floor 630.3   content 575.4   leftover  54.9 → 27.5 a side
      1201 × 900   floor 757     content 516.2   leftover 240.8 → 120.4 a side

  The floors are this rule's own expression resolved: 900 − 107 of `--i-chrome` − 17.1 of
  `--hero-pad-t` (1.9vh) − 18.9 of `--hero-pad-b` (2.1vh) = 757, and the same at 800 (32 of padding)
  and 768 (30.72). The 1201 content is the card again, 516.2 against a left column that the same
  six-part sum puts at roughly 473–479 there — the headline's `cqw` re-resolves to 48.10px on a
  572.6px copy track and the lead hits `--i-lead`'s 17px floor, and the two candidate readings of the
  promise row differ by about six pixels; the conclusion does not turn on which, because the card is
  the taller either way.

  **The hero still ends at the fold at all four windows**, and the leftover moved the direction the
  paragraph above asks for: 244.1 → 181.6 at 1440 × 900, and 148.2 → 85.6 at 1440 × 800, which is
  half of what it was. The window height where the floor stops being the binding constraint is
  575.4 + 107 of chrome + 30 of padding (both pads are at their `px` floors by then, 1.9vh and 2.1vh
  of 712 being 13.5 and 15.0) = **≈712px**. On the same arithmetic with the card back at 409 it would
  be the left column deciding, at 541.8 + 137 = ≈679. So this pass moved the first window height that
  scrolls up by about 33px, and 900, 800 and 768 all clear it.

  An earlier version of this paragraph claimed 578 of rows and 179 of leftover, and both were
  invented before the measurement above existed. The numbers in the table are what the browser
  returned; the two 1440 rows differ only in the grid's own height, which is the point.

  So `align-content: center` splits the leftover above and below the two rows, and the alternatives
  are worse in ways that are easy to name: `start` leaves all 244 of it in one hole between the
  category row and the bottom edge; a `1fr` first row leaves it between the promises and the category
  row, which is the "giant empty gap" the instruction rules out by name; `space-between` does both;
  `space-evenly` turns a third of it into that same gap — (244.1 + 34) / 3 = 92.7px between the
  promises and the cards at 1440 × 900, and 110.7 at 1201 × 900. Half and half is also what the home
  hero does with its own 134px of leftover, for the reason stated there — it is the only
  distribution that looks chosen at more than one window height.

  What half and half costs is recorded rather than rounded off, because it is the one judgement in
  this rule a reader might want to overturn. The visible band is the leftover plus the hero padding
  on that side, so it is a little larger than the table's figure: at 1440 × 900 the eyebrow starts
  139px below the bar's hairline and the category row ends 141px above the fold (122.0 + 17.1, and
  122.0 + 18.9); at 1201 × 900 the same two bands are about 169 and 171; at 1440 × 800 they are 89
  and 91, which is the one of the three that reads as drawn. The two levers if that is
  judged too airy are `--visual-h` on the category visuals and this `row-gap` — both grow the
  composition instead of distributing air around it — and neither was pulled here, because the
  instruction that produced this rule is a viewport correction that says twice not to redesign the
  composition again.

  `align-items: start` stays, and now it is doing something the centring cannot: the two rows are
  centred as a block, and within them the card sits at the top of the two-row area it spans rather
  than floating in the middle of it. Both lines are needed, and they are not two ways of saying the
  same thing.

  `grid-template-rows: auto auto` for the same reason as before: the inherited `minmax(0, 1fr)`
  first row exists so /nl/'s strip can be pinned to the bottom of the fold, and here the rows are
  content-sized and the leftover is distributed around them rather than poured into one of them.

  The row gap is its own value rather than the inherited `clamp(14px, 1.8vh, 22px)`. That number
  is measured in `vh` because it is the space above a hairline that has to survive a short window;
  this one is the space between a paragraph of promises and a row of cards, which is a horizontal
  rhythm and takes `vw` — 34px at 1440, the same step as the gap over the promises themselves.
*/
.isn-hero--scan .isn-hero-grid {
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(440px, 40vw, 700px));
  grid-template-rows: auto auto;
  grid-template-areas:
    'copy scan'
    'cats scan';
  align-items: start;
  align-content: center;
  row-gap: clamp(22px, 2.4vw, 34px);
  min-height: calc(100vh - var(--i-chrome) - var(--hero-pad-t) - var(--hero-pad-b));
  min-height: calc(100svh - var(--i-chrome) - var(--hero-pad-t) - var(--hero-pad-b));
}

.isn-hero--scan .isn-hero-copy {
  grid-area: copy;
}

.isn-hero--scan .isn-scan {
  grid-area: scan;
}

.isn-hero--scan .isn-hero-cats {
  grid-area: cats;
}

/*
  ── The scan hero's eyebrow: blue, and in normal case ────────────────────────

  Two instructions, one page, one rule. "dus deze eyebrow moet blauw" put `--i-blue` on it; "zorg
  dat de eyebrow niet all caps zij maar gewoon zijn" takes the capitals off. Both are scoped rather
  than global because both point at one screenshot of /nl/besparingsscan/, and the two heroes have
  never had to match — /nl/ prints a different string, `ISN_HERO.eyebrow`'s `Hét inkoopcollectief
  voor MKB Nederland`, and it stays grey and uppercase.

  ── Why `.isn-hero-copy` is in the selector ──────────────────────────────────

  This was `.isn-hero--scan .isn-eyebrow`, and that was wrong in a way the colour alone hid.
  `isnScanHeroSection()` in build.mjs renders `isnScanCard(page, 'compact')` *inside* the hero
  section, and that card contains the outcome panel's own eyebrow. So the old selector matched two
  elements, and at (0,2,0) it tied with `.isn-outcome .isn-eyebrow` above — equal specificity, later
  in the file, so it won, and the green `Wat we zien` was rendering blue on this page. It went
  unseen because the panel sits inside `<fieldset data-step="3" hidden>` and never appears in a
  screenshot of the fold. Adding `.isn-hero-copy` (0,3,0) is not about out-specifying the green
  rule; it stops matching that element at all, which is the actual fix. `text-transform` below
  would have leaked exactly the same way, into a card this rule has no business touching.

  ── The capitals come off, and the tracking goes with them ───────────────────

  `letter-spacing` is the part that is not just deleting a declaration. `--i-track-label` is 0.1em
  and it exists *because* the string was uppercase: capitals have no ascenders or descenders to
  separate them, so tracking supplies the rhythm the letterforms withhold. On lowercase that same
  0.1em is the spaced-out mid-2010s logotype look — and it is a lot of it. 0.1em at `--t-mono`'s
  12px is 1.2px a gap, and `Gratis inkoopcollectief voor MKB` is 32 characters, so its 31 gaps
  carry 37.2px, or 38.4px of inline box once the increment engines add after the final glyph is
  counted. Dropping it is not a cosmetic tidy; it takes 37.2px out of the line's length.

  It goes to `--i-track-body` — which is 0 — rather than to a literal `0`, so the declaration
  records what the value now *is*, the site's setting for ordinary running text, instead of reading
  as a number somebody deleted. A future reader who finds tracking dropped here and kept on the
  other six eyebrows needs that distinction, or the next tidying pass puts 0.1em back.

  Zero rather than a smaller non-zero step, and IBM Plex Mono is the reason. The face is
  monospaced: every glyph occupies the same advance, so its narrow lowercase letters already sit in
  side bearings far wider than a proportional face would give them. A monospace lowercase string
  needs less added tracking than a proportional one, not more — so the honest answer here is none.

  ── The size stays at `--t-mono`, and that is a decision, not an omission ────

  Lowercase at 12px genuinely does read smaller than capitals at 12px: caps fill the cap height,
  lowercase sits at the x-height, and in this string only the `G` and the acronym `MKB` reach cap
  height at all. The step to 13px is the obvious correction, and it is not taken, for three reasons.

  The 37.2px the tracking gave back is density, and density is most of what "presence" means in a
  label — the line gets shorter and tighter rather than smaller. `--i-blue` is already a louder
  signal than `--i-muted`. And there is a cost, though a concurrent pass changed its shape: this
  element is the first child of the copy column, so its box is part of what the fold arithmetic at
  `.isn-hero--scan .isn-hero-grid` adds up, and 13px makes it 13 × 1.4 + 18 = 36.2px instead of
  34.8 — 1.4px more of left column, inside the `max(card content, left column)` that decides whether
  the card beside it stretches. What it no longer does is move the card. An earlier version of this
  paragraph argued that `.isn-hero--scan .isn-scan` derives the card's *top offset* from this box —
  true when that rule carried `margin-top: clamp(22px, 3.3vw, 48px)`, and that declaration is gone:
  the card is now pinned to the **top** of this element rather than stepped below it, on the later
  instruction recorded in that rule's own docblock, and the top edge of a grid row is where it is
  whatever the height of the row's first child. So the step is still not taken, and the third reason
  is now a 1.4px fold cost rather than a broken alignment. There is no
  token for the step either: the mono scale is `--t-mono` 12px and `--t-mono-sm` 10.5px, so it would
  be a raw `13px`. If a later pass does want the optical correction, it owes that docblock an update
  and this one a note; it must not be a silent one-line bump.

  ── 500 survives lowercase ───────────────────────────────────────────────────

  Checked rather than carried over, because the base rule argues 500 for a label in capitals. It
  holds, and from both directions. Upward there is nothing to go to: build.mjs requests
  `IBM+Plex+Mono:wght@400;500`, so 600 would be synthesised from 500. Downward, 400 would remove the
  second cue at the same moment as the capitals, leaving the label only family and colour to
  separate it from the lead paragraph beneath it. 500 stays, now carrying more of that distinction
  than it did when the caps were doing half the work.

  The blue: `--i-blue` and not a darker variant, because there is no darker variant —
  `--i-blue-soft` is the #EEF3FF tint and #2463FF is the ink. On white it measures 4.88:1, which
  clears AA for normal text at any size, so 12px of it is legible rather than decorative. Worth
  checking rather than assuming, since a label is the smallest type on the page and the one most
  often recoloured into invisibility.

  All of this stays an override on the base rule's declarations rather than becoming
  `--eyebrow-ink` and `--eyebrow-case` custom properties: two exceptions do not pay for a token,
  and a token here would invite a third. Going global instead is two lines in `.isn-eyebrow` —
  `text-transform: none` and `letter-spacing: var(--i-track-body)` — and it is safe whenever it is
  wanted, because all seven source strings are already written in sentence case.
*/
.isn-hero--scan .isn-hero-copy .isn-eyebrow {
  color: var(--i-blue);
  letter-spacing: var(--i-track-body);
  text-transform: none;
}

/*
  ── Both of the card's edges are pinned, and the offset that used to be here is gone ──

  "zorg dat de form hoger is dus dat die aan de boven kant top aligned is met de eyebrow van de
  linker sectie en aan de bottom moet die bottom aligned zijn met die categorie carousel."

  So: the card's top edge on the top of the eyebrow — `ISN_SCAN_PAGE.hero.eyebrow` in
  `content/isn.js`, `Gratis inkoopcollectief voor MKB`, and rendered in that case since
  `.isn-hero--scan .isn-hero-copy .isn-eyebrow` dropped `text-transform` a few rules above — its
  bottom edge on the bottom of the category rail, and the card taller as a result. "zorg dat de form
  hoger is" is the instruction, not a side effect of the other two.

  The element is named rather than its string, everywhere below, and deliberately: what the top edge
  is pinned to is the *first child of the copy column*, and the copy of that child is being edited in
  the same pass this rule was written in. If a later brief replaces the eyebrow with something else,
  this rule keeps working and only the quotation above goes stale.

  ── What was here, and why it is superseded rather than deleted ──────────────

  `margin-top: clamp(22px, 3.3vw, 48px)`, and it was argued: an earlier brief said the card "mag
  iets lager beginnen dan de headline links", `.isn-eyebrow` is 34.8px tall including its 18px
  bottom margin, so 35px would put the card's top edge exactly on the *headline's* and 48 puts it
  13px below that — far enough to read as a deliberate step rather than as a 0.2px miss.

  **That argument is sound and it is superseded.** The two instructions genuinely conflict: the
  earlier one steps the card down from the headline, this one aligns it to the element *above* the
  headline, and 48px of offset is exactly the distance between those two positions plus 13. The
  later instruction wins, and it is also the more specific one — it names both edges and it names
  the element each is measured against, where the earlier one said only "iets lager". The offset is
  therefore 0 in the two-column layout, and the 1200px block's reset of it has become the reset of
  *this* rule instead.

  ── Three declarations, and the grid supplies the geometry for free ──────────

  Nothing here computes a distance, and that is the point. The card already spans both grid areas —
  `copy` over `cats` at the track rule above — row 1 begins with the copy column, whose first child
  is the eyebrow with no top margin, and row 2 ends with the rail. So the spanned area's top edge
  *is* the eyebrow's top and its bottom edge *is* the bottom of the rail's row. With the offset at 0,
  `align-self: stretch` makes the card's box that area, and the requested alignment is a consequence
  of the grid rather than a pair of numbers to keep in step with the copy column's contents.

  `align-self: stretch` has to be explicit: the track rule sets `align-items: start`, which is what
  keeps the copy column and the rail at the top of their own rows, and it overrides the default
  `stretch` for every item including this one.

  ── Where the surplus goes when the box is taller than the form ──────────────

  A stretched box is `max(its own content height, the area the two rows make)`, so something has to
  absorb the difference. `justify-content: center` in a flex column splits it above and below the
  form, which is the same distribution `align-content: center` makes one level up on the grid itself —
  two levels of the same composition disagreeing about where slack goes is how a layout starts
  looking accidental. `space-between` was the alternative and it is wrong: it would pin the pill to
  the top edge and the terms line to the bottom and stretch the *fields* apart, and the distance
  between a label and its input is a form's rhythm, not a layout's. Those gaps are set at
  `.isn-field--url` and stay exactly what the size instruction made them.

  **As of this pass that surplus is zero at every two-column width, and the machinery is still
  required.** Computed, not measured: the card's own content is 575.4px at 1440 (the table in the
  02b block) against a left column of 402.7 + 34 of row gap + 105.3 of rail = 542.0, and at the two
  ends of the band it is 516.2 against 478.9 at 1201 and 575.4 against 545.8 at 1750 — the card's
  content is the taller of the two everywhere, so `stretch` currently resolves to the content height
  and `justify-content` has nothing to place. What makes the three lines load-bearing anyway is that
  the left column is the half of this that is still moving: the category row is being rebuilt as a
  drifting rail, and a rail that ends up taller than 105.3 puts the left column over 575.4 and the
  card into the stretched case. Written this way that is a no-op; written as `align-self: end` it
  would be a card floating with a hole under its last field.
*/
.isn-hero--scan .isn-scan {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/*
  The rail sits at the *bottom* of its row, and this is a guard against one specific failure rather
  than a second alignment.

  In the ordinary case it does nothing: row 2's height is the rail's own height, so the rail's bottom
  edge is already the row's, which is already the card's. It earns its place in the case the rule
  above describes — **the card's content taller than the left column**, which is where this pass
  leaves the page at every two-column width. There the card grows the area it spans, the surplus is
  distributed across *both* auto rows (equally, per the grid spec's extra-space distribution), and at
  1440 that hands row 2 about 16.7px more than the rail needs. With the track rule's
  `align-items: start` the rail would hang at the top of that taller row with the card's bottom edge
  16.7px below it — which is precisely what the instruction's second half rules out.

  It is one declaration in a rule of its own rather than a line added to `.isn-hero-cats`, because
  the row's own rules are being rewritten from a six-column grid into a rail in the same pass and
  this has to survive that. It does: `align-self` positions the grid item, and nothing about it
  depends on whether the item's contents are six tracks or a drifting track. `.isn-hero--scan`
  scopes it to this page, so /nl/'s rail is untouched.

  Below 1200 it is inert rather than reset, and the inertness is structural: the areas stack into one
  column, every row is content-sized, and the floor's surplus goes to `align-content` *around* the
  rows rather than into them — so there is no free space in the row for `end` to move anything
  through. Nothing beside anything means nothing to align to.
*/
.isn-hero--scan .isn-hero-cats {
  align-self: end;
}

/*
  ── The rail stops at the right edge of `Collectieve inkoopkracht` ───────────

  *"zorg dat de carousel hier op de besparingsscan hero section loopt tot de rechterkant van de
  samen sterker copy daarboven. zo zit er wat meer ruimte tussen de linker section en de form
  waardoor de form meer aandacht krijgt."*

  Two things in that sentence, and only one of them is the goal. **The goal is air between the left
  half and the form card**, so the card draws the eye; aligning the rail's right edge to the third
  promise's subline is the *means* the instruction picks. They do not fight here — the alignment
  opens 49.6px to 148.5px of extra air depending on the window (table below), which is far more than
  enough to read as chosen. Where they would fight is at the wide end, and that is recorded at the
  foot of this block rather than quietly traded away.

  ── Which edge, exactly ─────────────────────────────────────────────────────

  The third promise is `Samen sterker` over `Collectieve inkoopkracht`. Three candidate right edges
  sit in that cell and only one of them is the one a reader sees:

  * **its grid column's** right edge — which *is* the copy column's right edge, because the row is
    `repeat(3, minmax(0, 1fr))` and the third track ends where the row does. Aligning to that is
    aligning to what the rail already does, i.e. no change at all.
  * **the title's** right edge — `Samen sterker`, 13 characters of Figtree 800 at
    `clamp(14px, 1.05vw, 15.5px)`, so ≈13 × (0.52 − 0.014)em × 15.12px = 99.4px at 1440. Shorter
    than the subline, so it is not the edge the eye reads as the end of the copy.
  * **the subline's** right edge, which is the widest ink in the cell and therefore the one the
    screenshot points at. `.isn-hero-perk` is `justify-items: start`, so both text boxes are
    shrink-to-fit and left-aligned against the track's left edge — the subline's right edge is
    `track start + rendered text width` and nothing else.

  So: **subline.** And the arithmetic for it is algebra on two rules in this file, not a measurement.

  ── The algebra, and where each number comes from ───────────────────────────

  With `C` the copy column's inline size and `g` the promise row's gap, the third track starts at

      2 · ((C − 2g) / 3 + g) = (2C + 2g) / 3

  — the same step the promise docblock below states as `(708.8 − 2 × 15.8) / 3 + 15.8`, rearranged
  so it can be written in CSS against `100%` of the grid area instead of against a literal. `max-width`
  on a grid item resolves its percentage against the **grid area's** inline size, and this item's area
  is the `cats` area, whose track is the same `minmax(0, 1fr)` the copy above sits in. So `100%` here
  *is* `C`, exactly, at every window and with no number written down. That is the whole reason this is
  a `calc()` with a `%` in it rather than a table of `max-width`es per breakpoint.

  Then the rendered width of the subline, which is **an estimate and stays labelled as one**:
  24 characters (`Collectieve inkoopkracht`, counted out of `ISN_SCAN_PAGE.hero.perks` in
  content/isn.js) at **0.48em each**, this file's validated per-character advance for Figtree regular
  — the constant argued at `.isn-hero-perk-sub` below and at the compact-card block above, which also
  records that *"per-character constants are ±5% at best"*. `--i-track-body` is `0`, so there is no
  tracking term to subtract; the subline sets no `letter-spacing` of its own and inherits that zero.

  `clamp(10px, 1.1vw, 16px)` and `clamp(11.5px, 0.88vw, 13.5px)` are restated from
  `.isn-hero-perks`'s `gap` and `.isn-hero-perk-sub`'s `font-size`. **Restated, not tokenised**, and
  that is a choice with a cost: the two clamps now exist twice and can drift. Tokenising would have
  meant hoisting both onto `.isn-hero--scan .isn-hero-grid` — the nearest common ancestor of the
  promise row and this rail, since `.isn-hero-perks` is inside `.isn-hero-copy` and this is its
  sibling — and rewriting two live declarations in a rule this pass otherwise never touches. The
  restatement is the same device `.isn-hero-perk .isn-perk-img` uses for its 26, and the same one
  `.isn-hero-cat`'s width formula uses for `--hero-cat-gap`: mirrored value, comment naming the rule
  it mirrors. If either clamp changes, this one changes with it.

      window  C        g      3rd track starts  subline px   RIGHT EDGE   as % of C   air gained
       1201    572.57  13.21   390.52            132.48       523.00       91.34%      +49.57
       1440    708.80  15.84   483.09            145.98       629.07       88.75%      +79.73
       1750    894.00  16.00   606.67            155.52       762.19       85.26%     +131.81
       1912    944.00  16.00   640.00            155.52       795.52       84.27%     +148.48

  Every figure is **computed** off the tokens in this file — `--i-shell: 1800px`,
  `--i-gutter: clamp(20px, 5vw, 56px)`, the grid's `column-gap: clamp(28px, 3vw, 44px)` and its
  `clamp(440px, 40vw, 700px)` card track — and reproduces the `C` column of the table at
  `.isn-hero-cats` to the second decimal, which is the check that this reading of the layout is the
  same one that rule was written against. **None of it is measured.** No browser was available in
  this pass either.

  The `as % of C` column is why this is a `calc()` and not a flat percentage: the target edge is 91.3%
  of the column at 1201 and 84.3% of it at 1912, because the subline's font-size is a clamp that hits
  its 13.5px ceiling at 1534 while the column keeps growing. A flat 88.75% — exact at 1440 — would
  fall 14.8px short of the text at 1201 and overrun it by 42.2px at 1912. Seven points of drift is
  an order of magnitude more error than the ±5% on the advance constant (±7.3px at 1440), so tracking
  the width is worth the longer expression even though the input is an estimate.

  ── The rail narrows; it is not clipped ─────────────────────────────────────

  Two ways to stop a rail short, and they look different on screen.

  **Narrowing the container** — this rule — shrinks the cards with it, because `.isn-hero-cat`'s
  width is `(100cqi − 4 × var(--hero-cat-gap)) / 5.4` and `.isn-hero-cats` is the inline-size
  container `100cqi` resolves against. Same composition, everything ~12% smaller, and the same ~5.3
  cards a screen at every width.

  **Clipping the viewport** — a cap on `.isn-hero-rail` or `.cats-viewport`, leaving `.isn-hero-cats`
  full-width — keeps the cards at today's size and shows fewer of them: 629.07 / (125.33 + 8) = 4.72
  a screen at 1440 instead of 5.31. It is structurally possible (this element's `container-type`
  gives it inline-size containment, so the capped child could not shrink it) and it is the wrong
  answer twice. The count is not incidental — the divisor 5.4 is picked at `.isn-hero-cats` precisely
  to show *"five whole cards and about a third of a sixth at every width"*, and 4.72 is four and
  three-quarters. And it would put the row's width in two places, which the ≤1200 cap's docblock
  rejects in its own words: *"capping a container means the formula at `.isn-hero-cat` is the only
  place a width is decided."*

  So this is the same lever the ≤1200 branch already pulls, at a computed width instead of 600px.
  That is the strongest argument for it: the mechanism is already in the file, already argued, and
  already known to carry the card width and the phone query with it.

  ── The wrap stays seamless, and the proof is the one already here ──────────

  `js/categories.js` wraps modulo `span = cards[count].offsetLeft − cards[0].offsetLeft`, so a seam
  opens the moment `span < viewport`. With `v` the container's inline size and `g` `--hero-cat-gap`:

      w = (v − 4g) / 5.4        span = 6(w + g) = 1.1111·v + 1.5556·g
      span − v = 0.1111·v + 1.5556·g   > 0 for every v > 0 and g ≥ 0

  **The proof is scale-invariant, which is exactly why narrowing was safe to choose:** `v` appears on
  both sides and the inequality does not care what it is. Capping `v` narrows the cards in the same
  proportion and the slack shrinks proportionally without ever reaching zero. Computed, not measured:

      window   g      v before  v after   w after   span after   slack after   (slack before)
       1201     7.21   572.57    523.00    91.51     592.32       +69.32         +74.83
       1440     8.00   708.80    629.07   110.57     711.42       +82.34         +91.20
       1750     8.00   894.00    762.19   135.22     859.32       +97.13        +111.78
       1912     8.00   944.00    795.52   141.39     896.36      +100.84        +117.33
        390     6.00   350.00    350.00   112.67     712.00      +362.00        +362.00

  Never less than 69px, which is most of a card. The 390 row is unchanged because this rule does not
  apply there — see the breakpoint paragraph. `measure()` re-runs from a `ResizeObserver` on the
  viewport, so a cap that moves with the window is re-measured on every resize rather than going
  stale. Nothing here adds a `::before` to the rail, so `rest()` still parses `NaN`, still falls to
  `|| 0`, and the row still opens flush with the column's left edge.

  ── Above the seam only, and the query is the exact complement of it ────────

  `not all and (max-width: 1200px)` rather than `min-width: 1201px`. Below 1200 the three areas stack
  into one column, the rail takes the card's `max-width: 600px` so the four stacked blocks share a
  right edge, and *"the right edge of the promise copy"* stops meaning anything — the promise row is
  one column of disc-beside-text by then. So the cap has to be off there, and it would not be: this
  selector is `0,2,0` against that branch's `0,1,0`, so an unscoped version of this rule would beat
  the 600px cap inside its own media query and open the row to 865.95px at a 1200 window. The exact
  complement is used instead of `min-width: 1201px` because the latter leaves 1200 < w < 1201 with
  neither cap applying; that fractional window is reachable on a scaled display, and while the
  fallback there is benign — today's behaviour, the rail at full column width — closing it costs one
  keyword. **The ≤1200 rule at `.isn-hero-perks, .isn-hero-cats` is deliberately not edited.**

  ── What this costs, recorded rather than rounded off ───────────────────────

  Two knock-ons, both computed:

  * **The card gets one line taller at 1440, and only at 1440.** The card's height is
    `2 × padding + --visual-h + 4 + lines × (font-size × 1.18)` — every term a `vw` clamp, so a
    narrower card is not a shorter one. What *can* change is the line count of the longest label,
    `Kantoorartikelen`, which carries a U+00AD the browser will use if the word does not fit.
    Unbroken it sets 16 × (0.52 − 0.014)em ≈ 101.20px at the 12.5px ceiling. The text box is
    `w − 2 × padding`: 105.33px before at 1440 and 90.57px after, so the word fits by 4.1px before
    and misses by 10.6px after. **4.1px is inside the ±5% error bar and 10.6px is outside it**, so
    the honest statement is that the row was 90.59 *or* 105.34 before and is 105.34 after. At 1201
    it is two lines either way (89.06 against a 83.88px box before); at 1750 and 1912 one line either
    way (101.20 against 139.63 / 148.89 before, 115.22 / 121.39 after). Card heights after:
    **90.01 at 1201, 105.34 at 1440, 90.75 at 1750 and 1912, 68.98 on a 390 phone.**
  * **The fold does not move.** The rail's row is the third term of the left column's height, and
    105.34 is the figure the sum at `.isn-hero--scan .isn-hero-grid` already carries as 105.3 — so
    that 541.8 is now *arrived at* rather than assumed, which is the one number this pass makes more
    true than it found it. Against the card's 575.4 the card still drives the grid at all four
    windows, by 33.5px at 1440 instead of 33.5-or-48.3, and the floor / leftover table there stands
    unchanged: 757 / 181.6, 661 / 85.6, 630.3 / 54.9, 757 / 240.8.

  And the tension worth naming: at 1912 this leaves the rail 148.5px short of the copy above it, on
  top of the grid's own 44px column gap — 192.5px of air between the rail's last card and the form.
  That is a lot, and at some width above about 1750 the rail stops reading as *aligned to the copy*
  and starts reading as *cut short*. The instruction is explicit about the alignment and about
  wanting the air, so it is implemented as asked; the lever below is where to trim it if the wide
  case looks wrong once someone has actually looked at it.
*/
@media not all and (max-width: 1200px) {
  .isn-hero--scan .isn-hero-cats {
    /*
      ── THE ONE TUNABLE ──

      The estimated rendered width of `Collectieve inkoopkracht` as a multiple of its own font-size:
      24 characters × 0.48em = 11.52. Nothing else in the expression below is a judgement — the rest
      is the promise row's own geometry, mirrored.

      In `em` rather than `px` on purpose, so a nudge scales with the subline instead of drifting
      against it. One unit moves the rail's right edge by the subline's font-size at that window:
      11.50px at 1201, 12.67px at 1440, 13.50px at 1750 and above. So −0.5 pulls the rail ~6px left,
      +1.0 pushes it ~13px right, and 0 would end the rail at the third promise's *track* start.

      This is the value to change after a screenshot, and it is the only one.
    */
    --hero-cat-sub-adv: 11.52;

    max-width: calc(
      (200% + 2 * clamp(10px, 1.1vw, 16px)) / 3 +
        var(--hero-cat-sub-adv) * clamp(11.5px, 0.88vw, 13.5px)
    );
  }
}

/*
  ── The three promises ──────────────────────────────────────────────────────

  Three equal thirds of the copy column, each a tinted disc over a title over one muted line.

  Equal tracks rather than `.isn-hero-claims--row`'s content-sized flex, and the reason is the
  opposite of the reason that modifier exists. The homepage's three claims are short sentences of
  roughly equal length, so equal tracks left ~100px of air inside each and the row read as three
  separate blocks; these three are a title of 13-16 characters over a subline of 19-24, and the
  longest of the six strings is 24 characters against the shortest's 13. Content-sized, the third
  column would be half again as wide as the first and the row would read as a list that ran out of
  room. Thirds put the three discs on a rhythm — 0, 241.5, 483.0 at 1440, computed as
  (708.8 − 2 × 15.8) / 3 + 15.8 per step from the copy column and this rule's own `1.1vw` gap — and
  a rhythm is what makes three separate objects read as one row. (0 / 187 / 374 stood here; that was
  the 564.8px column, measured. These three are arithmetic on the widened one and not a reading.)

  ── The disc ────────────────────────────────────────────────────────────────

  50px, `border-radius: 999px`, filled with `--a-soft` off `.isn-hero-perk--<accent>`. The brief
  asks for "klein rond icoontje / zachte achtergrondkleur", and this is the only round plate in the
  ISN stylesheet — every other icon here is drawn straight onto the page, because a rounded square
  behind every glyph is the icon-pack look the original brief rejects by name. A disc is not that
  square: there are three of them, they are the only ones, and they are what makes the row scannable
  at a glance in a column this narrow.

  ── 44 → 50, and the mark deliberately stays at 26 ──────────────────────────

  "de circle om die benefits icons heen moeten net wat groter zijn dan nu." **The disc is what was
  asked to grow, not the drawing,** so the ring is where the six pixels go: 26px of mark inside 50
  leaves a **12px ring** of tint where it left 9. That is the whole change, and it is the reading
  that matches the reference layout, where the marks are the same size as ours inside visibly larger
  plates.

  Growing the mark with it was the alternative and it is the wrong one twice over. The three marks
  are supplied artwork, sized by `optical()` in icons.py so that a tag, a bar chart and three figures
  *read* as one set at 26px — apparent sizes 23.4, 22.7 and 21.0, which is the whole reason that
  function exists — and re-rendering them at 30 to hold the 9px ring means regenerating three PNGs to
  answer an instruction about a circle. It would also undo what the instruction is for: 26-in-50 is
  52% coverage against 59% at 44, and the drop in coverage *is* the "iets groter" being asked for. A
  proportional 30-in-50 would look identical to today at a larger scale.

  9px was argued as the ring that "reads as a badge", with 38px (`--lg`, the claim row's size) named
  as where the drawing touches the edge and the disc reads as a cropped square. 12px moves in the
  safe direction from both: further from the cropped-square failure, and a plate rather than an
  outline. There is a ceiling somewhere above this — a 26px mark in a 70px disc is a dot in a
  circle — and it is not near 50.

  `color: var(--i-ink)` overrides `.isn-ic`'s accent edge for the same reason
  `.isn-hero-claim .isn-ic` does, and here it is a contrast number rather than a house style:
  `--i-green` on `--i-green-soft` is about 2.7:1, which is thin for a 1.6px stroke, and ink on any
  of the five tints is over 15:1. `--ic-soft` is deliberately *not* overridden, so the icon's filled
  plane is the same colour as the disc under it and disappears — which is what keeps the three marks
  a set, because `merge` has no filled plane at all (`soft: ''` in content/isn-icons.js) and a
  visible plane on the other two would make three marks out of two systems.

  ── The subline's size is measured against the narrow case ──────────────────

  At 1201px the copy column is **572.6px** and a third of it, after two 13.2px gaps, is **182.1px**.
  `Collectieve inkoopkracht` is the longest of the three sublines: 24 characters, which at Figtree
  regular sets about 0.48em each, so the 11.5px floor sets ~132px and the 13.5px ceiling ~156. Both
  fit, which is the point of the floor — one line for all three at every two-column width, rather
  than two lines under one promise and one under the others, which is the ragged bottom edge the
  brief's "visueel rustig" rules out.

  **That constraint is no longer the binding one, and the floor is kept anyway.** Three track widths
  have stood here: 428.5px, then 452.5 after the split moved to `clamp(560px, 50vw, 720px)`, then
  572.6 now that "de titel moet groter" moved it to `clamp(440px, 40vw, 700px)` (all three at 1201,
  the narrowest two-column window; the arithmetic is at `.isn-hero--scan .isn-hero-grid`). At 452.5
  the third was 142.0px against a 132px subline — 10px of slack, and the floor was doing real work.
  At 572.6 it is 182.1 against the same 132, so there is ~50px spare and the fit no longer decides
  anything. 11.5px stays because it is the smallest size this stylesheet sets a sentence at, which is
  the reason to keep a floor once the geometry stops being one; it is not evidence that 11.5 is
  still measured.
*/
.isn-hero-perks {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.1vw, 16px);
  margin: clamp(26px, 2.6vw, 36px) 0 0;
  padding: 0;
  list-style: none;
}

.isn-hero-perk {
  display: grid;
  justify-items: start;
  gap: 3px;
}

.isn-hero-perk .isn-ic {
  width: 50px;
  height: 50px;
  align-items: center;
  justify-content: center;
  margin-bottom: 7px;
  border-radius: 999px;
  background: var(--a-soft);
  color: var(--i-ink);
}

/*
  The supplied-artwork route: `mark` in ISN_SCAN_PAGE.hero.perks instead of `icon`, an `<img>`
  inside the same disc instead of an inline `<svg>`.

  26px is `.isn-ic > svg`'s number, restated because that selector is a child combinator on
  `svg` and does not reach an image — so without this the file's own intrinsic size would set
  the box and the three discs would stop being a set. Everything above this rule still applies:
  the disc, its tint and the 12px ring are `.isn-hero-perk .isn-ic`'s. **This 26 does not follow
  the disc when the disc grows**, and the paragraph above is why — the marks are sized by
  `optical()` in icons.py at this number, so changing it here alone would break the set and
  changing it in both places would undo the instruction that grew the disc.

  `object-fit: contain` is belt-and-braces. icons.py's marks are written square and centred on
  transparency, so contain is a no-op on a correct file — and a wrong one is then letterboxed
  rather than stretched, which is the failure that is visible instead of the one that is not.
*/
.isn-hero-perk .isn-perk-img {
  width: 26px;
  height: 26px;
  object-fit: contain;
}

.isn-hero-perk-title {
  font-family: var(--i-display);
  font-size: clamp(14px, 1.05vw, 15.5px);
  font-weight: 700;
  letter-spacing: var(--i-track-h3);
  line-height: 1.2;
}

.isn-hero-perk-sub {
  font-size: clamp(11.5px, 0.88vw, 13.5px);
  line-height: 1.3;
  color: var(--i-muted);
}

/*
  ── The category row, which drifts ──────────────────────────────────────────

  Six cards in one row under the copy column and not under the whole hero. It is the third grid
  area — see the track rule at the top of this block — so it is as wide as the copy above it and
  the form card stands beside both.

  It was a six-column grid until the instruction *"dit moet net als die carousel van home page
  bewegen"*. It is now the same rail /nl/ runs, driven by the same module at the same 40px/s:
  `js/categories.js`, unchanged, mounted on every `[data-isn-cats]` by js/isn.js. `.cats-rail`,
  `.cats-viewport` and `.cats-track` are that module's DOM contract, and the markup carries those
  class names alongside the ones below because styles/categories.css — the sheet where they are
  actually styled — belongs to the other identity and **no ISN page loads it**. So the structural
  rules are restated here, exactly as `.isn-rail` restates them for the fourteen-card band, and
  deliberately *not* by reusing that selector: its `.cats-viewport` carries a full-bleed
  `padding: 4px max(var(--i-gutter), calc((100vw - var(--i-shell)) / 2 + var(--i-gutter)))`, which
  puts a row's first card on the page's left gridline. Inside a 573px grid column that padding is
  wider than the column.

  ── The card width is a container query, and that is the whole difficulty ────

  A rail with a seam in it is worse than a row that does not move, and the condition for no seam
  is arithmetic the module states about itself. `measure()` takes the wrap distance as
  `cards[count].offsetLeft − cards[0].offsetLeft` with `count = cards.length / 2` — so `span` is
  the width of **one** set of six, exactly two sets are assumed, and `apply()` wraps the offset
  modulo it. The moment `span` is smaller than the visible box, every cycle uncovers empty ground
  at the right edge:

      no seam  ⟺  span ≥ viewport width

  Across the page that is free — one set of fourteen 180–220px cards is wider than any window. In
  a column it is not, and a *fixed* card width cannot satisfy it; it can only satisfy it at the
  widths someone happened to check. The copy column here runs 572.6px at a 1201 window to 944px at
  1912 (table below), and a `clamp(100px, 7.6vw, 116px)` card — the size this row's cards actually
  are at the wide end — gives a 744px set against a 944px box: 200px of nothing, once a cycle, on
  the widest screen the page ever centres at.

  So the card is a fraction of the row's own inline size, and the fraction is picked so that six of
  them always overrun it. With `v = 100cqi` and `g = var(--hero-cat-gap)`:

      w    = (v − 4g) / 5.4
      span = 6 · (w + g) = (6 / 5.4) · v + 6 · (1 − 4 / 5.4) · g = 1.1111 · v + 1.5556 · g

  so `span − v = 0.1111 · v + 1.5556 · g`, which is **positive for every v and every g ≥ 0**. The
  condition holds identically rather than numerically, and the slack is never less than a ninth of
  the box — about half a card. It also means the gap can stay the `vw` clamp it always was instead
  of becoming a container fraction for consistency's sake: no term of the inequality depends on it.

  5.4 is the divisor, and any divisor under 6 would satisfy the inequality, so the choice is about
  margin and about what the row looks like. At 5.8 the slack is 3.4% of the box — 19px at 1201,
  which is inside a single gap and inside the sub-pixel rounding `offsetLeft` can carry. At 5.0
  exactly five whole cards fill the window, nothing is cut at either edge, and a rail with no
  partial card in it reads as a static row that is inexplicably sliding. 5.4 shows five whole cards
  and about a third of a sixth at every width: `v / (w + g) = 5.4v / (v + 1.4g)`, which approaches
  5.4 from below — 5.31 cards at 1201, 5.34 at 1912.

  Every number below is derived from the rules in this file, not measured; no browser was available
  in this pass. `.isn-shell` is `min(100vw, --i-shell) − 2 × --i-gutter` with `--i-shell: 1800px`
  and `--i-gutter: clamp(20px, 5vw, 56px)`; the copy column is that content width less
  `.isn-hero-grid`'s `column-gap: clamp(28px, 3vw, 44px)` and less the card track's
  `clamp(440px, 40vw, 700px)`:

      window  gutter  content   gap    card    copy = v   g     w        span      span − v
       1201     56     1089    36.03  480.40    572.57   7.21   100.69    647.40    +74.83
       1440     56     1328    43.20  576.00    708.80   8.00   125.33    800.00    +91.20
       1750     56     1638    44.00  700.00    894.00   8.00   159.63   1005.78   +111.78
       1912     56     1688    44.00  700.00    944.00   8.00   168.89   1061.33   +117.33

  `g` is `--hero-cat-gap` at that window, `w` is the formula above, `span` is `6 · (w + g)`. The
  last column is the slack the wrap has, and it is half a card or more in every row. At 1912 the
  shell is at its 1800px cap, which is why the content width stops growing there.

  **Above 1200 `v` is no longer the copy column, and this table is kept as the uncapped case.** A
  later pass ends the rail at the right edge of the third promise's subline rather than at the
  column's — `@media not all and (max-width: 1200px)` at `.isn-hero--scan .isn-hero-cats`, above the
  promise row — so `v` is that cap: 523.00 / 629.07 / 762.19 / 795.52 at these four windows against
  the 572.57 / 708.80 / 894.00 / 944.00 here. The proof one paragraph up is what makes that safe
  without restating it: `span − v = 0.1111 · v + 1.5556 · g` has `v` on both sides, so a narrower
  container narrows the cards in the same proportion and the inequality is untouched. That rule
  carries the same table recomputed, and the card counts below become 5.30 at 1201 and 5.33 at 1912.

  `cqi` and not `vw`: this row's width is a grid column, and above 1200 the column and the window
  do not track each other — it is `minmax(0, 1fr)` against a `clamp(440px, 40vw, 700px)` card, so
  it grows at 60% of the window until that card's 700px ceiling and at 100% of it afterwards.
  Below 1200 it is capped at 600px and stops tracking the window altogether. A `vw` fraction would
  be wrong in three ranges out of three.

  The container is `.isn-hero-cats` itself, and it is **unnamed**, which was checked rather than
  assumed. An unnamed `@container` query resolves against the nearest container ancestor of its
  *subject*, so introducing one here re-targets any unnamed query whose subject can appear inside a
  hero cat. This stylesheet has exactly one other, `@container (max-width: 210px)` above, and its
  subject is `.isn-cc-wm` — which lives inside `.isn-cc`, itself `container-type: inline-size`, so
  that query resolves against the card it is about whatever else on the page becomes a container.
  tokens.css and base.css open none, styles/categories.css is not loaded, and a hero cat contains
  only `.isn-visual`, its `<img>` and the name, not one of which uses a `cq*` unit. Naming would
  also have been false comfort: `container-name` scopes `@container` *rules*, it does not scope
  `cqi`, which always resolves to the nearest inline-size container. Unnamed is what keeps the
  width formula and the phone query below resolving against the same element, which is the one
  thing that has to stay true of them.

  `container-type: inline-size` pays for itself twice: it also gives the row inline-size
  containment, so the `max-content` track inside can never contribute to the width of the grid
  column it sits in, and `100cqi` cannot become self-referential.

  ── The card ────────────────────────────────────────────────────────────────

  It is the rail's card at a third of the size and not a new object: `--visual-h` and `--visual-pad`
  are the two properties `.isn-visual` exposes for exactly this, the tint comes from the same accent
  contract, and `grid-template-rows: minmax(0, 1fr) auto` is `.isn-cat`'s own arrangement — the
  visual absorbs whatever height the tallest name forces on the row, so all six names sit on one
  baseline instead of floating at six different heights. `flex: none` is what the track needs of it;
  the width above is why it is not a fraction of anything.

  `--i-r-sm` and not `--i-r`: a 20px radius on a 100px card leaves 60px of straight edge and the
  card reads as a lozenge. 12px is the radius the chips and the small panels use, and this is that
  size of object.

  The name cannot overrun the card at any width now, which is the measurement the six-column rule
  was tightest on. `Kantoor-` — the shortest half the longest name breaks into, at the soft hyphen
  the content module writes — sets about 48px at the 11px floor. Against
  `w − 2 × clamp(6px, 0.7vw, 10px)` of text: 83.9px at 1201, 105.3px at 1440, 148.9px at 1912 and
  100.7px on a 390px phone. The narrowest of those clears 48 by 36px, where the six-column grid
  cleared it by 4.6 at its own widest measurement and by 0.6 at the one before that.

  **Those four are the uncapped case and three of them have moved.** With the rail capped to the
  promise subline's right edge (the rule above the promise row) the text boxes are **74.70px at 1201,
  90.57px at 1440 and 121.39px at 1912**; the 390px phone is unchanged at 100.7, because that cap is
  scoped above 1200 and the `@container` branch below still measures the same 350px container. The
  conclusion survives — the narrowest, 74.70, still clears `Kantoor-`'s ~48px by 26.7px — but one
  thing about it changed that is worth having written down: `Kantoorartikelen` **unbroken** sets
  ≈101.2px at the 12.5px ceiling, which fits the old 105.3px box at 1440 by 4.1px and misses the new
  90.57px box by 10.6px. So at 1440 the soft hyphen goes from *possibly unused* to *certainly used*,
  and the row is 105.34px tall rather than 90.59 — which is the figure the fold sum at
  `.isn-hero--scan .isn-hero-grid` already assumes. 4.1px is inside the ±5% these advance constants
  carry and 10.6px is outside it, so the "before" of that sentence is genuinely two-valued and the
  "after" is not.

  ── Still no controls, and still no heading ─────────────────────────────────

  No dots, no arrows, no `scroll-snap`, no label above the row — all four are on the brief's list of
  things that must not be there, and the markup prints not one `[data-cats]` button. The module
  looks for both arrows, finds neither and skips them (`if (!control) continue`), so what is left of
  it here is the drift, the drag, the pause on a pointer that comes to rest, and the stop under
  reduced motion. The row's accessible name is an `aria-label` on the `<ul>`, which a screen reader
  announces and a sighted reader never sees; the second set of six is `aria-hidden`, so it is read
  six categories rather than twelve. The one thing that changes with the rail is that a card is now
  cut at the right edge, and that is not clutter — it is the only thing in a row without controls
  that says the row continues.

  Nothing here answers `prefers-reduced-motion`, deliberately, and `.isn-rail` has no such rule
  either. The drift is a `transform` the module writes per frame, so base.css's blanket rule cannot
  reach it and must not pretend to; the module gates it on
  `matchMedia('(prefers-reduced-motion: reduce)')` itself and leaves the drag working, which is
  "do not animate" rather than "do not respond". A CSS rule here could only fight it.

  This row used to be the small half of a pair: the drifting rail of twenty-five cards with real
  arrows stood directly under this hero, and the six here were the ones worth naming beside the form.
  That rail is off this page — `isnScanPage()` in build.mjs no longer calls
  `isnCategoriesSection()`, on the instruction that the row *replaces* the rail here rather than
  previewing it — so these six are the whole category presence of /nl/besparingsscan/, and they carry
  its `#categorieen` anchor with them. What is new in this pass is that the replacement moves the way
  the thing it replaced did. `.isn-cats` still renders on /nl/, unchanged.
*/
.isn-hero-cats {
  --hero-cat-gap: clamp(6px, 0.6vw, 8px);
  container-type: inline-size;
}

/*
  There is no bare `.isn-hero-rail` rule, and the two absences are load-bearing.

  No `position: relative`, which `.isn-rail` needs because an arrow overlay and two edge fades hang
  off it. Neither is printed here, so nothing inside this rail is positioned against it.

  No `::before`, and that is the one the module reads. `rest()` does
  `parseFloat(getComputedStyle(rail, '::before').width) || 0` and treats the answer as the width of
  a fade the row has to open clear of — with no pseudo-element generated the parse is `NaN`, `|| 0`
  takes over, `origin` stays 0 and `measure()` opens the row at `at = 0`, first card flush with the
  left edge of the column. That is right here and wrong on /nl/, where the row bleeds to both window
  edges under a gradient and its first card has to start past it. Adding a decorative `::before` to
  this rail would silently move the row's resting phase.
*/
.isn-hero-rail .cats-viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}

.isn-hero-rail .cats-viewport::-webkit-scrollbar {
  display: none;
}

/*
  Once the module is driving it, it moves by transform and the native scroller gets out of the way —
  one property, set only under the flag the module raises, which is what keeps the no-JS fallback
  below honest.

  `touch-action` and `user-select` are the module's own contract in styles/categories.css and are
  restated because that sheet is not loaded: without the first, a swipe down that begins on a card
  is a race between the browser's pan and the drag handler; without the second, a drag across the
  row selects six category names instead of moving it. `pan-y pinch-zoom` rather than categories.css's
  bare `pan-y`, because the bare value also takes two-finger zoom away from a reader whose fingers
  happen to land on a 100px row of decorations, and nothing here needs that. `.isn-rail` omits both
  properties; correcting that is a change to /nl/'s band and not this row's business.
*/
.isn-hero-rail[data-rail='live'] .cats-viewport {
  overflow: hidden;
  touch-action: pan-y pinch-zoom;
  user-select: none;
}

.isn-hero-cats-track {
  display: flex;
  gap: var(--hero-cat-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
}

/*
  And if js/categories.js never arrives: six cards in an ordinary horizontal scroller the reader can
  push, which is what the fourteen-card band ships as and the fallback this site keeps. One set
  without its wrap gap comes to `6w + 5g = 1.1111v + 0.5556g`, so the row overruns its box by
  `0.1111v + 0.5556g` — 67.6px at a 1201 window, which leaves a third of the sixth card showing. The
  scroll is real, and the edge says the same thing it says when the rail is live.

  The clones are hidden by the *absence* of the flag rather than shown by its presence, and the
  ordering is what makes that safe: `js/categories.js` sets `rail.dataset.rail = 'live'` before it
  measures anything, so by the time `measure()` reads `cards[count].offsetLeft` this rule has already
  stopped applying and the second set is laid out. Written the other way round — `display: none` on
  the clones by default, lifted by `[data-rail='live']` — it would be the same CSS and the same
  hazard; written as a rule the module has to *remove* in time, it is a rule whose one requirement is
  stated in the module's own comment ("First, and before a single measurement").

  `[aria-hidden]` on presence and not `[aria-hidden='true']`, matching `.isn-rail`'s own selector and
  the hook build.mjs names at `isnScanHeroCat`.
*/
.isn-hero-rail:not([data-rail='live']) .isn-hero-cat[aria-hidden] {
  display: none;
}

.isn-hero-cat {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  flex: none;
  width: calc((100cqi - 4 * var(--hero-cat-gap)) / 5.4);
  gap: 4px;
  padding: clamp(6px, 0.7vw, 10px);
  border-radius: var(--i-r-sm);
  background: var(--a-soft);
  color: var(--i-ink);
  text-align: center;
}

/*
  ── Three a screen on a phone, which is the count the row already had ────────

  This replaces an `@media (max-width: 460px)` rule that made the row a 3 × 2 grid, and the count is
  the only thing that survives of it. `span = 6(w + g) = 2v + 2g` with three cards a screen, so the
  no-seam condition holds here for the same reason and with far more room: 712px of set against a
  350px box at a 390px window.

  That window is the arithmetic worth writing down, because it is the one most of this page's readers
  are on. `--i-gutter` floors at 20px there (5vw is 19.5), so the shell holds 350; `--hero-cat-gap`
  floors at 6px (0.6vw is 2.34); `w = (350 − 12) / 3 = 112.67px`, which is the same card the outgoing
  three-column grid produced from `(350 − 12) / 3`, to the pixel. After 2 × 6px of padding that is
  100.7px of text against `Kantoor-`'s ~48px, so the name that decided the desktop count is not close
  to binding here. `v / (w + g)` = 350 / 118.67 = 2.95 cards visible.

  420cqi and not 460px, because what decides whether three cards fit is the row's width and below
  1200 that is `min(shell content, 600px)`, not the window. Expressed in the same measurement the two
  boundaries are 414 (the old 460px window's row) and 420, so the switch moves from a 460px window to
  a 466.7px one — one CSS pixel of gutter, in exchange for a rule that is about the thing it governs.

  The objection the old rule recorded is answered rather than dodged. It rejected a scrolling strip at
  this width on the grounds that it "hides three of the six behind an edge with no control allowed to
  reveal them" — and the drift *is* that control, the one the brief permits because it is not a
  control surface. Under reduced motion, or with a pointer resting on the row, the drag still reveals
  them; with the module blocked entirely the native scroller does. Every path shows all six.
*/
@container (max-width: 420px) {
  .isn-hero-cat {
    width: calc((100cqi - 2 * var(--hero-cat-gap)) / 3);
  }
}

.isn-hero-cat .isn-visual {
  --visual-h: clamp(40px, 3.6vw, 52px);
  --visual-pad: 0;
  /* A grid item's own automatic minimum must not out-vote the fraction above it. */
  min-height: 0;
}

.isn-hero-cat-name {
  font-family: var(--i-display);
  font-size: clamp(11px, 0.87vw, 12.5px);
  font-weight: 700;
  letter-spacing: var(--i-track-h3);
  line-height: 1.18;
}

/*
  A step down from `--i-hero`, and not because the copy column got narrower — `--i-hero` is
  `12.4cqw` and would have followed it. Because the card is the dominant object now, and a 76px
  headline beside it is two things competing to be looked at first.

  `cqw` and not `vw`, because `.isn-hero-copy` is the query container and the headline has to
  respond to *its* width — the whole reason the home hero's own size token is written that way.
  Keep that in mind reading the numbers below: every one of them is a percentage of the *column*,
  so widening the column at `.isn-hero--scan .isn-hero-grid` is what makes the headline grow, and
  this rule only decides where the growth stops.

  ── The two ceilings, and why they are no longer one number ──────────────────

  It was one rule at `clamp(34px, 8.4cqw, 48px)` for both heroes. 48 was measured against a line
  that is not on the page: the comment here said `bedrijf kan besparen.`, 21 characters, where
  content/isn.js:3017 actually reads `['Koop samen in.', 'Check je besparing.']` — 19. The
  0.52em-per-character figure it measured at Figtree 800 is kept, because that part was real.

  **"deze titel moet groter zoals hier" is what splits the rule,** and it could not be answered by
  this rule alone: at 8.4cqw the size was already 84% of the column's arithmetic maximum, so raising
  the ceiling inside a 564.8px column bought a few pixels and then a third line, which is the one
  thing the two-line break in content/isn.js exists to prevent. The column had to widen first; that
  argument, and the reference proportion it rests on, is at `.isn-hero--scan .isn-hero-grid`.

  With the column at 708.8px at 1440, **60px** is where it stops, and 60 is not a taste:

      reference headline / its copy column   = 0.0852   (measured off the supplied screenshot)
      this rule's 8.4cqw                     = 0.0840   (already within 1.5% of it)
      0.0840 × 708.8px copy column at 1440   = 59.5px   → a 60px ceiling engages just above 1440

  So the multiplier is untouched and only the cap moves. What that buys, and the reason a constant
  is worth stating: below the cap the fill is **the same at every window**, because both the size
  and the column scale with the column —

      19 characters × 0.52em × 0.0840 = 0.830 of the column, at 1201, 1280, 1440, any width

  — and above the cap the size is fixed while the column keeps growing, so the fill only falls
  (0.795 at 1750's 894px column). 83% is therefore the worst case anywhere, against the 93% the
  old 21-character measurement was working to. Two lines are safe by a wide margin now, which is
  the point of doing it this way round rather than by testing a few widths.

  ── The category heroes keep 48, and that is a change in kind ────────────────

  They used to share the rule because they share the arrangement: /nl/categorie/<slug>/ puts the
  savings calculator where this page puts the scan card, and a 76px headline beside a card competes
  with it either way. That is still true, and the ceiling is still 48 for them — what is no longer
  shared is the *column*, because only `--scan`'s track split moved. A category hero's copy column
  is unchanged, so a 60px ceiling there would be the overflow the old comment warned about, at a
  window (1280) that is well inside the range. The instruction named one page; the split keeps the
  change on that page.

  The cost is a second number to keep in step, which the old comment explicitly avoided. It is worth
  it here only because the two are now measured against different columns; if the category hero's
  track ever moves, these two rules should be re-derived together rather than merged back on sight.
*/
.isn-hero--scan .isn-hero-h {
  font-size: clamp(34px, 8.4cqw, 60px);
}

.isn-hero--cat .isn-hero-h {
  font-size: clamp(34px, 8.4cqw, 48px);
}

/*
  ── The hand-drawn note, and what is honest about it ────────────────────────

  `Ontdek waar jij kunt besparen` in blue, leaning, with a drawn arrow curving up out of it
  towards the card beside it.

  **The words are Figtree's own italic — the family the entire site is set in — and not a
  handwriting face.** There is none in this repo, and the brief rules out adding one, so the
  hand is in the arrow's path (`ISN_HAND_ARROW_SCAN` in build.mjs: two deliberately unequal
  cubics, unequal barbs) and the type only leans. It reads as an annotation rather than as
  handwriting, which is the closest the constraint allows; drawing the four words as SVG paths
  is the next step if that is not close enough.

  ── Where it sits ──────────────────────────────────────────────────────────

  Last in the copy column, as a flex row of [words][arrow], and not absolutely positioned over
  the card. The brief's own condition is that it must not sit over anything important, and a
  decoration pinned in the space between two grid tracks has to be re-pinned at every width —
  which is how one ends up across a URL field at 1320px. Here the card is simply to its right
  and the arrow points into it.

  `aria-hidden` in the markup, because it says what the `<h1>` two elements above it says.

  ── It is now used twice ────────────────────────────────────────────────────

  /nl/'s besparingsscan section prints the same three parts at the foot of its own copy column,
  with its own words, and adds `.isn-scan-note` for the two things that differ there: the pair is
  pushed to the column's right edge, and the gap above it is larger. Everything else — the italic,
  the blue, the arrow's size and rotation, the baseline alignment — is this rule, so the two cues
  cannot drift apart. `hero` in the class name is a role rather than a place, the way `.isn-eyebrow`
  is used inside the card's third step.
*/
.isn-hero-note {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  margin: clamp(18px, 2vw, 28px) 0 0;
  color: var(--i-blue);
}

.isn-hero-note-text {
  font-style: italic;
  font-size: clamp(17px, 1.5vw, 21px);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.15;
  /* Off the baseline, which no typesetting does and every annotation does. */
  transform: rotate(-2.5deg);
}

/*
  Bigger and shallower than `.isn-hand-arrow`'s 46 × 32, because this one has to cross the
  hero's column gap to land on the card rather than point at the line above it. Rotated the
  other way from `--up`: it leaves the words at their right end and rises to the right.
*/
.isn-hand-arrow--scan {
  width: 84px;
  height: 58px;
  color: var(--i-blue);
  transform: rotate(2deg);
}

/* ── 03 · Categorieën ─────────────────────────────────────────────────────── */

/*
  ── A band under the hero, not a section of its own ─────────────────────────

  The hero and this rail are one composition: a reader looks at the headline, at the scan
  beside it, and then at what can actually be bought through it. So there is no `.isn-section`
  padding here and **nothing at all above the row** — no `<h2>`, and no longer the 12px mono
  label that used to caption it either. Only the hero's own bottom padding separates the two,
  which lands at 36–58px and inside the 40–64 the brief asks for.

  That composition is /nl/'s, and since this pass it is /nl/'s alone. /nl/besparingsscan/ used to
  render this band directly under its own hero as well; it does not any more, because that hero grew
  a row of six category cards inside its left column and the two together showed the same categories
  twice. `isnScanPage()` in build.mjs no longer calls `isnCategoriesSection()`, and every measurement
  in this block is therefore a measurement of /nl/ — where `isnHeroSection()`'s hero is still the
  element directly above it and its bottom padding is still what separates the two.

  That label cost more than its own line: at 17px of type plus a 14–22px margin it held the
  cards ~39px further down, and a caption over a row is what makes the row a section. Removing
  it is the whole of moving the carousel up.

  What is under the band is a `.isn-section`, and its own top padding is what separates the
  two. So the only vertical space this rule owns is a few pixels of shadow room and the line
  under the cards.

  ── The rail is `js/categories.js`'s, unchanged ─────────────────────────────

  `.cats-rail`, `.cats-viewport`, `.cats-track` and the two `[data-cats]` arrows are its DOM
  contract, and the module supplies the 40px/s drift, the drag, the pause on hover, the snap
  to card boundaries and the stop for reduced motion. The structural rules are restated here
  rather than loading styles/categories.css, which brings the other identity's card with them.

  The cards are printed twice so the loop has no seam. The duplicates are `aria-hidden` and
  `display: none` until the module marks the rail live, so a reader whose script never ran gets
  an ordinary horizontal scroller with fourteen cards in it — not twenty-eight.
*/
.isn-cats {
  padding: 0 0 clamp(10px, 1.2vw, 18px);
}

/*
  ── The line under the row ──────────────────────────────────────────────────

  `Staat jouw categorie er niet bij? Plan een kennismaking →`. On the shell, so it starts where
  the first card starts; small and muted, with the link in ink rather than in the blue the
  scan card's inline link uses. That was first argued against the saturated row — four of the
  twenty cards were a #2463FF block and a blue link 20px under them competed for the same glance
  — and the row is tints now, which inverts the reason without changing the answer: `--i-blue` on
  white is the *only* saturated blue left in this band, so a link in it would be the loudest thing
  in a composition whose subject is twenty product photographs. Ink, and the underline carries it.

  Underlined, and permanently: it is the only inline text link on this page, so there is no
  colour convention around it a reader could learn, and an underline is the one signal that
  needs no learning. `text-underline-offset` keeps it off the descenders.
*/
.isn-cats-ask {
  margin: clamp(14px, 1.6vw, 20px) auto 0;
  font-size: var(--i-small);
  color: var(--i-muted);
}

.isn-cats-ask a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-left: 4px;
  color: var(--i-ink);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.isn-cats-ask a:hover {
  color: var(--i-blue);
}

.isn-rail-arrows {
  display: flex;
  flex: none;
  gap: 8px;
}

/*
  ── One at each end of the row, vertically centred ──────────────────────────

  The pair used to sit in the top-right corner beside the label. Two things were wrong with
  that and only one of them was the label: a pair of arrows in a corner is a control panel,
  and which end of the row each one moves has to be read off the glyph. Standing at the two
  ends, the button *is* the direction — the same arrangement the reference carousel uses.

  One overlay rather than two positioned buttons: `inset: 0 X` with `space-between` puts them
  at equal distance from the rail's two edges, which is the one thing that has to stay true at
  every width, and it stays true without either number being written twice.

  `pointer-events` is not a nicety here. The overlay spans the full width and height of the
  rail, so left as it is it would swallow every hover, drag and wheel event the module binds to
  the viewport — the drift would never pause and the row would never drag. So the strip is
  transparent to the pointer and the two buttons opt back in.

  The inset is under the gutter (56px at the widest) and the button is 38, so at desktop widths
  each arrow sits in the white margin beside the row rather than on a card. On a phone the
  gutter is 20 and it overlaps the outermost card by ~30px, which is what a carousel control
  does there and why the button is an opaque white disc with a border.
*/
.isn-cats .isn-rail-arrows {
  position: absolute;
  inset: 0 clamp(9px, 1vw, 18px);
  z-index: 2;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
}

.isn-cats .isn-rail-arrow {
  pointer-events: auto;
  box-shadow: 0 2px 10px rgb(10 10 10 / 8%);
}

.isn-rail-arrow {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: 1.5px solid var(--i-line);
  border-radius: 50%;
  background: var(--i-white);
  color: var(--i-ink);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease),
    background-color var(--dur-fast) var(--ease);
}

.isn-rail-arrow:hover {
  border-color: var(--i-ink);
  background: var(--i-grey);
}

.isn-rail-arrow > svg {
  width: 15px;
  height: 15px;
}

.isn-rail {
  position: relative;
}

.isn-rail .cats-viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  /*
    The rail runs to both window edges but its first card lines up with the shell, so the
    row reads as continuing past the page rather than as a box inside it.
  */
  padding: 4px max(var(--i-gutter), calc((100vw - var(--i-shell)) / 2 + var(--i-gutter)));
  scroll-snap-type: x proximity;
}

.isn-rail .cats-viewport::-webkit-scrollbar {
  display: none;
}

/* Once the module is driving it, it scrolls by transform and the native one gets out. */
.isn-rail[data-rail='live'] .cats-viewport {
  overflow: hidden;
  scroll-snap-type: none;
}

.isn-rail .cats-track {
  display: flex;
  gap: clamp(10px, 1vw, 14px);
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
}

.isn-rail:not([data-rail='live']) .isn-cat[aria-hidden] {
  display: none;
}

/*
  180–220 wide and ~215–245 tall, which is the brief's card and about a third of the size the
  card was: it used to be 226–292 wide with a name *and* a line of explanation on it, which
  made it 250px tall of which half was type. The line of explanation is gone — it said what the
  name already said — so what is left is one object on a colour with its name along the bottom,
  and at that content a smaller card is a better one. Six of them are on screen at 1240 rather
  than four, which is also what makes a drifting row read as a catalogue.

  ── The ground is the accent's *tint*, and it is literally the same declaration ──

  `background: var(--a-soft)`, which is the identical declaration `.isn-svc` carries on the four
  Analyse / Bundelen / Onderhandelen / Resultaat cards further down the page. Measured in the
  browser on the built page, a blue rail card and the Analyse card are both
  `rgb(238, 243, 255)`; orange `rgb(255, 241, 231)`, yellow `rgb(255, 248, 217)`, green
  `rgb(234, 248, 241)`. Red has no process card, and takes the same token the mega-menu's red
  marks already paint — `--i-red-soft`, `rgb(255, 240, 240)`. That equality is the rule and not
  a coincidence: one brand colour has one light background shade on this site, so the value is
  read from the accent block at the top of this file and never restated at a component.

  It is also what the palette's ratio asks for. 65–75% white and warm off-white against 10–15%
  accent was being spent by twenty saturated blocks in the second screen of the page; the
  saturated `--a` now appears only where something has to be loud — the step numbers, the ticks,
  the links, the final block. The row still reads as five repeating colours, because a tint
  carries hue at a glance; what it stops doing is out-shouting the headline above it.

  ── Which takes `--card-fg` off this card ────────────────────────────────────

  Every ground here is now a tint, so there is no per-accent ink to choose: the name is
  `--i-ink` on all twenty. Computed against these five tints that is 17.8:1 on blue, 17.9 on
  orange and red, 18.1 on green and 18.6 on yellow — the whole set inside a fifth of a stop of
  each other, where the saturated fills needed white on blue at 4.9:1 and ink on yellow at
  12.9:1 to clear AA at all. One ink for twenty names, and it is the darkest one in the palette.

  ── What the cut-outs paid for it, and why nothing was added back ────────────

  The seventeen PNGs were shot to sit on a saturated ground, so the ground moving under them is
  a real cost and it was measured rather than assumed: for each image, the share of its
  silhouette edge whose pixels land within 1.2:1 of the ground behind them. On that measure
  fourteen of the seventeen got *better* — `bouwmaterialen` from 40.4% of its edge to 1.3%,
  `schoonmaakproducten` 26.7% to 0.5%, `it-hardware` 22.3% to 0.0% — because most of these
  subjects are dark objects that were losing their outline against a strong colour, and mean
  edge contrast rose on all seventeen. `meubels` is unchanged at 0.0%.

  Two got worse, and one of those two matters. `werkkleding` went 5.7% to 6.5%, which is inside
  the noise of a hi-vis jacket's own outline. `transport` went 4.7% to 26.9%: the cargo box of a
  white box truck is near-white and #FFF1E7 is near-white, so a quarter of that truck's contour
  is now carried by the PNG's own shading rather than by the ground. It reads at card size and
  it is the weakest of the twenty.

  No shadow, border, outline or darker orange was added to compensate, and that is a decision
  and not an omission. Every one of those would be a new value on one card, which is the thing
  this section just spent twenty cards removing; and the fix that costs nothing structural is a
  cut-out re-shot with an edge, which is an asset change. So the defect is recorded here with a
  number instead of being hidden behind a hairline.
*/
.isn-cat {
  flex: none;
  width: clamp(180px, 15vw, 220px);
  /* The card's name is a link now, on both surfaces that print one, and its overlay is what
     makes the whole card the target. See `.isn-cat-link` in section 16. */
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 6px;
  padding: clamp(12px, 1.2vw, 16px);
  border-radius: var(--i-r);
  background: var(--a-soft);
  color: var(--i-ink);
  scroll-snap-align: start;
}

/*
  `overig` is not in the rail today — `ISN_CATEGORIES.cards` leaves it out and says why — so this
  is the defensive rule for the day it is. Its two colour declarations are now restatements of
  what the accent block already resolves for `--grey` (`--a-soft` is `--i-grey`, `--card-fg` is
  `--i-ink`), and they stay written out because the hairline is what the rule is *for*: grey is
  the one tint that is a neutral, so its card is the one whose edge does not survive on white
  even as a hue difference. The five coloured cards carry no border and do not get one here.
*/
.isn-cat--grey {
  background: var(--i-grey);
  border: 1px solid var(--i-line);
  color: var(--i-ink);
}

.isn-cat .isn-visual {
  --visual-h: clamp(132px, 11vw, 158px);
  /* A grid item's own automatic minimum must not out-vote the fraction above it. */
  min-height: 0;
}

.isn-cat-name {
  font-family: var(--i-display);
  font-size: clamp(15px, 1.3vw, 18px);
  font-weight: 700;
  letter-spacing: var(--i-track-h3);
  line-height: 1.15;
}

/* ── 04 · Samen inkopen ───────────────────────────────────────────────────── */

/*
  ── Five columns, and two of them are the arrows ────────────────────────────

  `1fr auto 1fr auto 1fr`: three equal steps with a thin arrow between them, and the arrow
  is a grid cell rather than something hung off a card's edge. That is what makes the row
  survive a resize — the arrow is measured, so the air either side of it is the same
  `column-gap` as everywhere else and no number has to track a column's width. It is also
  why the three steps are sibling `<figure>` elements and not an `<ol>`; see build.mjs.

  ── 1180 and not 1060, and 258 and not 168 ──────────────────────────────────

  The row is half again as tall as the one it replaces and 120px wider, and that is the
  brief: "maak het geheel groter dan de huidige illustratie zodat de sectie beter gevuld is."
  `--flow-h` runs 196 → 258 where it ran 112 → 168.

  1180 is still a cap and not the shell. The shell is 1328px at 1440, and three figures
  spread over that much width stop reading as a sequence because the eye travels further
  between them than the story is long. At 1180 with a 28px gap and a 48px arrow a step
  column is 324px, and the widest figure in the row — the company cluster at 278.6 — clears
  it by 45px, so nothing in this row is ever sized by its column.

  ── Three custom properties, because the row is one object at three sizes ────

  `--flow-hub` and `--flow-res` are fractions of `--flow-h` rather than clamps of their own:
  0.76 for the middle bubble, 0.68 for the right one. One clamp drives the whole row, so the
  three bubbles keep their ratio to each other at every width, and the hierarchy — cluster,
  hub, result — is the same picture at 1024 as at 1440. Three separate clamps would cross
  somewhere in the middle of the range and reorder it.

  The hub is the largest single bubble because it is the sentence's verb, and the result is
  the smallest because it is the consequence and not the mechanism. All three carry the same
  `--i-blue-soft`: "zodat links/midden/rechts als één systeem voelen" is in the brief twice,
  and tint is what makes three unlike objects one system. Size is then free to say which
  matters, which it could not do if the three were also three colours.
*/
.isn-flow {
  --flow-h: clamp(196px, 18.2vw, 258px);
  --flow-hub: calc(var(--flow-h) * 0.76);
  --flow-res: calc(var(--flow-h) * 0.68);
  --flow-arrow: clamp(32px, 3.4vw, 48px);
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr;
  align-items: start;
  column-gap: clamp(16px, 2vw, 28px);
  max-width: 1180px;
  margin-inline: auto;
}

/*
  On the bubbles' centreline and not the step's. The arrow points *between the figures*, so
  it belongs at half `--flow-h` and not at the centre of the whole step, which sits lower by
  half a two-line caption. `var(--flow-arrow) * 6 / 44` is half the arrow's own rendered
  height, derived from its 44 × 12 viewBox rather than measured, so the alignment holds at
  both ends of the clamp instead of only at the wide one.
*/
.isn-flow-arrow {
  width: var(--flow-arrow);
  height: auto;
  margin-top: calc(var(--flow-h) / 2 - var(--flow-arrow) * 6 / 44);
  color: var(--i-muted);
}

/*
  ── This head was centred, and is now ranged left like every other ──────────

  It was the one centred head on the page, on the brief's own words — "Kop en lead staan
  gecentreerd, net als in referentie 1" — with the argument that a symmetrical diagram wants a
  symmetrical head, since a left-ranged head over a row whose middle column is its focus
  points the reader at the wrong column before they start. That is overruled on request: this
  head is to read like the others. The instruction wins over the reference, and it is recorded
  rather than quietly dropped, because the next person to reach for `text-align: center` here
  should know it was tried and taken out on purpose.

  Three declarations went with it, and only one of the three was about the diagram. The `h2`'s
  `max-width: 900px` existed to force a second line — at the clamp's 54px ceiling the
  45-character headline sets about 1215px, which the shell's 1328 accommodates — and a cap that
  exists to shape a centred block has nothing to say about a ranged one, so the head now sets
  against the shell like the rest and `text-wrap: balance` decides the break. That `balance`
  lives on `.isn-h` already; repeating it here only looked load-bearing. The lead's
  `margin-inline: auto` was pure centring machinery and is gone with it, leaving the 62ch
  measure the base rule gives, which is now read from a rule rather than line by line.

  What survives is the margin: 40 at 1024 and 52 at 1440, against the page's own
  `clamp(38px, 4.4vw, 66px)`, which is 63 here. That was never an alignment argument. Every
  other section on this page opens onto a grid of cards or a full-bleed rail, where a large gap
  reads as air, and this one opens onto a single row — at 63px that row floated away from the
  sentence explaining it.
*/
.isn-section--collective .isn-head {
  margin-bottom: clamp(40px, 3.6vw, 56px);
}

/*
  `minmax(0, 1fr)` and not the implicit `auto` track a bare `display: grid` would give. An
  `auto` track is max-content sized, and the widest label unwrapped is wider than the 324px
  column, so the track would have overflowed the column and run under the arrow. With a 1fr
  track the column is the measure, which is what `.isn-flow-cap` relies on.
*/
.isn-flow-step {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(14px, 1.3vw, 20px);
  justify-items: center;
  margin: 0;
}

/*
  One fixed height for all three figures, and each figure centred in it. That is what makes
  the three bubbles sit on one centreline while being three different sizes, and it is what
  the arrow's `margin-top` reads. A `height: auto` box would give each column the height of
  its own bubble and the two arrows would land at three different places.
*/
.isn-flow-art {
  display: grid;
  place-items: center;
  width: 100%;
  height: var(--flow-h);
}

/*
  ── The six bubbles, and why the box has a fixed aspect ratio ───────────────

  The cluster is a coordinate space: `--x` and `--y` are percentages of this box, so the box's
  proportion decides the geometry. `aspect-ratio: 1.08` fixes that proportion at 108 × 100 at
  every viewport, which is what lets one arithmetic check hold everywhere.

  It is not decoration. Without it the box would keep `--flow-h` for its height and lose width
  in the narrower columns a 720–1080px viewport gives — the clamp is on its 196px floor there
  while the column is still shrinking — pulling every horizontal distance in by up to an
  eighth while the vertical ones stayed. Measured on the table in build.mjs, the top bubble
  would have touched its left neighbour at about 760px wide.

  With it, the closest pair is the middle bubble and the bottom-right one: 3.05 of those 108
  units apart, which is 7.9px at `--flow-h` 258 and 6.0px at 196. Every other pair has more,
  the tightest being 3.7 between the top bubble and each of the two below it. The two bounds
  that matter are as tight — the top bubble sits 1.8 units below the box's top edge and the
  bottom-left one 1.3 above its bottom — and nothing here clips, so what those margins buy is
  that the six bubbles' own bounding box *is* the box `aspect-ratio` declares. That is what
  makes centring the cluster in the art column the same thing as centring the drawing.

  So: raising the dots past 30% of the box, or moving any pair inward, spends that 3.05. Two
  bubbles that touch turn a group into a blob, and the brief asks for a group.

  `width: calc(var(--s) * 30%)` with `aspect-ratio: 1` sizes each bubble from the box rather
  than from `--flow-h`, which is the same scale-invariance seen from the other side: one
  number changes the row's size and no distance inside the cluster has to be re-derived.
*/
.isn-flow-cluster {
  position: relative;
  width: min(100%, calc(var(--flow-h) * 1.08));
  aspect-ratio: 1.08;
}

.isn-flow-dot {
  position: absolute;
  left: var(--x);
  top: var(--y);
  display: grid;
  place-items: center;
  width: calc(var(--s) * 30%);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--i-blue-soft);
  transform: translate(-50%, -50%);
}

/*
  55% of the widest bubble is 48.7px, which is the box icons.py wrote the file at — and that
  direction of dependency is deliberate, because the box was chosen from the drawing's own
  1.3% stroke rather than from this layout. The docstring in icons.py has the measurement and
  the DPR 1 test that set it. `height: auto` and not 100%: the ink is 1019 × 927, so a square
  box would letterbox it and the declared 48 × 48 would stop being true of anything.
*/
.isn-flow-dot img {
  display: block;
  width: 55%;
  height: auto;
}

/*
  ── The middle bubble holds the real mark ───────────────────────────────────

  "Gebruik NIET weer een blauwe cirkel met de tekst Inkoop Service Nederland erin" — so what
  was a saturated blue disc with three lines of display type in it is now the five-petal mark
  in the same soft tint as the six bubbles beside it. The name it used to carry is in the
  label under the figure, where it is real text at real weight instead of type inside a
  drawing.

  196px at the top of the clamp with the mark at 50% of it, so 98px of logo. The mark is the
  one object in this row with five colours in it, and half the bubble is the largest it can be
  before the tint stops reading as a bubble and starts reading as a plate behind a logo.
*/
.isn-flow-hub {
  display: grid;
  place-items: center;
  width: var(--flow-hub);
  height: var(--flow-hub);
  border-radius: 50%;
  background: var(--i-blue-soft);
}

.isn-flow-hub img {
  display: block;
  width: 50%;
  height: auto;
}

/*
  The right bubble, and the only figure in the row that is drawn rather than delivered — no
  contract illustration came with the brief, and build.mjs says so where the paths are. 175px
  with the sheet at 56% of it, so 98px of drawing: the same height as the logo in the middle
  bubble, in a smaller circle, which is the third step reading as a consequence of the second
  rather than as another step of equal weight.

  `color` here rather than on the `<svg>`, because the sheet strokes `currentColor` and its
  three ticks override it. Blue for the paper, `--i-green` for the ticks: the outcome colour
  on this page, on the one element in the row that is an outcome, echoing the quote card below
  it. `--i-green` and not the card's darker `#0d7a4a` — at 1.2 units of stroke, about 1.1px
  rendered, the lighter green is the one that survives being a hairline.

  `--i-blue` is not a near-match to the delivered artwork, it is the same blue: the modal
  stroke pixel of `assets/src/isn-flow/company.png` is (43, 103, 251) against this token's
  (36, 99, 255), four points a channel apart. What does differ is rendered weight — the sheet
  puts 1.11px on screen where the company icons put 0.60px — and that is the relative rule
  build.mjs argues rather than a mismatch: both are drawn at about 1.2% of their own height,
  and the sheet is simply displayed at twice the size. Absolute weights that matched would
  mean the larger drawing was drawn thinner than the smaller one.
*/
.isn-flow-res {
  display: grid;
  place-items: center;
  width: var(--flow-res);
  height: var(--flow-res);
  border-radius: 50%;
  background: var(--i-blue-soft);
  color: var(--i-blue);
}

.isn-flow-doc {
  width: auto;
  height: 56%;
}

.isn-flow-tick {
  stroke: var(--i-green);
}

/*
  ── Two lines per figure, and the second one is the qualifier ───────────────

  `Individuele bedrijven` / `verschillende volumes`, `Gebundelde vraag` / `via Inkoop Service
  Nederland`, `Betere prijzen` / `en sterkere voorwaarden`. Read across, the first lines are
  the sentence the section makes and the second lines are the detail each step adds — which is
  why the weight and the colour differ and the size barely does. 16 against 13 is a step; 16
  against 11 would make the second line a footnote, and it is not a footnote, it is the half
  of the label that says *how*.

  No `max-width`, deliberately: the column is the measure, so the longest label wraps where
  324px says it wraps and all three stay centred under their own figure rather than under a
  width invented for them.
*/
.isn-flow-cap {
  display: grid;
  gap: 4px;
  text-align: center;
}

.isn-flow-cap-t {
  font-size: clamp(14px, 1.1vw, 16px);
  font-weight: 700;
  line-height: 1.3;
  color: var(--i-ink);
}

.isn-flow-cap-s {
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--i-muted);
}

/*
  ── The line the section closes on was deleted, twice ───────────────────────

  A mint pill at `width: fit-content` held `Samen bereiken we meer dan ieder voor zich.` under
  the row. The pill was rejected in those terms — "de quote onderaan moet meer lijken op
  afbeelding 1 en niet de kleine pill die er nu staat" — so it became a card at the row's own
  1180px, with a green `&ldquo;` on its left, about seventy lines of measurement arguing where
  that glyph's ink sat against the sentence's ascender.

  Then the sentence itself was deleted. So the card is not a smaller pill again and the glyph
  arithmetic is not preserved here for reuse: `.isn-flow-quote`, `.isn-flow-quote-mark` and
  `.isn-flow-quote-text` are gone, `C.quote` is gone from content/isn.js, and the `<p>` is gone
  from build.mjs. The section ends on `.isn-flow`, whose last row already sets `Betere prijzen`
  / `en sterkere voorwaarden` under the figure that draws it.

  What is left of it is one number, and it is deliberately left: the last thing in the section
  is a `.isn-flow-step`, so the space under the row is the section's own bottom padding and
  nothing here adds to it. That is why removing three rules needed no fourth to compensate.
*/

/* ── 05 · Zo werkt het ────────────────────────────────────────────────────── */

.isn-step-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(16px, 1.8vw, 26px);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;

  /*
    The drawings' shared cycle, declared on the list because it belongs to the three of them
    together and not to any one card. `--una-turn` is a third of `--una-cycle` — that is the
    whole scheme, and the two must stay in that ratio or the three turns overlap. `--una-beat`
    is the gap between elements inside one drawing. See the drawing block below.
  */
  --una-cycle: 13.2s;
  --una-turn: 4.4s;
  --una-beat: 200ms;
}

.isn-step-card {
  display: grid;
  grid-template-rows: auto 1fr;
  border: 1px solid var(--i-line);
  border-radius: var(--i-r-lg);
  background: var(--i-white);
  overflow: hidden;
}

/*
  The illustration surface: the accent's tint, the step number in the corner, and a drawing
  made of boxes with text in them. Not an SVG and not a picture — speech bubbles, growing
  bars and collapsing offer rows are type and rectangles, so they inherit the face, they
  reflow at 320px, they animate in CSS and they stop when the reader has asked for
  stillness. The thin grey line illustrations the brief rules out by name were the
  alternative.
*/
.isn-step-art {
  position: relative;
  height: clamp(178px, 19vw, 238px);
  padding: clamp(16px, 1.7vw, 24px);
  background: var(--a-soft);
  overflow: hidden;
}

.isn-step-n {
  position: absolute;
  top: clamp(12px, 1.4vw, 18px);
  right: clamp(14px, 1.6vw, 22px);
  font-family: var(--i-display);
  font-size: clamp(30px, 3.4vw, 46px);
  font-weight: 800;
  letter-spacing: var(--i-track-hero);
  line-height: 1;
  color: color-mix(in srgb, var(--a) 26%, transparent);
}

.isn-art {
  position: relative;
  height: 100%;
}

/*
  ── The three drawings, and the one loop on this page ────────────────────────

  One outline drawing per card, in `--a-ink` on the accent's own tint, and **the drawing
  itself never moves.** Each card holds one addition — words inside the speech bubbles, a
  magnifier over the chart, terms written onto the sheet and a price marked down on it —
  and that addition is the only animated thing on the card. The reference works exactly this
  way: the three icons stand there permanently and something arrives inside one of them.

  That is the opposite of what stood here first, which redrew each entire drawing on its turn.
  Redrawing reads as loading. For a moment the card has no picture, and a bar chart that
  assembles itself says "wait" in a card whose job is to say "analysis". Holding the drawing
  and moving one part of it says the thing the drawing is about instead.

  It also fixes the state nobody looks at long enough to notice is broken: with the drawing
  static, every frame of the loop is a finished illustration.

  ── Two levels of stagger, and both live in one calc ─────────────────────────

  Every animated element runs the *same* cycle, and where it sits in that cycle is its delay:

    var(--turn) * --una-turn    which of the three cards this is — set on the <li>
    var(--n) * --una-beat       which beat inside that card's turn the element enters on

  Because the duration is shared the three can never drift: card 1 is spoken at 0s, card 2 is
  examined a third of a cycle later, card 3 is agreed at two thirds, and then card 1 again. Each
  animation moves for about a quarter of the cycle and holds its gone state for the rest,
  which is what leaves room for the other two turns.

  `backwards` covers the first pass, and it is the reason the additions carry the animation
  rather than the drawings: before its turn an element holds the 0% frame, so card 3 sits there
  as a whole sheet of A4 — folded corner, empty badge on its bottom corner — with nothing written
  on it yet. A complete drawing waiting, not a blank.

  The easing sits on the keyframe that *starts* a moving segment, never on the animation. A
  timing function on the animation is spread across the whole cycle, so it spends its curve
  on the hold and arrives at the motion in a straight line.

  `.is-in` is read from the *list* and not from the card, so all three start on one instant.
  The comment on the <ol> in build.mjs says why that matters on a phone.
*/
.isn-una {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--a-ink);
}

/*
  The two filled shapes in the set: the front speech bubble on card 1, and the badge on the corner
  of card 3's sheet. Both are filled with the surface they sit on so that what is behind them is
  occluded rather than crossed, which is what makes two outlines read as one shape in front of
  another — card 1's near bubble over the far one, card 3's badge over the sheet's corner. Two
  outlines that merely cross at 4 units of stroke are one flat tangle.

  `--a-soft` and not `--i-white` in both cases. A white shape would be a third colour in a
  two-colour drawing, and on card 3 it would be worse than that: a white disc on the paper reads
  as a hole punched through it rather than as a badge sitting on it.

  Card 2 needs nothing: the lens comes to rest clear of the bars, which is the whole reason its
  resting position is where it is.
*/
.isn-una-plane {
  fill: var(--a-soft);
}

/* Move and scale from the shape's own box, not from the viewBox origin. */
.isn-una-scan,
.isn-una-pop {
  transform-box: fill-box;
  transform-origin: center;
}

.isn-step-cards.is-in .isn-una-write,
.isn-step-cards.is-in .isn-una-scan,
.isn-step-cards.is-in .isn-una-pop {
  animation-duration: var(--una-cycle);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
  animation-delay: calc(var(--turn) * var(--una-turn) + var(--n) * var(--una-beat));
}

.isn-step-cards.is-in .isn-una-write {
  animation-name: isn-una-write;
}

.isn-step-cards.is-in .isn-una-scan {
  animation-name: isn-una-scan;
}

.isn-step-cards.is-in .isn-una-pop {
  animation-name: isn-una-pop;
}

/*
  A stroke drawing itself, then leaving — the four lines of speech on card 1, and the three rules
  written onto card 3's sheet, which is the same mechanism and gets the same keyframes rather
  than a near-identical copy of them. On card 3 it is also the literal thing: a contract being
  filled in is lines appearing left to right, which is what these keyframes already did for
  speech. `--len` is the stroke's length in user units, taken from the geometry in build.mjs: the
  dash pattern is a single dash that long, pushed off the end of the line and then pulled back
  onto it.

  The dash lives in the keyframes and not on the element, which is what lets the
  reduced-motion block drop the animation and get an ordinary unbroken line rather than one
  frozen at whatever offset it was holding. Every frame declares `stroke-dasharray`, including
  the last: declared in one keyframe only, its other endpoint would be the element's own
  `none`, and a dash length does not interpolate to `none`.

  The fade-out is at each line's own 30% rather than at a shared instant, so the four lines
  leave in the order they arrived. Their beats are 200ms apart at most, so this is a hair of
  stagger, not a wave.
*/
@keyframes isn-una-write {
  0% {
    stroke-dasharray: var(--len);
    stroke-dashoffset: var(--len);
    opacity: 1;
    animation-timing-function: var(--ease);
  }

  7% {
    stroke-dasharray: var(--len);
    stroke-dashoffset: 0;
    opacity: 1;
  }

  20% {
    stroke-dasharray: var(--len);
    stroke-dashoffset: 0;
    opacity: 1;
    animation-timing-function: var(--ease);
  }

  26%,
  100% {
    stroke-dasharray: var(--len);
    stroke-dashoffset: 0;
    opacity: 0;
  }
}

/*
  The magnifier: it arrives over the middle bar, sweeps up across the tallest one, comes to
  rest clear of the chart and goes again. Three translate stops rather than two, because a
  single straight glide reads as a slide and the arc reads as looking.

  It rests where the drawing puts it with no transform at all — the same place the
  reduced-motion still shows it — so the resting frame of the loop and the static frame are
  the same composition, and the geometry comment in build.mjs only has to be true once.
*/
@keyframes isn-una-scan {
  0% {
    opacity: 0;
    transform: translate(-58px, 22px) scale(0.62);
    animation-timing-function: var(--ease);
  }

  5% {
    opacity: 1;
    transform: translate(-58px, 22px) scale(1);
    animation-timing-function: var(--ease);
  }

  12% {
    opacity: 1;
    transform: translate(-30px, -4px) scale(1);
    animation-timing-function: var(--ease);
  }

  20% {
    opacity: 1;
    transform: translate(0, 0) scale(1);
    animation-timing-function: var(--ease);
  }

  26%,
  100% {
    opacity: 0;
    transform: translate(0, 0) scale(0.94);
  }
}

/*
  Card 3's tick, and since the arrow beside it went, the only thing this keyframe drives: it appears
  where it belongs, holds, and goes. A fade with a small scale under it — 0.9 in, 0.96 out — because
  a pure fade at this stroke weight reads as a layer being switched on, and 4 units of stroke
  arriving at full size looks stamped rather than written. Scale and not translate: nothing on this
  card slides, so nothing needs a path.

  The 0% frame is held to 6% rather than left at 0%, and that is the sequencing. The three rules
  above are `isn-una-write` on beats 0, 1 and 2, and the last of them finishes drawing at 7% of
  the cycle *plus* its own beat — 10.12s against the tick at 6% + beat 4, which is 10.39s. So the
  price cannot be agreed while a term is still being written.

  The three remaining percentages are set by one requirement the other two keyframes do not have:
  this card has a *result*, and the frame where the sheet is filled in and its price agreed has to
  last long enough to be a state rather than a transition. 11% in and 19% out make that frame
  1.06s. The same two percentages used to make 0.86s, because two elements a beat apart shared this
  keyframe and a shared full-ink frame is only (out − in) minus the 200ms between them — so losing
  the arrow handed that beat back instead of needing new numbers here. The obvious 12% / 17% would
  give 0.66s, and 11% costs the tick's fade-in about an eighth of a second it does not need at this
  stroke weight.

  The rules leave before the coin's mark does, and that is `isn-una-write`'s 20% against this
  keyframe's 19% plus four beats: every stroke on the card is at full ink from 11.05s, the first
  rule starts fading at 11.44s, and the tick holds to 12.11s. The sheet emptying while the mark is
  still there is the right order for the reading — the paper was the working, the agreement is the
  result.

  Out at 26%, which is where the other two sit. This one was 24% for one reason: the arrow entered a
  full second into the turn, so 26% + 1.0s would have run the tail of card 3's fade over the start
  of card 1's next turn. Nothing this keyframe drives enters later than beat 4 now, and 26% + 0.8s
  lands at 13.03s against a cycle that turns over at 13.2s, so the exception goes with the arrow.
*/
@keyframes isn-una-pop {
  0%,
  6% {
    opacity: 0;
    transform: scale(0.9);
    animation-timing-function: var(--ease);
  }

  11% {
    opacity: 1;
    transform: scale(1);
  }

  19% {
    opacity: 1;
    transform: scale(1);
    animation-timing-function: var(--ease);
  }

  26%,
  100% {
    opacity: 0;
    transform: scale(0.96);
  }
}

.isn-step-body {
  padding: clamp(20px, 2.2vw, 30px);
}

/*
  The step name, and it is the only heading on the card.

  This rule used to typeset a mono uppercase eyebrow in `var(--a)`, with the display type
  below it in `.isn-step-claim`. The eyebrow is out — two headings over one paragraph is a
  level more than a card needs — so the display type moves up into the heading and
  `.isn-step-claim` is gone. Ink, not accent: the accent already runs the card's tinted
  surface and its number, and a third coloured element in the same card competes with them.
*/
.isn-step-h {
  margin: 0;
  font-family: var(--i-display);
  font-size: clamp(21px, 1.9vw, 26px);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
}

.isn-step-copy {
  margin: 12px 0 0;
  font-size: var(--i-small);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/* ── 06 · Waarom samen inkopen ────────────────────────────────────────────── */

.isn-section--compare {
  background: var(--i-paper);
}

/*
  The head keeps the page's left edge; the comparison under it does not.

  Every other section on this page runs its content the full width of the shell, and this one
  cannot: a comparison is a *pair*, and at the 1328px shell the two cards are 652 wide with the
  thing being compared 650px away from the thing it is compared to. The eye reads that as two
  unrelated panels. Capped at 1060 and centred they are 518 each — narrow enough to read as a
  pair, wide enough that the longest row on either side still takes one line, which is the whole
  mechanism behind the rows lining up pair for pair. 1060 is the top of the band the brief asks
  for; below about 1000 the longest row wraps at this type size and the pairing goes.

  Centred rather than flush left, and against a headline that is flush left. That asymmetry is
  deliberate and it is asked for: the head belongs to the page, the cards belong to each other.
*/
.isn-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  max-width: 1060px;
  margin-inline: auto;
}

/*
  Two tracks and never `auto-fit`, which is what this was.

  `repeat(auto-fit, minmax(300px, 1fr))` is the right tool for a set of cards whose count the
  content decides. Here the count is two and always two — the section is one comparison — and
  `auto-fit` costs the one thing that matters: it lets the pair reflow to one column at a width
  the media query below does not know about, which is where the cards' equal height silently
  stops being a comparison and becomes a list.

  Equal height needs no rule. Grid items stretch, so both cards take the row's height, which is
  set by whichever of them is taller — measured, both 305 at 1440.
*/

/* A lead this section keeps to itself: the global 62ch runs 744px here, and the head reads
   stronger against the capped block below it at roughly 650. */
.isn-section--compare .isn-lead {
  max-width: 54ch;
}

.isn-cmp {
  padding: clamp(22px, 2.4vw, 34px);
  border-radius: var(--i-r-lg);
}

.isn-cmp--alone {
  background: var(--i-white);
  border: 1px solid var(--i-line);
}

/*
  The right-hand card is the one the page is arguing for, so it is the one that carries
  colour: the blue tint and a blue border against the neutral card beside it. Nothing about
  the left card is drawn as wrong — the brief's own framing is "Alleen inkopen kan", and a
  crossed-out list would be making a claim it does not make.
*/
.isn-cmp--together {
  background: var(--i-blue-soft);
  border: 1.5px solid var(--i-blue);
  /* The page's lightest existing shadow, and it is here to lift the card a millimetre off the
     paper — not to glow. Anything with a spread reads as a SaaS pricing table. */
  box-shadow: 0 2px 6px rgb(10 10 10 / 6%);
}

.isn-cmp-h {
  margin: 0 0 22px;
  font-family: var(--i-display);
  font-size: clamp(21px, 1.9vw, 26px);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
}

.isn-cmp-rows {
  display: grid;
  gap: 20px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-cmp-rows li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  font-size: var(--i-body-size);
  line-height: 1.4;
}

.isn-cmp--alone .isn-cmp-rows li {
  color: var(--i-muted);
}

.isn-cmp--together .isn-cmp-rows li {
  font-weight: 600;
}

/*
  One mark per side: a 22px disc with its glyph knocked out in white.

  This replaces five different line icons in the left column — `isnIcon` drawings, one per row,
  each an object naming what the row was about. Five drawings is five things to read before the
  five sentences beside them, and the column only says one thing. The disc says that one thing
  five times, so the reader stops seeing it after the first row and reads the words instead.

  `currentColor` fills the disc and the glyph is `--i-white` on top of it, so a card decides its
  own mark in one line and there is no hex in build.mjs. Grey on the left is the same #9b9c98
  the retired icon rule used, which is dark enough to read on white and light enough not to
  compete with the blue opposite it.

  `margin-top: 1px` centres the 22px disc on the first line of a 16.5px row at line-height 1.4 —
  a 23px line box — rather than on the whole row, which is what `align-items: center` would do
  and would drift as soon as a row took two lines.
*/
.isn-cmp-mark {
  flex: none;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  fill: currentColor;
}

.isn-cmp-glyph {
  fill: none;
  stroke: var(--i-white);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.isn-cmp--alone .isn-cmp-mark {
  color: #9b9c98;
}

.isn-cmp--together .isn-cmp-mark {
  color: var(--i-blue);
}

/*
  The cue under the cards, and it is the *third* use of `.isn-hero-note` — the blue italic phrase
  with a drawn arrow curving out of it that /nl/'s hero and /nl/besparingsscan/'s scan card both
  print. It used to be its own thing here: display type, 800, ink, centred under both cards, and
  sharing a base rule with `.isn-final-note`. Two blue-italic-plus-arrow cues and one black one
  saying the same kind of thing was a drift, so this one joins the pair and the rule it left
  behind now belongs to `.isn-final-note` alone.

  `ISN_HAND_ARROW_UP` rather than the scan page's longer arrow: this note sits directly under the
  thing it points at and has one line of gap to cross, which is the case that arrow was drawn for.

  ── Why the foot is a grid at all ────────────────────────────────────────────

  It repeats the cards' own two tracks so the cue can be placed in the second one. Centred across
  the whole block — which is what it was — the arrow rises out of the seam between the cards and
  points at neither. In the right-hand track the words start on the seam and the arrow, which is
  after them, lands inside the right card. That is what "leaning toward the right card" means
  here, and it holds at every width without a magic offset.
*/
.isn-compare-foot {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  max-width: 1060px;
  margin-inline: auto;
}

.isn-compare-note {
  grid-column: 2;
  margin-top: clamp(20px, 2.2vw, 30px);
}

/*
  Two lines on purpose, and the cap is what buys the arrow its place.

  Uncapped, the sentence takes 470px of a 459px opening — eleven pixels over — so it wraps
  anyway, but as a full-width box with a short second line, which leaves the arrow stranded
  200px to the right of the words it is supposed to grow out of. Capped at 12em the break moves
  to `aan / de`, the two lines come out 230 and 230, and the arrow sits beside them. The unit is
  `em` so the cap tracks the cue's own clamped size rather than needing a second breakpoint.
*/
.isn-compare-note .isn-hero-note-text {
  max-width: 12em;
}

/* ── 07 · Voordelen ───────────────────────────────────────────────────────── */

/*
  Six large coloured objects on the page's own white, with space between them and no boxes.
  The brief asks for exactly that — "geen zware card-grid als dat niet nodig is" — and by
  this point in the page a third bordered grid would also be the third bordered grid.
*/
/*
  Three tracks, stated rather than `repeat(auto-fit, minmax(272px, 1fr))`. Auto-fit asks how
  many 272px columns fit and has no opinion about how the six items then divide: inside the
  old 1240px shell it answered three, and inside the 1800px one it answers four at 1440 —
  which lays six benefits out as 4 + 2 with two empty tracks beside the orphans. Six divides
  by three, so the count belongs in the rule.

  The two steps below are the widths auto-fit changed its own answer at, so the narrow
  behaviour is unchanged: two tracks under 900px, one under 460px.
*/
.isn-benefits {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 3.2vw, 52px) clamp(24px, 2.6vw, 44px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-benefit-h {
  margin: 16px 0 0;
  font-family: var(--i-display);
  font-size: var(--i-h3);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
}

.isn-benefit-copy {
  max-width: 34ch;
  margin: 8px 0 0;
  font-size: var(--i-small);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/* ── 08 · Oprichter ───────────────────────────────────────────────────────── */

.isn-section--founder {
  background: var(--i-paper);
}

/*
  Quote first, portrait second — on request, and the markup is in that order too, so nothing
  here reorders anything (build.mjs's section 09 docblock has the argument).

  The second track is `192px` flat rather than a `minmax()` or a fraction, because this column's
  width is not a layout decision: 384px is the largest derivative of the photograph that exists,
  so 192 is the widest circle it can fill at 2×. A track that grew with the viewport would grow
  the circle past its own source.

  It was `minmax(0, 300px) minmax(0, 1fr)` with a 4/5 frame on the left. The quote consequently
  gets 108px wider than it was, which is why it now carries a measure of its own below.
*/
.isn-founder {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 192px;
  gap: clamp(26px, 4vw, 64px);
  align-items: center;
}

/*
  A circle, and `overflow: hidden` is what actually makes it one: the photograph is square, so
  the radius has to be clipped onto it rather than asked of it. The grey stays underneath as the
  colour of a face that has not decoded yet — a 192px hole in a paper section is more visible
  than a 192px disc.
*/
.isn-portrait {
  width: 192px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--i-grey);
  overflow: hidden;
}

/*
  The class is on the `<picture>`, and a `<picture>` is inline by default — inline leaves a
  descender's worth of white under the image inside a clipped circle, which reads as the face
  sitting high in its own frame. Both boxes are therefore blocks at the frame's full size, and
  `object-fit: cover` holds the crop if a future derivative is not square.
*/
.isn-portrait-img,
.isn-portrait-img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
  A measure, now that the portrait takes 108px less of the row than the frame it replaces. The
  quote is the display face at up to 30px: given the whole track it sets 78 characters to the
  line, which is a paragraph's measure and not a pull quote's. This holds it at the 69 it has
  always set, and the slack falls between the text and the circle, where air belongs in a
  composition whose right-hand element is a face.

  `ch` and not pixels, so the measure follows the clamp on the font size — but `ch` here is the
  width of a `0` in the display face, which is far wider than its average letter: 62ch measured
  646px and 44 characters to the line, not 62. 92ch is what 69 characters costs at 30px.
*/
.isn-quote {
  max-width: 92ch;
  margin: 0;
}

.isn-quote-text {
  margin: 0;
  font-family: var(--i-display);
  font-size: clamp(20px, 2.1vw, 30px);
  font-weight: 700;
  letter-spacing: var(--i-track-h3);
  line-height: 1.22;
  text-wrap: pretty;
}

.isn-quote-by {
  margin-top: clamp(20px, 2vw, 28px);
}

.isn-quote-name {
  display: block;
  font-size: var(--i-body-size);
  font-weight: 800;
}

/*
  One line under the name, where there were two. `.isn-quote-parent` carried
  `Onderdeel van Buyers & Co.` at micro size and is out on request — "haal dit weg bij die
  founders quote sectie" — so both its shared declarations here and its own rule below went
  with it. The sentence survives in the footer, from `ISN_FOOTER.parent`.
*/
.isn-quote-role {
  display: block;
  font-size: var(--i-small);
  color: var(--i-muted);
}

/* ── 09 · Wat wij regelen ─────────────────────────────────────────────────── */

.isn-service {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: clamp(14px, 1.6vw, 22px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-svc {
  padding: clamp(20px, 2.2vw, 30px);
  border-radius: var(--i-r);
  background: var(--a-soft);
}

.isn-svc-n {
  display: block;
  font-family: var(--i-display);
  font-size: var(--i-micro);
  font-weight: 800;
  letter-spacing: var(--i-track-label);
  color: var(--a);
}

.isn-svc .isn-ic {
  margin-top: 16px;
}

.isn-svc-h {
  margin: 14px 0 0;
  font-family: var(--i-display);
  font-size: var(--i-h3);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
}

.isn-svc-copy {
  margin: 8px 0 0;
  font-size: var(--i-small);
  line-height: var(--i-lead-body);
  color: #5b5c58;
}

/* ── 10 · Technologie ─────────────────────────────────────────────────────── */

/*
  The smallest section on the page that is not the footer, deliberately: less vertical
  room, a narrower measure, no illustration, no screenshot and no product name. A
  purchasing collective whose home page leads with its software is selling software.
*/
.isn-section--tech {
  padding: clamp(48px, 5vw, 76px) 0;
  border-top: 1px solid var(--i-line);
  border-bottom: 1px solid var(--i-line);
}

.isn-tech .isn-head {
  max-width: 760px;
  margin-bottom: clamp(24px, 2.4vw, 34px);
}

.isn-tech .isn-h {
  font-size: clamp(24px, 2.4vw, 34px);
}

.isn-tech .isn-lead {
  font-size: var(--i-small);
}

.isn-caps {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-cap {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 15px 9px 11px;
  border: 1px solid var(--i-line);
  border-radius: var(--r-pill);
  font-size: var(--i-small);
  font-weight: 600;
}

.isn-cap .isn-ic > svg {
  width: 20px;
  height: 20px;
}

/* ── 11 · Missie ──────────────────────────────────────────────────────────── */

/*
  The one photograph on this page that is not framed by the page: it *is* the section. A Dutch
  business park — `assets/photos/src/isn-mission.png` — running the full width behind the copy,
  which is the request: "Gebruik de meegestuurde brede foto van het Nederlandse bedrijventerrein
  als background image van de volledige missie-sectie."

  ── A background, and not an <img> ───────────────────────────────────────────

  Asked for directly ("Geen losse <img> naast de tekst"), and it is also the only thing that
  works here: the section's height comes from its copy and its `min-height`, so a picture in the
  flow would set the height instead of filling it. A photograph that carries no information the
  text does not carry wants no `alt` and no place in the reading order either.

  `cover` and anchored **right**, because the crop has to eat from the left. photos.py cuts the
  source to 9:4 (2.25) — its `wide` profile argues that ratio — and every viewport this section
  sees is squarer than 2.25, so `cover` scales the picture past the box's width and discards the
  overflow. Discarded on the left it costs nothing: that side is under a nearly opaque veil.
  Discarded on the right it would cost the warehouse door and the yard, which are the subject.

  ── The veil is anchored to the text, not to the viewport ────────────────────

  The brief describes the gradient in percentages of the section — nearly opaque to 35%, fading
  to 60%, nearly transparent after. Percentages alone break the sentence they are meant to
  serve: at 1440 the text runs to x=716, which is 50%, so a plateau ending at 35% would leave
  the last third of every headline line sitting on half-veiled photograph, and "de foto achter
  de tekst volledig onderdrukt" is the harder of the two requirements.

  So the plateau ends where the text ends: `--i-mission-veil` is the gutter plus the measure plus
  40px of air, and the fade runs `--i-mission-fade` from there. Above 1200 that is 756px at every
  width — `--i-gutter` is `clamp(20px, 5vw, 56px)`, and 5vw has passed 56 long before here — so the
  plateau is a fixed edge and only the section grows past it. Which puts the picture's share of the width on the viewport rather than on a
  percentage, and that is the brief's proportions at the widths it was written for. Taking "clear"
  to be the point the veil drops under a tenth, 1008px in: at 1800 the photograph is clear across
  the right 44%, at 1440 the right 30%, at 1280 the right 21%. Measured at 1440, the row at y=220
  reads flat paper (244 of 247) to x=760 and is photograph by x=1000. The picture giving up width
  as the viewport narrows is the tablet instruction ("de foto iets verder naar rechts geduwd")
  falling out of the geometry rather than being a second set of numbers.

  Six stops over that run, not two, and that is the whole of "geen harde scheiding": a two-stop
  fade between 95% and 0% opacity reads as an edge in the middle no matter how long it is,
  because the eye finds the straight line in the ramp. 95 → 62 → 26 → 8 → 2 → 0 is that ramp
  bent into an ease, and its last third is doing almost nothing, visibly.

  No dark layer and no `filter`. A wash of dark over the photo was ruled out, and the optional
  desaturation was left off deliberately: `filter` on the section would wash the type with the
  picture, and the right-hand side is allowed to stay bright and photorealistic.

  ── Height, and why the section became a flex container ──────────────────────

  700px of `min-height` against 408px of copy at 1440, so the section is mostly picture with the
  text standing in it — the brief's 650–750 band, at the top of that band because this is the one
  place on the page where the photograph needs room to be a photograph rather than a texture. The
  292px left over is 146 above the copy and 146 below.

  `align-items: center` then centres the shell vertically, which is the brief's first option
  ("verticaal gecentreerd of net iets boven het midden"). The shell is already `width: 100%`, so
  becoming a flex item changes nothing horizontally — the copy stays on the page's 56px line with
  every other section's, and `margin: 0 auto` has no free space left to centre it in.

  ── Two `background-image` declarations, and the first one is not dead ──────

  `image-set()` with `type()` gets AVIF (140kB) instead of WebP (202kB), and Safari only understood
  it unprefixed from 17. The declaration above it is what Safari 16 keeps — the same gradient over
  the WebP, out of the same custom property, so the veil can never be dropped while the photograph
  is kept. That ordering matters: a fallback of photo-without-veil would be unreadable, which is
  worse than either. Only one file is ever fetched — a `background-image` a later declaration has
  overridden is never loaded, which the network panel confirms: one request, `isn-mission-1876.avif`.
*/
.isn-section--mission {
  --i-mission-measure: 660px;
  --i-mission-veil: calc(var(--i-gutter) + var(--i-mission-measure) + 40px);
  --i-mission-fade: 340px;
  --i-mission-wash: linear-gradient(
    to right,
    color-mix(in srgb, var(--i-paper) 97%, transparent) 0,
    color-mix(in srgb, var(--i-paper) 95%, transparent) var(--i-mission-veil),
    color-mix(in srgb, var(--i-paper) 62%, transparent) calc(var(--i-mission-veil) + var(--i-mission-fade) * 0.26),
    color-mix(in srgb, var(--i-paper) 26%, transparent) calc(var(--i-mission-veil) + var(--i-mission-fade) * 0.5),
    color-mix(in srgb, var(--i-paper) 8%, transparent) calc(var(--i-mission-veil) + var(--i-mission-fade) * 0.74),
    color-mix(in srgb, var(--i-paper) 2%, transparent) calc(var(--i-mission-veil) + var(--i-mission-fade)),
    transparent 100%
  );

  display: flex;
  align-items: center;
  min-height: 700px;
  background-color: var(--i-paper);
  background-image: var(--i-mission-wash), url('../assets/photos/isn-mission-1876.webp');
  background-image: var(--i-mission-wash),
    image-set(
      url('../assets/photos/isn-mission-1876.avif') type('image/avif'),
      url('../assets/photos/isn-mission-1876.webp') type('image/webp')
    );
  background-repeat: no-repeat;
  background-position: right center;
  background-size: cover;
}

/*
  The 900px cap that used to be on this element is on its headline now, and the reason is the
  page's one vertical line. This div is a `.isn-shell`, so a `max-width` here narrowed the
  *container* and the container's own `margin: 0 auto` then centred it — 900px centred in 1440
  starts the mission at x=326 while the wordmark, the hero and every other headline start at 56.
  A cap on the container is a cap on the left edge too; a cap on the text is only a measure.

  The measure is now one number for both the headline and the body, `--i-mission-measure`, and it
  is in pixels where the headline's was 15em. The `em` was the better unit while the cap only had
  to hold a line break steady; it is the wrong one now that the veil's plateau is anchored to the
  text's right edge, because a measure that grows with the font size moves the edge the gradient
  was built around. 660px is inside the brief's 650–720 band and holds the headline at the three
  lines it has always broken to.
*/
.isn-mission-h {
  max-width: var(--i-mission-measure, 660px);
  margin: 0;
  font-size: clamp(30px, 4vw, 52px);
  font-weight: 800;
}

.isn-mission-copy {
  max-width: var(--i-mission-measure, 660px);
  margin: clamp(20px, 2vw, 28px) 0 0;
  font-size: var(--i-lead);
  line-height: var(--i-lead-body);
  color: #4c4d49;
}

.isn-mission .isn-btn {
  margin-top: clamp(24px, 2.4vw, 34px);
}

/* ── 12 · Veelgestelde vragen ─────────────────────────────────────────────── */

/*
  Same move as the mission above: the cap is on the list, not on the shell that holds it, so the
  `Veelgestelde vragen.` heading starts on the page's line and the eight questions keep the width
  they have always had. 768px is exactly what an 880px shell measured inside its two gutters, so
  no question rewraps and the chevrons stay where they were relative to the text.

  It has to be a real measure rather than the full page: a `<summary>` is a flex row with the
  question on the left and its chevron pushed to the right edge, and across 1328px the chevron
  ends up so far from the words that it stops reading as belonging to them.
*/
.isn-faq-list {
  max-width: 768px;
  border-top: 1px solid var(--i-line);
}

.isn-qa {
  border-bottom: 1px solid var(--i-line);
}

.isn-qa summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 22px;
  padding: 22px 0;
  cursor: pointer;
  font-family: var(--i-display);
  font-size: clamp(17px, 1.5vw, 21px);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
  line-height: var(--i-lead-h3);
  list-style: none;
}

.isn-qa summary::-webkit-details-marker {
  display: none;
}

.isn-qa summary:hover {
  color: var(--i-blue);
}

.isn-qa-sign {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--i-grey);
  color: var(--i-ink);
}

.isn-qa-sign > svg {
  width: 15px;
  height: 15px;
}

.isn-qa[open] .isn-qa-sign {
  background: var(--i-blue);
  color: var(--i-white);
}

/* Plus becomes minus: the vertical bar is the only thing that goes. */
.isn-qa[open] .isn-qa-bar {
  opacity: 0;
}

.isn-qa-bar {
  transition: opacity var(--dur-fast) var(--ease);
}

.isn-qa > p {
  max-width: 68ch;
  margin: 0 0 24px;
  font-size: var(--i-body-size);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/* ── 12b · Hoe kunnen we je helpen ────────────────────────────────────────── */

/*
  ── It belongs to the FAQ above it, so there is no space between them ─────────

  Every other pair of sections on this page is separated by two `--i-y`s, and that is right
  when the second one is a new topic. This one is not: the eight questions raise a ninth —
  and if mine is not on the list? — and this panel is the answer to it. 236px of white
  between the last answer and the answer to that question would read as a change of subject.

  So the top padding goes and the FAQ's own bottom padding is the whole distance: 64–118px,
  which is still more air than any two elements inside either block. The bottom padding stays,
  and it is now the distance to `.isn-section--visit` rather than to the blue CTA — which does the
  same job a second time, because that block does the same thing to this panel that this panel
  does to the FAQ. See its own comment for the arithmetic of the two steps together.
*/
.isn-section--help {
  padding-top: 0;
}

/*
  The panel: one grey rectangle holding a question on the left and its answers on the right.

  `--i-r-lg`, 26px — the largest corner in the system, which the scan card's comment reserves
  for exactly this: a full-width panel rather than a card. There is no border. The tone
  difference against the white section is the edge, and a hairline as well would draw the
  rectangle twice.

  `0.42fr / 1fr` and not two equal halves. The headline is four words and the channels are
  three columns of running copy, so the split is what each side needs: at the 1328px shell
  that is 349px for the question — wide enough for `Hoe kunnen we je` on one line, which is
  where the reference breaks it — and 832 for the three answers. Both tracks are `minmax(0, …)`
  so a long word in either column cannot push the panel wider than the shell.
*/
.isn-help {
  display: grid;
  grid-template-columns: minmax(0, 0.42fr) minmax(0, 1fr);
  gap: clamp(28px, 3vw, 56px);
  padding: clamp(28px, 3.4vw, 52px);
  border-radius: var(--i-r-lg);
  background: var(--i-grey);
}

/*
  Smaller than the section heads above it — 40px against the FAQ's 52 — because it is a label
  on a panel and not a station on the page. It is also the reason this block has no
  `.isn-head`: a head here would announce a new section, and the panel is the section.
*/
.isn-help-h {
  margin: 0;
  font-size: clamp(26px, 2.9vw, 40px);
  font-weight: 800;
  letter-spacing: var(--i-track-display);
  line-height: 1.12;
}

/*
  `auto-fit` here, where the compare section argues against it, and the difference is that
  this count is not fixed: `ISN_HELP.channels` is three today and drops to two if the number
  turns out not to accept WhatsApp — content/isn.js states that in as many words, and a
  `repeat(3, …)` would leave a hole when it happens. `auto-fit` collapses the tracks it
  cannot fill, so three items in room for four still divide the row in three.

  170px is the floor, not a target: it is small enough that three columns survive down to
  the 720px breakpoint where they stack, which is what keeps a 2 + 1 orphan out of the widths
  in between — one channel alone on a second row is the arrangement that makes three peers
  look like two and an afterthought.
*/
.isn-help-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: clamp(22px, 2.4vw, 34px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-help-top {
  display: flex;
  align-items: center;
  gap: 10px;
}

/*
  A disc with a role, which is the one exception the icon rules allow — see `.isn-qa-sign` and
  `.isn-scan-tick`. Three marks in a row need a common holder or the WhatsApp logo, which is a
  filled glyph, reads as twice the weight of the two drawn ones beside it.

  A *tint* of the accent and not the accent itself. The reference fills all three discs solid
  and puts white marks in them, and one of these three cannot follow it: the WhatsApp mark is
  delivered in its own green (#00BC39) and may not be recoloured, so a saturated disc under it
  would be green on green. Tints instead, on all three, which is also what `.isn-scan-tick`'s
  comment argues for a row of small discs.

  22% over white rather than the accent's `--a-soft` token, because this disc is not on white:
  `--i-blue-soft` (#eef3ff) against this panel's #f1f1ef is a difference of hue and almost none
  of lightness, and the disc disappears into the panel. 22% is the tint measured against that
  grey rather than against white — 16% was tried first and the mint one read as a smudge — and
  it is still light enough that the delivered green mark is the darkest thing in its circle.
*/
.isn-help-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--a, var(--i-blue)) 22%, var(--i-white));
}

/*
  20px inside a 32px disc: the drawn icons cover 19.5 of their 24 grid, so this is 16.3px of
  ink with about 8px of ground around it, and the supplied mark is written at 15 to match — see
  `HELP_BOX` in icons.py, where that number is derived from these two.
*/
.isn-help-mark .isn-ic > svg {
  width: 20px;
  height: 20px;
}

/*
  The interior plane has to be re-stated here, and it is the whole reason this disc needs a
  rule of its own. `.isn-ic` sets `--ic-soft` to `--a-soft` — the same tint family the disc is
  now drawn in — so the plane inside the envelope and the handset would land on a ground of
  nearly the same value and the two-tone drawing would flatten to an outline.

  44% is exactly twice the disc's 22%, and the doubling is the rule rather than the number: the
  plane has to be a step of the same size away from the disc that the disc is from the panel, or
  one of the two edges stops being visible. At 30% the handset — whose plane *is* its whole body
  — read as an outline on a tinted circle.

  Same move as `.isn-on-accent` one block up, for the same reason and in the other direction.
*/
.isn-help-mark .isn-ic {
  --ic-soft: color-mix(in srgb, var(--a) 44%, transparent);
}

.isn-help-label {
  margin: 0;
  font-family: var(--i-display);
  font-size: clamp(16px, 1.4vw, 18px);
  font-weight: 800;
  letter-spacing: var(--i-track-h3);
}

/* 30ch, which is roughly what a 254px column sets at this size — the cap only does something
   once the panel is one column and a channel could otherwise run the whole shell. */
.isn-help-copy {
  max-width: 30ch;
  margin: 12px 0 0;
  font-size: var(--i-small);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/*
  The action is a blue bold link and not a button. Three buttons in a row would be three equal
  calls to action a screen above the one the page actually makes, and the finale CTA is that
  one. Same treatment as `.isn-scan-book a`: colour and weight carry it, and the underline
  arrives on hover.
*/
.isn-help-action {
  display: inline-block;
  margin-top: 12px;
  font-size: var(--i-small);
  font-weight: 800;
  color: var(--i-blue);
  text-decoration: none;
}

.isn-help-action:hover {
  text-decoration: underline;
}

/* ── 12c · Of kom je liever langs ─────────────────────────────────────────── */

/*
  ── One step, one step, then two ─────────────────────────────────────────────

  The panel above dropped its top padding so that the FAQ's own bottom padding was the whole
  distance between a question and the answer to it. This block stands in the same relation to
  that panel — `Hoe kunnen we je helpen?` and then `Of kom je liever langs?`, one exchange in two
  blocks — so it does the same thing, and the four sections come out as three steps: 64–118px
  from the FAQ to the strip, the same again from the strip to here, and then 2 × `--i-y` before
  the blue slot, because that one *is* a new topic and is the end of the page.

  `.isn-section`'s own bottom padding is one of those two `--i-y`s and `.isn-section--final`
  contributes the other. Nothing here has to know that; what matters is that removing this
  section's top padding does not touch what is under it.
*/
.isn-section--visit {
  padding-top: 0;
}

/*
  Two equal halves: the invitation and the door.

  Equal, where `.isn-help` above splits 0.42/1, because these two are not a label and the thing
  it labels — the photograph is half of what the section says. At the 1328px shell that is 642px
  a side, which is a comfortable column for the copy and 126px inside the photograph's own width.

  `align-items: center` and not the default stretch-to-`start` reading. The copy is four short
  blocks and the frame is a fixed 3:2, so above the stack one side is always shorter than the
  other — 290px of copy against 428px of picture at 1440 — and top-aligning leaves the whole
  difference as a hole under the shorter one. Split in two it is air on both sides of the copy.

  The gap is `.isn-help`'s gap unchanged, down to the clamp. Two blocks in one conversation are
  not separated by two different distances.
*/
.isn-visit {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: center;
  gap: clamp(28px, 3vw, 56px);
}

/*
  The same 40px as `.isn-help-h`, for a different reason than that rule has: there the size is
  because a label on a panel is not a station on the page, here it is because this question is
  the second half of an exchange with that one, and two questions in one conversation are set at
  one size. Which is also why there is no `.isn-head` here — a 52px section head would open a
  topic instead of finishing one. Every other value is copied from that rule so the two headings
  are one heading at two positions.
*/
.isn-visit-h {
  margin: 0;
  font-size: clamp(26px, 2.9vw, 40px);
  font-weight: 800;
  letter-spacing: var(--i-track-display);
  line-height: 1.12;
}

/*
  The lead voice, `--i-lead`, and not `.isn-help-copy`'s `--i-small`: that size belongs to one of
  three columns in a panel and this paragraph has half the shell. Same size and leading as
  `.isn-mission-copy`, which is the other paragraph on this page that sits beside a photograph.

  54ch is a ceiling and not the width this paragraph usually gets. Figtree's `0` is wide — 1ch
  measures 11.99px at this size, so 54ch is 648px — and the half at 1440 is 642, which means the
  column is the measure there and the cap does nothing. Measured, not assumed: a `ch` estimated
  from the font size is off by a fifth in this face.

  It bites in the two places where the column runs wider than a paragraph should. Past a 1704px
  viewport the frame stops growing and the copy takes everything left over — 864px at the 1800
  shell, which is 72 characters. And stacked, between about 719 and 900, the shell is up to 810px
  wide. Below that the shell is narrower than the cap and it goes quiet again, which is the same
  thing `.isn-help-copy`'s 30ch says about itself.
*/
.isn-visit-lead {
  max-width: 54ch;
  margin: clamp(16px, 1.6vw, 22px) 0 0;
  font-size: var(--i-lead);
  line-height: var(--i-lead-body);
  color: var(--i-muted);
}

/*
  The address is a value, so weight sets it apart from the paragraph rather than a second face.
  Mono is this site's voice for data — styles/base.css argues that over `.utility-link`'s address
  and phone number — but mono on this page is uppercase metadata only, and a postal address is
  neither uppercase nor metadata. So it is the body face at 700 in full ink: heavier and darker
  than the `--i-muted` paragraph above it, and one step under the heading's 800, which is the
  only thing in this column that should be the boldest.

  `font-style: normal` because the UA default for `<address>` is italic, and `tabular-nums` for
  the same reason `.utility-link` carries it — `107-121` and `1018 VN` are digits a reader copies
  out, and proportional figures make two adjacent numbers read as one string.
*/
.isn-visit-address {
  margin: clamp(18px, 1.8vw, 24px) 0 0;
  font-size: var(--i-body-size);
  font-style: normal;
  font-weight: 700;
  line-height: var(--i-lead-body);
  font-variant-numeric: tabular-nums;
}

/* `.isn-help-action` in every value, because it is the same kind of link in the same
   conversation: the one thing in a contact block that leaves the page. */
.isn-visit-map {
  display: inline-block;
  margin-top: 12px;
  font-size: var(--i-small);
  font-weight: 800;
  color: var(--i-blue);
  text-decoration: none;
}

.isn-visit-map:hover {
  text-decoration: underline;
}

/*
  The frame, and `max-width` is the whole no-upscale rule in one declaration.

  `--i-r`, 20px — the card corner and not the panel's 26. This is one object standing in a white
  section, not a container holding other things.

  The photograph is supplied 768x512 and nothing on this site enlarges a photograph past its own
  pixels, so 768 is the ceiling. The half-column reaches it at about a 1704px viewport — the shell
  is `min(1800, 100vw) − 112` past 1120, less the 56px gap, halved — and above that the frame
  stops growing while the copy takes the rest. That is the honest failure mode: a picture drawn at
  its own size beside a slightly wider column, rather than a soft one stretched to match it. The
  same 768 is the `half` profile's top width in photos.py and in build.mjs's `PROFILES`, and the
  three agree by construction: the file decides the profile's ceiling and the profile decides this.

  `justify-self: end` keeps it on the section's right edge while that happens, so the page's right
  margin does not move at one viewport width and hold at another.

  No border and no shadow. The picture's own edges are the object; the site has no shadows at all;
  and a hairline around a photograph with a dark stone portico in the middle of it would be drawing
  the rectangle twice.
*/
.isn-visit-photo {
  width: 100%;
  max-width: 768px;
  justify-self: end;
  overflow: hidden;
  border-radius: var(--i-r);
}

.isn-visit-photo img {
  width: 100%;
}

/* ── 13 · De finale CTA ───────────────────────────────────────────────────── */

.isn-section--final {
  padding-bottom: clamp(72px, 7.4vw, 118px);
}

.isn-final {
  display: grid;
  justify-items: center;
  padding: clamp(44px, 6vw, 94px) clamp(24px, 4vw, 64px);
  border-radius: 24px;
  background: var(--i-blue);
  color: var(--i-white);
  text-align: center;
}

.isn-final-h {
  max-width: 22ch;
  margin: 0;
  font-size: clamp(31px, 4.4vw, 58px);
  font-weight: 800;
  letter-spacing: var(--i-track-display);
}

.isn-final-lead {
  max-width: 48ch;
  margin: clamp(16px, 1.8vw, 24px) 0 0;
  font-size: var(--i-lead);
  line-height: var(--i-lead-body);
  color: color-mix(in srgb, var(--i-white) 84%, transparent);
}

.isn-final-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: clamp(26px, 2.8vw, 38px);
}

.isn-final-note {
  justify-content: center;
  color: color-mix(in srgb, var(--i-white) 80%, transparent);
}

.isn-final-note .isn-hand-arrow {
  color: color-mix(in srgb, var(--i-white) 72%, transparent);
}

/* ── 14 · Footer ──────────────────────────────────────────────────────────── */

/*
  White, and quiet. The page ends on the blue block — that is the visual ending the brief
  asks for — and a black slab under it would be a second one, spending the 15–20% ink
  allowance on furniture.
*/
.isn-foot {
  padding: clamp(44px, 4.4vw, 68px) 0 clamp(26px, 2.6vw, 34px);
  border-top: 1px solid var(--i-line);
}

.isn-foot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(auto-fit, minmax(160px, 1fr));
  gap: clamp(28px, 3vw, 56px);
}

.isn-foot-brand .isn-mark {
  height: 38px;
}

/*
  Not the bar's lockup, and base.css is why. There, `.brand-text` is Figtree 17px/700/1.02 at
  -0.014em and the wordmark face is scoped to `.topbar .brand-text` alone — the footer carries
  the same markup and keeps the 17px Figtree setting, because the brief was the header lockup
  and the footer is a different job: a small name over an address block, not the mark.

  This was the bar's setting one point up. Following base.css instead means the two sites'
  footers set identically, and it is the header lockup that carries the logotype.
*/
.isn-foot-name {
  margin: 14px 0 0;
  font-family: var(--i-body);
  font-size: 17px;
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.014em;
}

.isn-foot-name > span {
  display: block;
}

.isn-foot-descriptor {
  max-width: 32ch;
  margin: 6px 0 0;
  font-size: var(--i-small);
  color: var(--i-muted);
}

.isn-foot-title {
  margin: 0 0 14px;
  font-family: var(--i-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-ink);
}

.isn-foot-group ul {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-foot-group a {
  font-size: var(--i-small);
  color: var(--i-muted);
  text-decoration: none;
}

.isn-foot-group a:hover {
  color: var(--i-ink);
}

.isn-foot-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 10px 26px;
  margin-top: clamp(34px, 3.4vw, 54px);
  padding-top: 20px;
  border-top: 1px solid var(--i-line);
  font-size: var(--i-micro);
  color: var(--i-muted);
}

.isn-foot-bottom p {
  margin: 0;
}

/* ── 15 · De zwevende intake-widget ───────────────────────────────────────── */

/*
  A pill in the bottom-right corner, and the scheduler it opens.

  ── This section keeps its own media queries, and that is the exception ──────

  Everything else on this page puts its responsive rules in `Narrow` below, in one place, in
  page order. This does not, and the reason is that it has no place in that order: it is
  `position: fixed` chrome that sits outside the document flow, and its one breakpoint — the
  width at which the panel stops being a floating card and becomes a bottom sheet — is decided
  by the panel's own 560px, not by the hero's fold at 1200 or the shell's gutters at 900. Four
  rules filed under four seams they have nothing to do with, 400 lines from the component, is
  harder to keep true than a query that sits under what it modifies.

  ── The launcher's z-index, and why the panel has none ──────────────────────

  60: above the bar's 50 and below the skip link's 100, both from base.css. The panel needs no
  z-index at all — `showModal()` puts a `<dialog>` in the browser's top layer, which is above
  every stacking context in the document by definition rather than by winning a number. That is
  the whole reason this is a real dialog and not a positioned div.
*/
.isn-intake {
  /* One place for the three distances the launcher and the panel both measure from. */
  --k-edge: 22px;
  --k-launch: 52px;
  --k-gap: 12px;
}

/*
  ── It is an `<a>`, and it is shaped like a button ──────────────────────────

  build.mjs ships a link to the booking page and js/isn-intake.js intercepts the click. So the
  element is a link and every rule below is about a pill, which is the one place on this page
  where those two disagree on purpose — see the head of the renderer for why.
*/
.isn-intake-launch {
  position: fixed;
  right: var(--k-edge);
  bottom: var(--k-edge);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  /* Asymmetric: 20px of air before the label, 6 after the photo. The photo is a circle and
     brings its own optical margin, so equal padding reads as too much on its side. */
  gap: 11px;
  min-height: var(--k-launch);
  padding: 5px 6px 5px 20px;
  border-radius: 999px;
  background: var(--i-blue);
  color: var(--i-white);
  text-decoration: none;
  /* Two shadows: a wide soft one that lifts it off whatever section it is over, and a tight
     one that keeps the edge defined against a white ground. */
  box-shadow:
    0 12px 32px rgb(10 10 10 / 18%),
    0 2px 6px rgb(10 10 10 / 10%);
  transition:
    background-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.isn-intake-launch:hover {
  background: #1b4fd8;
  transform: translateY(-1px);
}

.isn-intake-launch:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 3px;
}

/*
  ── It stands down while the categories are open ────────────────────────────

  The z-index note above is the whole cause: 60 is above the bar's 50, and the bar's
  `Categorieën` panel is a child of the bar, so the pill paints over the panel's
  bottom-right corner. Before the panel was packed and capped it was 152.3 × 45.8px of
  panel under a blue pill at 1440 × 900 — the foot of the right column and the corner of
  the `Alle categorieën` row.

  Packing it recovered most of that and not all of it, which is why this rule is still
  here. The panel is 710.2 tall when it has the room, so it ends at 807.1 and clears a
  launcher whose top edge is at `100dvh − 74`; below a 881.1px window it does not, and
  below 835.2 the cap takes over and holds the panel's bottom at `100dvh − 28.2`, which is
  inside that 74px band by 45.8 at every height. Measured: no overlap at 900 or 1080, and
  152.3 × 45.8 at 740 and at 640. The x-overlap is 152.3 at every width from 1200 to 1440,
  because panel and pill are both anchored the same distance from the right edge; at 1920
  it is 92.3, the shell having stopped growing at 1800 while the pill stays at the edge.

  Unconditional, and not behind `@media (min-height: 882px)`, because 881.1 is arithmetic
  about the current twenty-five categories: one more in the left column moves it 45px, and
  a media query carrying that number would go quietly wrong the first time someone adds a
  category. A pill that stands down whenever the menu is open is right at every height,
  including the two where it would merely have been unnecessary.

  Two remedies were measured and are worse.

  Stopping the panel above the pill — `- var(--k-launch) - var(--k-edge) - var(--k-gap)`,
  which is what `.isn-intake-sheet` does — puts its bottom at 814 at a 900px window and
  opens an 86px gap under it at every shorter one. The panel is measured for 28.2, and a
  menu floating a tenth of the window above the bottom of the screen is a different bug,
  not a fix for this one.

  Lifting the bar above the pill instead leaves a stub: the pill runs to 1418 and the panel
  stops at 1385.3, so 32.7px of blue would still stand beside the panel with the rest of it
  hidden behind. A CTA sliced down its length reads as breakage, where a CTA that is simply
  not there while a menu is open reads as a menu being open.

  `visibility` and not `opacity`, for base.css's reason on the utility strip: it is the
  property that takes the link out of the tab order as well as off the screen. A pill that
  cannot be seen and can still be reached by Tab is a focus ring on nothing. No transition,
  because there is nothing to animate — `visibility` steps, and the panel it makes way for
  appears in one frame too.

  Scoped to the two long panels by name. `Kennis` is three rows, 128.8px tall, and ends at
  y=225.6 — nowhere near the corner at any window height — so hiding the pill for it would
  be a change with no fault behind it. Verified: the pill's computed `visibility` is
  `visible` with `Kennis` open and `hidden` with `Categorieën` open.

  `Voor wie?` is the second name, added with the branche pages, and it is in this rule on a
  weaker case than `Categorieën`'s — which is worth stating rather than borrowing the paragraph
  above. In the bar it is 396.6px against 710.2 and its bottom lands at 493.5 in a 900px window,
  nowhere near the corner. In the burger panel it is long enough: measured at 390 × 844 its
  sixteen rows are 48.1px each and the list runs 262.9 → 1032.4, past the pill fixed at
  191–376 × 778–830. Two rows land in that band — `Zakelijke dienstverlening` with 14 of its
  48.1px covered and `Zorg & welzijn` with 38 — and *neither is an anchor today*, so unlike
  `Technisch onderhoud` next door nothing clickable is actually being taken away.

  It is here anyway, for two reasons that are not that one. A blue pill lying across an open
  menu is a defect whether the row under it is pressable or not — the fault the rule above
  answers is not only the lost tap. And those two rows become anchors the day a second branche
  page ships, from a config change in content/isn-branche-pages.js that has no reason to come
  back to this file: the covered row would then be a real link, silently, with the rule that
  would have prevented it deliberately left off.

  Not scoped to `.nav-links`, though, and it is the one rule here that is not: the fault it
  answers happens in the burger panel's copy of the menu too. At 390 × 844 the pill is fixed
  at 778–830 × 191–376 and the panel's open `Categorieën` list is 1600px inside a 775px
  scroller, so the row that lands in that band — `Technisch onderhoud` — has 179 of its 334px
  covered, and `elementFromPoint` four pixels inside its right edge returns the pill and not
  the link. The categories list is the only thing in the panel long enough to reach down
  there: with the panel open and `Categorieën` closed it is 410.2 tall and ends at 516.2,
  261.8px clear of the pill. (Chrome still reports rects for the rows inside the closed
  `details` — `::details-content` is `content-visibility: hidden`, which keeps the geometry
  without painting it — so the check that settles that is `checkVisibility()`, which reads
  false for every one of them.)
*/
body.isn:has(.nav-drop[open] > .isn-nav-menu--cats) .isn-intake-launch,
body.isn:has(.nav-drop[open] > .isn-nav-menu--branches) .isn-intake-launch {
  visibility: hidden;
}

/*
  ── And it stands down while the page's own form is on screen ───────────────

  The same fault as the rule above, from the other direction: there the pill covered a menu row,
  here it covers a form field. js/isn-intake.js puts `isn-form-in-view` on the body while any
  `form` that is not the widget's own intersects the viewport, and this is where that becomes a
  width.

  900 is measured, not chosen. A scroll sweep over both surfaces at nine widths, worst overlap
  per width: on /nl/categorie/energie/ the pill covers 162 × 48px of `Jaarlijkse energie-uitgaven`
  at 390 (51% of the control), 159 × 45 at 460, 152 × 44 of the submit at 560, 148 × 47 at 640,
  104 × 44 at 720, 28 × 44 at 800 — and nothing at 900, 1000 or 1200, at any scroll offset. On
  /nl/besparingsscan/ the same curve against the submit and the `website` field: 162 × 40 at 390
  down to 33 × 46 at 800, clear from 900. So the fault ends between 800 and 900 on both, and 900
  is a breakpoint this file already uses rather than a seventh one invented for one rule.

  At 390 it is not merely cosmetic and that is what makes it worth a rule: `elementFromPoint` at
  the centre of the spend field returns the pill on both pages, so the field cannot be tapped
  where a reader aims at it. From 460 up the centre is reachable and only the right-hand end is
  not, which is why the gate is drawn at the end of the overlap rather than at the end of the
  unreachability — a control a reader has to aim around is still the wrong way round.

  `visibility` for the same two reasons as above: the pill leaves the tab order with the pixels,
  and the box stays in layout so nothing reflows.

  ── One of the two surfaces measured above no longer has a pill ──────────────

  /nl/besparingsscan/ does not render the widget at all any more: `isnScanPage()` in build.mjs
  stopped calling `isnIntakeWidget()`, because that page has to open on its hero and nothing else,
  and a `position: fixed` pill is in the first viewport by definition — one frame before the
  observer's first callback, this rule has not run yet, so hiding it in CSS cannot make the
  guarantee the instruction asks for. The argument is at the call site.

  Nothing here changed for that: the rule is keyed on a body class and a width, both pages it was
  measured on still exist, and on the one that keeps the pill (/nl/categorie/<slug>/) the numbers
  above are still the numbers. The scan page's column of the sweep is kept as the record of a
  measurement that was taken, not as a claim about what ships there now.
*/
@media (max-width: 900px) {
  body.isn.isn-form-in-view .isn-intake-launch {
    visibility: hidden;
  }
}

/*
  ── The branche card needs the same stand-down at every width ────────────────

  The rule above ends at 900 because on the two surfaces it was measured against the overlap
  does: their forms are two fields and a button, so the submit lands high enough that a wide
  window clears the corner. `/nl/branche/<slug>/` inverts that. Its card is five money fields,
  an add control, an e-mail field and a button — 1084.5px tall at 390 and 801 at 1440 — so the
  submit sits at the *bottom* of the hero, which is exactly where the pill is.

  Swept the whole page at six widths, worst overlap at any scroll offset, pill visible only:
  nothing at 900, 1000 or 1200, and 126 × 46px of `Ontvang mijn besparingsindicatie` at 1280
  (32.7% of the button), 1440 (28.7%) and 1680 (28%) — the same 126 each time, because the
  pill's width and the card's right edge are both fixed to the shell. It happens at scrollY 0:
  not a state a reader scrolls into but the state the page opens in. The button's centre is
  still the button — `elementFromPoint` there returns `.isn-btn`, and 8px inside its right edge
  returns `.isn-intake-launch-label`, so it is the aim-around case the rule above draws its own
  gate at, one breakpoint the other way.

  So the condition here is the card and not a width, which is why there is no media query: the
  fault is at 1201 and up, the rule above covers 900 and down, and a `min-width: 1201px` on this
  one would put two opposed width gates on one class for one page. Below 1201 it costs nothing
  to also stand down — the card is itself the lead capture and the hero carries
  `Plan een kennismaking`, so the pill is the third route to the same intake, and the rule above
  already says out loud that a floating scheduler competing with the form it floats over is the
  fault rather than the fix.

  `:has(.isn-scan--brcalc)` and not `body.isn` for the reason the whole task is additive: no
  other page carries that class, so /nl/categorie/energie/ keeps exactly the behaviour it has, and
  /nl/besparingsscan/ — which carries `.isn-scan--compact` and not this class — did too, until it
  stopped rendering the pill altogether for the reason given one rule up. `visibility` for the two
  reasons the rules above give.
*/
body.isn.isn-form-in-view:has(.isn-scan--brcalc) .isn-intake-launch {
  visibility: hidden;
}

.isn-intake-launch-label {
  font-size: var(--i-small);
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/*
  The photograph, at both sizes it is used. `object-fit: cover` on a square box is what crops a
  1254×1254 portrait to a circle without squashing the face; the ring is what keeps the crop
  from dissolving into the blue pill behind it.
*/
.isn-intake-face {
  display: block;
  flex: 0 0 auto;
}

.isn-intake-face img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  /* The face is off-centre in the source frame and the top of the head is what a small circle
     should keep. */
  object-position: 50% 22%;
}

.isn-intake-launch .isn-intake-face {
  width: 40px;
  height: 40px;
}

.isn-intake-launch .isn-intake-face img {
  box-shadow: 0 0 0 2px rgb(255 255 255 / 55%);
}

/* ── The panel ─────────────────────────────────────────────────────────────── */

/*
  The `<dialog>` itself carries no surface: no background, no border, no padding. Everything
  visible is `.isn-intake-sheet` inside it, and the reason is the outside click — the dialog's
  own box is what a press on the backdrop resolves to, so `event.target === panel` in
  js/isn-intake.js identifies the backdrop exactly, and never a press on padding around the
  content that the reader would call inside.
*/
.isn-intake-panel {
  position: fixed;
  inset: auto var(--k-edge) calc(var(--k-edge) + var(--k-launch) + var(--k-gap)) auto;
  /* The UA centres a modal dialog with `margin: auto` and caps it at the viewport. Both have
     to go for `inset` above to mean anything. */
  margin: 0;
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--i-ink);
  overflow: visible;
}

.isn-intake-panel::backdrop {
  /* Light. This is a booking panel over a page the reader was reading, not a modal that owns
     the session — enough dim to say the page is waiting and to make "press outside" findable. */
  background: rgb(10 10 10 / 22%);
}

/*
  Fades in and does not fade out. `@starting-style` gives the entrance for free; an exit needs
  `transition-behavior: allow-discrete` on `display` and `overlay`, and a browser that supports
  the first and not the second would leave a panel that is closed and still on screen. A close
  that is instant is not a defect. base.css's blanket rule caps both at 1ms under reduced
  motion, so there is nothing for that query to add here.
*/
.isn-intake-panel[open] {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

@starting-style {
  .isn-intake-panel[open] {
    opacity: 0;
    transform: translateY(8px);
  }
}

.isn-intake-sheet {
  /* 560 sits in the 520–600 the brief asks for. `100vw − 2 × edge` is the floor, so the card
     never reaches an edge it is measured from. */
  width: min(560px, calc(100vw - 2 * var(--k-edge)));
  /* Whatever is left above the launcher, and never more than 640. `dvh` rather than `vh`
     because a mobile browser's toolbars are part of what is left. */
  max-height: min(640px, calc(100dvh - var(--k-edge) * 2 - var(--k-launch) - var(--k-gap) * 2));
  overflow-y: auto;
  padding: 18px 20px 20px;
  border: 1px solid var(--i-line);
  border-radius: var(--i-r);
  background: var(--i-white);
  box-shadow:
    0 28px 64px rgb(10 10 10 / 16%),
    0 4px 14px rgb(10 10 10 / 6%);
}

/* ── The head: who the reader is about to meet ─────────────────────────────── */

.isn-intake-head {
  display: flex;
  align-items: flex-start;
  gap: 13px;
  padding-bottom: 15px;
  border-bottom: 1px solid var(--i-line);
}

.isn-intake-head .isn-intake-face {
  width: 52px;
  height: 52px;
}

.isn-intake-who {
  min-width: 0;
}

.isn-intake-title {
  margin: 0;
  font-family: var(--i-display);
  font-size: var(--i-lead);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: var(--i-track-display);
}

.isn-intake-person {
  margin: 3px 0 0;
  font-size: var(--i-small);
  font-weight: 600;
}

.isn-intake-meta {
  margin: 2px 0 0;
  font-size: var(--i-micro);
  color: var(--i-muted);
}

/* Pushed to the corner by `margin-left: auto` rather than by a third grid column, because the
   two blocks beside it are a photo and a stack of three lines that must not be moved by it. */
.isn-intake-x {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin: -4px -6px 0 auto;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--i-muted);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.isn-intake-x:hover {
  background: var(--i-grey);
  color: var(--i-ink);
}

.isn-intake-x:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 1px;
}

/* ── Step 1: the day and the time ──────────────────────────────────────────── */

.isn-intake-step {
  margin-top: 16px;
}

/* The step is `tabindex="-1"` and focused when the reader is moved to it, which draws a focus
   ring around the whole panel body for no information at all. */
.isn-intake-step:focus {
  outline: none;
}

.isn-intake-h {
  margin: 0;
  font-family: var(--i-display);
  font-size: var(--i-body-size);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.isn-intake-pickhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px 16px;
  flex-wrap: wrap;
}

.isn-intake-zone {
  margin: 0;
  font-size: var(--i-micro);
  color: var(--i-muted);
  text-align: right;
}

/*
  ── The week: an arrow, five columns of times under their dates, an arrow ────

  There is one row of controls in step 1 and the dates are not in it. A reader must be able to
  press a time without first choosing a day — "geen extra klik op een dag nodig" — which means
  every time in the week is on screen at once, and *that* is what makes this a grid of columns
  rather than a row of days above a row of times.

  The date sits inside its own column rather than in a header row of its own. Two things come
  free from that and both would otherwise be work: a label and the times it names cannot drift
  apart, because they are the same grid item and there is no second track definition to keep in
  step; and the whole week scrolls as one object, in both directions, with the labels travelling
  with their columns.
*/
.isn-intake-week {
  display: flex;
  /* The arrows to the top, so they sit on the date line — see the arithmetic on
     `.isn-intake-date`, which is what actually aligns them. */
  align-items: flex-start;
  gap: 6px;
  margin-top: 13px;
}

/*
  The scroller, and the only one in the panel besides the sheet itself. `1fr` columns with a
  floor: the tracks divide the width evenly where five of them fit and stop shrinking at 64,
  which is what turns a 390px phone into a horizontal scroll instead of five columns too narrow
  for `16:30`.

  `overscroll-behavior: contain` because the sheet behind this also scrolls — without it a flick
  that reaches the last time carries on into the sheet and takes the head off the screen.
*/
.isn-intake-grid {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(64px, 1fr);
  gap: 8px;
  /* Seven rows of the sixteen a day holds. The cap is what keeps the panel's own height under
     the sheet's 640 with the head, the zone line and the status line all still in it; the rest
     of the day is a scroll, which is what every calendar with working hours in it does. */
  max-height: 340px;
  overflow: auto;
  overscroll-behavior: contain;
}

.isn-intake-col {
  display: grid;
  /* The columns are as tall as the longest one, and a Wednesday with eight times left must not
     stretch its eight buttons over a Thursday's sixteen. */
  align-content: start;
}

/*
  ── The date is a label, and it is not a control ────────────────────────────

  "De dagen bovenaan zijn ALLEEN visuele kolomlabels. Ze mogen NIET klikbaar zijn." So there is
  no hover here, no pressed state, no cursor and no border: it is a `<p>` set as metadata over
  the times it names, in the same mono-adjacent weight the rest of the panel uses for a label.

  Sticky, with its own white padding under it. The column can be seven times taller than the
  scroller, and a date that scrolled away would leave the reader looking at a grid of times with
  nothing saying which day each one is on. The padding is what the times pass behind.

  36 with 8 of it padding, so the label's 28px centre line is exactly the arrows' centre line.
  The two are aligned by arithmetic rather than by a nudged margin, which is why the arrow's own
  height is the same 28 and neither number may move alone.
*/
.isn-intake-date {
  position: sticky;
  top: 0;
  display: grid;
  place-items: center;
  min-height: 36px;
  margin: 0;
  padding-bottom: 8px;
  background: var(--i-white);
  font-size: var(--i-micro);
  font-weight: 700;
  letter-spacing: var(--i-track-body);
  white-space: nowrap;
  color: var(--i-muted);
}

.isn-intake-times {
  display: grid;
  gap: 6px;
}

/*
  ── The two arrows ──────────────────────────────────────────────────────────

  The `×` in the panel head at 28px, and not the 38px bordered circle the category rail uses.
  "Pijlen compact en subtiel": this pair sits beside a date inside a booking panel, and two
  outlined circles either side of the week would read as two more things to decide rather than as
  a way to look further ahead.
*/
.isn-intake-wnav {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--i-muted);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    opacity var(--dur-fast) var(--ease);
}

.isn-intake-wnav:hover:not(:disabled) {
  background: var(--i-grey);
  color: var(--i-ink);
}

.isn-intake-wnav:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 1px;
}

/*
  Faded and still in the row. Taken out — `hidden`, `visibility` or `display: none` — the grid
  would gain 34px on the first week and lose it again on the second, so the columns would change
  width as the reader pages through them. The shape of the week is not something a week may move.
*/
.isn-intake-wnav:disabled {
  opacity: 0.32;
  cursor: default;
}

.isn-intake-slot {
  min-height: 42px;
  padding: 0 6px;
  border: 1.5px solid var(--i-line);
  border-radius: var(--i-r-sm);
  background: var(--i-white);
  font-family: var(--i-body);
  font-size: var(--i-small);
  font-weight: 600;
  color: var(--i-ink);
  white-space: nowrap;
  cursor: pointer;
  /* Lining figures, so 09:00 and 11:30 are the same width and the column reads as a table. */
  font-variant-numeric: tabular-nums;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.isn-intake-slot:hover {
  border-color: var(--i-ink);
}

.isn-intake-slot:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 2px;
}

/*
  One selected state on the whole step, and it is the brand blue filled in. There is nothing else
  to press here — the day is a label and the arrows are navigation — so the reader who glances
  back at the panel sees exactly one thing marked, and it is the thing they committed to.
*/
.isn-intake-slot[aria-pressed='true'] {
  border-color: var(--i-blue);
  background: var(--i-blue);
  color: var(--i-white);
}

.isn-intake-slot[aria-pressed='true']:hover {
  border-color: #1b4fd8;
  background: #1b4fd8;
}

.isn-intake-status {
  margin: 12px 0 0;
  font-size: var(--i-micro);
  color: var(--i-muted);
}

.isn-intake-status[data-tone='error'],
.isn-intake-report[data-tone='error'] {
  color: var(--i-red);
  font-weight: 600;
}

/* ── Step 2: who is coming ─────────────────────────────────────────────────── */

/*
  The chosen moment, restated. It is not decoration: the reader made that choice on a screen
  that is now gone, and the alternative to showing it here is a form that asks for a name
  without saying what for.
*/
.isn-intake-chosen {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  border-radius: var(--i-r-sm);
  background: var(--i-blue-soft);
}

.isn-intake-chosen-label {
  margin: 0;
  font-family: var(--i-mono);
  font-size: var(--i-micro);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-blue);
}

.isn-intake-chosen-when {
  margin: 2px 0 0;
  font-size: var(--i-body-size);
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* A link in behaviour and a `<button>` in markup, because it changes the panel rather than the
   page. Underlined for the same reason a link is: there is no other affordance on it. */
.isn-intake-change {
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--i-body);
  font-size: var(--i-small);
  font-weight: 700;
  color: var(--i-blue);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.isn-intake-change:hover {
  color: #1b4fd8;
}

/* `.isn-fields`' `auto-fit, minmax(200px, 1fr)` gives two columns in a 518px sheet, which is
   what the four fields want — two rows of two rather than a column of four in an overlay. */
.isn-intake-fields {
  margin-top: 14px;
}

.isn-intake-consent,
.isn-intake-report {
  margin: 12px 0 0;
  font-size: var(--i-micro);
  color: var(--i-muted);
}

.isn-intake-submit {
  width: 100%;
  justify-content: center;
  margin-top: 14px;
}

/* ── Step 3: the receipt ───────────────────────────────────────────────────── */

.isn-intake-done {
  padding-top: 4px;
}

.isn-intake-tick {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  margin-bottom: 12px;
  border-radius: 50%;
  background: var(--i-green-soft);
  color: var(--i-green);
}

.isn-intake-done .isn-intake-h {
  font-size: var(--i-lead);
  font-weight: 800;
  letter-spacing: var(--i-track-display);
}

/*
  `Maandag 14 september om 13:30 · Amsterdam` is one line in the panel and two on a phone, and a
  greedy wrap puts the break in the worst available place: it fills the first line to
  `… om 13:30 ·` and starts the second with `Amsterdam`, or fills to `… om` and starts the second
  with `13:30 · Amsterdam`. `balance` evens the two instead, which keeps the day on the first line
  and the time with its zone on the second. Cheap here for the same reason it is cheap on the card
  titles in styles/learn.css — one short string, never more than two lines.
*/
.isn-intake-when {
  margin: 8px 0 0;
  font-size: var(--i-body-size);
  font-weight: 700;
  text-wrap: balance;
}

.isn-intake-done-body {
  margin: 8px 0 0;
  font-size: var(--i-small);
  color: var(--i-muted);
  max-width: 42ch;
}

.isn-intake-doneclose {
  width: 100%;
  justify-content: center;
  margin-top: 18px;
}

/* ── The bottom sheet ──────────────────────────────────────────────────────── */

/*
  640px, which is the width below which `min(560px, 100vw − 44px)` stops being a card floating
  in a corner and starts being a card that nearly fills the screen anyway. At that point the
  honest shape is the one the platform already has a name for: a sheet on the bottom edge, full
  width, its top two corners round and its bottom two square because there is no bottom edge to
  round.
*/
@media (max-width: 640px) {
  .isn-intake {
    --k-edge: 14px;
  }

  .isn-intake-panel {
    inset: auto 0 0 0;
  }

  .isn-intake-sheet {
    width: 100%;
    max-height: 88dvh;
    border-width: 1px 0 0;
    border-radius: var(--i-r-lg) var(--i-r-lg) 0 0;
    /* The home-bar area on a modern phone. `max()` rather than the inset alone, because on a
       phone without one the inset is 0 and the sheet would end flush with its last button. */
    padding-bottom: max(20px, env(safe-area-inset-bottom));
  }

  /*
    A modal `<dialog>` does not stop the page behind it scrolling, and on a touch screen a
    sheet that slides with the page under it reads as broken. Only here: on desktop the same
    rule would take the scrollbar away and shift the whole page by its width as the panel
    opens.
  */
  html:has(.isn-intake-panel[open]) {
    overflow: hidden;
  }

  /* 44px is the floor for a target that is pressed with a thumb, and the times are the only
     thing in the panel that is pressed rather than typed into. */
  .isn-intake-slot {
    min-height: 46px;
  }

  /*
    The week on a phone. Five columns in a 350px sheet leaves 290 for the grid once the two
    arrows have their 28px, so the numbers are tightened until five columns of `16:30` fit on the
    line: 4px gaps, a 54px floor, and 5 × 54 + 4 × 4 = 286.

    Below about 360px they stop fitting and the grid scrolls sideways instead — which costs
    nothing here, because each date travels with its own column. That is the whole reason the
    labels are inside the columns and not in a header row: a row of dates and a separate row of
    times would have to be scrolled in step by a script, or drift.

    The `54` floor is what keeps the sheet on the screen, and it is not interchangeable with
    `max-content`. A `<dialog>` is `width: fit-content` in the UA stylesheet, so this sheet
    shrink-to-fits and can never go below its own min-content width — a `max-content` floor on
    five columns pushes that past 390px and the right edge of the panel ends up off the phone.
  */
  .isn-intake-grid {
    grid-auto-columns: minmax(54px, 1fr);
    gap: 4px;
    /* Shorter than the desktop's 340: the sheet is 88dvh and the form under this still has to be
       reachable without the reader wondering whether the panel ends at the last visible time. */
    max-height: 268px;
  }

  .isn-intake-times {
    gap: 4px;
  }
}

/* ── /mijnisn ─────────────────────────────────────────────────────────────── */

/*
  The member door, and it is nine declarations because the page is four elements: a headline, a
  paragraph, two buttons and a line of small print, all of them in classes this file already
  defines. See `isnMijnisnPage` in build.mjs for why the page is that small.

  ── Why the section has a height at all ─────────────────────────────────────

  Because there is one section on the page and it holds four lines. `var(--i-y)` above and below
  them is 64–118px, which on a laptop puts the footer a third of the way down the screen with
  nothing under it — a page that reads as having failed to load rather than as short. So the
  section is given exactly the first viewport minus the chrome above it, the same expression the
  hero uses, and the footer lands just under the fold where a footer belongs.

  `100vh` first and `100svh` second, in that order and both: the second is the honest number on
  a phone, where the address bar makes `100vh` taller than the screen, and the first is what a
  browser without `svh` falls back to. Identical to the pair on `.isn-hero`.

  `align-content: center` rather than `place-items`, because the child is a full-width grid item
  whose own text alignment is the default: this centres the block in the leftover height and
  changes nothing horizontally.
*/
.isn-section--portal {
  display: grid;
  align-content: center;
  min-height: calc(100vh - var(--i-chrome));
  min-height: calc(100svh - var(--i-chrome));
}

/*
  62ch, which is `.isn-lead`'s own measure. The headline is short enough not to need one and the
  small print is one sentence, so this exists for the paragraph — and it is on the wrapper
  rather than restated on the paragraph so the three text elements and the button row all break
  on the same right edge.
*/
.isn-portal {
  max-width: 62ch;
}

/* The `.isn-final-actions` row without the centring, because this column is left-aligned. */
.isn-portal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: clamp(26px, 2.8vw, 38px) 0 18px;
}

/* ── 16 · /nl/categorie/ en /nl/categorie/<slug>/ ─────────────────────────── */

/*
  ── The hero's split, and why the card is narrower than the scan's ───────────

  400–500 against /nl/besparingsscan/'s 540–720, and what is inside decides it. That card
  carries a step bar, three steps, twenty-six chips and up to four fields; this one carries
  two fields and a button, and a two-field form set 720px wide reads as a wide empty box
  rather than as the object the page is built around.

  The copy column takes what is left, which is more than it gets on the scan page — so the
  brief's "clean and spacious" left column and its "prominent" card are the same decision
  rather than two competing ones.

  Everything else about the row is inherited: this is the same `.isn-hero-grid`, with the same
  1200px collapse and the same centring, and only its tracks are written here.
*/
.isn-hero--cat .isn-hero-grid {
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(400px, 34vw, 500px));
}

/*
  ── The same card object, one step of padding up ─────────────────────────────

  `.isn-scan`'s 13–16px is the padding of a card that has to fit a step bar, three steps and
  four fields inside a fold beside a headline. This one has two fields and no fold to meet,
  and it contains a panel of its own — the green outcome, at 14–18px. A parent padded more
  tightly than its child reads as a frame around a frame, so the card goes to 18–24 and the
  panel inside it is a step below it again.

  Nothing else about the card is touched: the border, the radius, the white and both shadows
  are `.isn-scan`'s, which is what makes this the same object on a second page rather than a
  second card that resembles it.
*/
.isn-scan--calc {
  padding: clamp(18px, 2vw, 24px);
}

/*
  One field per row, against `.isn-fields`' own `auto-fit` pair.

  `minmax(200px, 1fr)` fits two tracks inside this card at a laptop width, and the second
  label does not fit one of them: `Jaarlijkse energie-uitgaven` is 27 characters at the
  label's 14px/700. A label wrapping to two lines beside one that keeps a single line is a
  card that looks assembled rather than laid out.

  It is also the brief's low-friction argument made visible. Two fields stacked is a form
  whose end you can see; two side by side read as the first row of a longer one.
*/
.isn-scan--calc .isn-fields {
  grid-template-columns: minmax(0, 1fr);
}

/*
  ── The € is in the field and not in its value ───────────────────────────────

  An `aria-hidden` sign over the input's own left padding, rather than a character in the
  value: a prefix inside the value is something the reader has to delete to correct their
  amount, and something js/isn-calc.js would have to strip on the way back out.
  `aria-hidden`, because the label above it already fixes the currency — a yearly spend in
  this country is in euros — and a screen reader reading "euro" between the label and the
  reader's own figure is a third voice for a decoration.

  ── A shared grid cell, not `position: absolute` ─────────────────────────────

  Absolute placement inside the field would have to know the height of the label above it,
  which is the one box in this field that is type and can reflow onto two lines. Instead the
  sign and the input are both put in row 2 of the field's own grid, both carry `.isn-input`'s
  6px top margin and the sign is `.isn-input`'s own 48px tall — so the two content boxes
  centre on the same line by construction, and neither has an offset to keep in step with the
  other. `z-index`, because the input is painted after the sign and its background is white.
*/
.isn-field--money {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.isn-field--money > .isn-field-money-prefix,
.isn-field--money > .isn-input {
  grid-row: 2;
  grid-column: 1;
}

.isn-field-money-prefix {
  align-self: start;
  z-index: 1;
  display: flex;
  align-items: center;
  height: 48px;
  margin-top: 6px;
  padding-left: 14px;
  font-size: var(--i-field);
  color: var(--i-muted);
  /* The field is entirely inside its own `<label>`, so a press on the sign already focuses
     the input. This only stops the sign from being what was pressed. */
  pointer-events: none;
}

/* The input's own 14px, the sign, and space before the first digit. */
.isn-field--money > .isn-input {
  padding-left: 34px;
}

/*
  ── The result, in the card the form was in ──────────────────────────────────

  Two amounts, one above the other: what the reader told us, then what it could be worth. The
  first is a row with a hairline under it and set at the size of the card's own small type,
  because it is the input read back — a reader checking they typed 25.000 and not 2.500. The
  second is the panel.
*/
.isn-calc-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 2px 14px;
  margin: clamp(14px, 1.5vw, 18px) 0 0;
  padding-bottom: clamp(12px, 1.2vw, 14px);
  border-bottom: 1px solid var(--i-line);
}

.isn-calc-row-label {
  font-size: var(--i-small);
  color: var(--i-muted);
}

.isn-calc-row-value {
  font-size: var(--i-small);
  font-weight: 700;
  /* The two figures on this card sit one under the other, so their thousands separators are
     in the same column — which they only stay in with tabular figures. */
  font-variant-numeric: tabular-nums;
}

.isn-outcome--calc {
  margin-top: clamp(14px, 1.5vw, 18px);
}

/* In step 3 of the scan this label follows a heading; here it is the panel's first child, so
   the 14px above it separates it from nothing. */
.isn-outcome--calc .isn-outcome-label {
  margin-top: 0;
}

/*
  The largest thing on the page after the headline, which is the brief: the saving is what the
  reader pressed the button for.

  `--i-h2` — the section-heading size — and not a size of its own, spent here on the one
  number a category page exists to produce. It is in the display face at the site's display
  weight and tracking, by being in the same rule as `.isn-h` and `.isn-hero-h` rather than by
  restating them.

  `per jaar` sits beside it, small: the period is a qualifier on the figure, and set in the
  same type it would read as a second fact of equal weight. Tabular figures for the same
  reason as the row above — the two amounts line up.
*/
.isn-calc-figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  font-size: var(--i-h2);
  font-variant-numeric: tabular-nums;
}

.isn-calc-per {
  font-family: var(--i-body);
  font-size: var(--i-small);
  font-weight: 600;
  letter-spacing: var(--i-track-body);
  /* `.isn-outcome-body`'s colour, which is the one text already reads in on this panel. */
  color: #3f5c4e;
}

.isn-calc-fine {
  margin-top: clamp(12px, 1.2vw, 14px);
}

/*
  The appointment and the reset, stacked and full width, because they are both inside a card
  that is already the narrowest column on the page — and because the second one is not a
  second offer. `.isn-btn--quiet` under `.isn-btn--solid` is the site's own pairing, in the
  order that puts the thing we want the reader to do first.
*/
.isn-calc-actions {
  display: grid;
  gap: 8px;
  margin-top: clamp(14px, 1.5vw, 18px);
}

/*
  ── The two benefit grids a category page carries ────────────────────────────

  `.isn-benefits` is `repeat(3, 1fr)` because the home page's six divide by three. A category
  page's `Wat vergelijken we?` has five and its `Waarom via ons?` has four, and both would
  leave a row with empty tracks beside an orphan — the exact fault the three-column rule was
  written to fix.

  `auto-fit` and not `auto-fill` is the whole mechanism: a column that is empty on every row
  is *collapsed*, and the tracks that survive take a `1fr` share of the width. So five items
  fill a five-track row, and four items collapse the fifth track and fill the row as four.
  One rule, no count in it, and a sixth item in a future category needs nothing here.

  Deliberately declared above this file's own two column drops rather than below them, so at
  900px and at 460px `.isn-benefits`' 2-column and 1-column rules still win: `auto-fit` would
  hold three tracks at 900 where the page has decided on two.
*/
.isn-benefits--auto {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

/* Without an icon above it — `Wat vergelijken we?` has none — the heading is the item's first
   box, and its 16px of separation has nothing to separate it from. */
.isn-benefit-h:first-child {
  margin-top: 0;
}

/*
  ── /nl/categorie/ — the twenty-five cards as a grid ─────────────────────────

  The same `.isn-cat` the rail prints, four declarations different: the rail's fixed width
  gives way to the track, and the card stops being a scroll-snap target because there is
  nothing to scroll.

  Five tracks is the count this grid wants, and 25 is why — it is 5 × 5, and every other
  column count leaves one card alone on the last row. So the floor is set where five tracks is
  what a laptop shell resolves to, and narrower screens drop to four and then to two with one
  orphan, which is what a square number costs at widths that are not square.
*/
.isn-catgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: clamp(12px, 1.4vw, 18px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.isn-catgrid .isn-cat {
  width: auto;
  scroll-snap-align: none;
}

/*
  ── The whole card is the link, and the name is what it is called ────────────

  The `<a>` is around the category name and nothing else, and an empty `::after` stretched
  over the card is what makes the card the target. Three things fall out of that arrangement
  and all three are the point: the accessible name is exactly `Energie` rather than the
  photograph's alt text plus a name plus a word like "bekijken"; the hit area is the whole
  248px card rather than the eight characters at the bottom of it; and the focus ring stays
  around the words, because an outline is drawn on the element's own box and not on its
  positioned pseudo-element.

  The underline on hover is `.isn-cats-ask a`'s, offset and all — the one link affordance this
  section already had. No colour change: these cards sit on six different accent tints and a
  blue that reads as a link on the grey one disappears on the blue one.
*/
.isn-cat-link {
  color: inherit;
  text-decoration: none;
}

.isn-cat-link::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--i-r);
}

.isn-cat-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/*
  The overview page has one section and it is the page, so its heading is an `<h1>` and the
  grid starts closer to it than a mid-page section's would: `.isn-head`'s 38–66px is the space
  between two things a reader scrolled to, and this is the first thing under the bar.
*/
.isn-section--catindex .isn-head {
  margin-bottom: clamp(24px, 2.6vw, 38px);
}

/*
  The one sentence that explains why two cards behave differently — a category with a page of
  its own opens it, and the other twenty-four open the savings scan with that cost line
  already ticked. Under the grid rather than over it, because it is an answer to something the
  reader will have noticed by then and not a warning about what they are looking at.
*/
.isn-catgrid-note {
  max-width: 76ch;
  margin-top: clamp(18px, 2vw, 26px);
}

/* ── 17 · /nl/branche/ en /nl/branche/<slug>/ ─────────────────────────────── */

/*
  ── Section 16, one page over, and almost nothing new ────────────────────────

  A branche page is a category page with the logic inverted: `/nl/categorie/energie/` is one
  cost line for every kind of company, `/nl/branche/bouw-installatie/` is one kind of company
  across several cost lines. The consequence for this file is that the whole of section 16 is
  reused as-is — `.isn-hero--cat`'s split, `.isn-scan--calc`'s padding, `.isn-field--money`'s
  shared grid cell, `.isn-calc-row`, `.isn-outcome--calc`, `.isn-calc-figure`,
  `.isn-calc-fine`, `.isn-calc-actions` and `.isn-benefits--auto` — and this section is only
  what a *list* of money fields needs that a single one did not.

  Three things, in order of how much CSS they take: a row that is a field plus a way to remove
  it, an in-flow menu for putting one back, and two small pieces for the index page.

  Section 17 adds nothing to the `Narrow` block below it, which is a claim and not an omission.
  Both new pages wear section 16's classes, so every breakpoint that already stacks a category
  page stacks these — measured at 1200 the hero is one track and at 1440 two, the same widths as
  `/nl/categorie/energie/`. What is new here is intrinsic instead: `minmax(0, 1fr) auto` with a
  wrapping label, `auto-fill` at a floor, and a panel capped in px. Each was swept and each
  holds — the sweeps are in the two blocks that follow and in `.isn-brgrid`'s.
*/

/*
  ── The card keeps the category page's width, and that was checked ───────────

  Nothing here overrides `.isn-hero--cat .isn-hero-grid`, and the temptation was to. This card
  carries five rows where that one carries two, and each row is a field *and* a 48px button
  beside it, so the field loses 58px of the track it used to have all of.

  It has the room. Measured at 1440, where the hero is two tracks of 795.2 and 489.6: the card
  is 489.6 wide, the row resolves to `381.594px 48px`, and the widest label pair —
  `Zakelijke verzekeringen` at 151.7 plus the 8px gap plus `Jaarlijkse uitgaven` at 116.4 — is
  276.1px, so 105.5 spare with none of the five labels reporting a second line. The proof that
  nothing was overridden is that `/nl/categorie/energie/` measures the same in the same pass:
  `795.219px 489.594px` and a 489.6 card at 1440, `1088px` and a 600 card at 1200.

  Below 1201 the hero is one track and the card is `.isn-scan`'s own 600px maximum down to 720,
  then the shell: field columns of 504 at 720 and 560, 322 at 460, 258 at 390, 227 at 359, all
  measured. The pair stops fitting between 460 and 390, and what answers that is the label's
  `flex-wrap` rather than a breakpoint — one row grows from 75.7 to 99.4 at 390
  (`Zakelijke verzekeringen`), two at 359 (`Kantoorartikelen`'s pair is 232.0 against 227).
  Nothing on the page crosses the viewport at any of 359, 390, 460, 560, 720, 900, 1000, 1080,
  1200 or 1440: `documentElement.scrollWidth` equals `innerWidth` at all ten, and a sweep of
  every element under `.isn-main` for a box outside the viewport returns empty at each.

  A widened card would have cost the copy column at 1201 where the two-column hero starts, and
  the headline's size is a `cqw` of that column — so the change with no measurement behind it
  was also the one with a consequence two sections away. The numbers this block first carried,
  532.8 and 424.8, were 37vw and its field column: they described the override, not the page.
*/

/*
  ── One cost line: a field, and the way to take it off ───────────────────────

  `minmax(0, 1fr) auto` and `align-items: end`, which is what puts the remove button's bottom
  edge on the input's rather than on the label's. The `<label>` is the first column entire —
  label text, sign and input, in `.isn-field--money`'s own grid — so its bottom *is* the
  input's bottom, and the button aligned to it lands on the field it removes without either
  box knowing the other's height. That is the same trick `.isn-field--money` uses one level
  down, and the reason neither needs a magic offset.

  The message slot is deliberately *outside* the `<label>` and spans both columns. Inside, it
  would be inside the first grid column and its 6px would push the input up away from the
  button; outside and full width, a message opens the row downwards and the button stays where
  the reader last saw it. js/isn-branche-calc.js's `showProblem` scopes its lookup to the row
  rather than to `.isn-field` for exactly this, and says so.

  14px between rows, not `.isn-fields`' 12: these rows are two-line objects with a button in
  them, and at 12 the gap between two rows was smaller than the 6px+label distance inside one,
  so five rows read as ten loose boxes.
*/
.isn-brcalc-rows {
  display: grid;
  gap: 14px;
  margin-top: clamp(14px, 1.4vw, 18px);
}

.isn-brcalc-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 0 10px;
}

.isn-brcalc-problem {
  grid-column: 1 / -1;
}

/*
  The category, then what the figure beside it means, on one line.

  `.isn-field-label` is 14px/700 and stays that for the name: this is the row's title, and a
  branche card is a list of five categories before it is a list of five amounts.
  `Jaarlijkse uitgaven` is the same sentence five times, so it takes the weight and colour of
  a caption and gets out of the way — 500 and muted, which is `.isn-field-opt`'s treatment for
  the same job on the scan card, restated here because that class marks an *optional* field and
  this is not one.

  No separator character between them, and that is a content decision as much as a visual one:
  two weights and two colours already separate them, and a `·` or a `—` drawn in `content:`
  would be a typographic mark in a stylesheet that the language cannot reach. `flex-wrap` with
  a 2px row gap is what happens instead if a future name is long enough to need two lines.
*/
.isn-brcalc-row .isn-field-label {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
}

.isn-brcalc-for {
  font-weight: 500;
  color: var(--i-muted);
}

/*
  ── The remove control: a target, not a decoration ───────────────────────────

  48 × 48, which is `.isn-input`'s own height, so the button is exactly as tall as the field it
  belongs to and the row has one edge along its bottom. It is also over the 44px a target has
  to be, which is the whole reason it is not the 24px `×` the design would otherwise want:
  five of these sit in a column on a phone directly beside five text inputs, and a 24px target
  between two 48px ones is the mis-tap this card cannot afford — the reader loses a figure they
  typed.

  Quiet, and quiet in the way the brief asks for: no border, no plate, muted at rest, and the
  page's red on hover so that what it does is unambiguous the moment the pointer is on it. The
  `×` is U+00D7 in the markup and not an `x` — a multiplication sign is the character this
  affordance is drawn with, and at 20px it is the same optical weight as the 16.5px field
  beside it.

  `margin-top: 6px` matches `.isn-input`'s, so `align-items: end` has the same box to work
  against in both columns even before the label's height is known.
*/
.isn-brcalc-x {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  margin-top: 6px;
  padding: 0;
  border: 0;
  border-radius: var(--i-r-sm);
  background: none;
  font-size: 20px;
  line-height: 1;
  color: var(--i-muted);
  cursor: pointer;
}

.isn-brcalc-x:hover {
  background: var(--i-paper);
  color: var(--i-red);
}

.isn-brcalc-x:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 1px;
}

/* The two card-level messages — no cost line, or no figure in any of them — are answers to a
   press rather than to a field, so they sit clear of the rows and of the button. */
.isn-brcalc-none {
  margin-top: 10px;
}

/*
  ── Putting a line back: a disclosure in the flow ────────────────────────────

  `<details>` and not a `<select>`, because twenty-five options in a native picker is the one
  control on this card a reader cannot see the shape of — and because the list has to hide the
  five that are already on, which a `<select>` cannot do without an `<option disabled>` that
  still occupies a row.

  In flow, and that is the answer to the brief's "the add-dropdown must stay inside the
  screen". An absolutely positioned panel over a card in a hero is the version that needs a
  cap, a flip and a scroll container; this one opens *inside* the card and pushes the email
  field and the button down, so there is no position for it to be wrong at and nothing above
  it that can clip it. base.css already makes the bar's own disclosures static for the burger
  panel, so this is that precedent rather than a new idea, and js/isn-branche-calc.js's own
  note records that the third manner `mountDrops` adds — repositioning — has nothing to do
  here.

  The toggle reads as a link and not as a third button: `.isn-btn--solid` is taken by
  `Ontvang mijn besparingsindicatie` directly under it, and two solid controls in one card is
  the crowded interface the brief rules out. `list-style: none` plus the `::-webkit-details-marker`
  reset is what removes the native triangle in both engines; the `+` in the markup is the
  marker this control actually wants, and it rotates to nothing — it is a plus that becomes a
  minus by rotation, which is one glyph instead of two.
*/
.isn-brcalc-add {
  margin-top: 12px;
}

.isn-brcalc-add-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  font-size: var(--i-small);
  font-weight: 700;
  color: var(--i-ink);
  cursor: pointer;
  list-style: none;
}

.isn-brcalc-add-toggle::-webkit-details-marker {
  display: none;
}

.isn-brcalc-add-toggle:hover {
  color: var(--i-blue);
}

.isn-brcalc-add-toggle:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/*
  A 22px square in the accent, which is the smallest thing on this card that is a shape rather
  than type. 22 and not 24: the toggle's line is 14px at 1.4, so 19.6px tall, and a mark two
  pixels taller than its own line reads as beside it where a mark four taller sets the row.
*/
.isn-brcalc-plus {
  display: grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 7px;
  background: var(--i-blue-soft);
  font-size: 15px;
  line-height: 1;
  color: var(--i-blue);
  transition: transform var(--dur-fast) var(--ease);
}

.isn-brcalc-add[open] .isn-brcalc-plus {
  transform: rotate(45deg);
}

/*
  ── The list of what is left ─────────────────────────────────────────────────

  Twenty options at the start, so it is a scroller and not a full list: measured with all twenty
  showing, a row is 41.7px and the content is 876px tall, which unclipped would be two thirds of
  a 1440 × 900 window opening inside the card.

  266 and not 240, and the screenshot is why the first number was wrong. The content above the
  first row is 32.3px — 10 of padding and 22.3 of the `KIES EEN CATEGORIE` label — so 240 left
  207.7 for rows, which is 4.98 of them: the panel showed five whole options and no sixth, and
  read as a complete list of five, which is the failure the next sentence was written to avoid.
  32.3 + 5 × 41.7 puts the sixth row's top edge at 240.8, so 266 shows 25.3 of its 41.7 —
  measured at 60.7%, with the seventh at 0. Half a row is the affordance here, because macOS
  overlay scrollbars are not one until the reader already scrolls. `overscroll-behavior: contain` for the mega-menu's reason —
  a wheel that reaches the end of this list must not carry on and scroll the hero out from
  under the card the reader is filling in.

  On the paper tone rather than on white, because the card is white and a white panel inside it
  would need a border to exist — and a bordered box inside a bordered card is the frame around
  a frame `.isn-scan--calc`'s padding argument already refuses.
*/
.isn-brcalc-add-panel {
  max-height: 266px;
  margin-top: 8px;
  padding: 10px;
  border-radius: var(--i-r-sm);
  background: var(--i-paper);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.isn-brcalc-add-title {
  margin: 0 0 6px;
  padding: 0 8px;
  font-family: var(--i-mono);
  font-size: var(--t-mono-sm);
  font-weight: 500;
  letter-spacing: var(--i-track-label);
  text-transform: uppercase;
  color: var(--i-muted);
}

.isn-brcalc-add-list {
  display: grid;
}

/*
  A full-width row, left-aligned, which is the shape of a menu item and not of a button — the
  same row the mega-menu's `.nav-drop-list a` is, at the card's own reading size because this
  list is inside a form and the bar's 14px would read as small print here.

  `text-align: left` is a `<button>` reset and load-bearing: a button centres its text, and
  twenty centred category names of different lengths have no left edge to scan down.
*/
.isn-brcalc-option {
  padding: 10px 8px;
  border: 0;
  border-radius: var(--r-xs);
  background: none;
  font-family: var(--i-body);
  font-size: var(--i-small);
  text-align: left;
  color: var(--i-ink);
  cursor: pointer;
}

.isn-brcalc-option:hover {
  background: var(--i-blue-soft);
}

.isn-brcalc-option:focus-visible {
  outline: 2px solid var(--i-blue);
  outline-offset: -2px;
}

/*
  ── The figures, live, above the address ─────────────────────────────────────

  The one place this card differs from the category card's *sequence* rather than its parts.
  There, the sum is the answer to the press and appears in the panel that replaces the form.
  Here it also appears while the form is still open, because a reader who is deciding which of
  twenty-five lines to add is answering a question the total is the feedback for — and hiding
  it until the address is given would make the address the price of finding out.

  `.isn-calc-row` and `.isn-outcome--calc` are section 16's, unmodified, printed twice on the
  page; js/isn-branche-calc.js writes to every node carrying the output attributes rather than
  to the first, so the second copy costs it nothing. This rule is only the block's own
  separation from the rows above it.
*/
.isn-brcalc-live {
  margin-top: clamp(16px, 1.8vw, 22px);
}

/*
  The en dash between the two ends of the band, at the figure's own size and a step quieter.

  The character is in the markup and not in `content:` — it is punctuation in a language, and
  js/isn-branche-calc.js's docblock records why no glyph of this kind is in the script either.
  Muted, because a range is two numbers with a relationship, and a separator drawn in the same
  ink as the figures makes it three things of equal weight.
*/
.isn-calc-sep {
  color: var(--i-muted);
}

/*
  ── The five cost lines as cards, each one clickable to its own page ─────────

  `.isn-benefit` with `position: relative` added, which is the one declaration it lacks for
  this: it is a boxless item — no border, no plate — so nothing in section 07 ever needed a
  containing block. The link is around the category name only and an empty `::after` stretched
  over the item is what makes the whole thing the target, which is `.isn-cat-link`'s
  arrangement in section 16 and the same three reasons: the accessible name is exactly
  `Energie` rather than an icon plus a name plus a verb, the hit area is the whole item rather
  than the eight characters in its heading, and the focus ring stays around the words because
  an outline is drawn on the element's own box and not on its positioned pseudo-element.

  No radius on the `::after`, where `.isn-cat-link`'s takes `--i-r`: that one covers a card
  with corners and this one covers a rectangle of white space.

  Where each link goes is build.mjs's `categoryHref`, not this file's business, and it is what
  keeps the brief's "no dead link" promise: `Energie` has a page and opens it, and the other
  four resolve to `/nl/besparingsscan/?categorie=<slug>` — the same destination the
  twenty-four pageless cards on /nl/categorie/ already take, so the reader lands on the scan
  with that cost line ticked rather than on a 404.
*/
.isn-spot {
  position: relative;
}

.isn-spot-link {
  color: inherit;
  text-decoration: none;
}

.isn-spot-link::after {
  content: '';
  position: absolute;
  inset: 0;
}

.isn-spot-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/*
  ── /nl/branche/ — sixteen names, one of which is a page ─────────────────────

  Not `.isn-catgrid`. That grid prints twenty-five photographs on tinted grounds at 230px a
  track, and there is no photograph of a bouw- or installatiebedrijf in this project — a
  branche is a kind of company, not an object on a table. So the index is a list of names set
  as rows, which is also the honest shape for a page where fifteen of sixteen entries have
  nothing behind them yet.

  `auto-fill` with a 260px floor rather than a stated count, and the count it resolves to was
  measured: one track of 350 at 390, two of 319 at 720, three of 292.7 at 1000, four of 321.5
  at 1440. Sixteen divides by one, two and four, so the phone and the laptop are the shells
  with no orphan and the three-track band between them leaves the sixteenth name alone on its
  row. `.isn-catgrid` one section up records the same trade for twenty-five, and a stated count
  would have traded an orphan at one width for an orphan at most of them.
*/
.isn-brgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: clamp(10px, 1.1vw, 14px);
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
  ── The difference between the two kinds of row is the row, not a badge ──────

  A live branche is a bordered row on white that answers the pointer; the other fifteen are
  the same row on paper with no border and muted text. No `Binnenkort`, no `Pagina volgt`, no
  second colour: a badge on fifteen of sixteen rows is a page apologising for itself, and the
  one sentence under the grid says the same thing once. content/isn-branche-pages.js argues
  the same choice for the nav panel, where it is fifteen `<span>` elements against one `<a>`.

  `min-height` and not a height, and today the minimum is the height: 12 + 12 of padding around
  a 21.7px line is 45.7, so 58 is this rule's number, and all sixteen rows measure 58 at 390,
  720, 1000 and 1440. It stays a minimum because the name is the only thing in the row and the
  widest of them, `Sport, cultuur & verenigingen`, is 184.4px at this weight against the 228 of
  content a 260px track leaves after the padding — 43.6 of margin, which one longer branche name
  in a later config would spend, and then the row should grow rather than clip. No name wraps at
  any of the four widths measured.
*/
.isn-br {
  display: grid;
  min-height: 58px;
  border-radius: var(--i-r-sm);
  background: var(--i-paper);
}

.isn-br--live {
  background: var(--i-white);
  border: 1px solid var(--i-line);
}

.isn-br-link,
.isn-br-name {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  font-size: var(--i-small);
  font-weight: 700;
}

.isn-br-name {
  font-weight: 500;
  color: var(--i-muted);
}

.isn-br-link {
  color: var(--i-ink);
  text-decoration: none;
}

.isn-br-link:hover {
  border-color: var(--i-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/*
  The way to the scan, under the note that sends the reader there. `.isn-cats-ask a`'s
  affordance — underlined, ink, blue on hover, the arrow beside it — rather than a button,
  because the page's two actions are already in the closing block below it and a third pill
  here would compete with them.
*/
.isn-brgrid-scan {
  margin: clamp(10px, 1.1vw, 14px) 0 0;
}

.isn-brgrid-scan a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--i-small);
  font-weight: 700;
  color: var(--i-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.isn-brgrid-scan a:hover {
  color: var(--i-blue);
}

/* ── Narrow ───────────────────────────────────────────────────────────────── */

/*
  ── The hero stacks earlier than everything else ─────────────────────────────

  1200 rather than the 1080 the rest of the page uses, and the reason is `--i-hero` again.
  Two columns means the headline is sized off a copy column of `shell − gap − 520`, and that
  column shrinks faster than the viewport does: at 1240 it is 564px and the headline 70px, at
  1100 it is 428px and the headline 53px — under the band the brief gives for a laptop, on a
  screen that is not narrow. One column hands the headline the whole shell and the 76px cap
  takes over.

  So the hero's two-column arrangement only exists where it is actually better than stacking,
  and the seam is a step *up* in size rather than down.
*/
@media (max-width: 1200px) {
  /*
    One column, so the shorthand is the row gap and nothing else — the card is under the copy
    now, and the strip under the card. 24–34 rather than the 28–40 this was: three rows of a
    stacked hero are three of these, and the fold is lost here whatever they are.
  */
  .isn-hero-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(24px, 3vw, 34px);
  }

  .isn-hero-lead {
    max-width: 52ch;
  }

  /*
    One column, so nothing caps the card but this. 600px is the top of the brief's range and
    the point past which a form stops reading as a card and starts reading as a page: at the
    1240px shell the fields would otherwise set 1180px wide.
  */
  .isn-scan {
    max-width: 600px;
  }

  /*
    The scan hero's promises and category row take the card's cap, so the four stacked blocks share
    a right edge.

    Uncapped they would run the full shell: at 1200 that is 1088px, which puts three promises in
    tracks of 352 with 44px of disc in each, and category cards of `(1088 − 4 × 7.2) / 5.4` = 196px
    with a 43px object in each. Both read as a grid that has lost its contents. 600 is where the card
    stops, and a column of things that all stop in the same place is the one arrangement that does
    not need a rule of its own to look composed. `.isn-hero-lead`'s 52ch (about 610px at the lead's
    size) is the same measurement arrived at from the other direction.

    A `max-width` is still the right lever now that the row is a container rather than a grid, and it
    is a cleaner one than it was: it sizes the container, so `100cqi` becomes 600 and both the card
    width and the phone query follow it without either being told. At 1200 that gives
    `(600 − 4 × 7.2) / 5.4` = 105.8px per card — wider than the row is anywhere in the two-column
    layout below about a 1600px window — and a set of `6 × (105.8 + 7.2)` = 677.9px against a 600px
    box, so the wrap keeps 77.9px of slack. Capping a *grid* would have meant re-deriving a column
    count for the capped width; capping a container means the formula at `.isn-hero-cat` is the only
    place a width is decided.
  */
  .isn-hero-perks,
  .isn-hero-cats {
    max-width: 600px;
  }

  /*
    The scan section stacks on the same seam as the hero, and for a reason of its own: the card's
    track has a 460px floor, so at 1200 the copy column is 573 and every pixel below that comes
    off the copy alone. Under about 1150 the headline at 46px would be wrapping to three lines
    beside a card that has stopped shrinking — which is the point where two columns are worse than
    one.

    Pitch first, card second — which is the source order, so nothing needs reordering. `gap` is
    the row gap now, and it is larger than the column gap was: stacked, this distance separates
    the argument from the form rather than sitting beside both, and the trust line at the bottom
    of the pitch needs to read as the end of the copy rather than as a caption over the card.

    Nothing further is needed to un-cap the card. There used to be a `.isn-section--scan .isn-scan`
    override here, purely to beat that 0-2-0 selector's own 860px cap on specificity; the cap is
    gone from the base rules, so the plain `.isn-scan { max-width: 600px }` above governs and the
    grid item stretches to it from the start edge.
  */
  .isn-scan-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(30px, 4vw, 44px);
  }

  /*
    The copy takes the card's width, so the hairline over `Zo werkt het` ends exactly where the
    card's right edge is and the two stacked blocks read as one column rather than as a wide
    paragraph with a narrow form under it. 600 and not `.isn-lead`'s 62ch: at the 1140px shell
    62ch is 685px, which would leave the rule overhanging the card by 85px — close enough to
    aligned to look like a mistake rather than a choice.

    The white this leaves to the right at, say, 1000px is the page's, not this section's: the hero
    directly above stacks on the same seam and caps `.isn-scan` and `.isn-cgrid` at the same 600,
    so a tablet reader sees one measure down the whole page. Widening this one section to fill the
    shell would be the section that looks bespoke.
  */
  .isn-scan-pitch {
    max-width: 600px;
  }

  /*
    The category grid needs a cap for the same reason and takes the same number. Two columns of
    a 1240px shell is a 615px card, and at that width the company row and the panel sit in the
    middle third of a card wide enough to be a banner; at 600 the four come out 293px, which is
    within 7% of the 312 the hero gives them, so nothing inside them changes.

    `container-type` and `align-self` come off together, and they have to. Stacked, the hero's
    second row is `auto`, so a size container has no height to contain and the grid resolves to
    zero — the same failure the two-column rule needs them to avoid, in the other direction.
  */
  .isn-cgrid {
    container-type: normal;
    align-self: auto;
    max-width: 600px;
  }

  /*
    And the height budget comes off with it. In the two-column hero the list is the inscribed
    square of a row pinned to the fold, so both the column and the height are real constraints.
    Stacked, the fold is gone whatever this does — the strip below has already been pushed off it
    by the copy — so a height term would keep shrinking the cards on a short window to protect a
    fold that is not there.

    The column is the only constraint left, and the square comes from `aspect-ratio` instead:
    with a definite width and `height: auto` it derives the height, which is the one direction
    that combination resolves. The reverse — a percentage height with `width: auto` — is what
    stood here in an earlier draft and is a real trap: `aspect-ratio` sets the width from the
    height, then `max-width` clamps the width to the column *without* re-deriving the height, and
    the list comes out 677×855 with non-square cards and a horizontal overflow.
  */
  .isn-cgrid-list {
    width: 100%;
    height: auto;
    aspect-ratio: 1;
  }

  /*
    The scan hero's overrides come off with the row that needed them.

    The tracks first: one column means there are no tracks to split, and the shorthand two
    rules up is what applies. Then the headline — it was stepped down because the card was
    beside it and dominant, and stacked it is neither, so `--i-hero` and its 76px cap take the
    headline back. This is the same seam the comment above this block argues for the home page:
    the size goes *up* when the columns collapse, because the column it was measured against
    has just become the whole shell.
  */
  .isn-hero--scan .isn-hero-grid,
  .isn-hero--cat .isn-hero-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
    And the scan hero's three areas become three stacked rows, in source order.

    `copy` / `scan` / `cats` — headline and subtext, then the promises (they are inside `copy`),
    then the form, then the categories. That is the order the brief asks for on narrow, and it is
    the DOM order, so nothing has been moved: the two-column layout is the special case and this is
    the plain one. Restating the areas rather than deleting them is what keeps it that way — a bare
    `grid-template-columns: minmax(0, 1fr)` with the two-area template still in force would put
    `scan` in an implicit second column and the row would overflow by a card's width.

    `grid-template-rows: auto` for the same reason the row count is not left implicit: the
    two-row template above names two, and three areas in a two-row grid puts the third in an
    implicit row that the `auto auto` shorthand does not describe. One `auto` and the implicit
    rows take their size from their content, which is what all three want here.

    The card's alignment comes off with the same row, and what is reset here has changed. It used to
    be `margin-top: 0`, undoing a 48px step that dropped the card one element below the headline it
    stood *beside* — stacked there is nothing beside it, and kept it would have been 48px of air
    between the last promise and the pill. A later instruction pinned the card's two edges to the
    eyebrow's top and the rail's bottom instead, so the base rule carries `align-self: stretch` and a
    centring flex column and no margin at all. Resetting a margin nothing sets would be a line
    describing history, so it goes and the two live declarations are reset in its place.

    Both replacements are inert here by arithmetic — one column, every row content-sized, and the
    floor's surplus handed to `align-content` *around* the rows rather than into them, so there is no
    free space for `stretch` to claim and nothing for `justify-content: center` to place. They are
    written anyway, because inert-by-arithmetic is not inert-by-construction: those declarations exist
    to absorb a difference between the card and a column beside it, there is no column beside it here,
    and `display: flex` would additionally change what happens to a second child if one is ever added
    to `.isn-scan` — which today holds exactly one, the form.
  */
  .isn-hero--scan .isn-hero-grid {
    grid-template-rows: auto;
    grid-template-areas:
      'copy'
      'scan'
      'cats';
  }

  .isn-hero--scan .isn-scan {
    display: block;
    align-self: auto;
  }

  .isn-hero--scan .isn-hero-h,
  .isn-hero--cat .isn-hero-h {
    font-size: var(--i-hero);
  }

  /*
    And the hand-drawn note goes — both of them, the scan hero's and the home section's — which
    the brief permits by name in both places.

    It is not a size problem. The arrow points up and to the right at a card that is now
    *underneath* the copy, so the one thing the drawing says is wrong — and an arrow pointing
    away from the thing it annotates is worse than no arrow. Rotating it to point down was the
    alternative and it buys a decoration nobody asked for in a column that has just lost its
    second half.

    Section 06's cue is the exception, and it is the one place the drawing survives narrow: its
    arrow points up at the card above it, and that card is above it whether the pair is side by
    side or in one column. Nothing about that gesture is a column artefact, so nothing about
    narrow makes it wrong. Its words are content besides — a sentence out of `ISN_COMPARE`
    rather than a repeat of a heading — so hiding it would cost a line meant to be read.

    `.isn-scan-foot` goes with the home section's cue rather than staying behind it. The row held
    two things and now holds only that cue, so hidden it is an empty flex row with
    `margin-top: clamp(20px, 2.2vw, 26px)` — 26px of air between the last promise and the card,
    which is exactly the gap this breakpoint has no reason to add.
  */
  .isn-hero-note:not(.isn-compare-note),
  .isn-scan-foot {
    display: none;
  }

  /*
    The mission photograph, on the smaller derivative and with the veil pulled in behind it.

    Three numbers move and they move together. The measure comes in 100px, which the headline can
    afford because its `clamp` has come down to 48px by here, and every pixel it gives up is a
    pixel of photograph: the plateau ends at 656 at 1200 rather than 756, so the picture is clear
    across the right quarter instead of the right tenth. The fade shortens with it — a 340px ease
    inside a 1200px section is a third of the width spent on the transition — and the height comes
    down 60px because 700px of section against a column that is now taller reads as a gap.

    The 1200-wide derivative rather than the 1876, which is a deliberate under-provision rather
    than a fit: `cover` on a 1200x640 box draws the picture 1440 CSS pixels wide, so 1200 is
    stretched about a fifth. Half of that width is under the plateau and the visible half is a
    photograph rather than a diagram, so a fifth of softness there costs less than 60kB does on a
    tablet connection — and the alternative is the desktop file, not a better-fitting one.

    What this breakpoint does *not* try to do is hold the brief's "rechter 40–50%" all the way
    down. It cannot: at 768 the gutters and a readable measure account for 640 of the 768, so the
    only way to keep half the width photographic is to break the sentence the veil exists to
    protect. So the picture gives up width steadily from here — 25% clear at 1200, a sliver at
    900 — and by the mobile rule below it is a texture, which is what the brief asks for there.
  */
  .isn-section--mission {
    --i-mission-measure: 560px;
    --i-mission-fade: 260px;

    min-height: 640px;
    background-image: var(--i-mission-wash), url('../assets/photos/isn-mission-1200.webp');
    background-image: var(--i-mission-wash),
      image-set(
        url('../assets/photos/isn-mission-1200.avif') type('image/avif'),
        url('../assets/photos/isn-mission-1200.webp') type('image/webp')
      );
  }
}

/*
  Designed for narrow, not shrunk to it. Two things change shape rather than size here: the
  collective figure swaps to its stacked line layer, and the founder's portrait drops below the
  quote instead of standing beside it. The hero has already stacked, 120px earlier.
*/
@media (max-width: 1080px) {
  /*
    One column, and that is the whole rule — the portrait is already the last child, so it lands
    under the attribution by itself, and the circle keeps the 192px its source allows at every
    width. The `max-width: 260px` that used to sit here belonged to the 4/5 frame, which was the
    one element on this page whose height came from the track it was in.
  */
  .isn-founder {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
    Spacing first, items later — the brief's own order for this row. Both gaps come in by
    about a third, which is the whole of what happens to the strip between 1080 and 900, and
    it is enough: six items and 32px of gutter still leave slack at 900.
  */
  .isn-utility-claims {
    gap: 18px;
  }

  .isn-utility-nav {
    gap: 16px;
  }

  /*
    The help panel's question moves above its answers. The two-column split holds while the
    channel track can still be three columns *and* the headline has room for two lines: at 1080
    the question column is 273px and the three channels 650, so the next hundred pixels come off
    a headline that is already wrapping to three lines. Stacked, the headline gets the panel's
    full width and the channels keep three tracks down to 720.

    The row gap is the smaller clamp, because this distance is now between a question and its
    own answers rather than between two columns of a panel.
  */
  .isn-help {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(22px, 2.6vw, 32px);
  }
}

/*
  One breakpoint, one section, and it is set by a sentence.

  Section 06's two cards stay side by side as long as the longest row on either side —
  `Wij onderhandelen scherpere prijzen en voorwaarden`, 49 characters — takes one line. That row
  needs 350px, and the space it gets is `0.402 × viewport − 49` once the shell has taken its 5%
  and the card its padding, its border, the mark and the gap. The two meet at 993px. Above it
  every row is one line and the ten rows read as five pairs; below it that one row wraps, the two
  rows under it slide down half a line each, and the comparison stops comparing.

  So the pair stacks here rather than at the 900 the rest of the page uses. A tablet in landscape
  keeps the two columns, which is what was asked for; a tablet in portrait gets the stack, and at
  that width it is the better reading anyway.

  `Zelf inkopen` is first stacked because it is first in the markup — no `order` anywhere, which
  is also what keeps the tab and screen-reader order honest.
*/
@media (max-width: 1000px) {
  .isn-compare,
  .isn-compare-foot {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
    And the cue leaves the second track it was placed in. `grid-column: 2` surviving into a
    one-column grid does not fall back to the first column — it conjures an implicit second one
    and sits in that, with the first left empty, which reads as a 50% indent nobody wrote.
  */
  .isn-compare-note {
    grid-column: 1;
  }
}

@media (max-width: 900px) {
  /*
    The shortcuts leave the strip, and this is the width the brief's "geen overvolle dubbele
    header" starts to be a real risk: the burger has already been the only navigation for a
    hundred pixels, so a second row of navigation beside it is two menus on one screen.

    They are not lost. `isnTopbar` prints the same links inside the burger panel — the ones
    whose destination the panel does not already hold — so every address in this row is
    reachable at every width, and nothing has to move a focusable node on a resize.

    The claims stay, all three, and they are what the strip is for: at 900 the three measure
    about 480px of a 828px row, so the row is still a third empty.
  */
  .isn-utility-nav {
    display: none;
  }

  /* Three 272px tracks stop fitting here — the width auto-fit used to drop to two at. */
  .isn-benefits {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /*
    And the mission band loses 80px of its floor, because by here the photograph it was holding
    room for is a strip at the right edge: at 900 the veil's plateau alone accounts for 645 of the
    900. A `min-height` sized for a picture that is no longer visible is a `min-height` sized for
    air. 560 is not tight either — at 768 the copy is a 280px box inside 64px of section padding,
    so the floor is still adding 76px of air on top of that, split above and below.
  */
  .isn-section--mission {
    min-height: 560px;
  }

  /*
    And the invitation stacks, at this width rather than the 720 the help panel's channels use.
    Two equal halves of the shell are 391px each at 900 — a 21-word measure for the paragraph and
    a 261px-tall photograph, which is a picture too small to see a door in beside a column too
    narrow to read the invitation to it. One column gives the copy its own measure back and the
    frame the shell's full width.

    `justify-self` turns from `end` to `start`, and that is not cosmetic. Stacked, the frame's
    `max-width: 768px` is smaller than the shell between 853 and 900 — 90vw of 900 is 810 — so
    something has to hold the leftover, and at `end` it would be a photograph 42px off the left
    edge every other block on this page starts on. At `start` the slack goes to the right instead,
    where it is white beside white, and below 853 there is none.
  */
  .isn-visit {
    grid-template-columns: minmax(0, 1fr);
  }

  .isn-visit-photo {
    justify-self: start;
  }
}

@media (max-width: 720px) {
  /*
    The three steps stack, at the width the Library of Trade strip stacks at and for the same
    reason: three tracks of a 360px screen is a fifteen-character measure, and the two-line
    bodies the shell gives them become six-line ones.

    The dividers turn with the layout. The hairline that stood between two columns becomes the
    hairline between two stacked steps, drawn as a real border rather than the pseudo-element
    the row uses — the reason for that pseudo-element, that a left padding would widen the
    first column, does not exist in a single column, and a border is the plainer thing.

    Subgrid comes off with the row: aligning three names into one band is what it was for, and
    stacked there is one of each per band already.
  */
  .isn-hero-steps {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    row-gap: 16px;
    padding-top: 24px;
  }

  .isn-hero-step {
    grid-row: auto;
    grid-template-rows: none;
    row-gap: 5px;
  }

  .isn-hero-step + .isn-hero-step {
    padding-top: 16px;
    border-top: 1px solid var(--i-line);
  }

  .isn-hero-step + .isn-hero-step::before {
    display: none;
  }

  /*
    The object zone, one override for a phone, and it is *smaller* than the desktop share rather
    than larger.

    The cards are square at every width, so a card is half the stacked column: 293px at 720, about
    167px on a 390px screen. `--cc-vis` is a share of the card's own width, and the desktop 43%
    would put 72px of object above a low module that has 82px to work with at that size — the
    percentage and the four tiles both come off a floor there, so what the object takes it takes
    from them and not from slack. 38% is 63px, the rows come to about 158 inside a 167px card, and
    the difference comes out of what was left over.

    Two rules stood here, 42cqw and then 38cqw, the second silently winning on source order and each
    with its own paragraph arguing the opposite of the other. One rule, one number.

    Still two columns, at every width down to 320: "mobiel 1 of 2 kolommen afhankelijk van
    breedte", and two is what tells the story — one column would put the fourth card three
    screens down, and the loop the brief describes is a thing you watch four of at once.
  */
  .isn-cc {
    --cc-vis: clamp(44px, 38cqw, 116px);
  }

  /*
    Below this width the rail is swiped, not clicked. The arrows stay in the document — they
    are the keyboard's way through the row, and `js/categories.js` is what unhides them — but
    they shrink to the label's own height so the caption line stays one line.
  */
  .isn-rail-arrow {
    width: 34px;
    height: 34px;
  }

  /*
    Both chip rows keep their rows — one on /nl/besparingsscan/, two on /nl/ — and lose their
    arrows: below this width the row is swiped, and the finger is better at it than a 30px circle
    is. Nothing becomes unreachable — `Tab` still walks the fourteen chips and the browser scrolls
    the row to each one, which is the same guarantee the row has with a script that never arrives.

    `display: none` on the container rather than on the buttons, so it holds whatever
    `js/isn-scan.js` does with their `hidden` attribute — and it also takes the 30px out of the
    label row's height, which is the second reason.

    Two rules that used to sit here are gone with the grid they were for: `.isn-chips` at two
    columns, and `.isn-chip { min-height: 46px }`. The heights are now the rows' own — 54 in the
    single row, 46 in the two-row variant — and a 0-1-0 `.isn-chip` cannot reach past either.
  */
  .isn-chiprow-arrows {
    display: none;
  }

  /*
    A smaller mark, which on a phone buys horizontal room rather than vertical: the rows scroll
    sideways here, so four pixels off the mark is four more pixels of every label on screen at
    once. Both halves of it, or a selected chip would jump.
  */
  .isn-chip-mark {
    width: 24px;
    height: 24px;
  }

  .isn-chip .isn-ic > svg {
    width: 22px;
    height: 22px;
  }

  .isn-chip-img {
    width: 24px;
    height: 24px;
  }

  .isn-step-actions {
    flex-direction: column;
    align-items: stretch;
  }

  .isn-step-actions .isn-btn {
    justify-content: center;
  }

  .isn-step-actions .isn-micro {
    order: 2;
    margin: 4px 0 0;
    text-align: center;
  }

  /*
    ── Stacked, and the arrows turn with the layout ──────────────────────────

    One column below 720, because three figures across a 390px screen is 105px each and six
    bubbles at that size are a texture rather than a group. The track list is replaced outright
    rather than narrowed: five columns left standing would keep two of them for the arrows, and
    `minmax(0, 1fr)` is the one-column list that cannot be widened by a label's max-content.

    The arrow keeps its drawing and turns. Its box goes square — 30 × 30 — so the 44 × 12
    viewBox letterboxes to 30 × 8.2 centred inside it and a 90° rotation about that centre
    stays inside the box it started in; rotating the 44 × 12 box itself would have swung 16px
    of arrow out of the column on each side. `margin-top` goes, because there is no bubble
    beside it to line up with any more — the arrow is now between two rows, where the row gap
    is the only offset it needs.

    `--flow-h` is fixed at 176 rather than clamped, and 20 under the clamp's own floor. Stacked,
    the three figures plus two arrows plus three labels come to about 810px at 176 — already
    more than one 390 × 664 screen — and the clamp's 196 adds sixty more, which is what pushes
    the quote card past a second screenful. The figures do not need 196 on a full column: at
    176 the widest bubble is 60px and the mark inside it 33px, still a downscale of the 48px
    file icons.py wrote, so nothing is being enlarged to fill the width.
  */
  .isn-flow {
    --flow-h: 176px;
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(16px, 3.4vw, 22px);
  }

  .isn-flow-arrow {
    width: 30px;
    height: 30px;
    justify-self: center;
    margin-top: 0;
    rotate: 90deg;
  }

  /*
    base.css already moves the bar's CTA into the burger panel below 1200px — but it does it
    with `.nav-end > .btn`, and this page's button is `.isn-btn`, so the rule matched nothing
    and the CTA stayed in the row. At 375px that made the bar 503px wide and gave the whole
    document a horizontal scrollbar; the widest element on a phone was the header.

    720 rather than base.css's own 1199: between those two widths the CTA still fits beside the
    lockup and a one-tap CTA is worth keeping while it does. The panel carries its own copy
    either way, so nothing becomes unreachable here.
  */
  body.isn .nav-end > .isn-btn {
    display: none;
  }

  /*
    The three channels stack, and they stack here rather than at the width `auto-fit` would
    have dropped one of them. Three 170px tracks still fit inside the panel at 720 — 592px of
    content against the 554 they need — and stop fitting about forty pixels below it, which
    would put WhatsApp and Telefoon on one row and E-mail alone under them. One channel on a
    row of its own reads as an afterthought rather than as the third of three peers, so the
    stack is declared at the breakpoint instead of left to arithmetic nobody can see.

    The row gap grows to 28 on the way, and it has to: side by side the 22 was a column gap and
    nothing was ever above or below anything, and stacked it becomes the distance from one
    channel's action link to the next channel's disc. The two steps inside a channel are 12
    each, so 22 is not a large enough step away from them to make three blocks read as three —
    at 390 they ran together into one list of nine lines. 28 is where the grouping came back.
  */
  .isn-help-list {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 28px;
  }

  /* And the copy loses its measure, because the column is now the measure. */
  .isn-help-copy {
    max-width: none;
  }

  /*
    On a phone the photograph stops being a view and becomes a surface. "Mobiel: leesbaarheid
    eerst. De foto mag meer een subtiele achtergrond worden, eventueel met een sterkere witte
    overlay" — this is that, and the geometry leaves no other option: at 390px the copy is the
    column, so there is no right-hand half for a picture to be visible in.

    The wash is therefore near-uniform rather than a fade, 95% to 86% across the whole width. The
    business park still reads — the low sky and the roofline come through as a faint grain behind
    the type, which is why the photograph is here at all on a phone rather than switched off. The
    grain is a real cost and it is measured: sampled across the band at 390, the darkest background
    pixel behind the copy is 221, which leaves the body's #4c4d49 at 6.3:1 and the headline at
    14.6:1. AA wants 4.5 for the first of those.

    `min-height` goes. 700 or 640 against a headline that breaks to four lines here and two
    paragraphs that break to four each would be padding: the copy alone makes the section 570px
    tall, so a floor under that is a floor that never applies.

    The 640-wide derivative, at 30kB. It upscales — `cover` on a 390x570 box draws the picture 1282
    CSS pixels wide, twice the file — and is visibly soft. Under a 90% veil that softness is
    invisible, and the 50kB it saves against the tablet derivative on a phone connection is not.

    The photograph cannot overflow: a background is painted inside the section's own box, and the
    section is the shell's full width with no transform and no negative margin on it.
  */
  .isn-section--mission {
    --i-mission-wash: linear-gradient(
      to right,
      color-mix(in srgb, var(--i-paper) 95%, transparent) 0,
      color-mix(in srgb, var(--i-paper) 91%, transparent) 55%,
      color-mix(in srgb, var(--i-paper) 86%, transparent) 100%
    );

    min-height: 0;
    background-image: var(--i-mission-wash), url('../assets/photos/isn-mission-640.webp');
    background-image: var(--i-mission-wash),
      image-set(
        url('../assets/photos/isn-mission-640.avif') type('image/avif'),
        url('../assets/photos/isn-mission-640.webp') type('image/webp')
      );
  }

  /* The column is the measure now, exactly as `.isn-help-copy` above. */
  .isn-mission-h,
  .isn-mission-copy {
    max-width: none;
  }
}

/*
  ── The strip keeps two claims on a phone, and the third is not lost ──────────

  The three measure 463px together at the 18px gap, and the row runs out of width at about
  515px of viewport: 520 gives it 5px of slack and 480 is 30px short. So 560 is the last
  round number with real slack above the crossover, and this is where the third claim goes.

  Which is a decision the brief offers — "houd de belangrijkste benefits of maak de strip
  compact/scrollable" — and the alternative is worse here. A scroller with the third claim
  half-cut at the right edge reads as a broken row rather than as a swipeable one, and the
  usual fix for that is a gradient fade, which this strip is told not to have. A row of two
  that fits is calmer than a row of three that does not.

  Nothing is lost with it. `Persoonlijke inkoopservice` is the strip's shortest form of the
  hero's own third claim, `Samen sterker inkopen.`, and the hero's three are directly
  underneath at every width — one column from 460px down, in full sentences. The strip is a
  reminder of them, not their only appearance.

  `Altijd gratis voor kopers` and `Bespaar tot 25%` are the two kept, in that order, because
  they are the two facts a first-time visitor is deciding on: what it costs them and what it
  is worth.
*/
@media (max-width: 560px) {
  .isn-utility-claim:nth-child(3) {
    display: none;
  }

  /* Two items, so the gap is doing less work and can give the pair its air back. */
  .isn-utility-claims {
    gap: 14px;
  }
}

@media (max-width: 460px) {
  /*
    ── The 11 pixels that keep the headline at two lines ──────────────────────

    -0.035em rather than base.css's -0.021em, and it is the only place on this site that
    departs from the site's own heading tracking. It buys width: sixteen characters at 0.014em
    is about 0.38em, so `Slimme bedrijven` goes from 7.86em wide to 7.48em, and at the 44px
    floor that is 329px of text in a 375px phone's 335px column instead of 346 in 335.

    Without it the brief's two constraints for this headline cannot both hold on a phone — two
    fixed lines, and 44–54px — because the crossover between them is 42.6px. With it they both
    hold from 375px up, which is every iPhone still sold. Below about 370px `Slimme bedrijven`
    does break in two and the headline reads as three lines; 320px would need 42.8px, under the
    band, so that is the trade rather than an oversight.

    It is a display size, so a tighter setting is also the right typography — optical tracking
    opens up at text sizes, not at 44px.
  */
  .isn-hero-h {
    letter-spacing: -0.035em;
  }

  /*
    320–430. The three claims go to one column with the drawing beside its label: at 320px a
    third of the screen is 93px, and `Binnen enkele minuten inzicht` sets four lines in it.

    Which is the default row's problem and not the compact row's — the category and branche heroes
    are the pages whose claims are that long. The `--row` overrides below put the vertical build
    back for the homepage, where the same three tracks are unnecessary because the row simply wraps.
  */
  .isn-hero-claims {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }

  .isn-hero-claim {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: 12px;
  }

  .isn-hero-claim .isn-ic > svg {
    width: 30px;
    height: 30px;
  }

  /*
    The scan hero's three promises take the same treatment, one breakpoint later than they could
    have and for a measured reason.

    At 460 the shell holds 414px, so a third of it after two 10px gaps is 131px, and
    `Collectieve inkoopkracht` sets ~132 at the 11.5px floor — one pixel over, on the widest window
    this block covers. At 390 it is 110px against 132 and the subline is two lines under one promise
    and one under the others. So: one column, disc beside the pair, which is the claim row's own
    arrangement two rules up and needs no new idea.

    The disc spans both text rows rather than sitting on the first, because it is 50px against a
    title of ~18 and a subline of ~15 — centred against the title alone it would hang 23px below
    the pair. `margin-bottom` off, since there is nothing under it any more. (44px when this was
    written, 20px of hang; the disc grew and the argument only got stronger, which is why the
    number is restated rather than the rule revisited.)
  */
  .isn-hero-perks {
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
  }

  .isn-hero-perk {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 12px;
    row-gap: 0;
  }

  .isn-hero-perk .isn-ic {
    grid-row: 1 / span 2;
    margin-bottom: 0;
  }

  /*
    The category row's own phone rule is not here any more. It was
    `grid-template-columns: repeat(3, minmax(0, 1fr))` while the row was a grid; the row is a rail
    now and the count is decided by `@container (max-width: 420px)` at `.isn-hero-cat`, where the
    390px arithmetic and the reason the boundary moved from a window width to the row's own width
    are argued. Nothing about the phone layout changed with it — the card comes out at the same
    112.67px.
  */

  /*
    The compact row keeps its build on a phone: two claims on the first line and one under
    them, which is the arrangement the brief permits at this width. It fits because the copy is
    short — `Samen sterker inkopen.` needs ~165px against 280 of usable width at 320px,
    measured — so the wrap is the whole of the phone layout.

    ── Every declaration here is undoing a tie, not making a choice ─────────────

    All four rules above are exactly as specific as the `--row` rules at `.isn-hero-claims--row`
    and they come later in the file, so they win the tie on source order and reach the homepage
    too. Both things they did there were wrong, and both were found by measuring rather than by
    reading: `gap: 12px` took the column gap from the base row's interval down to 12, and the
    30px svg override hit the drawn tag while leaving the two supplied files at 34, so the row
    had one mark 4px smaller than its neighbours and its label sitting 4px lower. That is the
    defect §6 forbids by name, on the one viewport where nothing else would have shown it.

    So the box stays 34px at every width — inside the brief's 32–36 on a phone as much as on a
    desktop — and both kinds of mark are named again, because the tie has to be broken for each.
  */
  .isn-hero-claims--row {
    gap: 16px 26px;
  }

  .isn-hero-claims--row .isn-hero-claim {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: 13px;
  }

  .isn-hero-claims--row .isn-claim-img,
  .isn-hero-claims--row .isn-ic > svg {
    width: 34px;
    height: 34px;
  }

  .isn-scan {
    padding: 18px 16px;
  }

  /*
    A fixed 148px rather than 74vw. The row is a catalogue drifting past, and a card three
    quarters of the screen wide is a slideshow with one slide in it — the reader cannot see
    that there is a next one.
  */
  .isn-cat {
    width: 148px;
  }

  .isn-cat .isn-visual {
    --visual-h: 106px;
  }

  /* One benefit per row — two 272px tracks no longer fit either. */
  .isn-benefits {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
  359px, which is base.css's own floor for the same reason — the width where the site's bar
  stops trying to fit a row and starts fitting a screen.

  The strip's remaining pair measures 275px at the 14px gap and a 320px screen gives it 280,
  so the two fit by five pixels: a fit that depends on the exact advance widths of `Altijd
  gratis voor kopers` in one particular face is not a fit. So the second claim goes too, and
  the strip carries the one line that is the offer — free for buyers — on the narrowest
  screens still in use. `Bespaar tot 25%` is nine millimetres below it in the hero.
*/
@media (max-width: 359px) {
  .isn-utility-claim:nth-child(2) {
    display: none;
  }
}

/* ── Reduced motion ───────────────────────────────────────────────────────── */

/*
  base.css's blanket rule already caps every animation and transition at 1ms, which is what
  stops the tiles fading. Two things it cannot do — and on the step-card drawings a 1ms cap
  would be actively worse than nothing, because that animation is the one thing here that
  loops: an infinite cycle compressed to a millisecond is a strobe, not a still.

  The first is an *end* state rather than motion: the three step-card additions must sit where
  the drawing puts them — words in the bubbles, magnifier beside the chart, terms on the sheet and
  the tick on its coin — rather than parked at whatever frame of the cycle the cap froze.

  The second: it caps `transition-duration` and says nothing about `transition-delay`, which is
  where the hero grid keeps its whole sequence. See the last rule in this block.

  The chip's mark is switched off outright rather than left at 1ms: a reader who asked for
  stillness still has to see the object become a tick when they select a category, and what is
  dropped is only the crossfade between the two.
*/
@media (prefers-reduced-motion: reduce) {
  .isn-step-cards.is-in .isn-una-write,
  .isn-step-cards.is-in .isn-una-scan,
  .isn-step-cards.is-in .isn-una-pop {
    animation: none;
  }

  .isn-chip-mark > * {
    transition: none;
  }

  /*
    The hero grid, still rather than at 1ms.

    base.css caps every transition in the document at 1ms under this query, which is right for a
    fade and wrong for this sequence: a plate that grows from a tile row to a mint panel in one
    frame is a flicker in the first viewport, and a card that then jumps to its discount is a
    second one. So the whole choreography is switched off at the source, and `js/isn-cgrid.js`
    runs no timers here either — it lays the four cards out on three different states and stops.

    What is left is a still of the same markup, and it says the same thing in one frame that the
    animation says in ten seconds: one card on its company row, one on the bundled volume under the
    ISN logo, two on their collective discount. Nothing here is a second design — every one of those
    three is a state the animation passes through, reachable with the script removed.

    The company rows are whole, and that comes for free: the module sets no `data-grow` under this
    query, and `.isn-cc:not([data-grow])` is the built state — four tiles with their amounts. The
    build-up's hide rules are all keyed on the attribute, so there is nothing here to switch off.

    The unlock and the sweep are animations and not transitions, so base.css's cap does not reach
    either and both need naming — a bloom of light and a sheen, on a bundled card that is never
    going to change again. They are also the reason the still composition can hold a bundled vak at
    all: the moving grid does not seed one, precisely so that no animation runs at mount, and here
    the animations are off, so a bundled vak is just a picture of a bundled vak.
  */
  .isn-cc,
  .isn-cc-tile,
  .isn-cc-bm,
  .isn-cc-more,
  .isn-cc-spend,
  .isn-cc-glow,
  .isn-cc-plate,
  .isn-cc-lockup,
  .isn-cc-vol,
  .isn-cc-vt,
  .isn-cc-cap,
  .isn-cc-kicker,
  .isn-cc-pct {
    transition: none;
  }

  .isn-cc-glow,
  .isn-cc-plate::after {
    animation: none;
  }
}

/* ── The note beside the hero's second button ──────────────────────────────────

  `Ontdek hoeveel jij` / `kunt besparen` in blue Figtree italic, with `ISN_HAND_ARROW_CTA` sweeping
  out from under the words and down-left into `Doe gratis besparingsscan`. `isnScanAnnotation()` in
  build.mjs carries the markup argument, the home-page gate and the accessibility decision; this
  is where the geometry is argued, because every number in it was measured rather than chosen.

  ── It is the fifth `.isn-hero-note`, and it borrows that rule's type ────────

  `.isn-hero-note-text` at line 4493 is the type: italic, 700, `-0.01em`, blue, off the baseline.
  Four cues on this site already use it, and this is the fifth. The declarations are restated below
  rather than shared, because everything *around* the type differs — that rule is an in-flow flex
  row and this is an absolutely positioned column — and a selector list joining the two would tie a
  hero-column layout to a margin note's `left`. What is shared is the argument: change the face
  there and this must follow, which is why both notes name each other.

  A `--i-hand` token used to sit here, carrying a system script stack for this one component. It is
  gone; build.mjs's note on `isnScanAnnotation` records why, and the short version is that the
  reference this was measured against turned out to be the /nl/besparingsscan/ cue, which is
  Figtree. Nothing else consumed the token, so the stylesheet is back to four families.

  ── Why it is at the end of the file ────────────────────────────────────────

  Base rules after the media queries, which is the one place in this stylesheet that happens. It
  is deliberate: this is an additive component whose own breakpoints are below, and appending kept
  the diff out of the hero region and out of the `max-width: 1200px` block, both of which other
  work was editing at the same time. Nothing above targets these class names, so cascade order
  costs nothing — and the one rule that *is* shared, `.isn-hand-arrow` at line 1894, is overridden
  here in full (width, height, colour and the `rotate(-8deg) scaleX(-1)` it would otherwise
  inherit) precisely because being later is the only reason that works.
*/

/*
  The note's positioning parent, and the reason it is the CTA row rather than a new wrapper: the
  row's left edge is the copy column's left edge, so one `left` here is measured from the same
  origin as the buttons themselves — and the buttons are content-sized, which makes that origin
  the only stable thing in the hero. Measured either side of this declaration, the hero is 811.9px
  tall at both 1440 and 1280, so `position: relative` on a static flex container with no z-index
  and one out-of-flow child changes nothing it did before.
*/
.isn-hero-ctas {
  position: relative;
}

/*
  ── The two constants, and what they are measured against ───────────────────

  `left: 410px` is the only magic number here and it is the claims row's own width: the third
  claim, `Samen sterker inkopen.`, ends 401.9px from the copy column's left edge at every width
  from 1201 to 1920 — the row is a content-sized `nowrap` flex row, so that figure is a property
  of the Dutch copy and not of the viewport — plus 8px of air. Left of it is the claim's ink;
  right of it is empty column. The note therefore overlaps the `.isn-hero-claims` *box*, which
  spans the full column, and touches none of its three items. That overlap is the deliberate one:
  the box's right third is empty at every width, and the alternative — a note that starts after
  the box — is a note in the card column.

  The second constant is not written here at all. The secondary button's right edge sits 452.3px
  from the same origin at every width from 560 to 1920, and the note's box starts 42.3px to the
  left of it, which is what lets the arrow's tip land *over* the button rather than beside it — 414px
  from the origin, 38.3px inside that right edge. `bottom: calc(100% + 10px)` then keeps the whole
  box 10px clear of the button's top edge, and the tip's own distance is that 10px plus whatever
  slack the drawing leaves under its lowest ink, which is 14.6px, for a measured **24.6px** of white.
  One number here, one in the path, and nothing tuned against the other.

  `width: max-content` with `white-space: nowrap` on the lines used to mean the box was exactly its
  longest line. Since the arrow was redrawn at 200 x 144 it is the *SVG* that sets the width — 200px
  against 171.5px of text at 1440 — and the `max-width` stopped being decorative because of it: the
  old cap, `100% - 386px`, is 142px at 1280 where the drawing is 146.4px, so it would have started
  squeezing the arrow rather than the words.

  The cap now says the one thing worth capping: *the note may reach to 6px short of the card column.*
  That is `100% - 410px` to the column's right edge, plus the hero grid's own gutter, minus 6. The
  gutter is `column-gap: clamp(28px, 3vw, 44px)`, and 3vw is the live branch through the whole
  two-column range (36.0px at 1201, 43.2px at 1440), which is why `3vw` can appear here without
  re-deriving the clamp. It leaves 4px of slack at 1201 and at 1280 and 35.2px at 1440, so it stays a
  guard rather than the operative constraint — at 1440 the box's right edge is 41.2px from the cards
  and the ink 46.5px, both measured.
*/
.isn-scan-annotation {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 410px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: max-content;
  max-width: calc(100% + 3vw - 416px);
  color: var(--i-blue);
  /*
    `.isn-hero-note-text`'s four type declarations, restated — see the note at the top of this
    section for why they are restated and not shared. No `font-family`: the words inherit the
    page's own Figtree, which is the whole correction this revision makes.
  */
  font-style: italic;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.12;
  /*
    Fluid with a negative offset rather than a plain `Nvw`, because the space this has to fit into
    grows far faster than the viewport does: the `max-width` below resolves to 102.5px at 1201 and
    222px at 1440, a 2.2x change across a 1.2x change in width, so any proportional size that fits
    the wide end overflows the narrow one.

    Every figure here is measured, against the face that actually renders. Figtree italic 700 at
    `-0.01em` sets `Ontdek hoeveel jij` — the longer of the two lines, and so the box's width — at
    exactly 8.15x the font size: 106px at 13px, 171.2px at 21px, linear between. Which makes the
    ceiling arithmetic rather than a judgement, and the curve is fitted to it:

      1201  12.34px → 100.5px of text in 102.5px of room   (2.0px spare — the binding width)
      1280  15.18px → 123.7px in 142px
      1440  20.94px → 170.7px in 222px
      1445+ 21px capped → 171.2px, and the column keeps growing underneath it

    The 21px ceiling is `.isn-hero-note-text`'s own maximum, reached here at 1440 rather than at
    that rule's 1400, so the two cues agree at every width a reviewer is likely to be sitting at.
    The 12px floor is inert by construction — the fluid term does not fall below it until 1192, and
    the block below takes over at 1200 — and exists only to make the `clamp` well-formed.

    Figtree is 5.4x wider per point than the script face this replaced, which is why the ceiling had
    to be re-derived rather than kept: the old curve resolved to 13.8px at 1201, and 13.8px of
    Figtree is 112.5px of text in 102.5px of room.
  */
  font-size: clamp(12px, calc(3.6vw - 30.9px), 21px);
  /*
    It can never eat a click meant for the button it overhangs. The box's bottom edge clears the
    button by 10px, but its rotated ink and the arrowhead's barbs reach further than the box does,
    and a decoration that swallows a press on a conversion CTA is the one failure this component
    could actually cause.
  */
  pointer-events: none;
}

/*
  Each line unbreakable, and both tilted by the same -2.5°.

  The revision before this one tilted them differently — -3° on the first, -1.5° and half an em of
  indent on the second — on the theory that three small asymmetries read as hand-placed where one
  uniform tilt reads as a rotated block of type. Measured on the page, that theory buys a wedge: two
  baselines 1.5° apart converge to the right, so the gap between the lines is visibly narrower at the
  end of `jij` than at the start of `Ontdek`, and the indent steps the left edge out from under the
  line above it. It reads as three separate accidents rather than one gesture, and the note it copies
  does not do it — `.isn-hero-note-text` at line 4493 sets one block at `rotate(-2.5deg)`, and taking
  that same figure here is the shortest defensible answer to "die hoek van die text staat ook raar".

  Each line still rotates about its own centre rather than the block rotating as a unit, which is a
  0.7px difference in where the two left ends land (`Ontdek hoeveel jij` is 170.7px at 1440 and
  `kunt besparen` about 130px, so their half-widths differ by ~20px and 20px x sin 2.5° is 0.9px).
  Below the threshold where a second wrapper element would earn its markup.

  The rotations are on the lines and not on the wrapper for a mechanical reason build.mjs's note also
  gives: the wrapper carries `data-reveal`, and base.css resets `transform` to `none` on `.is-in`,
  which would flatten the tilt the moment the hero revealed itself.
*/
.isn-scan-annotation-line {
  white-space: nowrap;
  transform: rotate(-2.5deg);
}

/*
  ── One custom property, because width and tuck are the same number ─────────

  This used to be 84 x 58: `.isn-hand-arrow--scan`'s size to the pixel, chosen so both cues rendered
  2.26px of ink and a reader met one hand twice. That kinship is what made the arrow too quiet — the
  sibling is a small cue under a note, this one has to carry the eye 200px across to a button — so
  the arrow was redrawn at 200 x 144 (build.mjs's constant argues the shape and keeps the *ratio* of
  pen to drawing rather than the pen's width) and this rule grew with it.

  `--isn-cta-arrow` exists because two declarations need the same number and must not drift apart:
  `width`, and the negative `margin-top` derived from it below. `height: auto` is the third — the
  144 comes from the viewBox's own ratio, so the size is one number in one place.

  The upper bound is 200px, the size the drawing is authored at, and it is reached at 1381px of
  viewport. Below that the pocket, not the drawing, sets the width: the cards column's left edge
  tracks `0.53vw - 56` through the whole two-column range (verified at 1201, 1280 and 1440), so
  `53vw - 532` shrinks at exactly the rate the space to the arrow's right does and holds a constant
  clearance — 46.5px of it at 1440, measured against the cards' box.

  The floor, 104px, is the sibling's own viewBox width, and that is not a coincidence dressed up as
  one: at the 1201 pinch the drawing arrives at 104px and 2.82px of ink, which is `--scan` at
  `--scan`'s weight. So the fluid range runs between two defensible endpoints — the sibling's cue at
  one end, the annotation at the other — rather than drifting to whatever the arithmetic gives. That
  is the answer to the objection the previous revision recorded against `em` sizing, which is
  otherwise the same objection: a pen whose nib changes width down the page. Here it changes between
  two widths that both exist elsewhere on the site, and it changes with the drawing rather than with
  the type.

  ── `margin-top` moves the text, not the arrow ──────────────────────────────

  Worth stating plainly, because the obvious reading is wrong and cost this revision a wrong first
  answer: the SVG's position is pinned by the wrapper's `bottom` anchor and the SVG's own height, so
  a negative `margin-top` on it cannot lift it. It pulls the two lines of text *down* — into the
  drawing's own empty upper-left. That used to be the notch beside the arrow's vertical descent,
  14.2px wide at 1440; since the arrow became an ordinary one starting at (188,44) it is the whole
  corner, and build.mjs's constant does the arithmetic: 27.6px of white at the closest approach at
  1440, which is the width at which the tuck is deepest.

  So the tuck must be zero where there is no room and largest where the room is: `(width - 104px)
  * -0.42` is exactly that, 0 at the floor and -40.32px at 200. An earlier attempt tied it to `em`
  instead and left 0.9px between the descender of `jij` and the drawing's first ink at 1201, because
  `em` does not know the arrow shrank. 0.42 rather than more: at 1440 it leaves 4.1px between this
  note's first line and the hero lead's last line (462.3 against 458.2), and 0.46 would touch it.

  ── The inherited values, all of which are replaced ─────────────────────────

  `.isn-hand-arrow` above sets 46x32, yellow, and `rotate(-8deg) scaleX(-1)`. The mirror is the one
  that matters, and the reason is now simpler than it was: it used to be that the flip swung the long
  barb to the inside of the curve, which the constant's note said no hand does; the head is symmetric
  since the arrow became an ordinary one, so what is left is the whole of it — a horizontal flip
  points the arrow down-*right*, away from the button it exists to indicate. The rotation is `2deg`,
  the sibling's own value, and a
  counter-lean — `--scan` tilts +2° against its own -2.5° note, and the two lines here lean -2.5°
  too, so +2° keeps the shaft crossing the last line's slope rather than running parallel to it.
  Its cost and benefit are both computable from the default 50% 50% origin: the tip sits at
  (-94, +58) from the centre of a 200 x 144 box, and 2° clockwise moves it to (-95.97, +54.69) —
  1.97px further left, 3.31px higher. It buys 3.3px there and 2.4px at the head's lowest ink, which
  is the right-hand barb rather than the tip and so is what sets the clearance over the button —
  19.7px of it since the arrow was redrawn, not the 24.6px this note used to quote. It spends 2.0px
  of the 9.4px gap to the claims row, which is still the tighter of the two: the leftmost ink is the
  tip's own cap and is unmoved at 467.3.
*/
.isn-hand-arrow--cta {
  --isn-cta-arrow: clamp(104px, calc(53vw - 532px), 200px);
  width: var(--isn-cta-arrow);
  height: auto;
  margin-top: calc((var(--isn-cta-arrow) - 104px) * -0.42);
  color: var(--i-blue);
  transform: rotate(2deg);
}

/*
  ── Stacked: the pocket stops being the constraint ──────────────────────────

  At 1200 the hero grid becomes one column and the cards move under the copy, so the space to the
  right of the secondary button goes from 72.2px to 635.7px across that one pixel — both measured —
  and the note is suddenly the smallest thing in a large gap. Three changes, and no repositioning:
  the type size comes back up, the arrow's size stops tracking the pocket, and the reach into the
  grid gutter goes, because there is no gutter any more — past the column's right edge here is the
  page's own padding, and at 640 that is 32px with the viewport edge behind it.

  ── The arrow goes flat at 150px, and the lead is what sets 150 ──────────────

  Above 1200 `--isn-cta-arrow` is `clamp(104px, 53vw - 532px, 200px)`, and every term of that is
  about the card column: 53vw tracks the cards' left edge, and the floor is where the pocket has
  shrunk to the sibling's own 104px. None of it means anything once the cards are underneath, so the
  fluid expression is replaced rather than left to keep computing — at 1200 it would return 104px, the
  narrowest the arrow ever gets, in the widest space it ever has.

  150px, and the number comes from the one thing that gets *worse* when the hero stacks. The lead
  paragraph's last line is 328px of ink at 1440 because a 608px column breaks it early; at 1200 the
  column is 1144px, the lead sets in two lines instead of three, and its second line runs to 554.5px
  of ink — straight under where this note's first line sits. The note's own height is
  `2 lines + 144 * (arrow / 200) - tuck`, so the arrow's width is what pushes its text up into that
  line: at 180px the first line's ink lands 3.6px above the lead's second — measured — and dropping
  to 150px buys 9px more, because 30px less arrow is 21.6px less height against 12.6px less tuck. So
  about 12.6px of clearance at 1200, and it only grows below it: the arrow is now a constant here, and
  the lead's line box moves *down* as the column narrows. Checked by that arithmetic at 1200, 1000,
  800, 700 and 641. It is also the wrong constraint to solve by moving anything else, since the hero's
  spacing is not this component's to change.

  The step from 104px at 1201 to 150px at 1200 is a jump, deliberately, and in the direction the
  layout jumps: the pocket goes from 72.2px to 635.7px across the same pixel. An arrow that stayed at
  104px there would be the only thing on the page that got smaller as its room got larger.

  The size that it comes back up to is `.isn-hero-note-text`'s own range exactly, 17px to 21px, and
  that is the whole reason this block still exists: with the pocket gone as a constraint there is
  nothing left to trade against, so the right answer is simply to be the same size as the four cues
  this one is a fifth of. It used to reach 22px here, one point past that rule's ceiling, which made
  the note larger below 1200 than above it for no gain a reader could name.

  Both ends are checked against the widest text, 8.15x the font size: 21px sets 171.2px inside the
  678px this `max-width` allows at 1200, and the 17px floor sets 138.6px inside the 166px it allows
  at 641. At that tightest width the widest item is no longer the text at all but the 150px arrow, so
  the cap's real margin there is 16px, not 27px — the one place in this block where the drawing rather
  than the sentence is what has to fit.

  What does *not* change is `left: 410px`. The claims row keeps its 401.9px of ink down to 560,
  and the buttons keep their 452.3px, so the note stays in the same relation to both while the
  column around it triples in width.

  The cards are now 24px to 34px below the buttons rather than beside them, which the note never
  reaches: it lives entirely above the button row.
*/
@media (max-width: 1200px) {
  .isn-scan-annotation {
    max-width: calc(100% - 410px);
    font-size: clamp(17px, 1.9vw, 21px);
  }

  .isn-hand-arrow--cta {
    --isn-cta-arrow: 150px;
  }
}

/*
  ── Hidden at 640 and below, and this is the mobile decision ────────────────

  Not a compact variant. At 640 the copy column is 576px wide and shrinking, while the two buttons
  alone occupy 452.3px of it — so the space the note lives in measures 123.7px there and falls from
  there to nothing, and by 460 the buttons no longer fit on one line and stack (both measured). A
  margin note
  wedged into the 12px between two stacked full-width buttons is not a smaller version of this
  object; it is a layout fault that happens to contain the same words.

  Nothing is lost by removing it. The note's job is to carry the eye from the primary action
  across to the secondary one, and a single stacked column already does that: on a phone
  `Doe gratis besparingsscan` is the next thing under `Plan kennismaking`, unmissable and with no
  card column competing for the same glance. The words themselves are not content — they are
  `aria-hidden`, and the button they point at says what it does.
*/
@media (max-width: 640px) {
  .isn-scan-annotation {
    display: none;
  }
}
