/* shell.css — basis website-shell (B49). Kleuren komen uit /api/shell/skin.css
   (CSS-variabelen --color-*, per tenant_domain). Geen Max-layouts (M2). */

/* --- self-hosted fonts (geen Google-CDN) ------------------------------------
   Spectral = display/serif (koppen, prijzen), Public Sans = body. Beide latin +
   latin-ext (Nederlandse diakrieten), font-display:swap zodat tekst nooit
   onzichtbaar wacht op het font. Bestanden onder student-frontend/fonts/ →
   geserveerd via de /static-mount (main.py). */
@font-face {
  font-family: "Spectral"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/static/fonts/spectral-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Spectral"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/static/fonts/spectral-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Spectral"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/static/fonts/spectral-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Spectral"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/static/fonts/spectral-600-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/static/fonts/publicsans-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/static/fonts/publicsans-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("/static/fonts/publicsans-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("/static/fonts/publicsans-500-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/static/fonts/publicsans-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/static/fonts/publicsans-600-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/static/fonts/publicsans-700-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/static/fonts/publicsans-700-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* KLEUR-fallbacks. skin.css (/api/shell/skin.css) laadt NA dit bestand en emit
     ALTIJD deze zes (skin_generator merget elke site over DEFAULT_PALETTE), dus voor
     een echte site winnen die waarden — ook zonder eigen override. Deze zes zijn dus
     puur een vangnet als skin.css faalt te laden. Wil je de look van één site wijzigen:
     tenant_domains.color_scheme, niet hier. */
  --color-primary: #1f6feb;
  --color-accent: #0969da;
  --color-background: #ffffff;
  --color-surface: #f6f8fa;
  --color-text: #1f2328;
  --color-muted: #656d76;
  --font-scale: 1;

  /* ONTWERPTAAL — gedeelde default voor ELKE site. skin.css emit deze tokens niet,
     dus ze blijven staan; een tenant die afwijkt overschrijft ze later via de skin-
     laag. Bewust KLEURLOOS: de merk-kleur komt uit de zes palet-tokens hierboven. */
  --font-display: "Spectral", Georgia, "Times New Roman", serif;
  --font-body: "Public Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --radius: 12px;
  --radius-sm: 8px;
  --wrap: 1180px;
  /* v23.25 — LEESBREEDTE, naast --wrap. Die 1180px is de breedte van een PAGINA-raster
     (kaartenrijen, hero, footer); voor lopende tekst is hij veel te ruim: een regel van
     1180px dwingt het oog aan het eind van elke regel een sprong te maken en de volgende
     begin terug te vinden. De richtlijn is ~65-72 tekens per regel, en bij de 17px van de
     body komt dat neer op ~576px lopende tekst.

     v23.29 — 36rem WORDT 42rem, en dat is GEEN ruimere regel maar dezelfde. Sinds de
     berichten in een kaart staan is deze maat niet meer de tekstbreedte maar de KOLOM
     eromheen: de kaart pakt daar 90% van en trekt er links en rechts --space (0,85rem)
     binnenruimte van af. 42rem × 0,9 = 37,8rem, min 1,7rem binnenruimte = 36,1rem ≈ 578px
     lopende tekst — dezelfde regel als de 36rem hiervoor, nu mét het kader eromheen.
     Bleef dit op 36rem staan, dan zou de tekst in de kaart terugvallen naar ~56 tekens en
     dat is ónder de richtlijn.

     ⚠ DEZE TOKEN STUURT OOK .reply-form, en dat is gemeten en met opzet zo gelaten. Het
     formulier loopt dus van 0 tot 42rem terwijl geen enkele kaart dat doet — maar de
     Klantenservice-kaarten beginnen op 0 en de Jij-kaarten eindigen op 42rem, dus de
     linkerrand van het formulier valt samen met de ene familie en de rechterrand met de
     andere. Het formulier is precies de omhullende van het gesprek; smaller maken zou het
     juist bij één van de twee vandaan trekken. */
  --wrap-prose: 42rem;
  --gap: 22px;
  --sec-pad: 60px;

  /* v23.24 — zes maten die dit bestand FEITELIJK al hanteerde maar nergens als token,
     zodat het berichten-blok onderaan geen losse getallen hoeft te herhalen:
       --font-size-meta   bijschrift-grootte (.order-date, .line-vat, .att-download,
                          .att-error, .att-toggle stonden allemaal rond 0,85rem)
       --font-size-badge  statuslabel (.order-status, .lic-badge, .refund-badge)
       --radius-badge     de badge-hoek — kleiner dan --radius-sm, drie keer in dit
                          bestand als losse 5px
       --pad-badge        de badge-binnenruimte, in diezelfde drie regels 4px 9px
       --track-badge      de letterspatiëring van datzelfde statuslabel
       --line-hair        de haarlijn: de 1px die dit bestand ~40 keer trekt
     De vier badge-tokens horen bij elkaar: wie er één verandert hoort de badge overal
     mee te zien veranderen. Daarom is het badge-recept HEEL getokeniseerd en niet half.

     ⚠ v23.28 — --line-strong (3px, de "nadruk-lijn") IS HIER WEG. Hij is in v23.24 gemaakt
     voor de afzenderstreep in de thread, en die streep is met de DOCUMENT-herbouw
     verdwenen; daarmee had het token nul gebruikers in dit bestand. De streep onder de
     actieve accounttab (.account-tabs, ~regel 722) staat nog als losse 3px en zou de
     logische nieuwe drager zijn — dat is een sweep over de accountschil en dus een andere
     wijziging. Wie die doet zet dit token terug; tot dan is een variabele die nergens
     wordt aangeroepen alleen maar ruis.
     ⚠ De BESTAANDE regels zijn hierbij ONGEMOEID gelaten en dragen hun eigen literals nog.
     Ze hier omzetten zou een sweep over ~40 regels zijn en dus een andere wijziging dan
     deze; het token is vanaf nu wel de plek waar die waarde hoort te staan. */
  --font-size-meta: 0.85rem;
  --font-size-badge: 0.7rem;
  --radius-badge: 5px;
  --pad-badge: 4px 9px;
  --track-badge: 0.06em;
  --line-hair: 1px;

  /* AFSTANDEN (v23.24). Dit bestand kende alleen --gap (22px, het raster van de landing)
     en --sec-pad (60px, sectiehoogte); voor alles daaronder stonden er losse rem-waarden.
     Dat liep in het berichten-blok hieronder meteen mis: 0,75 / 0,8 / 0,85 / 0,9rem
     stonden er naast elkaar zonder dat één van die verschillen een bedoeling had.
     Vier stappen, genoeg voor de dichtheden die deze schermen kennen, in de
     modifier-naamstijl van --radius / --radius-sm en --color-tint / --color-tint-strong
     (dus GEEN numerieke ladder --space-1/2/3, dat is een andere huisstijl).
     ⚠ REIKWIJDTE: hier gedefinieerd en gebruikt door het berichten-blok. De ~1800 regels
     hierboven dragen hun eigen waarden nog; die omzetten is een sweep en een andere
     wijziging dan deze. Voor nieuw werk is dit wel de plek waar de afstand hoort.

     ⚠ v23.29 — --space-xl (2rem) IS HIER WEG, en zijn afwezigheid is óók een REPARATIE.
     Hij is in v23.28 toegevoegd voor de witruimte-hiërarchie van de "document"-vorm (2rem
     tussen beurten tegen 0,5rem erbinnen); die vorm is vervangen door kaarten en de kaart
     doet het groeperen nu zelf, dus het token had nul aanroepen. Wie ooit weer een vijfde
     stap nodig heeft zet hem hier terug — dezelfde route als bij --line-strong hierboven.

     ⚠⚠ EN DIT STOND STUK. De v23.28-alinea hierboven begon NA een commentaar-sluiter en
     eindigde met een tweede, dus stond hij BUITEN het commentaar. Van "v23.28 — VIJFDE
     STAP" tot de eerstvolgende puntkomma las de CSS-parser daardoor één ongeldige
     declaratie — en die slikte de regel `--space-xs` hieronder mee op. Dat token was dus
     sinds v23.28 ONGEDEFINIEERD, en de vier plekken die het aanroepen (.att-item,
     .msg-meta, .ticket-row > a, en de bijlagengroep in de kaart) vielen stil terug op de
     initiële waarde. Alleen het commentaarhek is hersteld; aan de waarden is niets
     veranderd.

     ⚠ Wie deze alinea bewerkt: schrijf de twee tekens van een commentaar-sluiter hier
     NOOIT letterlijk uit. Dit blok kapotmaken kost precies één zo'n paar, en de schade
     valt een stuk verderop stil (een token dat niet meer bestaat), niet hier. */
  --space-xs: 0.3rem;    /* binnen een regel: gaten tussen inline-onderdelen */
  --space-sm: 0.5rem;    /* tussen items in een lijst, en tussen twee kaarten in de thread */
  --space: 0.85rem;      /* binnenruimte van een blok, inspringing */
  --space-lg: 1.25rem;   /* tussen blokken onderling */

  /* HOVER-LIFT (v23.24). Kaarten die oplichten onder de muis (.cat, .book, .order-card en
     nu de ticketrij) delen één recept: 0,16s, twee pixels omhoog, een zachte schaduw in de
     MERK-kleur. Die vier getallen stonden als losse waarden in elke regel, en dan is
     "dezelfde lift" een afspraak in plaats van een feit. --lift-shadow draagt de color-mix
     mee: custom properties resolven bij GEBRUIK, dus de var(--color-primary) daarbinnen
     pakt gewoon de skin-kleur van de site. Zelfde reikwijdte-voorbehoud als hierboven —
     .order-card c.s. dragen hun eigen getallen nog. */
  --anim-quick: 0.16s;
  --lift-y: -2px;
  --lift-shadow: 0 12px 26px color-mix(in srgb, var(--color-primary) 10%, transparent);

  /* AFGELEIDE kleuren — color-mix uit de palet-tokens, GEEN literals: zo volgen ze
     automatisch de tenant-kleur i.p.v. teal/brass vast te pinnen. Custom properties
     resolven bij gebruik, dus de var()'s hierin pakken de skin-waarde die ná dit
     bestand geladen wordt. */
  /* --color-band — achtergrond van de footerband. DIT IS DE DEFAULT, NIET HET LAATSTE
     WOORD: /api/shell/skin.css laadt NA dit bestand en zet dezelfde variabele op :root
     met gelijke specificiteit, dus een site die zelf een band heeft overschrijft deze
     regel. Zet ze er geen, dan levert skin.css de sleutel helemaal niet en blijft deze
     afleiding staan. Die volgorde is dwingend en staat uitgelegd in index.html.

     v23.32 — DE AFLEIDING IS TERUG OP `primary 82%, #000`, precies zoals vóór v23.30.
     Twee ronden lang is hieraan gesleuteld en beide keren was dat verkeerd:
       v23.30 draaide de menging om naar `primary 88%, #fff` omdat de band op
       dev-mijnprivedocent te donker uitviel. Dat was de verkeerde dader — de band was
       daar donker doordat de PRIMARY van die site te donker stond, niet door deze
       formule. Op vastgoedboek deed hij het al die tijd goed, en dat is ook wat er nu
       op prod draait.
       v23.31 verving hem door een kale terugval op primary. Daarmee werd de footer
       even donker als .over eronder en verdween de scheiding tussen die twee.
     Wat overeind blijft van v23.31 is het goede deel: band IS een skin-sleutel, dus een
     site kan hier wél van afwijken. Alleen de terugval is weer de oude afleiding.

     ⚠ De bekende beperking blijft, en die is bewust geaccepteerd: een color-mix met een
     vast percentage schaalt mee met zijn bron, dus een merkkleur die zelf al zeer donker
     is levert een bijna-zwarte band. Dat is nu geen formulefout meer maar een
     signaal — staat de band te donker, dan staat de primary van die site te donker, of
     de site zet gewoon een eigen band. */
  --color-band: color-mix(in srgb, var(--color-primary) 82%, #000);
  --color-accent-strong: color-mix(in srgb, var(--color-accent) 85%, #000);   /* accent-hover */
  --color-line: color-mix(in srgb, var(--color-text) 12%, var(--color-background)); /* haarlijn */
  --color-tint: color-mix(in srgb, var(--color-primary) 8%, transparent);     /* badge/icon-chip */
  --color-tint-strong: color-mix(in srgb, var(--color-primary) 14%, transparent);
  /* Neutraal "subtiel vlak" (facetten, badges, chips, cover-placeholder, tabelkop).
     Deze plekken gebruikten --color-surface als lichtgrijs vlak; in het warme palet ís
     surface juist het WIT van de kaarten, waardoor ze onzichtbaar werden. Een eigen
     token die de tenant volgt: een vleugje tekstkleur over de achtergrond. */
  --color-subtle: color-mix(in srgb, var(--color-text) 6%, var(--color-background));
}

/* Formulier-elementen erven het font niet van <body> (UA-default = Arial/system).
   Zonder dit stonden de account-tabs, knoppen en invoervelden in een ander font dan de
   rest van de site. :where() = specificiteit 0, dus elke component-regel wint hierover. */
:where(button, input, select, textarea, optgroup) {
  font-family: inherit;
  font-size: inherit;
}

/* v23.143 — DE PLACEHOLDER, ÉÉN REGEL VOOR DE HELE STUDENTFRONTEND.
   Vóór deze regel stond er in dit bestand GEEN enkele `::placeholder`-declaratie: elk veld
   erfde de UA-default. Die is in Chromium `#757575` — gemeten, niet aangenomen — en dat is
   het enige stukje tekst op de pagina dat NIET uit de tokens komt. Het beweegt dus ook niet
   mee zodra een site zijn eigen kleuren zet, wat precies het half-werkende-knop-probleem is
   waar de literal-regel voor bestaat. Bewust ONGESCOOPT (geen selector op het postcodeveld):
   elk veld met een placeholder heeft hetzelfde gedrag, en een gescoopte regel zou de
   volgende vijf velden weer aan de UA overlaten.

   ⚠ `currentColor` EN NIET `var(--color-muted)`, en dat is een gemeten keuze. De invoer-
   velden dragen nergens een eigen `color`/`background`: ze staan op de UA-default zwart op
   wit — óók in het donkere thema, waar ze witte eilanden blijven. `--color-muted` is daar
   `#8b949e`, en dat levert op zo'n wit veld 3,08:1. Een palet-token zou dus een toestand
   ONDER AA introduceren die vandaag niet bestaat. `currentColor` is de kleur van het veld
   zelf: de placeholder is per definitie een verbleekte versie van wat je zou typen, en
   blijft dat ook als de velden ooit wél een palet-kleur krijgen.

   ⚠ DE DEKKING IS DE LAAGSTE DIE AA NOG HAALT, en dat is geen ruime marge maar de bodem.
   Gemeten op het veld zoals het vandaag rendert (zwarte tekst, wit vlak):
       55% -> 4,76:1   <- deze regel
       54% -> 4,59:1   <- de UA-default van vandaag
       53% -> 4,42:1   (onder AA)
   De ruimte om de placeholder VAGER te maken dan hij nu is, is daarmee nul: de UA-default
   zat al op de AA-grens. Wie hem zichtbaar lichter wil, kiest bewust voor sub-AA — dat is
   geen keuze die hier stilzwijgend gemaakt is. De getypte waarde staat er ter vergelijking
   op 21:1, dus het onderscheid hint/waarde is ruim. */
::placeholder { color: color-mix(in srgb, currentColor 55%, transparent); }

* { box-sizing: border-box; }

/* Scrollbar-gutter altijd reserveren. Zonder dit springt élk gecentreerd blok (.app-nav ul,
   .wrap — beide `margin: 0 auto`) een halve scrollbarbreedte naar rechts zodra je van een
   lange pagina naar een korte gaat: de scrollbar verdwijnt, het viewport wordt breder en de
   centrering verschuift mee. Zichtbaar bij o.a. Inloggen en Over ons. */
html { scrollbar-gutter: stable; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: calc(17px * var(--font-scale));
  color: var(--color-text);
  background: var(--color-background);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* Koppen dragen het display-font, site-breed. Bewust in :where() (specificiteit 0):
   de prose-basislaag verderop (óók :where(), dus gelijke specificiteit) staat LATER
   in dit bestand en wint daarmee op kleur/grootte/marge — die twee vullen elkaar aan
   in plaats van te vechten. Component-classes (.card-title, .pay-btn …) winnen sowieso.
   Een kale `h1,h2,h3{}`-regel (specificiteit 1) zoals in het mockup zou de prose-laag
   juist OVERSTEMMEN — daarom niet. */
:where(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
  color: var(--color-primary);
}

body[data-theme="dark"] {
  --color-background: #0d1117;
  --color-surface: #161b22;
  --color-text: #e6edf3;
  --color-muted: #8b949e;
}

/* --- meekijkbalk (v23.x) ---
   Alleen zichtbaar in een MEEKIJKSESSIE (`ctx.meekijken` uit /api/shell/context); een
   gewone bezoeker ziet hem nooit, want dan blijft #app-meekijk leeg. Amber en niet rood,
   net als in het partnerportaal: er is niets mis, er kijkt alleen iemand mee.

   ⚠ VASTE KLEUREN EN GEEN SKIN-VARIABELEN, bewust. Deze balk moet op ELKE site hetzelfde
   signaal geven; een site-palet dat 'm in de eigen huisstijl opneemt maakt hem onzichtbaar
   op precies de plek waar hij moet opvallen. Om dezelfde reden staat hij BOVEN de header
   en niet erin. */
.meekijk {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  background: #fef3c7; border-bottom: 1px solid #f59e0b; color: #78350f;
  padding: 10px 2rem;
}
.meekijk-tekst { font-size: 0.9rem; }
.meekijk button {
  background: #78350f; color: #fff; border: 0; border-radius: 6px;
  padding: 6px 12px; font-size: 0.85rem; cursor: pointer; font-family: inherit;
}
.meekijk button[disabled] { opacity: 0.6; cursor: default; }

/* --- header + nav ---
   Twee losse strooken: #app-header draagt ALLEEN het merk (logo XOR merknaam-tekst),
   #app-nav is de menubalk (menu_builder). Bewust niet samengevoegd tot één balk zoals
   het mockup: één renderer per menu-locatie (zie header.js). Ze delen wel de opmaak —
   papier-achtergrond + warme haarlijn eronder — zodat het als één kop leest. */
/* Inset bewust ruimer dan de nav eronder: het logo plakte tegen de linker- en bovenrand.
   Alleen deze strook — de nav-items hangen aan hun eigen wrap-uitlijning (.app-nav ul) en
   mogen niet meeschuiven. */
.app-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.5rem 2rem 0.85rem;
  background: var(--color-background);
}
.app-header .brand { display: flex; align-items: center; gap: 0.6rem; }
.app-header .brand img { height: 40px; width: auto; }
/* De merk-link is het wordmark, geen gewone link: geen browser-default blauw + streep.
   Zichtbaar op sites zonder logo (daar is de merknaam tekst, in het display-font). */
.app-header .brand a {
  display: inline-flex; align-items: center; gap: 0.6rem;
  color: var(--color-primary); text-decoration: none;
  font-family: var(--font-display); font-size: 1.6rem; font-weight: 600;
  letter-spacing: -0.01em; line-height: 1;
}
/* Sticky menubalk: blijft bij scrollen bovenaan (alleen #app-nav; #app-header scrollt weg).
   sticky (niet fixed) houdt de balk in de document-flow, zodat de scroll-restore
   (9a7a744/2018f8f) blijft kloppen. Geen overflow-val: de enige ancestors zijn html/body
   en geen van beide zet overflow/transform/contain/filter. De achtergrond is al dekkend
   (--color-background), dus content scrolt niet zichtbaar door de balk. z-index 50 = boven
   de pagina-content (max 40: .suggest-dropdown) en onder de cart-lade/overlay (90/91).
   Boven alles staat sinds v23.341 de toast (.mpd-toast, 100): een melding die onder de
   openstaande cart-lade valt is geen melding. */
.app-nav {
  position: sticky; top: 0; z-index: 50;
  background: var(--color-background); border-bottom: 1px solid var(--color-line);
}
.app-nav ul {
  list-style: none; display: flex; gap: 30px; margin: 0 auto;
  /* Symmetrische verticale ruimte (top == bottom): stond op 0 top / 0.9rem bottom,
     waardoor de inhoud tegen de bovenrand plakte — zichtbaar zodra de balk sticky is en
     tegen de viewport-rand staat. 0.5rem boven én onder centreert de inhoud optisch zonder
     de balk te veel te laten groeien. Horizontaal (28px) en align-items:center ongewijzigd.
     Net boven de #app-nav min-height van 48px (33,6px knop + 2× 0,5rem ≈ 49,6px), dus de
     verlaging blijft zichtbaar en de inhoud blijft gecentreerd. */
  padding: 0.5rem 28px; width: min(var(--wrap), 100%);
  /* align-items:center: zonder dit valt de nav terug op `stretch` en top-lijnen de items
     op hun eigen offset (menu-links padding-top 4px, cart-knop veel minder), waardoor de
     cart-indicator te hoog tegen de bovenkant plakt. Centreren zet icoon, teller, bedrag
     én de menu-/account-links op dezelfde middenlijn. */
  align-items: center;
}
/* Menu-link: brass-onderstreping die uitschuift bij hover (mockup .nav-links a::after).
   Géén gevulde primary-hover meer — dat vocht met de rustige papier-kop. */
.app-nav a {
  position: relative; display: inline-block; padding: 4px 0;
  text-decoration: none; color: var(--color-text);
  font-weight: 500; font-size: 0.97rem;
}
.app-nav a::after {
  content: ""; position: absolute; left: 0; bottom: -2px;
  width: 0; height: 2px; background: var(--color-accent);
  transition: width 0.18s ease;
}
.app-nav a:hover { color: var(--color-primary); }
.app-nav a:hover::after { width: 100%; }
/* Auth-link: geen menu_builder-item maar auth-conditioneel (navigation.js kiest label +
   route op basis van /api/student/profile). Daarom visueel afgezet — naar rechts geduwd —
   zodat hij niet als vierde menu-item leest.

   v23.247 — de haarlijn ervóór is eruit. De rest van de balk draagt geen enkele scheiding,
   dus één losse verticale lijn las als een restant in plaats van als structuur. De
   plaatsing doet het werk nu alleen: `margin-left: auto` slokt alle vrije ruimte op en
   duwt de groep tegen de rechterkant, ver van het laatste menu-item.

   `padding-left` blijft op 30px staan en gaat NIET omhoog. Op een ruime balk voegt hij
   niets zichtbaars toe (de auto-marge levert daar allang meer afstand dan welke padding
   ook) en zou een grotere waarde alleen de groep verder van de rechterrand duwen. Hij
   telt juist in het ENIGE geval waarin de auto-marge niets doet: een balk zónder vrije
   ruimte — smal venster, veel menu-items, of de gewrapte vorm onder 640px. Dáár is dit
   de laatste scheiding die er nog is, en samen met de 30px `gap` op `.app-nav ul` staat
   de groep dan op 60px van het laatste menu-item: het dubbele van de normale
   item-afstand, wat als groepsgrens leest zonder dat er een lijn voor nodig is. */
.app-nav .nav-auth {
  margin-left: auto; padding-left: 30px;
}
.app-nav .nav-auth a { color: var(--color-primary); font-weight: 600; }
/* v23.245 — de streep hoort bij de GROEP, niet bij elk lid. De auth-groep telt twee li's
   (uitgelogd Registreren + Inloggen, ingelogd Mijn account + Uitloggen) en de regel
   hierboven gaf ze allebei een border-left én een margin-left:auto. Dat zijn twee fouten
   tegelijk: een tweede haarlijn middenin de groep, en twee auto-marges die de vrije ruimte
   ONDERLING verdelen — waardoor de twee links ook nog eens uit elkaar getrokken werden in
   plaats van als paar rechts te staan. Deze regel neutraliseert allebei bij elke .nav-auth
   die DIRECT op een andere volgt; de 30px tussenruimte komt dan van de gap op .app-nav ul,
   net als tussen de menu-items.

   Waarom een selector en geen klasse die JavaScript bijhoudt: de streep moet vóór de EERSTE
   van de groep staan, en CSS leidt zelf af wie dat is. Wie er eerste staat, hoeveel leden
   de groep telt en of hun inhoud net gewisseld is doet niet ter zake — er is geen moment
   waarop de markup en een handgezette klasse uit de pas kunnen lopen.

   v23.247 — de border is uit de regel hierboven verdwenen, dus `border-left: 0` hieronder
   neutraliseert vandaag niets meer. Het blijft bewust staan als vangnet: komt de haarlijn
   ooit terug, dan verschijnt hij meteen weer één keer in plaats van bij elk lid van de
   groep. De twee andere declaraties zijn wél levend en dragen nu het hele gedrag —
   `margin-left: 0` is wat de twee links als paar bij elkaar houdt. */
.app-nav .nav-auth + .nav-auth {
  margin-left: 0; padding-left: 0; border-left: 0;
}
/* Geen menu-items voor deze site → navigation.js laat #app-nav leeg. Zonder dit zou de
   lege <nav> nog steeds z'n achtergrond + onderrand tonen: een kaal streepje. */
.app-nav:empty { display: none; }

/* --- content --- */
/* Ruimte RESERVEREN voor wat JS nog moet vullen. De vier mountpunten (header, nav,
   content, footer) beginnen leeg en worden pas na hun fetches gevuld. Zonder deze
   min-heights is de pagina bij de eerste tekening korter dan het scherm: de footer
   klimt omhoog, en zodra de inhoud binnenkomt klapt hij weer naar beneden. Gemeten op
   prod was dat de zwaarste layout-shift van de hele pagina (0,68) — zichtbaar als een
   hik bij elke paginaovergang. De waardes zijn de daadwerkelijk gerenderde hoogtes,
   niet gegokt: header 77,6px / nav 48,2px. Voor de inhoud is 60vh een ondergrens die
   de pagina meteen langer maakt dan het scherm, zodat de footer buiten beeld begint
   en zijn groei niemand meer opvalt. */
#app-header { min-height: 78px; }
#app-nav { min-height: 48px; }
.app-content { flex: 1; width: min(960px, 92vw); margin: 1.5rem auto; min-height: 60vh; }
.app-content h1 { color: var(--color-primary); }
/* Landing: full-bleed. De secties dragen hun eigen .wrap (max --wrap), zodat de
   donkere over-band en de papier-band van rand tot rand lopen. home.js zet deze class;
   shell.js haalt 'm bij elke andere route weer weg. */
.app-content.home-page { width: 100%; max-width: none; margin: 0; }

/* --- prose-basislaag -------------------------------------------------------------
   Tot nu toe had de shell GEEN element-styling: links, koppen en alinea's vielen terug
   op de browser-default (blauw onderstreept, geen ritme). Dat viel het hardst op bij de
   zes tekstpagina's — page.js zet de node_texts-HTML rauw in #app-content, zonder één
   class om op te haken.

   Alles hieronder staat in `:where()`, dus met specificiteit NUL. Dat is bewust: de
   component-regels verderop in dit bestand (.pay-btn, .vorm-opt, .subject-tag,
   .suggest-item …) zetten eigen kleuren op een enkele class en moeten ALTIJD winnen,
   ongeacht hun positie in het bestand. Een gewone `.app-content a`-regel zou ze
   overstemmen en bv. de witte knoptekst teal kleuren. Dit is dus een echte basislaag:
   hij vult alleen wat niemand anders invult. Kleuren komen uit de skin-tokens. */
:where(.app-content) {
  line-height: 1.6;
}
:where(.app-content) :where(a) {
  color: var(--color-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
:where(.app-content) :where(a):hover {
  color: var(--color-accent);
}
:where(.app-content) :where(h1, h2, h3, h4, h5, h6) {
  color: var(--color-primary);
  line-height: 1.25;
  margin: 1.8em 0 0.6em;
  font-weight: 600;
}
/* Eerste kop van een pagina niet onnodig van de bovenrand duwen. */
:where(.app-content) > :where(h1, h2, h3):first-child {
  margin-top: 0;
}
:where(.app-content) :where(h1) { font-size: 1.9rem; }
:where(.app-content) :where(h2) { font-size: 1.45rem; }
:where(.app-content) :where(h3) { font-size: 1.2rem; }
:where(.app-content) :where(h4, h5, h6) { font-size: 1rem; }
:where(.app-content) :where(p) { margin: 0 0 1em; }
:where(.app-content) :where(ul, ol) { margin: 0 0 1em; padding-left: 1.4em; }
:where(.app-content) :where(li) { margin: 0.3em 0; }
:where(.app-content) :where(li)::marker { color: var(--color-accent); }
:where(.app-content) :where(strong, b) { font-weight: 600; }
:where(.app-content) :where(blockquote) {
  margin: 1.2em 0;
  padding: 0.6em 1em;
  border-left: 3px solid var(--color-accent);
  background: var(--color-subtle);
  color: var(--color-muted);
}
:where(.app-content) :where(hr) {
  border: 0;
  border-top: 1px solid var(--color-muted);
  opacity: 0.35;
  margin: 2em 0;
}
:where(.app-content) :where(table) {
  border-collapse: collapse;
  width: 100%;
  margin: 0 0 1.2em;
}
:where(.app-content) :where(th, td) {
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--color-muted);
  text-align: left;
}
:where(.app-content) :where(th) {
  background: var(--color-subtle);
  font-weight: 600;
}

/* v23.102 — TABELLEN OP SMALLE SCHERMEN. De cookieverklaring is de eerste juridische
   tekst met een tabel (vier kolommen); op een telefoon liep de laatste kolom buiten
   beeld en was er geen manier om erbij te komen. Afgekapt is erger dan scrollen: de
   bewaartermijn is nu juist de kolom waarvoor iemand de pagina opent.

   De inhoud komt uit `node_texts` en is platte body-HTML zonder wrapper-element, dus
   de scroll moet op de tabel zelf. `overflow` doet niets op `display: table`, vandaar
   `display: block` — de rijen eronder blijven table-row-groups, dus border-collapse,
   de th-achtergrond en de kolomuitlijning blijven staan.

   ⚠ NIET zonder de min-width op de cellen. Een tabel die in `display: block` staat
   perst zichzelf liever samen dan dat hij overloopt: zonder ondergrens knijpt de
   doel-kolom tot een letter of drie per regel en ontstaat er nooit een scrolbalk.
   Met 7.5rem per cel is de tabel minstens 30rem breed en scrolt hij pas, en alleen,
   wanneer het scherm smaller is dan dat.

   Grens op 940px, hetzelfde breakpoint als de rest van dit bestand. Boven die breedte
   verandert er niets. Eronder ook niet zolang de tabel past — een tabel die binnen de
   30rem blijft krijgt geen scrolbalk te zien. */
@media (max-width: 940px) {
  :where(.app-content) :where(table) {
    display: block;
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }
  :where(.app-content) :where(th, td) {
    min-width: 7.5rem;
  }
}

:where(.app-content) :where(code, pre) {
  background: var(--color-subtle);
  border-radius: 4px;
}
:where(.app-content) :where(code) { padding: 0.1em 0.35em; font-size: 0.92em; }
:where(.app-content) :where(pre) { padding: 0.8em 1em; overflow-x: auto; }
:where(.app-content) :where(img) { max-width: 100%; height: auto; }

/* ===========================================================================
   ONTWERPTAAL (academicstore-landing.html) — GEDEELD, token-gedreven
   ---------------------------------------------------------------------------
   Alle merk-dragende kleuren verwijzen naar de skin-tokens (of een color-mix
   daaruit), nooit naar een literal — anders zou een tenant-override niet
   doorwerken. Alleen de decoratieve cover-gradients en schaduw-rgba's zijn
   literal: dat zijn placeholders, geen merkdragers.
   =========================================================================== */

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 28px; }

/* --- signatuur: brass-blokje + brass-onderstreping --- */
/* `display:flex` + `width:fit-content` — NIET inline-flex zoals het mockup. De kop
   ernaast draagt .u-rule (display:inline-block, nodig voor de brass-onderstreping onder
   de tekstbreedte), en twee inline-level elementen vloeien op dezelfde regel: de eyebrow
   schoof dan bovenop de kop zodra die kort genoeg was om ernaast te passen. */
.eyebrow {
  display: flex; width: fit-content; align-items: center; gap: 9px;
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--color-primary); margin-bottom: 18px;
}
.eyebrow::before {
  content: ""; width: 22px; height: 10px; border-radius: 2px;
  background: var(--color-accent); display: inline-block; flex: none;
}
.u-rule { position: relative; padding-bottom: 14px; display: inline-block; }
.u-rule::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 64px; height: 3px; border-radius: 2px; background: var(--color-accent);
}

/* --- knoppen --- */
.btn {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-body); font-weight: 600; font-size: 0.98rem;
  padding: 14px 24px; border-radius: var(--radius-sm); cursor: pointer;
  border: 1.5px solid transparent; text-decoration: none;
  transition: transform 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.btn-primary {
  background: var(--color-accent); color: #fff;
  box-shadow: 0 6px 18px color-mix(in srgb, var(--color-accent) 28%, transparent);
}
.btn-primary:hover { background: var(--color-accent-strong); color: #fff; transform: translateY(-1px); }
.btn-ghost { background: transparent; color: var(--color-primary); border-color: var(--color-line); }
.btn-ghost:hover { border-color: var(--color-primary); transform: translateY(-1px); }
.btn .arw { transition: transform 0.15s ease; }
.btn:hover .arw { transform: translateX(3px); }

/* --- secties --- */
.home-page section { padding: var(--sec-pad) 0; }
.sec-head { max-width: 640px; margin-bottom: 44px; }
.sec-head h2 { font-size: clamp(1.7rem, 3vw, 2.3rem); margin: 0; }
.sec-head p { color: var(--color-muted); margin: 16px 0 0; font-size: 1.06rem; }

/* --- hero --- */
.hero-grid { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center; }
.hero h1 { font-size: clamp(2.4rem, 4.6vw, 3.6rem); letter-spacing: -0.015em; margin: 0; }
.hero .lede { font-size: 1.16rem; color: var(--color-muted); margin: 22px 0 34px; max-width: 34em; }
.hero-cta { display: flex; gap: 14px; flex-wrap: wrap; }
.hero-meta {
  margin-top: 34px; display: flex; gap: 26px; flex-wrap: wrap;
  color: var(--color-muted); font-size: 0.9rem;
}
.hero-meta span { display: inline-flex; align-items: center; gap: 8px; }
.hero-meta span::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-accent); flex: none;
}

/* Showcase: drie gestapelde covers. Slot voor echte product-covers — is er een
   cover_url, dan vult .stack-cover-img 'm; anders de CSS-gradient-placeholder met
   de titel als tekst. Gradients bewust literal: decoratief, geen merkdrager. */
.stack { position: relative; height: 420px; }
.stack-cover {
  position: absolute; width: 222px; aspect-ratio: 3 / 4; border-radius: 8px;
  box-shadow: 0 22px 44px rgba(22, 58, 52, 0.2), 0 3px 10px rgba(0, 0, 0, 0.1);
  padding: 22px 20px; display: flex; flex-direction: column; color: #fff;
  overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1); text-decoration: none;
}
.stack-cover::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 7px; background: rgba(0, 0, 0, 0.16); z-index: 1;
}
.stack-cover .ctag {
  font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 700; opacity: 0.82;
}
.stack-cover .ctitle {
  font-family: var(--font-display); font-size: 1.28rem; font-weight: 600;
  line-height: 1.18; margin-top: auto; color: #fff;
}
.stack-cover .cfoot {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.22);
  font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
  display: flex; justify-content: space-between; align-items: center;
}
.stack-cover .cbrass { width: 16px; height: 8px; background: var(--color-accent); border-radius: 2px; }
/* Echte cover: vult de kaart, de tekstlaag valt weg. */
.stack-cover--img { padding: 0; }
.stack-cover--img img { width: 100%; height: 100%; object-fit: cover; }
/* Rotatiehoeken bewust klein: een geroteerd element wordt op een gedraaid pixelraster
   gerasterd en bilineair geïnterpoleerd, wat de fijne covertekst zichtbaar verzacht.
   Dat schaalt mee met de hoek — vandaar ~1-2° in plaats van 5-6°. Meer bronpixels
   helpen hier niet: de layout-box is 222px, dus de srcset-keuze verandert er niet van. */
.sc1 { background: linear-gradient(160deg, #215a51, #173f39); left: 0; top: 36px; transform: rotate(-2deg); z-index: 1; }
.sc2 { background: #2b322e; right: 6px; top: 0; transform: rotate(1.5deg); z-index: 2; }
.sc3 { background: linear-gradient(160deg, #caa25f, #b07f34); left: 96px; top: 96px; transform: rotate(-0.5deg); z-index: 3; }

/* --- categorie-grid ("Ons aanbod") --- */
.cat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.cat {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: var(--radius); padding: 26px 24px 22px; text-decoration: none;
  display: flex; flex-direction: column; min-height: 172px;
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.cat:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 30px color-mix(in srgb, var(--color-primary) 10%, transparent);
  border-color: color-mix(in srgb, var(--color-primary) 22%, var(--color-line));
}
.cat h3 { font-size: 1.24rem; margin: 0 0 9px; }
.cat p { color: var(--color-muted); font-size: 0.95rem; line-height: 1.55; margin: 0; }
.cat .go {
  margin-top: auto; padding-top: 16px; color: var(--color-primary);
  font-weight: 600; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 7px;
}
.cat .go .arw { color: var(--color-accent); transition: transform 0.15s ease; }
.cat:hover .go .arw { transform: translateX(4px); }

/* --- uitgelichte uitgaven (band) --- */
.band-paper {
  background: linear-gradient(180deg, var(--color-surface), var(--color-background));
  border-top: 1px solid var(--color-line);
  border-bottom: 1px solid var(--color-line);
}
.book-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }
.book {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: var(--radius); overflow: hidden; text-decoration: none;
  display: flex; flex-direction: column;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}
.book:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 34px color-mix(in srgb, var(--color-primary) 12%, transparent);
}
.book-cover {
  aspect-ratio: 3 / 4; position: relative; padding: 20px 18px;
  display: flex; flex-direction: column; color: #fff; overflow: hidden;
}
.book-cover::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 6px; background: rgba(0, 0, 0, 0.16); z-index: 1;
}
/* Placeholder-gradients: rouleren per positie zolang een product geen cover heeft. */
.bc-0 { background: linear-gradient(160deg, #22574e, #173f39); }
.bc-1 { background: linear-gradient(160deg, #3d5a63, #284049); }
.bc-2 { background: linear-gradient(160deg, #2f3733, #232a27); }
.bc-3 { background: linear-gradient(160deg, #c7a05c, #ac7c33); }
.book-cover .ctag {
  font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 700; opacity: 0.8;
}
.book-cover .ctitle {
  font-family: var(--font-display); font-size: 1.12rem; font-weight: 600;
  line-height: 1.2; margin-top: auto; color: #fff;
}
/* Echte cover vult de hele kaart-kop. */
.book-cover--img { padding: 0; }
.book-cover--img img { width: 100%; height: 100%; object-fit: cover; }
.book-body { padding: 16px 18px 18px; display: flex; flex-direction: column; flex: 1; }
/* Eigen naam (.book-badge, niet .badge): de shop-badges zijn een andere component. */
.book-badge {
  align-self: flex-start; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--color-primary); background: var(--color-tint);
  padding: 4px 9px; border-radius: 5px; margin-bottom: 10px;
}
.book-body .bt { font-weight: 600; color: var(--color-text); font-size: 0.98rem; line-height: 1.35; }
.book-body .ed { color: var(--color-muted); font-size: 0.85rem; margin-top: 4px; }
.book-foot {
  margin-top: auto; padding-top: 14px;
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.book-foot .price {
  font-family: var(--font-display); font-weight: 600;
  color: var(--color-primary); font-size: 1.18rem;
}
.book-foot .price-original { font-size: 0.9rem; }
/* Label in PRIMARY, pijl in accent — zelfde cue als .cat .go en .aud .link. Stond als
   enige volledig in brass: dat haalt op de witte kaart maar 3.0:1, te weinig voor tekst
   (primary: 9.4:1). Het pijltje blijft brass; als icoon valt het buiten de tekst-eis. */
.book-foot .go {
  color: var(--color-primary); font-weight: 600; font-size: 0.86rem;
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
}
.book:hover .go .arw { transform: translateX(4px); }
.book-foot .go .arw { color: var(--color-accent); transition: transform 0.15s ease; }

/* --- doelgroepen --- */
.aud-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
/* De kaart is zélf de link (zie doelgroepenSection). Kleur expliciet: als <a> zou de
   inhoud anders de UA-linkkleur erven i.p.v. de tekstkleur. */
.aud {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: var(--radius); padding: 32px 28px;
  color: var(--color-text); text-decoration: none;
  display: flex; flex-direction: column;
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
a.aud:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 30px color-mix(in srgb, var(--color-primary) 10%, transparent);
  border-color: color-mix(in srgb, var(--color-primary) 22%, var(--color-line));
}
.aud .ico {
  width: 46px; height: 46px; border-radius: 10px; background: var(--color-tint);
  display: flex; align-items: center; justify-content: center;
  color: var(--color-primary); margin-bottom: 18px;
}
.aud .ico svg { width: 24px; height: 24px; }
.aud h3 { font-size: 1.26rem; margin: 0 0 11px; }
.aud p { color: var(--color-muted); font-size: 0.96rem; line-height: 1.6; margin: 0 0 18px; }
.aud .link {
  color: var(--color-primary); font-weight: 600; font-size: 0.92rem;
  display: inline-flex; align-items: center; gap: 7px; text-decoration: none;
  margin-top: auto; align-self: flex-start;
}
.aud .link .arw { color: var(--color-accent); transition: transform 0.15s ease; }
.aud:hover .link .arw { transform: translateX(4px); }

/* --- over-band (donker) ---
   ⚠ DIT IS DE TWEEDE DONKERE BAND van de landing, en hij leest var(--color-primary) KAAL.
   De footer eronder leest --color-band, dat standaard 82% van deze kleur is. Ze bewegen
   dus samen mee met het merk, maar staan NIET op dezelfde waarde: op de landingspagina
   staan de twee onder elkaar en is de footer zichtbaar donkerder. Die trap is het
   ontwerp — hij is wat je op prod ziet.

   ⚠ v23.31 zette dit vlak óók op --color-band en haalde daarmee precies die scheiding
   weg (beide vlakken werden identiek, want band viel toen kaal terug op primary). Dat is
   in v23.32 teruggedraaid. Wie hier ooit weer aan wil: het punt van de band-sleutel is
   dat de FOOTER kan afwijken, niet dat de twee samenvallen.

   De tekstkleuren hieronder mengen met var(--color-primary) — hier terecht, want dat is
   ook de achtergrond van dit vlak. In de footer is dat anders; zie daar. */
.over { background: var(--color-primary); color: color-mix(in srgb, #fff 88%, var(--color-primary)); }
.over h2 { color: #fff; font-size: clamp(1.7rem, 3vw, 2.35rem); max-width: 16em; margin: 0; }
.over p {
  color: color-mix(in srgb, #fff 76%, var(--color-primary));
  max-width: 44em; margin: 22px 0 0; font-size: 1.08rem; line-height: 1.7;
}
.over .eyebrow { color: #fff; }
.over .quals { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin-top: 44px; }
.qual { border-top: 2px solid var(--color-accent); padding-top: 16px; }
.qual .qt { font-family: var(--font-display); color: #fff; font-size: 1.14rem; font-weight: 600; }
.qual .qd {
  color: color-mix(in srgb, #fff 62%, var(--color-primary));
  font-size: 0.92rem; margin-top: 6px; line-height: 1.55;
}

/* --- reveal ---
   Basisstaat is ZICHTBAAR (.reveal krijgt alleen een lichte verschuiving mee zodra
   JS 'm observeert). Faalt of ontbreekt JS, dan staat alles er gewoon — nooit een
   pagina die op opacity:0 blijft hangen. home.js zet .reveal--armed pas ná het
   aanmaken van de IntersectionObserver. */
.reveal--armed { opacity: 0; transform: translateY(14px); }
.reveal { transition: opacity 0.6s ease, transform 0.6s ease; }
.reveal.in { opacity: 1; transform: none; }

/* --- focus --- */
a:focus-visible, .btn:focus-visible, button:focus-visible {
  outline: 3px solid var(--color-accent); outline-offset: 3px; border-radius: 4px;
}

@media (max-width: 940px) {
  .hero-grid { grid-template-columns: 1fr; gap: 40px; }
  .stack { height: 360px; max-width: 420px; margin: 0 auto; }
  .cat-grid { grid-template-columns: repeat(2, 1fr); }
  .book-row { grid-template-columns: repeat(2, 1fr); }
  .aud-grid { grid-template-columns: 1fr; }
  .over .quals { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .cat-grid { grid-template-columns: 1fr; }
  .book-row { grid-template-columns: 1fr 1fr; }
  .home-page section { padding: 44px 0; }
  .wrap { padding: 0 18px; }
}
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal--armed { opacity: 1; transform: none; transition: none; }
  .btn, .cat, .book, .aud, .pay-btn, .btn-secondary,
  .pager button, .qty-stepper button { transition: none; }
  /* De hover-lift is beweging: uitzetten, maar de kleurwissel blijft (dat is de cue). */
  .pay-btn:not([disabled]):hover,
  .btn-secondary:not([disabled]):hover { transform: none; }
}

/* --- In-app reader -----------------------------------------------------------
   De VOLLEDIGE leesweergave (layout, TOC, koppen, tabellen, afbeeldingen,
   prev/next, resume-banner) staat in de GEDEELDE bron shared-ui/reader-view.css
   (gescoped op `.reader-view`), zodat de student-reader en de cockpit-preview
   er identiek uitzien. Zie index.html: <link href="/shared/reader-view.css">.
   Hier bewust NIETS meer over `.reader-*` — één bron van waarheid. */

/* De generieke `:where(.app-content) > :where(h1,h2,h3):first-child { margin-top: 0 }`
   duwde deze kop strak tegen de scheidslijn onder de nav. Hier bewust wél lucht: de
   accountkop is de start van een ingelogd scherm, niet van een tekstpagina. Eigen class
   (geen :where) → wint moeiteloos van die nul-specificiteit-regel. */
.account-title { margin-top: 2rem; }
/* v23.23 — de balk bevat sinds deze versie zowel <button> (de zes panelen, DOM-lokaal) als
   <a>: Berichten is een eigen pagina, en OP /berichten zijn alle zeven ingangen links terug
   naar /account#<tab>. Vandaar `:is(button, a)` — zonder dat viel de Berichten-ingang
   buiten de opmaak en stond er een blauw onderstreepte link tussen de tabs.
   De markering hangt nu aan de KLASSE .account-tab--active in plaats van aan
   aria-selected: op /berichten is niets een echte tab, dus daar zou aria-selected liegen,
   terwijl de streep eronder er in beide standen hetzelfde uit moet zien. */
.account-tabs { display: flex; gap: 0.5rem; border-bottom: 1px solid var(--color-muted); }
/* v23.52 — DE BERICHTEN-TAB STOND HOGER DAN DE ZES ANDERE. Browser-gemeten door Jelte, op elk
   venster, en de oorzaak zit in de combinatie van twee dingen die allebei in dit blok stonden:

   (1) DE BALK IS EEN FLEX-CONTAINER ZONDER `align-items`, dus de default `stretch` geldt en
       elk item wordt zo hoog als het hoogste;
   (2) ALLEEN DE ACTIEVE TAB HAD EEN 3px ONDERRAND, de rest `border: 0`. De items waren dus
       ongelijk hoog en de niet-actieve werden 3px opgerekt.

   In die extra 3px lopen een <button> en een <a> uiteen: een knop centreert zijn inhoud in de
   ruimte die hij krijgt, een (in een flexcontainer geblokkeerde) link zet zijn tekst bovenaan.
   Zes van de zeven ingangen zijn <button>; Berichten is de enige <a>, want dat is een eigen
   pagina (v23.23). Precies die ene stond dus ~1,5px hoger.

   TWEE INGREPEN, allebei op DEZE gedeelde regel en niet als uitzondering op die ene tab:
     - elke tab krijgt dezelfde 3px onderrand, transparant als hij niet actief is. Daarmee zijn
       alle items even hoog, heeft `stretch` niets meer te verdelen en kan het verschil tussen
       de twee elementsoorten niet meer ontstaan. Bijvangst: het wisselen van tab verschuift de
       balk niet meer, want de rand groeit niet — alleen zijn KLEUR wisselt;
     - `display: flex; align-items: center` zet de inhoud van beide soorten op dezelfde manier
       verticaal. Dat is de gordel naast de bretel: komt er ooit tóch extra ruimte in een item,
       dan centreren ze allebei in plaats van uiteen te lopen.
   ⚠ De 3px staat nu op ÉÉN plek. .account-tab--active wisselt alleen nog de kleur; hij droeg
   de maat eerst zelf, en dan zouden er twee literals zijn die samen moeten bewegen. Zie ook de
   noot bij --line-strong bovenin dit bestand: dit is de plek die dat token zou dragen als de
   accountschil-sweep ooit komt. */
.account-tabs :is(button, a) {
  border: 0; border-bottom: 3px solid transparent;
  background: none; padding: 0.6rem 1rem; cursor: pointer;
  display: flex; align-items: center;
  color: var(--color-text); font: inherit; text-decoration: none; white-space: nowrap;
  /* v23.53 — de overgang staat op de BASISregel en niet op :hover, zodat hij twee kanten op
     werkt: aanwijzen én weer wegbewegen. Alleen kleur, geen beweging — vandaar dat dit blok
     niet in de prefers-reduced-motion-uitzondering hierboven staat; die zet transities uit
     bij componenten die ook LIFTEN, en de noot daar zegt met zoveel woorden dat de kleurwissel
     juist de cue is.
     ⚠ v23.54 — `background` IS UIT DEZE LIJST. Met de hover-vulling weg verandert er nergens
     nog een achtergrond op een tab: de basisregel zet `background: none` en geen enkele
     toestand overschrijft dat. Een transitie op een eigenschap die niet beweegt is dode
     opgave, en ze zou bij de volgende ronde suggereren dat er nog een vulling te verwachten
     valt. Wat overblijft is precies wat er wél overgaat. */
  transition: border-color var(--anim-quick) ease;
}
.account-tabs .account-tab--active {
  border-bottom-color: var(--color-primary); font-weight: 600;
}

/* v23.53 — HOVER EN TOETSENBORDFOCUS. Browser-gemeten: de tabs waren de enige elementen op
   /account zonder enige reactie — klikbaar maar dood.

   ⚠ v23.53 HAD HIER TWEE REGELS; sinds v23.54 is het er ÉÉN. De tweede zette een vulling
   (var(--color-tint)) op elke tab, ook de actieve — browser-geverifieerd afgekeurd, zie de
   noot bij de verwijderde regel hieronder. Wat overblijft:

   DE ONDERRAND licht alleen op bij een tab waar je NIET op staat. Vandaar
   `:not(.account-tab--active)`. Zonder die uitsluiting zou hover de actieve tab OVERSTEMMEN
   — de hover-selector weegt zwaarder dan `.account-tabs .account-tab--active` — en dan
   verliest de tab waar je bént zijn markering precies op het moment dat je erover gaat.
   Dat is de omgekeerde wereld: "je bent hier" hoort te winnen van "je kunt hierheen".
   De uitsluiting is een TOESTAND op de gedeelde regel, geen uitzondering voor één tab.
   ⚠ EN DAARMEE KRIJGT DE ACTIEVE TAB GEEN ENKELE HOVER-REACTIE MEER. Dat volgt uit het
   schrappen van de vulling en is bewust zo gelaten: je bent er al, en de volle merkstreep
   zegt dat. Wie dat ooit terug wil moet iets kiezen dat de actieve markering NIET verzwakt.

   HOVER LIJKT NIET OP ACTIEF, en dat is het hele punt: actief draagt de VOLLE merkkleur plus
   vet, hover een verdunde versie daarvan (35%) zonder vet. Dezelfde kleur in twee sterktes
   leest als "dit is hetzelfde soort markering, maar zwakker" — precies de betekenis die je
   wil. Geen eigen hex: color-mix op de site-primary, zoals --lift-shadow en --color-tint dat
   ook doen, dus het valt op elk palet goed uit.
   ⚠ De 35% staat hier INLINE en niet als token. Hij wordt op precies één plek gebruikt, en
   een variabele die nergens anders wordt aangeroepen is een hernoeming en geen variabele —
   zelfde afweging als bij de 14rem van .ticket-subject.

   ⚠ NIETS AAN DE MATEN. Geen dikkere rand, geen andere padding, geen transform: de balk mag
   niet verspringen. Dat is precies wat v23.52 rechttrok met de transparante 3px-rand op elke
   tab, en een hover die aan de dikte zit breekt dat weer. Alleen KLEUR verandert.

   ⚠ :focus-visible ERBIJ, met dezelfde uitkomst als hover. De globale
   `a:focus-visible, button:focus-visible`-regel (3px accent-outline) geldt hier al en is voor
   beide elementsoorten gelijk; deze regels komen daar BOVENOP, zodat een toetsenbordgebruiker
   dezelfde markering ziet als iemand met een muis. Zelfde constructie als
   `.ticket-row > a:focus-visible` in het v23.24-blok.
   ⚠ En beide regels hangen aan `:is(button, a)`, dus ze pakken de zes knoppen en de ene link
   (Berichten) op exact dezelfde manier — dat verschil was in v23.52 de bron van het
   hoogteverschil en mag hier niet terugkomen. */
/* v23.54 — DE VULLING IS ERUIT. Hier stond
       .account-tabs :is(button, a):is(:hover, :focus-visible) { background: var(--color-tint) }
   en die regel is niet leeggehaald maar VERWIJDERD: zonder die ene declaratie hield hij niets
   over, en een lege regel met een uitgebreide selector leest bij de volgende ronde als iets
   wat er ooit toe deed. Browser-geverifieerd door Jelte: de streep werkt, het grijze blok
   erachter niet.
   ⚠ GEVOLG DAT HIERUIT VOLGT EN DAT NIET IS OPGELOST: de vulling was de enige reactie op de
   ACTIEVE tab, want de randregel hieronder draagt `:not(.account-tab--active)`. Hover en
   toetsenbordfocus geven daar nu dus niets meer. Verdedigbaar — je bent er al, en de volle
   merkstreep zegt dat — maar het is een gedragswijziging en geen neutrale opruiming. */
.account-tabs :is(button, a):not(.account-tab--active):is(:hover, :focus-visible) {
  border-bottom-color: color-mix(in srgb, var(--color-primary) 35%, transparent);
}
/* Lucht tussen de onderlijn van de tabbalk en de tabinhoud. Bewust een MARGE, geen padding:
   panelen die met een kop beginnen (Start "Welkom", Adressen "Factuuradres") dragen via die
   <h2> al een bovenmarge (1.8em). Padding telt daarbij op — dubbele lucht. Een marge collapt
   ermee, dus die panelen houden exact hun oude afstand, en panelen die met tekst of een
   formulier beginnen (<p> heeft `margin: 0 0 1em`) krijgen alsnog hun 1.25rem. */
.account-panel { margin-top: 1.25rem; }

/* --- Profiel: de vier blokken als kaart (v23.56) -----------------------------
   Profiel / E-mailadres wijzigen / Wachtwoord wijzigen / Factuur- + afleveradres stonden
   als vier formulieren onder elkaar, gescheiden door een <hr />. Drie Opslaan-knoppen op
   een pagina zonder dat te zien was welke knop bij welke velden hoorde: de streep is een
   scheiding die je moet LEZEN, een kaartrand is er een die je ZIET.
   De <hr />-elementen zijn uit de markup verdwenen (account.js) — niet verborgen. De
   kaartranden doen hun werk al; een streep ertussen zou een tweede scheiding zijn.
   Het vlak (oppervlak + haarlijn + radius) komt uit de gedeelde drager-regel bij de
   lijstrijen verderop in dit bestand — daar staat ook waarom alleen het VLAK gedeeld is
   en niet de rij-indeling. Hier staat wat een blok-kaart eigen is. */
.account-card {
  padding: var(--space) var(--space-lg) var(--space-lg);
  margin: 0 0 var(--space-lg);
}
/* De kop hoort BINNEN de kaart en bovenin. <h2> draagt in de prose-basislaag een forse
   bovenmarge (1.8em) die hier tegen de kaartrand aan duwt; binnen een kaart is de padding
   al de lucht. Onderlangs blijft er ruimte staan, want daaronder begint het eerste veld. */
.account-card > h2 {
  margin: 0 0 var(--space);
  font-size: 1.15rem;
}
/* v23.59 — HIER STOND `.account-card:last-child { margin-bottom: 0 }` EN DAT WAS FOUT.
   De bedoeling was "de laatste kaart op de pagina hoeft geen staartmarge", maar
   `:last-child` kijkt naar de CONTAINER en niet naar het scherm. En de kaarten zijn over
   twee containers verdeeld: #profile-box draagt Profiel + E-mailadres + Wachtwoord,
   #addresses-box draagt Factuuradres + Afleveradres (twee losse laadpaden, zie
   loadProfile/loadAddresses). Gemeten: `#password-change` is het laatste kind van
   #profile-box en kreeg dus margin-bottom 0 — terwijl er nog een hele kaart ONDER staat.
   Vandaar dat precies die twee tegen elkaar plakten en de andere niet.
   De uitzondering is er nu uit in plaats van bijgeschaafd: elke kaart draagt dezelfde
   ondermarge, ongeacht in welke container hij belandt. Dat kost onderaan het paneel
   var(--space-lg) aan witruimte en dat is de goede ruil — een regel die afhangt van de
   toevallige DOM-indeling gaat stuk bij de eerstvolgende herschikking, en die indeling is
   hier al twee keer verschoven (v23.19 en v23.56). */
/* Het "afleveradres = factuuradres"-vinkje staat sinds v23.56 binnen het factuurformulier,
   vlak vóór de knop (zie loadAddresses in account.js). Het draagt daar `.checkbox` en valt
   dus buiten de labelgrid hieronder — anders was het vinkje in de 12rem-kolom beland en
   zijn tekst in de kolom ernaast, hetzelfde gat dat de radio's hadden. Wat het hier nodig
   heeft is lucht: het scheidt het laatste adresveld van de knop.
   ⚠ Los van de #address-section-regel met dezelfde naam: die hoort bij de KASSA
   (checkout.js) en blijft ongemoeid. */
/* ⚠ De paneel-scope staat ervoor sinds v23.58 en dat is GEEN sier. Dit vinkje draagt óók
   `.checkbox`, en de nieuwe checkbox-regel hieronder (die "Push-notificaties" een eigen
   regel geeft) weegt (0,4,1) tegen de (0,2,0) die hier stond — zonder de scope won die en
   verloor dit vinkje zijn ruimere marge, die het nodig heeft omdat het het laatste
   adresveld van de knop scheidt. De inspringing naar de veldkolom erft het wél, en dat is
   gewenst: het vinkje lijnt daarmee uit met de velden erboven. */
.account-panel[data-tab-panel="profiel"] .auth-form label.ship-same-label {
  display: block;
  margin: var(--space-lg) 0 var(--space);
}

/* --- Profiel-/Adressen-formulieren: label naast het invulveld ---------------
   De rijen zijn `<label>Tekst<br/><input/></label>`. Basis (smal scherm): label
   netjes gestapeld bóven een volle-breedte input. Vanaf de drempel: twee kolommen
   (label links met vaste breedte, input rechts) zodat de velden uitlijnen; de
   interne <br/> vervalt dan. Strikt gescoped op de Profiel-tab —
   checkbox-rijen (`label.checkbox`) blijven hun inline-opmaak houden.

   ⚠ DE TWEEDE SELECTOR `[data-tab-panel="adres"]` IS HIER WEG EN DAT IS GEEN VERLIES:
   die tab bestaat sinds v23.19 niet meer (de adresvelden verhuisden naar Profiel, zie de
   noot in account.js). Gemeten met getComputedStyle op de echte pagina: er is geen enkel
   element meer met dat attribuut, dus de halve selector raakte niets. Alle vier de
   blokken hangen nu aan EEN regel — dat is wat ze op dezelfde labelkolom houdt.

   ⚠ WAT ER NIET AAN HANGT, en dat is nagemeten en geen aanname: login, registreren en de
   wachtwoord-resets dragen ook `.auth-form`, maar zitten in `.form-page`/`.form-card` en
   NIET in een `.account-panel`. Ze vallen dus buiten deze scope en hun labels blijven
   gestapeld. Verbreed je de scope naar kale `.auth-form`, dan kantelen die schermen mee. */
/* De labelkolom en het gat ernaast als tokens (v23.58): de veldgrid hieronder én de
   inspringing van de checkbox-rij moeten hetzelfde getal aanhouden. Stonden ze los, dan
   schuift de ene bij een wijziging wel op en de andere niet. Lokaal op dit paneel — het is
   een maat van dít scherm, niet van de huisstijl, dus hij hoort niet in de :root-ladder. */
.account-panel[data-tab-panel="profiel"] {
  --profiel-labelkolom: 12rem;
  --profiel-labelgat: 0.75rem;
}
.account-panel[data-tab-panel="profiel"] .auth-form label:not(.checkbox):not(.radio) {
  display: block;
  margin: 0.5rem 0;
}
/* v23.58 — DE CHECKBOX-RIJ KRIJGT EEN EIGEN REGEL. Een <label> is van huis uit `inline` en
   de Opslaan-knop is `inline-block`; twee inline-achtige elementen achter elkaar delen dus
   gewoon een regel, en zo stond "Push-notificaties" náást de knop in plaats van onder de
   velden. Het las als twee losse dingen naast elkaar terwijl het een instelling is die je
   met die knop opslaat. Er was geen enkele regel die `label.checkbox` een display gaf —
   nagemeten, niet aangenomen.
   ⚠ Dit werd zichtbaar toen "Energie-herinneringen" in v23.56 uit beeld ging: met twee
   vinkjes vulde de regel zich en viel de knop naar de volgende regel; met één vinkje bleef
   er ruimte over en schoof de knop ernaast. De oorzaak lag er al, het tweede vinkje
   verborg hem alleen.

   ⚠ `:not(.ship-same-label)` HOUDT DE TWEE UIT ELKAAR. Dat vinkje draagt óók `.checkbox`
   en heeft z'n eigen, ruimere marge (het scheidt het laatste adresveld van de knop). Beide
   selectors wegen (0,4,1), dus zonder deze uitsluiting won simpelweg degene die het laatst
   in het bestand staat — een volgorde-afhankelijkheid die niemand ziet tot ze verschuift.
   Gemeten: zonder de :not() zakte ship-same-label van var(--space-lg) terug naar 0.5rem. */
.account-panel[data-tab-panel="profiel"] .auth-form label.checkbox:not(.ship-same-label) {
  display: block;
  margin: 0.5rem 0;
}
.account-panel[data-tab-panel="profiel"] .auth-form label:not(.checkbox):not(.radio) :is(input, select, textarea) {
  width: 100%;
  box-sizing: border-box;
}
/* v22.90: `.field-stacked` blijft BUITEN deze grid — dat is het label-boven-het-veld
   dat de herkomstvraag draagt (zie de .field-stacked-regels verderop). Ook op de
   `br`-regel, anders verdwijnt de regelafbreking terwijl het label geen gridkolom
   krijgt en label + veld op één regel belanden.

   ⚠ `.radio` STAAT ER SINDS v23.56 BIJ, en dat is een REPARATIE, geen opruiming.
   `label.radio` (Particulier/Zakelijk) viel binnen deze selector en kreeg dus
   `display:grid` met een kolom van 12rem. Het bolletje ging in kolom 1, de tekst in
   kolom 2 — met 12rem ertussen. De regel die het bolletje strak naast z'n woord houdt
   (`.addr-kind .radio { display:inline-flex }`) verloor dat: (0,5,1) tegen (0,2,0).
   Vandaar dat de radio's "ver van hun label" stonden. Nu de :not()-keten ze overslaat
   wint de inline-flex-regel weer en staat de tekst op --space-xs van het bolletje.
   Het KLIKDOEL veranderde hier overigens niet: de <input> zat altijd al IN de <label>,
   dus de tekst was ook toen aanklikbaar — alleen zag je door het gat niet dat het een
   geheel was.

   ⚠ DE DREMPEL IS 600px → 40rem (640px). Twee redenen. Ten eerste eet de kaart nu aan
   weerszijden var(--space-lg) op: bij 600px bleef er van de 1fr-kolom naast een label van
   12rem (192px) nog ~370px over, en dat is met de kaartpadding erbij te krap voor een
   adresveld. Ten tweede is 40rem het breekpunt dat dit bestand al drie keer gebruikt
   (o.a. de factuurrij, zie het v23.52-blok) — een eigen getal ertussen laat schermen op
   verschillende breedtes omklappen. Tussen 600 en 640px staan de labels voortaan dus
   GESTAPELD in plaats van naast het veld; dat is de bewuste gedragswijziging.
   ⚠ jsdom evalueert geen media-query's, dus dit deel is NIET in de testronde gedekt —
   het moet op een echt toestel bekeken worden. */
@media (min-width: 40rem) {
  .account-panel[data-tab-panel="profiel"] .auth-form label:not(.checkbox):not(.radio):not(.field-stacked) {
    display: grid;
    grid-template-columns: var(--profiel-labelkolom) 1fr;
    align-items: center;
    gap: 0.4rem var(--profiel-labelgat);
  }
  .account-panel[data-tab-panel="profiel"] .auth-form label:not(.checkbox):not(.radio):not(.field-stacked) br {
    display: none;
  }
  /* De checkbox-rij springt in tot waar de INVOERVELDEN beginnen, zodat hij als instelling
     in dezelfde kolom staat en niet als losse regel tegen de linkerrand plakt. Vandaar dat
     de kolombreedte en het gat tokens zijn: deze inspringing en de grid hierboven moeten
     hetzelfde getal gebruiken, anders lopen ze bij de eerstvolgende wijziging uiteen. */
  .account-panel[data-tab-panel="profiel"] .auth-form label.checkbox {
    padding-left: calc(var(--profiel-labelkolom) + var(--profiel-labelgat));
  }
}

/* --- adres particulier/zakelijk + ⓘ-popover (v22.21) --- */
.addr-kind { display: flex; gap: 1.2rem; margin: 0.4rem 0 0.6rem; }
.addr-kind .radio { display: inline-flex; align-items: center; gap: 0.3rem; }
.info-wrap { position: relative; display: inline-block; }
.info-ic {
  border: 0; background: none; cursor: pointer; padding: 0 0.2rem;
  color: var(--color-primary); font-size: 0.95rem; line-height: 1;
}
/* Zwevend tooltip-ballonnetje bij het ⓘ — neemt geen layout-ruimte in (absoluut),
   opent onder/naast het icoontje, boven de form-velden. */
.info-pop {
  position: absolute; top: 100%; left: 0; z-index: 20; margin-top: 0.3rem;
  width: max-content; max-width: 240px; padding: 0.5rem 1.5rem 0.5rem 0.7rem;
  background: var(--color-surface); color: var(--color-text);
  border: 1px solid var(--color-muted); border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  font-size: 0.85rem; font-weight: normal; line-height: 1.4; white-space: normal;
}
.info-pop[hidden] { display: none; }
/* Sluitkruisje rechtsboven in het ballonnetje (extra naast klik-buiten). */
.info-x {
  position: absolute; top: 0.15rem; right: 0.3rem;
  border: 0; background: none; cursor: pointer; padding: 0; line-height: 1;
  color: var(--color-muted); font-size: 1.1rem; font-weight: bold;
}
.info-x:hover { color: var(--color-text); }
/* Label + ⓘ samen als één label-cel (linker grid-kolom), zodat de input op
   dezelfde regel rechts blijft staan — net als de gewone velden. */
.lbl-info { display: inline-flex; align-items: center; gap: 0.25rem; flex-wrap: wrap; }

/* --- ⓘ bij het kortingsveld: de codes per opleider (v23.161) ---
   Dit ballonnetje draagt een LIJST in plaats van één zin. De .info-pop-basis hierboven
   blijft ongemoeid; dit zijn alleen de regels voor die lijst-inhoud.
   Ruimer dan de 240px van een uitleg-zin: naam + code moeten op één regel passen, anders
   breekt "Academie voor Vastgoed: AVV15" in tweeën en leest het lijstje niet meer als
   naam-naast-code. De titel houdt rechts ruimte vrij voor het sluitkruisje. */
.info-pop .code-list-title { display: block; font-weight: 600; padding-right: 0.8rem; }
.info-pop .code-list { margin: 0.35rem 0 0; padding: 0; list-style: none; }
.info-pop .code-list li { margin: 0.15rem 0; white-space: nowrap; }
.info-pop .code-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* v23.162: de grens onder de lijst. Gedempt en met een scheiding erboven — het is een
   voorwaarde bij de lijst, geen vierde opleider. */
.info-pop .code-list-note {
  display: block; margin-top: 0.45rem; padding-top: 0.35rem;
  border-top: 1px solid var(--color-muted);
  color: var(--color-muted); font-size: 0.8rem; white-space: normal;
}
/* De link naar /acties, onder de grensregel. GEEN eigen scheiding erboven en NIET gedempt:
   hij hoort bij die regel als de weg naar het volledige verhaal, en een tweede haarlijn zou
   het ballonnetje in drie stukjes hakken. Blijft dus op de linkkleur — dit is meteen het
   enige element in het ballonnetje waar je op kúnt klikken (het sluitkruisje daargelaten). */
.info-pop .code-list-link {
  display: block; margin-top: 0.3rem; font-size: 0.8rem; white-space: normal;
}
.checkout-voucher .info-pop { max-width: 320px; }

/* Groene "opgeslagen"-bevestiging (profiel + adressen). Foutmeldingen
   (.auth-error) blijven hun eigen stijl houden. */
.form-success {
  margin: 0.5rem 0 0; padding: 0.5rem 0.75rem;
  background: #e7f6ec; color: #1a7f37;
  border: 1px solid #1a7f37; border-radius: 6px;
  font-size: 0.9rem;
}
.form-success:empty { display: none; }

/* Meldingenstrook (Start-tab van /account): container met een lijst melding-items.
   + tab-melding (.verify-notice) op een geblokkeerde tab. Sobere amber-stijl, in
   lijn met .form-success. Tonen alleen bij een onverifieerd account; verdwijnt
   zodra verified=true. */
.notifications-strip {
  display: flex; flex-direction: column; gap: 0.5rem;
  margin: 0 0 1rem;
}
.notifications-strip[hidden] { display: none; }
.notif-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; padding: 0.75rem 1rem;
  background: #fff8e6; color: #7a5b00;
  border: 1px solid #d9a700; border-radius: 8px;
}
.notif-item .btn-secondary { margin-top: 0; }

/* Order-statusflow (Start-tab, ná terugkeer van de bank). De spinner is een
   zichtbaar bewegend element zolang er nog iets loopt (pending/paid). */
.orderflow { display: flex; align-items: center; gap: 0.75rem; margin: 1rem 0 0.5rem; }
.orderflow-text { margin: 0; }
.orderflow-spinner {
  width: 1.1rem; height: 1.1rem; flex: none;
  border: 2px solid #d9a700; border-top-color: transparent;
  border-radius: 50%; animation: orderflow-spin 0.8s linear infinite;
}
@keyframes orderflow-spin { to { transform: rotate(360deg); } }
.orderflow-readers { margin: 0.25rem 0 0; }
/* v22.87 — plaatsings-bevestiging na de laatste betaling (in .orderflow-chain, dus met
   de scheidingslijn erboven). Zacht getint kader in de merk-kleur; leesbaar, niet luid. */
.orderflow-confirm {
  background: var(--color-tint); border: 1px solid var(--color-line);
  border-radius: var(--radius); padding: 0.9rem 1.05rem;
}
.orderflow-confirm p { margin: 0 0 0.5rem; }
.orderflow-confirm p:last-child { margin-bottom: 0; }

/* v22.72 winkelmandje-split — kassa-split, betaalketting en de [Betalen]-knop in Mijn
   bestellingen. Bewust licht: leunt op bestaande tokens (--color-line) en .cart-total. */
.checkout-summary--split .checkout-split-intro {
  margin: 0 0 1rem; padding: 0.75rem 0.9rem;
  background: #fdf6e9; border-left: 3px solid #d9a700; border-radius: 6px;
}
/* v23.303 — de boekhandel-melding boven het kassa-overzicht (deel C1). DEZELFDE kadervorm
   als .checkout-split-intro hierboven, en dat is opzet: dat is op precies dit scherm al de
   "let op, deze bestelling werkt anders dan je gewend bent"-vorm. Een tweede uitvoering
   ervan zou twee soorten mededeling suggereren waar er één is.
   ⚠ NIET aan .checkout-summary--split gebonden, anders dan de split-intro: deze melding
   hoort er ook bij een enkelvoudig mandje te staan — één verkoper is de normale stand. */
.checkout-summary .checkout-bookshop-notice {
  margin: 0 0 1rem; padding: 0.75rem 0.9rem;
  background: #fdf6e9; border-left: 3px solid #d9a700; border-radius: 6px;
}
.checkout-group {
  border: 1px solid var(--color-line); border-radius: 8px;
  padding: 0.75rem 0.9rem; margin: 0 0 0.75rem;
}
.checkout-group-seller { margin: 0 0 0.5rem; font-size: 1rem; }
.checkout-split-sentence { margin: 0.75rem 0 0; font-weight: 600; }
/* v22.86: scherm 2 van de kassa — één KAART per verkoper, in de vorm van "Mijn
   bestellingen". De kaart-chrome komt volledig uit .order-card/.order-head/.order-lines
   (verderop in dit bestand, class-gescoopt en dus niet aan de accountpagina gebonden);
   hier alleen wat de kassa-variant extra nodig heeft: de stap-aanduiding, de
   totaal-/btw-voet en het dimmen van niet-actieve stappen.
   Vervangt de compacte stepper-regels van v22.83 (.checkout-stepper/.stepper-*/.step--*),
   die met de kaartvorm zijn komen te vervallen. */
.checkout-stepper { margin: 0 0 1rem; }
/* "Stap X van N" / "Stap X van N — volgt hierna" / "… ✓" naast de verkopersnaam. Duwt
   zichzelf naar rechts, zoals .order-total dat in de bestellingen-tab doet. */
.checkout-card-step {
  margin-left: auto; color: var(--color-muted);
  font-size: 0.85rem; white-space: nowrap;
}
/* Totaalregel + btw-voet onder de regeltabel. Zelfde lijn-boven-en-vet als
   .checkout-summary .cart-total--grand, maar met een eigen modifier: die is aan
   .checkout-summary gescoopt en scherm 1 blijft daar de enige eigenaar van. */
.checkout-card-total {
  margin-top: 0.6rem; padding-top: 0.5rem;
  border-top: 1px solid var(--color-line); font-weight: 600;
}
.checkout-card-vat { margin: 0.15rem 0 0; color: var(--color-muted); font-size: 0.88rem; }
/* Het Payment Element + de Betaal-knop hangen ALLEEN in de actieve kaart (chainCardsHtml
   geeft `payHtml` aan één index mee) — nooit twee knoppen tegelijk, want iDEAL stuurt de
   klant weg naar de bank. */
.checkout-card-pay { margin-top: 0.8rem; padding-top: 0.7rem; border-top: 1px solid var(--color-line); }
/* Afgeronde en komende stappen zijn LEESBAAR maar duidelijk niet aan de beurt: gedimd,
   geen hover-lift (die suggereert klikbaarheid) en geen actie erin. */
.checkout-card--done, .checkout-card--upcoming { opacity: 0.62; }
.checkout-card--done:hover, .checkout-card--upcoming:hover {
  transform: none; box-shadow: none;
}
.checkout-card--done .checkout-card-step { color: var(--color-accent); }
.checkout-card--active { border-color: var(--color-primary); }
/* Ketting-vervolg op /account (na een geslaagde eerste betaling) + het inline betaalblok
   in Mijn bestellingen: het Payment Element krijgt lucht boven de bevestigknop. */
.orderflow-chain { margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--color-line); }
.orderflow-actions { margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.order-pay { margin-top: 0.6rem; }
.pay-element, .orderflow-pay { margin: 0.5rem 0; }
.orderflow-msg, .order-pay-msg { margin: 0.4rem 0 0; min-height: 1.2em; }

/* E-books-tab: één rij per beschikbare licentie (v23.34).
   -------------------------------------------------------------------------
   WAT HIER STOND EN WAAROM HET WEG IS. Dit waren dertien platte regels met een
   haarlijn ertussen: `padding: 0.6rem 0` + `border-bottom` op de pagina-achtergrond.
   Hetzelfde boek in twee jaargangen gaf twee volstrekt identieke regels, dus er viel
   niet te kiezen welke je opende. De rij draagt sinds v23.34 dus een metaregel
   (aankoopdatum + ordernummer, zie renderEbookItem) en dat past niet meer in een
   scheidingsstreepje — twee regels tekst zonder omlijsting lopen in elkaar over.

   DE VORM IS DIE VAN DE BERICHTENLIJST en die staat NIET hier: .ebook-item hangt bij
   de kaart-regels van .ticket-row > a onderin dit bestand (v23.24-blok), als tweede
   selector op precies dezelfde regels. Rand, radius, oppervlak, hover-lift en de
   randkleur bij focus zijn daarmee gedeeld en niet nagebouwd — een derde rijvorm naast
   de ticketrij en de kaart-thread zou de schermen uit elkaar laten lopen.
   ⚠ Die regels staan LATER in dit bestand, dus alles hieronder dat met dezelfde
   specificiteit tegen ze in zou gaan verliest. Vandaar dat hier alleen het AFWIJKENDE
   staat (de eigen kolom links, de badge) en niets uit het kaart-recept herhaald wordt.

   ⚠ NIET de kaart-vorm van de thread (.msg): dat is een gesprek met een as van twee
   afzenders, dit is een lijst met gelijkwaardige rijen. */
/* v23.48 — DE RIJ-AFSTAND, NU OP ÉÉN REGEL VOOR ALLE DRIE DE DIV-RIJEN.
   Browser-gemeten klacht: de rijen op Financieel stonden dichter op elkaar dan die op Mijn
   e-books. Doorgerekend met getComputedStyle bleek dat GEEN optisch effect maar een echt
   verschil — .ebook-item droeg `margin-bottom: var(--space-sm)` en .ticket-row
   `margin: 0 0 var(--space-sm)`, maar .invoice-row en .sub-row (allebei nieuw in v23.44)
   droegen NIETS. Die twee lijsten stonden dus met nul ruimte tussen de kaarten, terwijl de
   binnenruimte (padding uit het kaart-recept) op alle vier gelijk was.
   Ze hangen nu in deze ene selectorlijst in plaats van dat er per scherm een marge wordt
   bijgezet: dan blijft het één getal dat alle lijsten samen laat meebewegen.
   ⚠ .ticket-row STAAT ER NIET BIJ, en dat is structureel en geen omissie. Die marge hangt
   op de <li>-WRAPPER en niet op de kaart zelf — bij een ticketrij is de kaart het <a>
   daarbinnen (zie `.ticket-row > a` in het v23.24-blok). Hem hier bijzetten zou de marge op
   het verkeerde element leggen. Hij draagt daarom z'n eigen regel onderin dit bestand, met
   DEZELFDE waarde --space-sm; de test bewaakt dat die twee gelijk blijven. */
.ebook-item,
.invoice-row,
.sub-row { margin-bottom: var(--space-sm); }
/* Titel + metaregel als één kolom links, de actie rechts. Zonder deze knoop zou de
   metaregel als derde flex-item NAAST de titel komen te staan; met een flex-basis van
   100% erop zou hij de knop een regel verder duwen. Zelfde rol en zelfde omslagpunt als
   .ticket-subject in de berichtenlijst: groeit mee, breekt af onder 14rem, min-width:0
   zodat een lange titel kan krimpen in plaats van de rij op te rekken. */
/* ⚠ v23.49 — .invoice-main HING HIER MEE EN IS ER WEER UIT. v23.47 gaf de factuurregel een
   tweede regel (de verkopersnaam als bijschrift) en daarmee dezelfde kolomrol als deze; sinds
   v23.49 staat die naam ACHTER het nummer op de hoofdregel en is de rij weer één regel. Er is
   dan geen kolom-wrapper meer nodig: .invoice-doc is zelf het groeiende element. Zie de noot
   bij .invoice-doc onderin dit bestand. */
.ebook-main { flex: 1 1 14rem; min-width: 0; }
.ebook-title { display: block; font-weight: 600; }
/* Aankoopdatum + ordernummer zijn BIJSCHRIFT bij de titel, geen tweede titel: dezelfde
   demping en dezelfde maat als .ticket-date en .order-date. */
/* ⚠ v23.49 — .invoice-meta HING HIER MEE EN IS ER WEER UIT, samen met de metaregel zelf:
   de verkopersnaam staat nu op de hoofdregel en er bleef niets over om in een bijschrift te
   zetten. Een lege .invoice-meta laten staan zou suggereren dat de factuurregel nog een
   tweede regel heeft. */
.ebook-meta {
  display: block;
  color: var(--color-muted); font-size: var(--font-size-meta);
}
/* Alleen de afwijkende toestand krijgt nog een badge: de leesbare rij toont de
   Lezen-knop, die zegt "beschikbaar" al. .ebook-available is daarmee vervallen.

   v23.34 — HET BADGE-RECEPT, nu uit de tokens. Deze regel droeg 0.85em / 0.1rem 0.5rem /
   6px: drie waarden die van GEEN van de andere drie badges in dit bestand waren
   (.order-status, .lic-badge, .refund-badge staan alle drie op 0.7rem / 4px 9px / 5px).
   Een e-book-badge die net even anders is dan de statusbadge één tab verderop is geen
   accent maar ruis. Hij staat nu op --font-size-badge / --pad-badge / --radius-badge /
   --track-badge en is daarmee visueel gelijk aan .order-status.
   ⚠ Die drie andere badges dragen hun literals nog. Dat is bewust: de sweep gaat per
   scherm, en die drie horen bij Bestellingen (besluit 28-07). */
/* ⚠ .invoice-badge EN .sub-badge HANGEN HIER MEE (v23.44) in plaats van een vijfde en zesde
   kopie van dit recept te zijn. Ze staan in de selectorlijst en niet in een eigen regel
   verderop om dezelfde reden die bij .ticket-status staat: een badge die op de ene tab net
   even anders is dan op de tab ernaast is geen accent maar ruis. */
/* ⚠ .voucher-badge HANGT ER SINDS v23.312 BIJ, om exact dezelfde reden als .invoice-badge en
   .sub-badge hierboven: de stand van een vouchercode is een statuslabel als alle andere, en
   een badge die op de ene tab net even anders is dan op de tab ernaast is geen accent maar
   ruis. Hij staat dus in beide selectorlijsten (vorm + kleur) en heeft géén eigen regel. */
.ebook-status,
.invoice-badge,
.sub-badge,
.voucher-badge {
  display: inline-block;
  font-size: var(--font-size-badge); font-weight: 700;
  letter-spacing: var(--track-badge); text-transform: uppercase; white-space: nowrap;
  padding: var(--pad-badge); border-radius: var(--radius-badge);
}
/* v23.35 — GEDEMPT, WAS AMBER (#fdf0e0 / #8a5a1a).
   Browser-gemeten op dev: van de zeven rijen in beeld waren er vier reader-loos, en die
   vier domineerden het scherm. "NOG NIET BESCHIKBAAR" in amber, uppercase en gespatieerd
   trok álle aandacht, terwijl de rustige "Lezen"-knop de enige rij markeerde waar je iets
   kúnt. De hiërarchie stond precies omgekeerd.
   De oorzaak zit in de herkomst van het recept: het is gemaakt voor .order-status, waar
   amber "in behandeling" betekent — een toestand die vanzelf overgaat. Hier markeert het
   een AFWEZIGHEID die niet vanzelf overgaat (de operator moet de reader koppelen), en dan
   is een waarschuwingstint een belofte die niemand nakomt.
   Nu de neutrale variant van datzelfde recept: subtle + muted, exact wat .refund-badge en
   de standaard .order-status/.order-status-refunded dragen.
   ⚠ HET RECEPT ZELF BLIJFT INTACT — de maat, de radius, het uppercase en de spatiëring
   staan hierboven in .ebook-status en zijn NIET aangeraakt. Alleen de kleur is gedempt,
   zodat de badge vormgelijk blijft aan de andere badges; dat is net rechtgetrokken en
   moet niet opnieuw uiteenlopen.
   ⚠ EN ALLEEN DE BADGE. De rij en de titel blijven op volle sterkte: de klant HEEFT dat
   boek gekocht. Wat er niet is, is de leeslink — niet het bezit. */
/* ⚠ .invoice-badge ("in bewerking") en .sub-badge ("betaling open" / "gepauzeerd") delen ook
   de KLEUR, en dat is een keuze: geen amber. v23.35 dempte .ebook-soon omdat amber daar een
   afwezigheid markeerde die niet vanzelf overgaat. Bij een nog niet gerenderde PDF gaat het
   wél vanzelf over, dus amber zou verdedigbaar zijn — maar dit zijn de twee schermen die
   Jelte NIET met echte data kan bekijken (nul abonnementen op dev, alle 80 dev-facturen
   hebben een PDF), en dan is een waarschuwingstint invoeren op gevoel. Gedempt is de
   terugvalkeuze; het staat in de mededelingen. */
/* ⚠ .voucher-badge DEELT DE GEDEMPTE KLEUR, en voor alle VIER de standen dezelfde. Een
   gekleurde variant per stand zou van "verlopen" een waarschuwing maken terwijl er niets
   meer te doen valt — precies de fout die v23.35 hierboven bij .ebook-soon repareerde. Het
   verschil tussen de standen staat in het WOORD; de badge is de vorm eromheen. */
.ebook-soon,
.invoice-badge,
.sub-badge,
.voucher-badge { background: var(--color-subtle); color: var(--color-muted); }
/* De Lezen-knop is de primaire actie van de rij en draagt .pay-btn; die brengt een
   margin-top van 1rem mee, wat 'm in de flex-rij liet zakken. .btn-secondary blijft
   meegedekt: andere rijen kunnen hem nog dragen. */
.ebook-item .btn-secondary { margin-top: 0; }
/* .pay-btn is gemaat op de checkout-knop (Afrekenen/Betalen) en oogt op volle maat log
   als rij-actie in een lange e-book-lijst. Hier compacter: zelfde gevulde primary-kleur,
   kleinere maat. Bewust op .ebook-item gescoopt — de knop elders houdt z'n formaat.
   De radius volgde al de badge ernaast; die stond hier als losse 6px en beweegt dus mee
   naar --radius-badge (5px), anders is "volgt de badge" geen feit meer maar een afspraak.
   ⚠ De twee MATEN blijven losse waarden: er is geen token dat 0.4rem 0.9rem of 0.9rem
   exact dekt, en ze naar de dichtstbijzijnde stap trekken zou de knop zichtbaar
   verspringen. Dat is een keuze en geen omzetting, dus die hoort niet in deze sweep. */
/* ⚠ .invoice-row .pay-btn HANGT HIER MEE (v23.44): de Downloaden-knop op Financieel is
   dezelfde rij-actie als de Lezen-knop hier, en moest dus dezelfde verkleining krijgen. Op
   volle .pay-btn-maat oogt hij als een tweede kassaknop in een lijst van twintig facturen.
   De Opzeggen-knop op Abonnement staat er BEWUST niet bij: die draagt .sub-cancel en heeft
   z'n eigen maten al, en dat is de knop die iets wegneemt in plaats van iets ophaalt. */
/* ⚠ DE VIER FORMULIERKNOPPEN OP HET PROFIEL HANGEN HIER MEE (v23.61) — en dat is de reden
   dat deze regel een derde selector heeft in plaats van een kopie met dezelfde getallen.
   Op /account → Profiel staan vier gevulde knoppen op volle maat onder elkaar (Opslaan,
   Stuur code, Wachtwoord wijzigen, Opslaan); dat leest als vier kassaknoppen op één
   pagina, terwijl het bewerkingen in een kaart zijn. Zelfde probleem als de Lezen- en
   Downloaden-knop hierboven, dus dezelfde maat en dezelfde regel.

   ⚠ ZE DRAGEN GEEN .pay-btn. Het zijn kale <button type="submit"> binnen .auth-form
   (gemeten), opgemaakt door de .auth-form-knopregel verderop. Vandaar dat de selector hier
   op het ELEMENT mikt en niet op een class; een class toevoegen in de markup zou hetzelfde
   bereiken, maar dan staat de maat op twee plekken (markup én stylesheet) in plaats van op
   één, en de knoppen zijn niet met de hand geschreven maar door vijf templates.

   ⚠ GESCOOPT OP HET ACCOUNT-PANEEL, dat is essentieel. .pay-btn heeft zestien
   gebruiksplekken over acht bestanden, waaronder Afrekenen en Betalen op de kassa — daar
   IS de knop de reden dat het scherm bestaat en die houdt zijn volle maat. .account-card
   en .account-panel bestaan uitsluitend in account.js (gemeten), dus de kassa kan hier
   niet in meelopen.

   ⚠ EÉN DING WIJKT AF EN DAT IS BEWUST: margin-top. In een lijstrij moet die 0 zijn, want
   .pay-btn brengt 1rem mee en dat liet de knop in de flex-rij zakken. In een profielkaart
   is de knop juist een blok ONDER de velden en heeft hij die lucht nodig — hij krijgt 'm
   hieronder terug uit --space-sm, dezelfde waarde als de .auth-form-knopregel al gaf.
   Maat en vorm zijn dus identiek aan de lijstrijen; alleen de afstand tot wat erboven
   staat verschilt, en dat is een verschil in context en niet in de knop zelf.

   ⚠ DE SELECTOR IS `.account-panel .account-card`, NIET `.account-card .auth-form`, en dat
   verschil is geen stijlkwestie maar een bug die ik eerst gemaakt en toen gemeten heb.
   Twee van de vijf formulieren ZIJN de kaart: <form class="auth-form account-card"> (het
   profiel en de twee adressen). Bij die drie staan `.account-card` en `.auth-form` op
   HETZELFDE element, en een descendant-combinator eist een afstammeling — dus matchte hij
   precies de drie Opslaan-knoppen niet, terwijl de twee waar de kaart een <section> om het
   formulier is (E-mailadres, Wachtwoord) er wél in liepen. Vier knoppen op het scherm,
   waarvan er twee zouden krimpen: erger dan niets doen.
   `.account-panel` als voorouder werkt voor beide vormen en houdt de specificiteit op
   (0,3,1) — zwaar genoeg om van `.auth-form button[type="submit"]` (0,2,1) te winnen,
   ongeacht de volgorde in dit bestand. */
/* ⚠ DE TOEPASSEN-KNOP VAN HET KORTINGSVELD HANGT HIER MEE (v23.163) — en dat is de reden
   dat deze regel een vierde selector heeft in plaats van een kopie met dezelfde getallen.
   Besluit Jelte: een KLEIN veld met een KLEINE knop, in "de compacte maat die elders in
   deze frontend al bestaat". Dat is deze regel; er is er geen tweede. Zelfde probleem als
   bij de Lezen- en Downloaden-knop hierboven: op volle .pay-btn-maat leest hij als een
   tweede kassaknop naast "Doorgaan naar betalen", terwijl hij een veld afsluit.
   ⚠ De margin-top van 0 is hier NIET incidenteel maar nodig: de knop staat in een flex-rij
   naast het veld en zou met de 1rem van .pay-btn onder de invoerregel wegzakken. */
.ebook-item .pay-btn,
.invoice-row .pay-btn,
.checkout-voucher-row .pay-btn,
.account-panel .account-card button[type="submit"] {
  margin-top: 0; padding: 0.4rem 0.9rem;
  font-size: 0.9rem; border-radius: var(--radius-badge);
}
/* Zie de margin-top-noot hierboven: de kaartknop staat onder de velden en houdt zijn lucht.
   Staat NA de gedeelde regel en heeft dezelfde specificiteit (0,3,1) — de volgorde is hier
   dus dragend en geen toeval. */
.account-panel .account-card button[type="submit"] { margin-top: var(--space-sm); }

.verify-notice {
  padding: 0.75rem 1rem;
  background: #fff8e6; color: #7a5b00;
  border: 1px solid #d9a700; border-radius: 8px;
}
.verify-notice p { margin: 0 0 0.5rem; }
.verify-notice .btn-secondary { margin-top: 0; }

/* --- Bestellingen-tab -------------------------------------------------------
   Deze blokken (order-card/head/status/date/total/lines/licenses/docs) stonden
   volledig ZONDER css: platte tekstregels op de pagina-achtergrond. Ze krijgen hier
   het bestaande kaart-patroon van de landing (.cat/.book): surface + haarlijn +
   radius + hover-lift, en het badge-patroon voor de statuslabels. Alles via de
   tokens, dus de kaart volgt de tenant-kleur. */
.order-card {
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
  margin-bottom: 0.9rem;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}
.order-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 26px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.order-head {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  padding-bottom: 0.6rem; margin-bottom: 0.6rem;
  border-bottom: 1px solid var(--color-line);
}
.order-head strong { font-weight: 600; }
.order-date { color: var(--color-muted); font-size: 0.88rem; }
/* Bedrag in het display-font, net als de prijs op de boek-kaart (.book-foot .price). */
.order-total {
  margin-left: auto;
  font-family: var(--font-display); font-weight: 600; font-size: 1.1rem;
  color: var(--color-primary);
}

/* Statuslabel = badge-patroon. De neutrale/positieve statussen dragen de MERK-kleur
   (tint + primary), zoals de .book-badge op de landing. De uitzonderings-statussen
   hergebruiken de semantische kleuren die al in dit bestand staan (amber = er moet nog
   iets gebeuren, rood = gaat niet door; vergelijk .notif-item en .badge-sale):
   die zijn opzettelijk NIET merk-gebonden — rood moet rood zijn op elke tenant.
   ⚠ "rood = mislukt/geannuleerd" STOND HIER TOT v23.290 en dekt de lading niet meer:
   `.order-status-failed` is toen naar amber verhuisd, zie het blok onderaan.
   ⚠ .ebook-soon STOND HIER IN DIT RIJTJE als mede-drager van het amberpaar en is er per
   v23.35 uit: die badge is gedempt naar subtle+muted. Het paar #fdf0e0/#8a5a1a leeft
   sindsdien alleen nog op de twee selectors hieronder, en dát is nu de vindplaats.
   Reden van de demping: amber betekent hier "in behandeling" en gaat vanzelf over; bij
   een e-book zonder gekoppelde reader gaat het niet vanzelf over. Zie het E-books-blok. */
.order-status {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 5px;
  background: var(--color-subtle); color: var(--color-muted);
}
.order-status-paid,
.order-status-provisioned {
  background: var(--color-tint); color: var(--color-primary);
}
/* v23.290 — .order-status-failed IS HIER UIT HET RODE PAAR GEHAALD en staat nu bij
   pending/processing. Rood zei "er is iets stuk"; wat er werkelijk aan de hand is, is dat
   de betaling niet is afgerond en de klant hem gewoon kan hervatten — de Betalen-knop
   staat op diezelfde kaart (`orderPayArea` laat pending én failed door, precies zoals
   `services/orders.py::pay_order` doet). De TEKST verhuisde in dezelfde beweging naar
   "In afwachting", zie het blok bij ORDER_STATUS_LABELS in student-frontend/js/account.js.

   ⚠ DE CSS-HAAK BLIJFT `.order-status-failed` — de ENUM-waarde is de haak, alleen de
   opmaak verandert (zelfde regel als in het blok hierboven). Niet samentrekken tot één
   selector met pending: dan is de stand niet meer los herkenbaar in de DOM en is de
   node-toets die deze twee uit elkaar houdt niet meer te schrijven.

   ⚠⚠ `.order-status-cancelled` BLIJFT ALLEEN IN HET ROOD ACHTER, en dat is de hele reden
   dat deze twee selectors niet meer bij elkaar staan. Een geannuleerde order gaat écht
   niet meer door (`pay_order` weigert die met een 409) — die MOET van een afgebroken
   betaling te onderscheiden blijven, in woord én in kleur.

   Het amberpaar betekent hier "de bestelling loopt, er moet nog iets gebeuren". Dat de
   notitie hierboven amber ooit omschreef als "gaat vanzelf over" gold voor de twee
   standen die er tot nu toe in zaten; bij `failed` is de klant zelf aan zet, en juist
   dáárom staat er een knop naast. De kleur zegt "geen alarm", niet "leun achterover". */
.order-status-pending,
.order-status-processing,
.order-status-failed { background: #fdf0e0; color: #8a5a1a; }
.order-status-cancelled { background: #fdecef; color: #b00020; }
.order-status-refunded { background: var(--color-subtle); color: var(--color-muted); }

/* Regeltabel: de prose-basislaag verzorgt al border-collapse + celranden; hier alleen
   de dempende details (kleinere tekst, geen dubbele buitenrand).

   v23.71 — vertical-align: top OP DE GEDEELDE REGEL. De cellen lijnden verticaal in het
   MIDDEN uit, en dat was nergens gezet: het is de tabel-default. Kassa scherm 1 stond er
   sinds v22.93 wél expliciet op top via de .checkout-summary-scoping, dus de drie schermen
   liepen uiteen op iets waar niemand een keuze in had gemaakt. Sinds v23.67 draagt de
   omschrijvingscel twee regels (titel + editie · ISBN) terwijl de andere cellen er één
   hebben; bij middle-uitlijning zakken stuksprijs en regeltotaal daardoor tot HALVERWEGE
   de omschrijving, en dat verschil is met de subregel erbij groot genoeg om op te vallen.
   Top zet ze terug op de eerste regel van de titel — dezelfde redenering die v22.93 voor
   scherm 1 al gaf, nu voor alle drie. Zelfde vorm als text-align: right in v23.70: ÉÉN
   gedeelde regel, geen drie gescoopte.

   ⚠ GEEN padding-top op .line-vat, en dat is nagerekend in plaats van aangenomen. De zorg
   is dat de btw-noot (0.85em = 12,24px) bij top-uitlijning hoger zou komen dan de titel
   ernaast. Dat kan niet: .line-vat hangt met vertical-align: baseline aan dezelfde strut,
   en die strut is maatgevend. Gemeten uit de Public Sans-metrics (upem 2000, ascender
   1900, descender −450, lineGap 0, USE_TYPO_METRICS aan) bij line-height 1.6:
       strut 14,4px   reikt 16,74px boven de baseline, lijnbox 23,04px
       .line-vat      reikt 14,23px boven de baseline, lijnbox 19,58px
   De btw-noot past dus 2,51px BINNEN de strut, verhoogt de lijnbox niet en verschuift de
   baseline niet. Alle drie de cellen houden dezelfde baseline-offset van 16,74px vanaf de
   top van hun contentbox. Optisch begint de btw-noot zelfs 1,56px LAGER dan de titel
   (kapitaalhoogte 8,85 tegen 10,41px) — een padding-top zou hem verder omlaag duwen en de
   scheefstand juist veroorzaken die hij moest voorkomen. */
.order-lines { font-size: 0.9rem; margin: 0; width: 100%; }
.order-lines td {
  border-bottom: 1px solid var(--color-line); padding: 0.4rem 0.6rem 0.4rem 0;
  vertical-align: top;
}
.order-lines tr:last-child td { border-bottom: 0; }
/* v22.87 — driedeling van de basisregel. De omschrijving wordt met een max-width
   ingekort (ellipsis); de volledige titel leeft in het title-attribuut (hover). De
   inline-block + vertical-align:bottom houdt de tekst op de basislijn ondanks overflow. */
/* v23.65 — `min(22rem, 100%)` en niet langer een kale 22rem. De 22rem was een ABSOLUUT
   plafond: bij `display:inline-block` maakt de span zich zo breed als haar inhoud tot dat
   plafond, ongeacht hoe breed de CEL is. Zolang de tabel auto-layout had gaf de kolom de
   omschrijving altijd minstens haar eigen contentbreedte, dus viel dat nooit op. Sinds
   v23.64 (table-layout: fixed) kan de cel véél smaller zijn dan de span — en dan knipt
   `overflow:hidden` op de rand van de SPAN, niet op die van de cel, dus loopt de tekst de
   cel gewoon uit tegen de kolom ernaast aan. De ellipsis kwam er niet aan te pas: die
   trad pas in werking bij 22rem, niet bij de kolomrand. Met de 100%-tak bindt het plafond
   aan de cel, dus kort de tekst in wáár de kolom ophoudt. In een breed venster is de cel
   ruimer dan 22rem, dan wint de 22rem-tak en is het gedrag byte-identiek aan voorheen. */
/* v23.79 — HET ABSOLUTE PLAFOND IS WEG; alleen `100%` blijft over. De zin hierboven zegt
   het al zonder het als probleem te herkennen: in een breed venster wint de 22rem-tak. Dat
   is precies wat de titel te vroeg liet afkappen. `min()` kiest de KLEINSTE waarde, dus
   zodra de cel breder is dan 22rem bindt het absolute getal en slaat de ellipsis toe
   terwijl de kolom ruimte over heeft.

   Doorgerekend met de maten uit dit bestand — container .app-content `min(960px, 92vw)`
   (= 60rem), kaart 2,3rem padding + 0,125rem rand, vaste kolommen 11 + 5,4 + 7 + 7 =
   30,4rem:
     omslagpunt   0,92 × venster − 3,365 − 30,4 = 22    ->  venster ≈ 60,6rem (≈970px)
     bij ≥1043px  60 − 2,3 − 0,125 − 30,4        = 27,175rem kolom
   Boven ~970px kapte het plafond dus tot ~4,6rem weg die er wél was. Het getal 22 heeft
   geen vindbare onderbouwing: het stamt uit de v22.87-driedeling zonder meting, en v23.65
   nam het bewust ongewijzigd over omdat die commit alleen de cel-binding repareerde.

   ⚠ `table-layout: fixed` maakt dit veilig. De kolombreedte komt daar uit de
   width-declaraties en NIET uit de celinhoud, dus een bredere span kan de verdeling niet
   terugduwen — de omschrijving is bovendien de enige kolom zonder width en krijgt dus per
   definitie wat er overblijft. Het plafond bepaalde alleen of de span die al toegekende
   ruimte volmaakte.
   ⚠ Onder de 40rem-stapelvorm verandert er niets: daar staat `max-width: none` al.
   ⚠ De statuskolom hoeft niet mee te groeien: "Geannuleerd" meet 6,01rem incl. de 0,6rem
   celpadding (advance widths uit de Public Sans-woff2, 0,9rem cel) en past binnen de 7rem
   waar "In behandeling" (6,80rem) en "Geretourneerd" (6,79rem) al in passen. */
.line-desc-text {
  display: inline-block; max-width: 100%; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* v23.303 — de LEVERINGSVORM ("product" / "voucher") achter de vorm, op kassa scherm 1 en
   in de mandje-lade. Één regel voor allebei, want het is één aanduiding op twee schermen.
   Als etiket opgemaakt en niet als vetdruk: de vorm (`(ebook)`, <strong>) zegt WAT het
   product is, dit zegt HOE het geleverd wordt, en twee vette blokken naast elkaar in
   dezelfde regel maken geen van beide leesbaar.
   ⚠ De span bestaat alleen als de server het veld stuurt (uitsluitend bij een ingelogde
   boekhandel), dus er is geen lege staat om te stylen. */
.line-delivery,
.cart-row-delivery {
  display: inline-block; font-size: 0.85em; font-weight: 500;
  padding: 0.05em 0.4em; border-radius: 4px;
  border: 1px solid var(--color-line); color: var(--color-muted);
  white-space: nowrap;
}
/* v23.2 — de DRUK onder de titel ("editie 2025/2026"). Eigen regel (display:block) zodat
   hij niet met de titel meeloopt in de ellipsis-inkorting van .line-desc-text, en gedempt
   omdat hij bijschrift is bij de titel — zelfde rol en zelfde demping als .line-vat bij de
   stuksprijs. Één regel voor ALLE drie de schermen: de vorm (ebook/gedrukt) zit al in de
   titel zelf via het backend-suffix, de druk hangt eronder. Ontbreekt de editie, dan
   rendert de JS de span helemaal niet — er is dus geen lege-staat om te stylen. */
.line-edition { display: block; color: var(--color-muted); font-size: 0.85em; }
/* v23.67 — de subregel draagt naast de druk het ISBN: "editie 2026/2027 · 9789083695853".
   `.line-meta` is de wikkel die de BLOKROL overneemt, zodat de twee delen op ÉÉN regel
   naast elkaar staan in plaats van onder elkaar. De basisregel van .line-edition hierboven
   blijft bewust ongemoeid (hij geldt nog steeds voor zichzelf); binnen de wikkel wordt
   alleen zijn display omgezet. Zo blijft de bestaande regel leesbaar als de bron van de
   demping en de maat, en erven beide delen dezelfde typografie — geen tweede keuze naast
   de bestaande. De JS rendert de wikkel NIET als beide delen ontbreken, dus er is geen
   lege-staat om te stylen (verzendregel, abonnement).

   ⚠ HET SCHEIDINGSTEKEN HANGT AAN DE ADJACENTIE, niet aan een telling. Het staat op
   `.line-edition + .line-isbn::before`, dus het bestaat alleen als het ISBN DIRECT op de
   druk volgt. Geen druk → geen adjacentie → geen leidend teken. Geen ISBN → geen element
   om het aan te hangen → geen sluitend teken. Een bungelend teken kan daardoor structureel
   niet ontstaan; dat is sterker dan de :empty-oplossing van v23.66, die met N cellen moest
   werken terwijl het er hier precies twee zijn.
   ⚠ nowrap op het ISBN zelf: dertien cijfers over twee regels zijn onleesbaar en niet te
   kopiëren. De REGEL mag nog gewoon afbreken — bij de spatie vóór het scheidingsteken. */
.line-meta { display: block; }
/* v23.79 — HOOFDLETTER OP DE SUBREGEL: "Editie 2026/2027 · …" in plaats van "editie …".
   De subregel is een eigen regel onder de titel en hoort als zin te beginnen.

   ⚠ DIT MOET OP `.line-meta` EN KAN NIET OP `.line-edition`. Die span staat binnen de
   wikkel altijd op `display: inline` (de regel hieronder), en `::first-letter` geldt per
   CSS Pseudo-Elements Level 4 §3.2 uitsluitend op een block-container — op een inline box
   slaat het niet aan. De wikkel is wél een blok, dus daar landt het. Een test toetst
   precies dat: de regel hoort op .line-meta te staan en niet op .line-edition.

   ⚠ EN NIET VIA `pricing.edition_label`. Die functie is GEDEELD met de factuur
   (services/billing.py zet er de druk mee achter de factuurregel) en een factuur is een
   bevroren document: de tekst daar wijzigen laat oude en nieuwe facturen uiteenlopen, en
   breekt drie asserts waaronder tests/test_invoice_edition_line.py. De factuur blijft
   ongemoeid — dit is puur weergave, in de laag waar weergave hoort.

   Randgeval: een regel MÉT ISBN maar ZONDER editie begint met een cijfer, en
   text-transform op een cijfer is een no-op. Een regel met alleen de licentie begint met
   de "l" van "licentie" en wordt dus "Licentie: actief (onbeperkt)" — precies de vorm die
   de vervallen badge ook had. Geen enkele beginstand levert een vreemde uitkomst.

   ⚠ Geldt óók in de gestapelde vorm onder 40rem: dat blok raakt .line-meta niet aan, dus
   de wikkel blijft daar een blok binnen de .line-desc die op display:block wordt gezet. */
.line-meta::first-letter { text-transform: uppercase; }
.line-meta .line-edition { display: inline; }
/* v23.343 - HET DRUKNUMMER ALS TWEEDE DEEL VAN DE SUBREGEL:
       Editie 2025/2026 - 1e druk - 9789083566986
   Het jaartal zegt WELKE editie, het druknummer zegt DE HOEVEELSTE. Die twee lopen niet
   gelijk op (een titelreeks kan van uitgever wisselen en dan begint de nummering opnieuw -
   zie de noot bij `pricing.edition_label`), dus het een is geen vervanging van het ander.

   HIJ STAAT TUSSEN DRUK EN ISBN, en dat is de leesvolgorde van grof naar fijn: jaar,
   nummer, nummer-van-dit-exemplaar. Het ISBN blijft daarmee het laatste stuk voor de
   licentie, precies waar het sinds v23.67 stond.

   EEN VOORGANGER VOOR HET SCHEIDINGSTEKEN. Anders dan bij .line-license en .line-teacher
   is dat er hier maar een: het druknummer wordt uitsluitend door de kassa meegegeven en
   staat daar altijd achter de editie, of helemaal niet. Staat het er zonder editie voor
   (een boek zonder schooljaar), dan is er geen voorganger en dus geen leidend teken -
   hetzelfde structurele argument als bij v23.67.

   GEEN nowrap: "1e druk" is een woordgroep die best mag afbreken, anders dan het ISBN
   dat een ondeelbaar nummer is. */
.line-druk { color: var(--color-muted); font-size: 0.85em; }
.line-edition + .line-druk::before { content: " · "; }
.line-isbn { color: var(--color-muted); font-size: 0.85em; white-space: nowrap; }
/* TWEE VOORGANGERS, zelfde reden als bij .line-license hieronder: het druknummer dekt het
   gewone geval sinds v23.343, de editie dekt een regel MET jaar maar ZONDER druknummer
   (de orderkant geeft dat veld niet mee). Twee spans kunnen nooit allebei de directe
   voorganger zijn, dus er komt hooguit een teken. */
.line-edition + .line-isbn::before,
.line-druk + .line-isbn::before { content: " · "; }
/* v23.78 — DE LICENTIE ALS DERDE DEEL VAN DE SUBREGEL:
       editie 2026/2027 · 9789083695730 · licentie: actief (onbeperkt)
   Hij hing tot nu toe als losse badge ONDER de tabel, op ORDER-niveau: bij een order met
   twee e-books stond er twee keer "Licentie: actief" zonder dat zichtbaar was bij wélk
   boek. Op de regel is die vraag niet meer te stellen.

   Zelfde demping en zelfde maat als de twee delen ernaast — het is bijschrift bij de
   titel, geen badge meer. Bewust GEEN `.lic-badge`-achtige achtergrond: drie gekleurde
   blokjes onder elkaar in een regeltabel zijn ruis, en de kleur droeg geen informatie die
   het woord ("actief"/"verlopen"/"ingetrokken") niet al draagt.

   ⚠ GEEN nowrap, anders dan bij het ISBN. "licentie: actief (onbeperkt)" is een reeks
   woorden die bést mag afbreken; het ISBN is één ondeelbaar nummer van dertien cijfers.

   ⚠ TWEE VOORGANGERS voor het scheidingsteken, en dat is geen dubbeling. `.line-isbn +`
   dekt het gewone geval; `.line-edition +` dekt een boek MET druk maar ZONDER ISBN
   (`editions.isbn` mag leeg zijn). Zonder die tweede selector verliest zo'n regel stil
   zijn scheidingsteken. Twee spans kunnen nooit allebei de directe voorganger zijn, dus
   er komt hooguit één teken. En staat de licentie alléén (geen druk, geen ISBN), dan is
   er geen voorganger en dus ook geen leidend teken — hetzelfde structurele argument als
   bij v23.67, niet een telling die je kunt vergeten bij te werken. */
.line-license { color: var(--color-muted); font-size: 0.85em; }
.line-edition + .line-license::before,
.line-druk + .line-license::before,
.line-isbn + .line-license::before { content: " · "; }
/* v23.172 — DE DOCENTBADGE ALS VIERDE DEEL VAN DE SUBREGEL:
       Editie 2026/2027 · 9789083695839 · Licentie: onbeperkt · [Docentexemplaar] 1 gratis
   Hij stond op een EIGEN rij onder de productregel (.line-sub); twee bijschriften onder
   één regel lazen als twee mededelingen terwijl het er één is.

   ⚠ DE BADGE-VORM BLIJFT, expliciet besluit Jelte. Het grijze blokje (.badge-teacher,
   verderop) is wat de regel áánwijst als docentexemplaar; anders dan bij de licentie
   (v23.78) verliest hij zijn achtergrond hier dus NIET. De TEKST erachter ("1 gratis ·
   Max. voor deze titel bereikt") krijgt wél de demping en de maat van de drie delen
   ernaast — het is bijschrift, net als de rest van de regel.

   ⚠ DRIE VOORGANGERS voor het scheidingsteken, om dezelfde reden als bij .line-license
   twee: de licentie ontbreekt op een gedrukt boek, het ISBN mag leeg zijn en de druk
   ontbreekt bij een los product. Elk van de drie kan dus de directe voorganger zijn, en
   nooit twee tegelijk — er komt hooguit één teken. Staat de badge alléén, dan is er geen
   voorganger en dus geen leidend teken; hetzelfde structurele argument als v23.67.

   ⚠ GEEN nowrap: "Docentexemplaar 1 gratis · Max. voor deze titel bereikt" is een reeks
   woorden die best mag afbreken — anders dan het ISBN, dat één ondeelbaar nummer is. */
.line-teacher { color: var(--color-muted); font-size: 0.85em; }
.line-edition + .line-teacher::before,
.line-druk + .line-teacher::before,
.line-isbn + .line-teacher::before,
.line-license + .line-teacher::before { content: " · "; }
/* Stuksprijs + btw-noot blijven op één regel; de btw-noot is secundair (gedempt).

   v23.70 — RECHTS UITGELIJND. Onder table-layout:fixed is deze cel altijd even
   breed, maar wat erin staat varieert sterk: sinds v23.62/63 draagt een regel van
   één exemplaar zonder korting alleen nog "incl. 9% btw" (gemeten 4,94rem incl.
   celpadding), terwijl een 21%-regel in meervoud ook aantal én stuksprijs
   meeneemt (10,32rem). Links uitgelijnd bleef in dat korte geval ruim zes rem
   leegte staan tussen de btw-noot en de bedrag-kolom, terwijl de omschrijving
   ernaast werd ingekort met een ellipsis. Nu schuift de inhoud tegen het
   regeltotaal aan en lezen de twee getalkolommen als één blok aan de
   rechterkant — zonder dat er ook maar iets versmald hoeft te worden.

   ⚠ ÉÉN regel voor alle drie de consumenten, bewust niet drie. .checkout-summary
   overschrijft text-align hier nergens, dus kassa scherm 1 erft dit mee. Een
   tweede, gescoopte declaratie zou de schermen juist uit elkaar laten lopen zodra
   er ooit één van de twee wordt bijgesteld.
   ⚠ De gestapelde vorm onder 40rem raakt dit NIET: daar staan de cellen op
   display:inline, en text-align werkt niet op een inline box. Dat is exact wat de
   notitie bij .line-total in dat blok al vaststelt. */
.line-unit { text-align: right; white-space: nowrap; }
.line-vat { color: var(--color-muted); font-size: 0.85em; }
/* Regeltotaal: kale eindkolom, rechts uitgelijnd, nooit afbreken. */
.line-total { text-align: right; white-space: nowrap; }
/* v22.91 — docent-split-subregels op scherm 1 van de kassa: gedempt en licht ingesprongen
   onder de hoofdregel, en geen tussenrand (ze horen visueel bij de regel erboven). */
.order-lines .line-sub td { border-bottom: 0; color: var(--color-muted); font-size: 0.9em; }
.order-lines .line-sub .line-desc { padding-left: 1rem; }
/* v22.92 — scherm 1 van de kassa (bestellingoverzicht) leest als ÉÉN blok met de
   Subtotaal/Verzendkosten-regels eronder: de productregels krijgen dezelfde tekstgrootte
   als die totaalregels (de body-basis, net als .cart-total) i.p.v. de compacte 0,9rem uit
   .order-lines. Kleur was al gelijk (--color-text). UITSLUITEND gescoopt op .checkout-summary
   (alleen scherm 1, enkelvoudig én — via de geneste .checkout-group — gesplitst); de
   verkoperskaarten (scherm 2: .order-card.checkout-card) en "Mijn bestellingen" (.order-card)
   houden BEWUST de compacte maat, want daar is de hele kaart compact. Het eindbedrag
   (.cart-total--grand) blijft groot/vet; de btw-noot/.line-sub blijven secundair. */
.checkout-summary .order-lines { font-size: inherit; }
/* v22.93 — scherm 1 van de kassa: de omschrijving mag haar VOLLE breedte gebruiken en bij
   een lange titel over MEERDERE REGELS doorlopen (geen ellipsis) — er is ruimte zat. Heft
   de drie ellipsis-eigenschappen van .line-desc-text op (max-width → none, nowrap → normal,
   overflow hidden → visible). UITSLUITEND op .checkout-summary (scherm 1): de
   verkoperskaarten (scherm 2: .order-card.checkout-card) en "Mijn bestellingen"
   (.order-card) houden BEWUST de compacte ellipsis, want daar is de kaart smal. Het
   title-attribuut blijft in de markup staan (hier overbodig omdat de volle titel al
   zichtbaar is; op de kaarten doet het nog z'n werk).

   v23.71 — de bijbehorende regel `.checkout-summary .order-lines td { vertical-align: top }`
   is HIER WEG. Niet omdat scherm 1 het niet meer nodig heeft, maar omdat de gedeelde
   .order-lines td het nu voor alle drie de schermen zet: deze regel droeg die ene
   declaratie en verder niets, dus wat restte was een hoger-specifieke herhaling van de
   gedeelde waarde. Laat je zo'n regel staan, dan lijkt scherm 1 een eigen keuze te hebben
   die het niet meer heeft — en verandert iemand later de gedeelde regel, dan blijft scherm
   1 stil achter. De ellipsis-opheffing hieronder is wél scherm-1-eigen en blijft. */
.checkout-summary .order-lines .line-desc-text {
  display: inline; max-width: none; vertical-align: baseline;
  white-space: normal; overflow: visible; text-overflow: clip;
}
/* v23.64 — VASTE KOLOMBREEDTES. Zonder table-layout:fixed rekent élke .order-lines-tabel
   haar eigen kolommen uit op basis van haar eigen inhoud. In een LIJST van kaarten onder
   elkaar ("Mijn bestellingen", en scherm 2 met meerdere verkopers) schuiven de kolommen
   daardoor per kaart. Dat viel pas echt op sinds v23.62/63: de middenkolom draagt bij één
   exemplaar vaak alleen nog "incl. 9% btw" en is dan veel smaller dan de kaart ernaast.

   Alleen de omschrijving is nog auto — met fixed layout is dat de enige kolom zonder
   width, dus die krijgt alle resterende ruimte en blijft inkorten met ellipsis zoals
   voorheen. De bedrag-kolom houdt haar rechtse uitlijning.

   ⚠ GEMETEN, niet gegokt. De breedtes komen uit de advance widths van de Public Sans-
   woff2 die deze frontend zelf laadt (unitsPerEm 2000), bij de werkelijke celgrootte —
   .order-lines staat op 0.9rem, .line-vat op 0.85em daarvan, plus de 0.6rem padding-right
   van de cel. html zet geen font-size, dus 1rem = 16px.

   v23.70 — HERMETEN EN AANGESCHERPT. De v23.64-getallen reproduceren exact, maar de
   gekozen breedtes stonden ruim boven het geval waarvoor ze bedoeld waren: 11.5rem tegen
   een ondergrens van 10,32rem, 6rem tegen 4,95rem. Die marge kwam rechtstreeks van de
   omschrijving af, en juist die wilde Jelte breder. Opnieuw gemeten, nu niet op één
   voorbeeldstring maar op het breedste REALISTISCHE geval per kolom:

     .line-unit   "3× € 888,88 incl. 21% btw"    = 10,61rem  <- maatgevend
                  "10× € 999,99 incl. 21% btw"   = 10,86rem  (tweecijferig aantal)
                  "2× € 149,85 incl. 21% btw"    = 10,32rem  (het v23.64-anker)
                  -> 11rem, dus 0,14rem boven het tweecijferige geval
     .line-total  "€ 8.888,88"                   =  5,28rem  <- maatgevend
                  "€ 1.234,56"                   =  4,95rem  (het v23.64-anker)
                  -> 5.4rem, dus 0,13rem boven de maatgevende inhoud

   ⚠ DE CIJFERS VAN PUBLIC SANS ZIJN NIET TABULAIR: "1" is 0,407em en "8" is 0,648em, een
   verschil van 60%. Twee bedragen van gelijke LENGTE verschillen daardoor merkbaar in
   breedte — "€ 249,00" is 0,11rem breder dan "€ 149,85". Meten op één voorbeeldbedrag
   onderschat dus structureel; vandaar dat hierboven op de breedste cijfers is gerekend
   en niet op een bedrag dat toevallig in beeld stond.
   ⚠ NIET verder omlaag. Een unit-prijs bóven de duizend (11,42rem voor 10× € 1.234,56) is
   hiermee bewust NIET meer gedekt: dat is geen studieboekregel. Komt die inhoud er ooit
   wél, dan loopt hij zichtbaar over en hoort dit getal terug omhoog — niet de inhoud
   afgekapt.
   ⚠ Bewust GEEN overflow:hidden erbij. Deze twee cellen staan op white-space:nowrap, dus
   inhoud bóven het plafond loopt zichtbaar over in plaats van stil te worden afgekapt —
   een half afgeknipt bedrag is in een financiële kolom erger dan een lelijke overlap. */
.order-lines { table-layout: fixed; }
.order-lines .line-unit { width: 11rem; }
.order-lines .line-total { width: 5.4rem; }
/* De refund- en leverstatus-kolommen bestaan ALLEEN op "Mijn bestellingen" (account.js
   geeft ze als extraCells mee; de kassa houdt drie kolommen over, dus daar matcht dit
   niets). Ze dragen geen class, vandaar de positionele haak. Smaller gemaat dan hun
   uiterste inhoud omdat ze WEL mogen afbreken: "Terugbetaald" (6,22rem) en "Verzonden"
   (5,09rem) passen op één regel; "Deels terugbetaald" (8,18rem) en "Verzonden" mét de
   Volg-je-pakket-link (11,35rem) lopen netjes door naar een tweede regel.

   v23.70 — BLIJFT 7rem, als enige van de drie. De hermeting bracht twee leverstatussen
   aan het licht die v23.64 niet had nagerekend: "In behandeling" (6,80rem) en
   "Geretourneerd" (6,79rem). Die erven de celmaat (.line-status heeft geen eigen regel)
   en zijn dus fors breder dan de "Verzonden" waarop destijds is gemaat. 7rem houdt ze
   allebei nog net op één regel; er valt hier hooguit 0,2rem te halen en dat is de kans
   op een tweeregelige status niet waard. Dit getal is dus GEMETEN gelijk gebleven, niet
   ongemoeid gelaten omdat er niet naar gekeken is. */
.order-lines td:nth-child(n+4) { width: 7rem; }
/* v23.131 — "In verwerking": de ECHTE drukkersstatus (orders.fulfillment_status =
   'in_behandeling'), die tot nu toe in de default-tak viel en als "Betaald" las.

   ⚠ DIT IS DE EERSTE .line-status-*-REGEL IN DIT BESTAND. De vijf zusterklassen
   (-verzonden, -geretourneerd, -vertraagd, -betaald, -cancelled) staan wél in de markup
   van account.js maar hebben NOOIT een CSS-regel gehad — ze erven --color-text en de
   celmaat, precies zoals de notitie hierboven zegt (".line-status heeft geen eigen
   regel"). Er was hier dus geen bestaand tint-recept om te volgen; dit is er een nieuw.
   Wie de andere vijf ooit ook een kleur geeft, hoort dat rijtje in één keer af te maken
   en niet per status aan te vullen.

   KLEUR: --color-muted, het bestaande dempings-token (zelfde als .order-status-refunded
   en .line-sub). Neutraal, niet merk-gebonden, en het beweegt mee met het donkere palet
   (regel 288). Het botst niet met de twee statussen die iets DEFINITIEFS zeggen — die
   dragen geen eigen kleur. De keuze zegt: dit is een tussenstand, geen eindpunt.

   ⚠ ALLEEN KLEUR, GEEN BADGE. Bewust geen background/padding zoals .order-status: (a) de
   vijf buren zijn platte tekst en één pil ertussen leest als een ander soort ding, (b)
   binnenruimte kost kolombreedte en die is hier al krap — zie hieronder.

   ⚠ GEMETEN, EN HET PAST. In de basis waarvan de 7rem hierboven is afgeleid (v23.70:
   "In behandeling" 6,80rem, "Geretourneerd" 6,79rem) meet "In verwerking" 6,28rem —
   ruim binnen de kolom, op één regel, en niet eens de breedste leverstatus.

   v23.134 — DIT GETAL VERVING EEN PROBLEEM. Het oorspronkelijke woord "Wordt verwerkt"
   mat 7,20rem (hermeting met dezelfde ankers: 7,16rem), liep dus over de 7rem heen en
   brak over twee regels. Dat was toegestaan — deze kolommen "mogen WEL afbreken", zie de
   notitie hierboven, en "Verzonden" mét de Volg-je-pakket-link doet het al op 11,35rem —
   maar het hoefde niet: een korter woord in dezelfde toestandsvorm als de buren (Betaald,
   Verzonden, Geretourneerd) haalt hetzelfde zonder de kolom aan te raken. De kolom is dan
   ook NIET verbreed; dat zou de omschrijving smaller maken en de 59rem-drempelberekening
   van v23.70 opnieuw openen.

   In dezelfde beweging werd de synthetische buur 'vertraagd' van "In behandeling"
   (6,80rem) naar "Vertraagd" (4,86rem) gebracht — die twee woorden leken te sterk op
   elkaar terwijl ze het tegenovergestelde betekenen. Geen van beide labels raakt de 7rem
   nog aan. */
.line-status-in-behandeling { color: var(--color-muted); }
/* v23.253 — "Gepauzeerd" (orders.fulfillment_status = 'geannuleerd'): de drukker levert
   deze bestelling niet. Viel tot deze versie in dezelfde default-tak als hierboven en las
   dus óók als "Betaald".

   KLEUR: --color-muted, IDENTIEK aan .line-status-in-behandeling hierboven en om dezelfde
   reden. Dit is een neutrale, wachtende stand — geen storing en geen alarm. Een fout- of
   waarschuwingstint zou hier onwaar zijn: de bestelling loopt door, de factuur staat, er
   is niets misgegaan aan de kant van de klant. Bewust NIET de rode/blokkade-tint die de
   cockpit voor dezelfde toestand gebruikt: dáár is het een taak, hier een mededeling.

   ⚠ DIT MAAKT HET RIJTJE UIT DE NOTITIE HIERBOVEN NIET AF. De vijf kleurloze zusters
   (-verzonden, -geretourneerd, -vertraagd, -betaald, -cancelled) blijven precies zoals ze
   zijn; deze regel voegt een ZESDE klasse toe die vanaf zijn eerste dag een kleur nodig
   had, en trekt niet alsnog per status het rijtje bij. De opdracht daar ("in één keer
   afmaken") staat dus nog open en is door deze regel niet ingelost.

   ⚠ ALLEEN KLEUR, GEEN BADGE — zelfde twee redenen als hierboven (de buren zijn platte
   tekst; binnenruimte kost kolombreedte).

   GEMETEN, EN HET PAST. Op de ankers van v23.70/v23.134 ("In behandeling" 6,80rem,
   "Geretourneerd" 6,79rem, "In verwerking" 6,28rem, "Vertraagd" 4,86rem) meet
   "Gepauzeerd" ~5,6rem: ruim binnen de 7rem-kolom, op één regel, en smaller dan de
   twee die de kolombreedte destijds bepaalden. De kolom is dus NIET verbreed — dat zou
   de omschrijving smaller maken en de 59rem-drempelberekening van v23.70 heropenen. */
.line-status-geannuleerd { color: var(--color-muted); }
/* Kassa scherm 1 draagt dezelfde tabel op de body-maat (font-size: inherit, zie de
   .checkout-summary-regel hierboven), dus daar is dezelfde tekst breder en gelden eigen
   breedtes. De gedeelde classes blijven ongemoeid.

   ⚠ v23.70 — DE BODY-MAAT IS 17px, GEEN 16px. v23.64 rekende scherm 1 op 1rem en kwam zo
   op 11,40rem voor de ondergrens; body staat echter op calc(17px * var(--font-scale)) en
   html zet geen font-size, dus 1rem is 16px maar de CEL is 17px. Hermeten:
     .line-unit   "2× € 149,85 incl. 21% btw"  = 12,08rem  (v23.64 dacht: 11,40rem)
                  breedste realistische inhoud = 12,42rem
     .line-total  "€ 1.234,56"                 =  5,74rem
   13rem en 6.5rem dekken dat allebei, dus er is nooit iets zichtbaar misgegaan — maar de
   marge is met 0,58rem véél krapper dan v23.64 dacht. Daarom zijn deze twee als ENIGE
   NIET meegekrompen: die ruimte is er hier simpelweg niet, en scherm 1 heeft er ook geen
   last van (drie kolommen i.p.v. vijf, zie de drempelberekening hieronder). De winst voor
   de omschrijving komt op dit scherm volledig uit de rechts-uitlijning van .line-unit. */
.checkout-summary .order-lines .line-unit { width: 13rem; }
.checkout-summary .order-lines .line-total { width: 6.5rem; }
/* Onder deze drempel mag de tabel wéér zelf rekenen. Uitlijning tussen kaarten is winst
   bij een breed venster en betekenisloos zodra de ruimte er niet is.

   v23.65 — DREMPEL VAN 40rem NAAR 60rem. Browserverificatie liet zien dat de middenzone
   (venster rond de 48rem) stuk ging: de druk brak over twee regels en de omschrijving
   kwam tegen de kolom ernaast aan. 40rem was te laag gekozen — daar keek ik alleen naar
   "past het op een telefoon", niet naar hoeveel de omschrijving overhoudt.

   De som, zwaarste geval = "Mijn bestellingen" bij een BETAALDE order (vijf kolommen:
   omschrijving | stuksprijs | bedrag | refund | leverstatus), dus 11,5 + 6 + 7 + 7 =
   31,5rem vast. Beschikbaar voor de omschrijving:
       min(60rem, 92vw) − 0,94rem scrollbar-gutter − 2,3rem kaartpadding − 0,125rem rand
       − 31,5rem vaste kolommen
   Dat geeft  48rem venster →  9,30rem  ·  56rem → 16,66rem  ·  60rem → 20,34rem.
   De eis was ~20rem, dus 56rem haalt het NIET en 60rem is de eerste die het wel haalt.

   v23.70 — DREMPEL VAN 60rem NAAR 59rem. De drempel is nooit vrij gekozen: hij is de
   uitkomst van de som hierboven, en die som is kleiner geworden. De vaste kolommen staan
   nu op 11 + 5,4 + 7 + 7 = 30,4rem, dus de eis "~20rem voor de omschrijving" wordt eerder
   gehaald. Zelfde formule, alleen het laatste getal vervangen:
       0,92 × venster − 3,365rem overhead − 30,4rem vaste kolommen ≥ 20rem
       -> venster ≥ 58,44rem, en 59rem is de eerste hele rem die dat haalt.
   Ter controle:  58rem → 19,60rem (te weinig)  ·  59rem → 20,52rem (haalt het).
   ⚠ Anders dan in v23.65 is de drempel hier NIET de knop waar de winst vandaan komt. De
   omschrijving wint 1,10rem bij élke vensterbreedte boven de drempel (bij 60rem: 20,34 →
   21,44rem), en dat komt volledig uit de smallere kolommen. Het verlagen van de drempel
   volgt daar alleen uit — laat je hem op 60rem staan, dan schakelt de tabel terug naar
   auto-layout in een band waar fixed inmiddels prima past.

   Kassa scherm 1 (.checkout-summary) heeft bredere kolommen (13 + 6,5 = 19,5rem, in
   v23.70 bewust ongewijzigd) maar slechts drie, dus daar is 19,5rem vast i.p.v. 30,4rem:
   die haalt de 20rem al bij een venster van 47rem en kan deze drempel dus ruim aan
   (31,42rem bij 59rem). Eén drempel volstaat voor beide; een aparte, hogere is niet
   nodig. */
@media (max-width: 59rem) {
  .order-lines { table-layout: auto; }
  .order-lines .line-unit,
  .order-lines .line-total,
  .order-lines td:nth-child(n+4),
  .checkout-summary .order-lines .line-unit,
  .checkout-summary .order-lines .line-total { width: auto; }
}

/* v23.66 — GESTAPELDE REGELVORM ONDER 40rem, UITSLUITEND OP "MIJN BESTELLINGEN".
   ================================================================================
   Deze tab is de enige consument van `orderLineRow` met MEER dan drie cellen: naast
   omschrijving | stuksprijs | bedrag komen daar refund en leverstatus als extraCells bij
   (student-frontend/js/account.js, `renderOrderCard`). De kassa heeft er in alle drie haar
   regelvormen drie. Die twee extra cellen dragen `white-space: nowrap` (sinds v22.87) en
   leggen daarmee een BODEM in de tabelbreedte die geen kolomberekening nog kan wegnemen —
   op 535px liep de rij de kaart uit, met een horizontale schuifbalk. Kolommen krimpen lost
   dat niet op (dan overlapt het); vijf kolommen passen simpelweg niet op een telefoon.

   Onder 40rem wordt elke regel daarom een gestapeld blokje:

       Bouwkunde Materialen Werkboek (gedrukt)     <- .line-desc, VOLUIT, mag afbreken
       editie 2026/2027                            <- .line-edition, ongewijzigd
       incl. 9% btw · € 49,95 · Betaald            <- de overige cellen op één meta-regel

   ⚠ GESCOOPT OP .orders-list, NOOIT OP DE GEDEELDE CLASSES. De renderer
   (util/order_lines.js) is niet aangeraakt en de kassa ziet hier niets van; `.orders-list`
   staat alleen op de Bestellingen-container. Spiegelbeeld van `.checkout-summary`, dat
   hetzelfde andersom doet voor kassa scherm 1.
   ⚠ Boven 40rem verandert er NIETS: de tabelvorm, de 59rem-drempel en de gemeten
   kolombreedtes uit v23.64/65/69 blijven precies zoals ze zijn. Dit blok voegt toe.
   ⚠ De rechts-uitlijning van .line-unit (v23.70) reikt hier NIET tot: de cellen staan
   binnen dit blok op display:inline, en text-align werkt niet op een inline box. Zelfde
   reden als waarom .line-total zijn uitlijning hier vanzelf verliest, zie hieronder. */
@media (max-width: 40rem) {
  /* De tabel wordt een stapel blokken. tbody moet expliciet mee: laat je die op
     display:table-row-group staan, dan herstelt de browser de tabelstructuur eromheen. */
  .orders-list .order-lines,
  .orders-list .order-lines tbody,
  .orders-list .order-lines tr { display: block; }
  /* De celrand verhuist naar de RIJ — anders krijgt elk van de vijf cellen een eigen
     streep en staat er een ladder onder elke regel.

     ⚠ vertical-align: baseline is v23.71 en NEUTRALISEERT hier bewust de gedeelde
     `.order-lines td { vertical-align: top }`. Het idee dat vertical-align in deze
     stapelvorm "niets doet" omdat de cellen op display:inline staan klopt NIET: de
     eigenschap geldt óók voor inline boxen (CSS 2.1 §10.8.1) — alleen de TABELCEL-
     betekenis vervalt, de uitlijning binnen de LIJNBOX blijft. Doorgerekend met de
     Public Sans-metrics: de meta-cellen staan hier op --font-size-meta (13,6px) in een
     lijnbox waarvan de strut 14,4px is, dus hun bovenkant ligt 0,93px onder de top van
     de lijnbox. Met `top` zou die bovenkant naar de lijnboxtop schuiven en de hele
     metaregel 0,93px omhoog gaan t.o.v. de baseline. Uniform over alle meta-cellen, dus
     niet rafelig — maar wél een stille wijziging in een vorm die v23.66 zorgvuldig heeft
     uitgemeten, en die hier buiten scope is. Vandaar expliciet terug. */
  .orders-list .order-lines td {
    display: inline; border-bottom: 0; padding: 0; vertical-align: baseline;
  }
  .orders-list .order-lines tr {
    padding: 0.5rem 0; border-bottom: 1px solid var(--color-line);
  }
  .orders-list .order-lines tr:last-child { border-bottom: 0; }

  /* Regel 1 + 2: de omschrijving krijgt de volle breedte en mag AFBREKEN. Onder deze
     breedte is inkorten met ellipsis verkeerd: er is geen tweede kolom meer die de ruimte
     opeist, en een half woord met een hover-title helpt op een telefoon niemand. Dit is
     letterlijk hetzelfde recept dat kassa scherm 1 sinds v22.93 al gebruikt
     (.line-desc-text draagt sinds v23.79 `max-width: 100%` — de 22rem uit de
     `min(22rem, 100%)` van v23.65 is daar toen uit gehaald; hier gaat het plafond
     helemaal naar `none`).
     ⚠ De ellipsis-declaraties zelf blijven ongemoeid in .line-desc-text staan — ze worden
     hier alleen binnen dit blok overschreven. tests/test_order_line_render.py leest ze als
     brontekst en moet ze blijven vinden. */
  .orders-list .order-lines .line-desc { display: block; }
  .orders-list .order-lines .line-desc-text {
    display: inline; max-width: none; vertical-align: baseline;
    white-space: normal; overflow: visible; text-overflow: clip;
  }

  /* Regel 3: de overige cellen achter elkaar, in de metaregel-typografie van de e-books-rij
     (.ebook-meta — gedempte kleur, --font-size-meta). Eén keuze voor het hele bestand, geen
     nieuwe. `.line-total` verliest zijn rechtse uitlijning vanzelf: text-align werkt niet op
     een inline box. De nowrap op .line-unit/.line-total blijft en is hier juist gewenst —
     de losse stukjes blijven heel terwijl de regel ertussen mag afbreken. */
  .orders-list .order-lines .line-unit,
  .orders-list .order-lines .line-total,
  .orders-list .order-lines td:nth-child(n+4) {
    color: var(--color-muted); font-size: var(--font-size-meta);
  }

  /* ⚠⚠ HET BUNGELENDE SCHEIDINGSTEKEN. De verzendkostenregel heeft geen leverstatus (die
     cel komt leeg terug uit `lineFulfilmentBadge`) en de meeste regels hebben geen
     refund-badge (leeg uit `refundBadge`). Een naïeve "zet · achter elke cel" geeft daar
     "incl. 9% btw · € 4,40 ·" met een middenpunt dat nergens heen wijst.

     Oplossing, hetzelfde principe als `ebookMetaHtml` in account.js (die duwt alleen
     niet-lege delen in een array en joint met " · ", zodat een scheidingsteken alleen
     TUSSEN twee aanwezige delen kan staan), maar dan in CSS omdat de gedeelde renderer
     niet gewijzigd mag worden:

       1. een lege cel verdwijnt volledig  ->  td:empty { display: none }
       2. het scheidingsteken is LEIDEND (::before), niet volgend, en hoort dus BIJ de cel
          die erop volgt. Verdwijnt die cel, dan gaat zijn scheidingsteken mee.
       3. de eerste meta-cel (.line-unit, altijd gevuld — orderLineRow zet er altijd de
          btw-span in) krijgt er geen.

     Een SLUITEND scheidingsteken kan daardoor structureel niet ontstaan: er bestaat geen
     teken dat niet aan een zichtbare cel vastzit, en het staat altijd vóór die inhoud.
     ⚠ Voorwaarde is dat een lege cel echt leeg is: account.js schrijft `<td></td>` zonder
     witruimte, dus :empty grijpt. Zou daar ooit een spatie in komen, dan valt dit om — een
     test bewaakt dat. */
  .orders-list .order-lines td:empty { display: none; }
  .orders-list .order-lines td + td::before {
    content: " · "; color: var(--color-muted);
  }
  .orders-list .order-lines .line-unit::before { content: none; }
}

/* v23.78 — `.order-licenses` is hier UIT de selectorlijst gehaald: die wrapper bestond
   alleen om de licentie-badges onder de tabel te dragen en wordt sinds v23.78 nergens
   meer gerenderd (de licentie staat nu op de orderregel, zie `.line-license`).
   `.order-docs` deelde alleen de declaratie, niet het doel, en blijft ongemoeid. */
.order-docs { margin-top: 0.6rem; display: flex; flex-wrap: wrap; gap: 0.4rem 0.75rem; }
/* ⚠ v23.78 — .lic-badge WORDT NIET MEER GERENDERD, en staat hier tóch nog. Dat is een
   bewuste keuze, geen vergeten opruiming: deze regel is het REFERENTIEPUNT van het
   badge-recept elders in dit bestand. Vier comments wijzen ernaar — de token-definitie
   van --font-size-badge (".order-status, .lic-badge, .refund-badge"), de e-book-badge van
   v23.34, en twee bij .ticket-status — en één daarvan doet een feitelijke bewering over
   "alle drie" die niet meer klopt zodra deze verdwijnt. Weghalen is dus een eigen
   opruimstap: eerst die vier verwijzingen herformuleren naar een badge dat wél bestaat.
   Zolang dat niet gebeurd is, is de dode regel goedkoper dan vier kapotte verwijzingen. */
.lic-badge {
  font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 5px;
  background: var(--color-tint); color: var(--color-primary);
}
.lic-expired, .lic-revoked { background: var(--color-subtle); color: var(--color-muted); }
/* Factuurlink in PRIMARY, niet accent: brass op wit haalt 3.0:1 — te weinig voor
   tekst. Primary haalt 9.4:1 en spiegelt de linkkleur van de prose-basislaag. */
.order-docs a { color: var(--color-primary); font-size: 0.9rem; }
.pdf-pending { color: var(--color-muted); font-size: 0.9rem; }
.refund-badge {
  font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 5px;
  background: var(--color-subtle); color: var(--color-muted);
}

/* Laad-tekst: was een kale <p>. */
.loading { color: var(--color-muted); }

/* --- Formulieren: knop + velden in de ontwerptaal ---------------------------
   .auth-form/.reply-form hadden alleen LAYOUT-regels (profiel-/adres-tab), geen
   opmaak: knoppen en velden vielen terug op de UA-default (grijze #efefef-knop,
   #767676-rand) — zichtbaar op /binnenkort, /login, /register, /reset-password en
   het antwoordveld bij /berichten. Ze krijgen hier het bestaande knop-patroon van
   .pay-btn (gevulde primary-knop = de ACTIE-kleur van de app; de brass .btn-primary
   blijft gereserveerd voor de marketing-CTA's op de landing). Eén regel dekt alle
   formulieren, dus er ontstaat geen nieuw kaal formulier meer. */
.auth-form button[type="submit"],
.reply-form button[type="submit"] {
  display: inline-block; padding: 0.7rem 1.4rem; margin-top: var(--space-sm);
  border: 0; border-radius: var(--radius-sm); cursor: pointer;
  background: var(--color-primary); color: #fff;
  font-family: var(--font-body); font-size: 1rem; font-weight: 600;
  transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
/* .pay-btn deelt de hover met de formulierknoppen (Inloggen/Versturen): naar de accent-
   kleur, 1px omhoog, zachte schaduw. Hij had er zelf geen — Lezen, Afrekenen en Betalen
   voelden daardoor als een dood plaatje. Eén regel voor beide, zodat ze niet uit elkaar
   lopen. :active drukt 'm weer in, anders blijft hij tijdens de klik zweven. */
.auth-form button[type="submit"]:not(:disabled):hover,
.reply-form button[type="submit"]:not(:disabled):hover,
.pay-btn:not([disabled]):hover {
  background: var(--color-accent); transform: translateY(-1px);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
.auth-form button[type="submit"]:not(:disabled):active,
.reply-form button[type="submit"]:not(:disabled):active,
.pay-btn:not([disabled]):active {
  transform: translateY(0); box-shadow: none;
}
.auth-form button[type="submit"][disabled],
.reply-form button[type="submit"][disabled] { opacity: 0.6; cursor: default; }

.auth-form input:not([type="checkbox"]):not([type="radio"]),
.auth-form select,
.auth-form textarea,
.reply-form textarea {
  padding: 0.55rem 0.7rem;
  border: var(--line-hair) solid var(--color-line); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-text);
}

/* --- bijlagen bij een ticket-bericht (v23.18) -------------------------------
   De klant kon zijn eigen meegestuurde schermafbeelding nergens terugvinden; de cockpit
   toont ze sinds v23.13. De thread-bubbels zelf hebben (nog) geen eigen opmaak, dus dit
   blijft bewust klein: een onopvallende knop en een lijstje in de bestaande ontwerptaal
   (vars, haarlijn, radius). Geen kader eromheen — dat zou het enige omlijnde element in
   een verder kale thread worden.

   v23.27 — SWEEP naar de tokens, WAARDE-NEUTRAAL. Alleen maten met een EXACTE tegenhanger
   in :root zijn omgezet: 0,85rem → --font-size-meta (die token is in v23.24 juist óp deze
   drie .att-regels gedefinieerd), 0,5rem → --space-sm, 0,3rem → --space-xs, 1px →
   --line-hair. Geen gerenderde waarde verandert.

   WAT BEWUST BLIJFT STAAN, en waarom het geen sweep meer zou zijn:
     - 0,4 / 0,35 / 0,25 / 0,6 / 0,75 / 0,9rem hebben GEEN exacte token. Ze naar de
       dichtstbijzijnde stap trekken verandert de weergave (fracties van een pixel tot
       ~1,6px) en dat is een wijziging, geen notatie-omzetting. Los voorleggen.
     - 0,7rem staat wél als --font-size-badge in :root, maar dat is een FONT-maat en hier
       een padding. Zou iemand de badge-grootte bijstellen, dan schoof de knop mee. De vier
       badge-tokens horen bij elkaar (zie :root) en niet aan een paddingregel.
     - 0,15s in de transitie hierboven ligt naast --anim-quick (0,16s). Eén honderdste
       verschil, maar het ís een verschil.
     - 12rem op .att-naam en min(560px, 100%) op .reply-form textarea zijn OMSLAGPUNTEN en
       geen afstanden — zelfde redenering als de 14rem op .ticket-subject.
     - KLEUREN niet aangeraakt. De rode foutkleur hieronder staat 12× in dit bestand;
       die tokeniseren is een bestandsbrede beslissing en geen berichtenklus. */
.msg-attachments { margin-top: 0.4rem; }
.att-toggle {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--font-body); font-size: var(--font-size-meta); color: var(--color-muted);
}
.att-toggle:hover { color: var(--color-text); text-decoration: underline; }
.att-list { list-style: none; margin: 0.35rem 0 0; padding: 0; }
.att-item {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem;
  padding: 0.35rem var(--space-sm); border: var(--line-hair) solid var(--color-line);
  border-radius: var(--radius-sm); background: var(--color-subtle);
  margin-bottom: var(--space-xs);
}
/* De naam mag de regel niet uit duwen: hij komt uit de mail van de afzender en kan
   willekeurig lang zijn. Afkappen met ellipsis; de volledige naam staat in title. */
.att-naam {
  flex: 1 1 12rem; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem;
}
.att-meta { font-size: 0.75rem; color: var(--color-muted); }
.att-download { font-size: var(--font-size-meta); white-space: nowrap; }
.att-error { margin: 0.35rem 0 0; font-size: var(--font-size-meta); color: #b00020; }

/* --- formulier-kaart (contact, login, register) ---
   De velden hadden geen breedte en vielen terug op de UA-default (~20 tekens): piepklein
   op een verder lege pagina. Het formulier zit nu in dezelfde kaart als .cat/.aud op de
   landing (surface + haarlijn + radius) met volle veldbreedte. Bewust op .form-card /
   .form-page gescoopt en NIET op .auth-form: die class dragen óók account, /binnenkort en
   de wachtwoord-resets, en die hebben hun eigen layout (o.a. de profiel-/adres-tabs). */
.form-page { max-width: 640px; }
.form-intro { color: var(--color-muted); line-height: 1.6; margin: 0.6rem 0 1.5rem; }
.form-card {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: var(--radius); padding: 28px 26px;
  display: flex; flex-direction: column; gap: 1.05rem;
}
.form-card .field { display: flex; flex-direction: column; gap: 0.4rem; }
.form-card .field > span { font-weight: 600; font-size: 0.92rem; }
/* Turnstile-widget (M2). Leeg zolang de captcha uit staat of de widget nog laadt —
   dan neemt 'ie geen ruimte in (`:empty`), zodat het formulier niet springt. De
   iframe krijgt z'n eigen afmetingen van Cloudflare; wij zetten alleen de plek. */
.captcha-slot:empty { display: none; }
.captcha-slot { min-height: 65px; }
/* Velden in de PAGINA-kleur i.p.v. surface: de kaart is al surface, dus een veld in
   dezelfde tint verdween er visueel in. Nu tekenen ze zich af tegen de kaart.
   De :not()-keten is GEEN sier: de basisregel hierboven (.auth-form input:not(...)
   :not(...)) weegt zwaarder dan een kale `.form-card input` en won anders met haar
   surface-achtergrond. Voor textarea speelt dat niet — daar zijn beide even zwaar. */
.form-card input:not([type="checkbox"]):not([type="radio"]),
.form-card textarea {
  width: 100%; background: var(--color-background);
}
.form-card textarea { resize: vertical; min-height: 160px; }
.form-card button[type="submit"] { align-self: flex-start; margin-top: 0.25rem; }
/* Velden hadden geen focus-cue: de generieke a/button-outline pakt ze niet. Ook hier de
   :not()-keten, anders bleef de RAND van de inputs bij focus op --color-line staan (de
   box-shadow kwam er wél door, want die zet de basisregel niet). */
.form-card input:not([type="checkbox"]):not([type="radio"]):focus-visible,
.form-card textarea:focus-visible {
  outline: none; border-color: var(--color-primary);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
}
/* Vervolg-links onder de kaart (Registreren / Wachtwoord vergeten / Inloggen). */
.form-alt { margin: 1.1rem 0 0; color: var(--color-muted); font-size: 0.95rem; }
.form-alt + .form-alt { margin-top: 0.45rem; }
/* v23.139 — de per-site melding op de auth-schermen + de kassa (util/auth_notice.js).
   Sinds de correctie een KADER, en het staat BOVEN het formulier: wie van de oude
   webshop komt moet dit lezen vóór hij aan een inlogpoging begint die niet kan
   slagen. Boven het formulier zonder omlijsting leest als een intro-alinea en wordt
   overgeslagen; het kader maakt er een apart blok van.

   ⚠ INFORMATIE, GEEN WAARSCHUWING. Geen icoon, geen uitroepteken, en met opzet geen
   oranje/rood/geel — dat zijn de kleuren waarmee we fouten melden (.pay-msg--error,
   .cart-row-error), en die lading hoort hier niet. De vulling is een 4%-zweem van de
   SITE-EIGEN primaire kleur over het oppervlak: genoeg om het blok te laten staan,
   te weinig om te alarmeren, en het volgt automatisch het palet van elke site.

   Alleen bestaande custom properties, geen enkele nieuwe kleurliteral. --color-line
   is de bestaande haarlijn (r.232), --radius de bestaande hoekmaat (r.101).

   ÉÉN klasse, geen tweede ernaast: dezelfde .auth-notice draagt alle vijf de
   plaatsen (login, register, reset-aanvraag, mailbox-blok, kassa-inlog). */
.auth-notice {
  margin: 1rem 0 1.5rem;
  padding: 0.9rem 1.05rem;
  background: color-mix(in srgb, var(--color-primary) 4%, var(--color-surface));
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
  color: var(--color-text);
  font-size: 0.85rem;
  line-height: 1.65;
}
/* v23.141 — NIET LANGER --color-muted. Gedempt werkte toen dit een losse regel
   onderaan de pagina was: daar betekende dof "terzijde". In een kader betekent het
   iets anders — de omlijsting trekt de aandacht al naar het blok toe, en een doffe
   tekst dáárbinnen leest als weggeduwd, alsof je hem niet hoeft te lezen. Terwijl dit
   juist het blok is dat de klant van de oude webshop wél moet lezen. Kader = nadruk,
   tekst = gewoon leesbaar. De kop krijgt dezelfde kleur; alleen het gewicht verschilt. */
/* De twee alinea's in het kader dragen zelf geen marge — de padding van het kader is
   de ruimte. Twee klassen in de selector hieronder, zodat de kop-regel wint van
   `.auth-notice p` op specificiteit en er geen !important nodig is. */
.auth-notice p { margin: 0; }
.auth-notice .auth-notice-heading {
  font-weight: 700;
  margin-bottom: 0.35rem;
}
/* De link moet aanklikbaar ogen. ONDERSTREPING BLIJFT, en dat is functioneel: zonder
   onderstreping is een gekleurd stukje tekst in een gekleurd kader niet meer als link
   te herkennen, en dan gaan mensen het webadres overtypen in plaats van erop te
   klikken. Het VET is er in v23.141 af — dat maakte de link zwaarder dan de kop erboven
   en trok het blok uit balans. Kleur ongewijzigd. */
.auth-notice a {
  color: var(--color-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Knop die géén hoofdactie is (bv. "Stuur een nieuwe reset-link"): moet een <button>
   blijven — hij doet iets, hij navigeert niet — maar hoort er als tekstlink uit te zien.
   Zonder dit viel hij terug op de UA-default (grijs randje). */
.link-btn {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-weight: 600; color: var(--color-primary);
  text-decoration: underline; text-underline-offset: 2px;
}
.link-btn:hover { color: var(--color-accent); }

/* --- footer (ontwerp: donkere band) ---
   Drie kolommen (merk + navigatie + colofon) en een afsluitbalk met de juridische
   links. Kleur = --color-band: standaard 82% van de tenant-primary (zie :root), en een
   site die het anders wil zet via de cockpit haar eigen band. Dus elke site haar eigen
   donkere tint, niet een vastgepind teal.

   ⚠ Dit is het ENIGE vlak op dit token. De andere donkere band op de landing (.over)
   leest var(--color-primary) KAAL en is dus lichter — zie de opmerking daar. v23.31
   hing ze allebei hieraan; dat is teruggedraaid.

   DE TEKST IS EEN GEDEMPTE TINT, en de hover licht op naar kaal wit. Dat is het gedrag
   van vóór v23.30 en het is wat er op prod draait. v23.30 zette hem op kaal wit omdat de
   band toen lichter was gemaakt; daarmee werd de hover een no-op (van wit naar wit).
   Nagerekend op de teruggezette band:
     vastgoedboek band #194039   kaal wit 11,45:1   72%-tint 6,82:1
     dev-mpd      band #243D58   kaal wit 11,15:1   72%-tint 6,67:1
     DEFAULT      band #195BC1   kaal wit  6,35:1   72%-tint 4,10:1   <-- onder 4,5:1

   ⚠ DIE 4,10:1 OP HET DEFAULT-PALET IS BESTAAND GEDRAG en geen regressie van v23.32: de
   tint zat daar vóór v23.30 al onder de norm (toen 4,33:1, gemengd met primary i.p.v.
   met de band). Het is een openstaand besluit en bewust niet hier opgelost — dat zou het
   default-palet raken en dus elke site zonder eigen kleuren.

   ⚠ MENG MET --color-band, NIET MET --color-primary. Vóór v23.32 stond hier
   `#fff 72%, var(--color-primary)`: een tint van de MERKkleur op een ANDERSgekleurde
   ondergrond. Dat viel niet op zolang de band van primary was afgeleid, maar zodra een
   site een eigen band zet loopt de tint uit de pas met het vlak waar hij op ligt. De
   prijs van het rechtzetten is klein en gemeten: 7,00 -> 6,82 (vastgoedboek),
   6,88 -> 6,67 (dev-mpd), 4,33 -> 4,10 (default). */
.app-footer {
  background: var(--color-band);
  color: color-mix(in srgb, #fff 72%, var(--color-band));
  /* Extra bodem-marge (B-1): de drijvende 👤/❓-knoppen (position:fixed, bottom
     1.25rem + 48px hoog ≈ 4.25rem, linksonder) zouden anders de linker footer-
     legal-links overlappen en deels onklikbaar maken. 5rem houdt de links vrij. */
  padding: 0 0 5rem;
  font-size: 0.92rem;
}
.app-footer .wrap { padding-top: 0; }
.app-footer a { color: color-mix(in srgb, #fff 72%, var(--color-band)); text-decoration: none; }
.app-footer a:hover { color: #fff; }

.foot-top {
  display: grid; grid-template-columns: 1.4fr 1fr 1.3fr; gap: 40px;
  padding: 64px 0 44px;
}
.foot-brand .foot-mark {
  display: inline-flex; align-items: center; gap: 0.6rem; margin-bottom: 16px;
  font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; color: #fff;
}
.foot-brand .foot-mark:hover { color: #fff; }
.foot-brand p { max-width: 26em; line-height: 1.65; margin: 0; }
.foot-col h4 {
  color: #fff; font-family: var(--font-body); font-size: 0.8rem;
  letter-spacing: 0.11em; text-transform: uppercase; margin: 0 0 16px; font-weight: 700;
}
.foot-col a { display: block; padding: 6px 0; }
.colofon { line-height: 1.75; }
.colofon strong { color: color-mix(in srgb, #fff 90%, var(--color-primary)); font-weight: 600; }
/* De haarlijn staat op 0.1 alpha, en dat is sinds v23.32 weer exact de oude waarde op de
   oude band: v23.30 had hem naar 0.14 gezet om de lichtere band van toen te compenseren,
   v23.31 zette hem terug op 0.1, en met de band van v23.32 klopt dat getal ook weer
   precies. Gemeten contrast met de band eronder: 1,347 (vastgoedboek), 1,344 (dev-mpd),
   1,225 (default) — identiek aan de stand van vóór v23.30.
   Dit is een scheidingslijn en geen tekst of bedieningselement: er geldt geen
   contrastgrens voor. En het is DEZELFDE waarde als de 0.1-alpha op de kaart-overlay
   (~regel 518), zoals het vóór v23.30 ook was. */
.foot-bar {
  border-top: 1px solid rgba(255, 255, 255, 0.1); padding: 20px 0;
  display: flex; justify-content: space-between; align-items: center;
  gap: 16px; flex-wrap: wrap;
}
.foot-legal { display: flex; gap: 20px; flex-wrap: wrap; }
.foot-legal a { font-size: 0.86rem; }
.foot-bar .cr { font-size: 0.84rem; opacity: 0.8; }
.app-footer .partners { padding-top: 1.25rem; }

@media (max-width: 940px) {
  .foot-top { grid-template-columns: 1fr; gap: 32px; }
}

/* --- drijvende chrome --- */
.float-btn {
  position: fixed; bottom: 1.25rem; width: 48px; height: 48px;
  border-radius: 50%; border: 0; cursor: pointer;
  background: var(--color-primary); color: #fff; font-size: 1.3rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
#app-profile-menu .float-btn { left: 1.25rem; }
#app-help-widget .float-btn { left: 5rem; }
.popover {
  position: fixed; bottom: 5rem; left: 1.25rem; z-index: 20;
  background: var(--color-background); color: var(--color-text);
  border: 1px solid var(--color-muted); border-radius: 10px;
  padding: 1rem; width: min(320px, 90vw);
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.popover[hidden] { display: none; }
.popover h3 { margin-top: 0; }
.popover .row { display: flex; justify-content: space-between; align-items: center; margin: 0.5rem 0; }

/* --- shop + checkout (M1-12/M1-14) --- */
.cart-row { display: flex; justify-content: space-between; gap: 1rem; margin: 0.35rem 0; }
.cart-total { display: flex; justify-content: space-between; margin: 0.25rem 0; }
/* Cart-lade leesbaarheid (drawer-body = .cart-view). Alle lijnen delen var(--color-line),
   dezelfde haarlijn als de bestaande scheiding boven het totalen-blok. */
/* Scheiding TUSSEN producten: adjacent-sibling zet een lijn bóven elke rij behalve de
   eerste — dus alleen tussen producten, nooit onder de laatste (daar volgt de <hr> naar
   het totalen-blok). :not(:last-child) zou hier fout zijn: cart-rijen zijn nooit het
   laatste kind (hr + totalen volgen), dus dat gaf óók een lijn onder de laatste = dubbel. */
.cart-view .cart-row--editable + .cart-row--editable {
  border-top: 1px solid var(--color-line); padding-top: 0.55rem;
}
/* De bestaande scheiding boven het totalen-blok: buiten #app-content, dus geen default-hr;
   expliciet als dezelfde haarlijn tekenen zodat alle lijnen consistent zijn. */
.cart-view hr { border: 0; border-top: 1px solid var(--color-line); margin: 0.6rem 0; }
/* Bedrag rechts (label links regelt .cart-total via space-between); bedragen onderling
   rechts uitgelijnd doordat elke waarde tegen de rechterrand van de volle-breedte rij zit. */
.cart-view .cart-total-value { text-align: right; }
/* Scheiding boven Totaal + vet (Totaal blijft benadrukt na het wegvallen van <strong>). */
.cart-view .cart-total--grand {
  border-top: 1px solid var(--color-line);
  margin-top: 0.4rem; padding-top: 0.4rem; font-weight: 700;
}
/* "waarvan BTW": AFSPLITSING onder het totaal — gedempt en kleiner, GEEN optelpost
   (geen lijn erboven). Zelfde regel voor lade + kassa. */
.cart-view .cart-total--vat,
.checkout-summary .cart-total--vat {
  color: var(--color-muted); font-size: 0.85rem; margin-top: 0.1rem;
}
.checkout-summary { margin: 1rem 0; }
/* v22.84: de scheidingslijn boven "Subtotaal" erfde `margin: 2em 0` van de generieke
   `:where(.app-content) :where(hr)`-regel → een lege regel bóven én ónder de lijn. Strak
   trekken, UITSLUITEND op de kassa-<hr> (scherm 1). Gelijkgetrokken met `.cart-view hr`
   (0.6rem) zodat de lade en de kassa dezelfde ademruimte tonen. Raakt geen gedeelde class. */
.checkout-summary hr { margin: 0.6rem 0; }
/* Kassa deelt de totalen-/split-vorm van de cart-lade. De lade-regels zijn
   `.cart-view`-gescoopt (buiten #app-content); de kassa rendert in `.checkout-summary`
   binnen #app-content. ADDITIEF: dezelfde waarden hier onder .checkout-summary, zonder
   de browser-geverifieerde lade-selectors te herschrijven. */
/* v23.195: `.order-totals` erbij — het totalenblok van "Mijn bestellingen"
   (student-frontend/js/util/order_totals.js) draagt exact dezelfde `.cart-total`-rijen en
   rendert net als de kassa BINNEN #app-content, dus het hoort in deze lijst en niet bij de
   `.cart-view`-regels hierboven. PUUR EEN NAAM ERBIJ: geen enkele waarde in deze twee
   regels is aangeraakt, dus voor `.cart-view` en `.checkout-summary` verandert er niets. */
.checkout-summary .cart-total-value,
.order-totals .cart-total-value { text-align: right; }
.checkout-summary .cart-total--grand,
.order-totals .cart-total--grand {
  border-top: 1px solid var(--color-line);
  margin-top: 0.4rem; padding-top: 0.4rem; font-weight: 700;
}
/* v23.197 — DE SCHEIDING BOVEN "Subtotaal" OP /account.
   De kassa krijgt die lijn van een `<hr />` in de markup; `.order-totals` heeft er geen,
   en dat blijft zo (besluit Jelte). Een <hr> hier zou zijn marge halen uit
   `.checkout-summary hr` (:2346) — en die is gescoopt, dus op /account viel hij terug op
   de generieke `:where(.app-content) :where(hr)` met een ándere marge. Dan was er alsnog
   CSS nodig. Dit is die CSS, één regel, en de markup blijft ongemoeid.
   ⚠ De regeltabel biedt de scheiding zelf niet: `.order-lines` heeft `margin: 0` (:1523)
   en `tr:last-child td` zet `border-bottom: 0` (:1528) — zonder deze regel loopt Subtotaal
   dus tegen de laatste bestelregel aan.
   0.6rem is niet gekozen maar overgenomen: `.cart-view hr`, `.checkout-summary hr` en
   `.order-docs` staan alle drie op diezelfde waarde. Houd dat zo. */
.order-totals {
  border-top: 1px solid var(--color-line);
  margin-top: 0.6rem;
  padding-top: 0.4rem;
}
.checkout-summary .cart-row-split { display: flex; flex-direction: column; gap: 0.15rem; }
.checkout-summary .cart-row-splitline {
  display: flex; justify-content: space-between; gap: 0.75rem;
  font-size: 0.82rem; color: var(--color-muted);
}
.checkout-summary .cart-row-splitvalue { text-align: right; white-space: nowrap; }
.pay-btn {
  display: inline-block; margin-top: 1rem; padding: 0.7rem 1.4rem;
  border: 0; border-radius: 8px; cursor: pointer;
  background: var(--color-primary); color: #fff; font-size: 1rem; text-decoration: none;
  /* Zonder transition klapt de hover er hard in i.p.v. te vloeien. Zelfde drie
     eigenschappen (en tijd) als de formulierknoppen, zodat het één beweging is. */
  transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.pay-btn[disabled] { opacity: 0.6; cursor: default; }
/* Expliciete tegenhanger van de UA-regel [hidden]{display:none}: de author-regel
   .pay-btn{display:inline-block} overstemt 'm anders, zodat btn.hidden=true in
   account.js (order-Betalen) de knop NIET verbergt → dubbele/gestapelde knop bij het
   uitvouwen van het Payment Element. Zelfde patroon als .popover[hidden]/#payment-section[hidden]. */
.pay-btn[hidden] { display: none; }

/* v23.3 — akkoord-blok op scherm 1 van de kassa: het vinkje op een eigen regel, de
   primaire actie als EIGEN BLOK eronder. Zonder deze regels is <label
   class="checkout-consent"> ongestyled en dus inline, en gaat de .pay-btn
   (display:inline-block) op DEZELFDE regel achter de akkoord-zin staan — de knop leest
   dan als bijzin i.p.v. als de actie waar het scherm om draait.
   UITSLUITEND gescoopt op de wrapper: .pay-btn zelf blijft ongewijzigd, want die class
   is de gedeelde primaire actie (Betalen, Inloggen, Lezen). Niet verwarren met
   .btn-primary — dat is de marketing-CTA. */
/* v23.151 — het kortingscodeveld op de kassa (checkout.js `voucherBlockHtml`).
   Staat BINNEN .checkout-summary, direct onder de haarlijn boven de totalen, en
   BUITEN .checkout-consent-block: het hoort bij het overzicht, niet bij het akkoord.
   Toch heeft het een EIGEN selector in `hideFrozenOrderFields` — zie daar: na het
   plaatsen van de order is dit veld inert en moet het weg, net als het akkoord-blok. */
.checkout-voucher { margin: 0.6rem 0 0.9rem; }

/* --- v23.163: EEN KLEIN VELD, ALTIJD ZICHTBAAR ---
   Besluit Jelte 07-08 na meting in de browser: het inklappen van v23.162 vervalt. Eén
   regel, en alles staat erop:

       Kortingscode of voucher ⓘ  [_________]  [Toepassen]

   ⚠ DIT IS NIET DE BREDE BALK VAN VÓÓR v23.162 TERUG. Die was te dominant (label op een
   eigen regel, veld op volle breedte, knop op kassa-maat); de ingeklapte vorm had het
   omgekeerde probleem, onzichtbaarheid. Klein maar aanwezig is de middenweg. Het veld is
   daarom NIET `flex: 1 1 auto` (dat was de volle-breedte-vorm) maar `flex: 0 1 auto`, en
   houdt de breedte van zijn `size="15"`.

   De knopmaat is GEEN nieuwe maat: .checkout-voucher-row .pay-btn hangt in de gedeelde
   compacte regel bij .ebook-item .pay-btn, ver hierboven in dit bestand. */
.checkout-voucher-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.5rem;
}
/* Label + ⓘ zitten in één omhulsel, met een KLEINERE tussenruimte dan de rij-gap. Los in
   de rij zou het ⓘ even ver van het label als van het veld staan en dus bij geen van
   beide horen; het hoort bij het label. */
.checkout-voucher-lab { display: inline-flex; align-items: center; gap: 0.1rem; flex: none; }
.checkout-voucher-label { font-size: 0.9rem; }
.checkout-voucher-row input { flex: 0 1 auto; min-width: 0; }
.checkout-voucher-row .pay-btn { flex: none; }
/* SMAL SCHERM: het veld pakt de volle regel en duwt de knop daarmee naar de volgende —
   dat is `flex-wrap` hierboven aan het werk, niet een tweede layout. Het alternatief was
   het veld laten meekrimpen tot het onleesbaar is, en daar passen de 32 tekens die er
   maximaal in mogen allang niet meer in. */
@media (max-width: 30rem) {
  .checkout-voucher-row input { flex: 1 1 100%; }
  .checkout-voucher-row .pay-btn { width: 100%; }
}
/* --- v23.164: DE TWEEDE TOESTAND ---
   Ligt er een werkende code, dan verdwijnt de invoerregel en houdt dit blok alleen nog de
   CODEREGEL over (checkout.js `voucherBlockHtml`). Die regel is een `.cart-total` als de
   aftrekregels eromheen, dus mag het blok dan géén eigen buitenruimte meer meebrengen —
   anders staat één aftrekregel verder van zijn buren dan de andere.
   ⚠ Bij een GESPLITST mandje staan de coderegels in de verkopersblokken en houdt dit blok
   boven de blokken alleen de (lege) meldregel over; met `margin: 0` plus de `:empty`-regel
   hieronder is dat precies nul hoge ruimte, en dus geen gat onder de gele splitsmelding. */
.checkout-voucher--applied { margin: 0; }
/* De meldregel blijft in BEIDE toestanden in de DOM (zie de noot bij `voucherBlockHtml`:
   het is het enige kanaal voor een mislukt verwijderen), maar zolang er niets in staat mag
   hij geen ruimte innemen — `.pay-msg` brengt 0.75rem margin-top mee. `say()` vult en
   leegt hem, dus deze regel schakelt vanzelf mee. */
.checkout-voucher .pay-msg:empty { display: none; }
/* De toegepaste-code-regel deelt de totalen-vorm (.cart-total) maar krijgt de
   accentkleur, zodat een aftrekpost niet als zoveelste subtotaal leest. Zelfde regel
   voor de lade (.cart-view, buiten #app-content) en de kassa. */
.cart-view .cart-total--voucher,
.checkout-summary .cart-total--voucher,
/* v23.195: en "Mijn bestellingen", dat dezelfde aftrekregels toont. Alleen deze twee
   namen zijn aan de lijst toegevoegd; de declaratie is ongewijzigd. */
.order-totals .cart-total--voucher,
/* v23.161: elke aftrekregel (docentexemplaar, klantkorting, code) deelt deze vorm. De
   code-regel draagt daarnaast nog --voucher, zodat "de voucherregel" aanwijsbaar blijft. */
.cart-view .cart-total--deduct,
.checkout-summary .cart-total--deduct,
.order-totals .cart-total--deduct { color: var(--color-primary); }
/* v23.163: `.checkout-voucher-applied` is weg. Dat was de eigen regel ONDER de code met
   "Code verwijderen" erop; de link staat nu achter de code, in de coderegel zelf. */
.checkout-voucher-failed { margin: 0.35rem 0 0; }
/* De verwijderlink staat IN een aftrekregel (of in de faalzin) en erft daar de kleur van.
   `font: inherit` op .linklike houdt 'm op de maat van de regel eromheen; het enige wat
   hem als actie aanwijst is de onderstreping. Geen eigen kleur, want een tweede accent in
   een regel die al accentkleur draagt leest als een tweede bedrag. */
.cart-total--voucher .voucher-clear { color: inherit; }
/* "Code verwijderen" is een SECUNDAIRE actie en hoort geen knopvorm te krijgen naast de
   primaire "Doorgaan naar betalen" — anders concurreren twee knoppen om dezelfde blik.
   Een <button> en geen <a>, want het is een actie en geen navigatie; de linkvorm is
   puur weergave. */
.linklike {
  background: none; border: 0; padding: 0;
  color: var(--color-primary); text-decoration: underline; cursor: pointer;
  font: inherit;
}
.checkout-consent-block { margin-top: 1.25rem; }
/* De #guest-section-variant staat er BEWUST bij: `#guest-section label` (verderop:
   display:block) draagt id-specificiteit en zou het gast-pad anders als enige van de drie
   paden zonder deze uitlijning laten. Zelfde blok, zelfde plaatsing, alle drie de paden. */
.checkout-consent,
#guest-section .checkout-consent { display: flex; align-items: flex-start; gap: 0.5rem; }
/* De checkbox lijnt uit op de EERSTE tekstregel (niet op het midden van een zin die
   over twee regels loopt) en krimpt niet mee in de flexrij. */
.checkout-consent input[type="checkbox"] { flex: none; margin-top: 0.25rem; }
.checkout-consent-block .pay-btn { display: block; margin-top: 0.9rem; }
/* Expliciete tegenhangers van de UA-regel [hidden]{display:none} — zelfde patroon en
   zelfde reden als bij .pay-btn[hidden] hierboven, maar hier NODIG omdat de regels
   erboven zwaarder wegen: een author-regel verslaat de UA-regel sowieso, en
   `.checkout-consent-block .pay-btn` (twee classes) wint bovendien van `.pay-btn[hidden]`.
   Zonder deze blijven het vinkje en de knop staan waar checkout.js ze bij de betaalstart
   juist verbergt (consent-row.hidden / btn.hidden) → dubbele knop.
   De #guest-section-variant moet mee om dezelfde reden als hierboven (en dicht meteen het
   gat dat `#guest-section label{display:block}` daar al vóór deze wijziging sloeg). */
.checkout-consent[hidden],
#guest-section .checkout-consent[hidden] { display: none; }
.checkout-consent-block .pay-btn[hidden] { display: none; }

#guest-section label, #payment-section { display: block; margin: 0.75rem 0; }
/* De ID-regel hierboven overstemt de UA-regel [hidden]{display:none}; expliciete
   tegenhanger zodat <section id="payment-section" hidden> echt verborgen blijft
   tot beginPayment het onthult (zoals .popover[hidden] / .cart-summary-bar[hidden]). */
#payment-section[hidden] { display: none; }
/* v22.86: scherm 1 van de kassa (overzicht per verkoper + adres + akkoord + de knop
   "Doorgaan naar betalen"). Bij een GESPLITSTE keten zet enterPaymentScreen 'm op
   hidden; expliciete tegenhanger in dezelfde geest als #payment-section[hidden], zodat
   een author-regel op deze wrapper de UA-regel [hidden]{display:none} niet stilletjes
   kan overstemmen. Eén verkoper → de wrapper wordt nooit verborgen. */
#order-screen[hidden] { display: none; }
#guest-section input { display: block; width: min(360px, 100%); padding: 0.5rem; margin-top: 0.25rem; }
/* v23.325 — DE AKKOORD-VINKJES ZIJN GEEN GAST-FORMULIERVELD. De regel hierboven geeft
   elk <input> in de gast-sectie de veldvorm (eigen blok, 360px breed, padding) en dat
   is bedoeld voor e-mailadres en wachtwoord — maar hij raakt ook de twee checkboxen in
   `.checkout-consent-block`, die als enige van de drie kassapaden BINNEN #guest-section
   staan (checkout.js r.1272; het adres-pad en het ingelogde pad hangen hun blok als
   sibling van deze sectie op). Die checkboxen staan in een flexrij met `flex: none`,
   dus de 360px werd letterlijk de breedte van het vakje: het vinkje kreeg een eigen
   kolom links en de zin begon ruim 300px verderop. Gemeten in de browser op het
   gast-afrekenscherm, 15-08.
   HET IS ÉÉN PLEK: de markup is gedeeld (`consentHtml`/`consentBlockHtml` bouwen alle
   drie de paden uit dezelfde <label class="checkout-consent">), dus alleen deze
   ancestor-regel maakte het verschil. Hier wordt de veldvorm teruggenomen, niets meer:
   `.checkout-consent input[type="checkbox"]` hierboven levert flex/margin al.
   ⚠ DEZE REGEL MOET ONDER `#guest-section input` BLIJVEN STAAN. Hij wint op
   specificiteit, maar de jsdom-testrunner rekent de cascade op VOLGORDE en niet op
   specificiteit (gemeten: `#a .x` verliest daar van een later `#a p`). Zou hij naar het
   .checkout-consent-blok verhuizen, dan meet de test de oude stand en blijft groen
   terwijl de browser scheef staat. Zie student-checkout-consent-uitlijning.test.mjs. */
#guest-section .checkout-consent input[type="checkbox"] {
  display: inline-block; width: auto; padding: 0;
}
#payment-element { margin: 1rem 0; }
.pay-msg { margin-top: 0.75rem; color: var(--color-muted); }
/* Foutstaat van de kassamelding: dezelfde app-foutkleur als .cart-row-error/
   .card-error (#b00020), zodat een geweigerde actie (bv. akkoord niet aangevinkt)
   rood toont i.p.v. als neutrale muted-tekst. Alleen de kleur wijzigt. */
.pay-msg--error { color: #b00020; }
.field-hint { margin: 0.15rem 0 0; font-size: 0.85em; color: var(--color-muted); }

/* --- checkout-adressectie (student-frontend-lane) ---------------------------
   De adressectie in checkout.js had NUL eigen CSS: kale UA-fieldset-rand, inline
   vloeiende labels (→ "Plaats" losgeraakt van z'n veld), veldbreedtes op UA-default.
   Hier krijgt ze het .form-card .field-idioom (label bóven het veld) GESPIEGELD —
   niet hergebruikt: `.form-card .field` vereist een `.form-card`-voorouder en die
   sleept kaart-chrome (surface/rand/padding) mee die we hier niet willen. De sectie
   draagt `.auth-form` (checkout.js), dus de veld/knop-opmaak (shell.css:891-898) geldt
   al; deze regels voegen alleen layout toe.
   ⚠ ALLES onder #address-section: .addr-kind/.addr-block/.ship-same-label/.info-*
   worden óók door de Adressen-tab (account.js) gebruikt en mogen niet regresseren. */
#address-section .addr-fs { border: 0; padding: 0; margin: 0 0 1rem; min-inline-size: 0; }
#address-section .addr-fs legend {
  padding: 0; margin-bottom: 0.3rem;
  font-weight: 600; font-size: 1.05rem; color: var(--color-text);
}
#address-section .addr-kind { margin: 0.2rem 0 0.8rem; }
#address-section .field { display: flex; flex-direction: column; gap: 0.4rem; }
#address-section .field > span { font-weight: 600; font-size: 0.92rem; }
#address-section .field :is(input, select) { width: 100%; box-sizing: border-box; }
#address-section .field :is(input, select):focus-visible {
  outline: none; border-color: var(--color-primary);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
}
/* Raster: mobiel één kolom; ≥600px vier fijne kolommen zodat de logische paren
   asymmetrisch naast elkaar passen (Straat span3 | Huisnummer span1, enz.). */
#address-section .addr-grid {
  display: grid; grid-template-columns: 1fr; gap: 0.75rem 1rem; margin: 0.4rem 0 0.8rem;
}
/* ID-scope (#address-section) overstemt de UA-regel [hidden]{display:none}; expliciete
   tegenhanger zodat een verborgen .addr-block (particulier/zakelijk-toggle) écht weg is. */
#address-section .addr-grid[hidden] { display: none; }
#address-section .ship-same-label { display: block; margin: 1rem 0 0.5rem; }
#address-section #addr-continue { display: block; margin-top: 0.5rem; }
@media (min-width: 600px) {
  #address-section .addr-grid { grid-template-columns: repeat(4, 1fr); }
  #address-section .addr-grid .field { grid-column: span 2; }        /* halve breedte */
  #address-section .addr-grid .field-wide { grid-column: span 3; }   /* Straat / Plaats */
  #address-section .addr-grid .field-narrow { grid-column: span 1; } /* Huisnummer / Postcode */
  #address-section .addr-grid .field-full { grid-column: span 4; }   /* Bedrijfsnaam */
}
/* --- v22.90: gestapeld veld — label BOVEN, veld eronder op volle breedte -------
   Gedragen door de herkomstvraag op de KASSA (#source-question-section) én op het
   PROFIEL (die labels staan in .auth-form op de profiel-tab en kregen daar de
   12rem-grid; `:not(.field-stacked)` hierboven houdt ze daarbuiten).

   v22.88 gaf de vraag als enige veld een label ERNAAST (12rem-grid). Alle andere
   velden op de kassa — Voornaam, Straat, Postcode, Land — dragen hun label erboven,
   dus dat las al scheef. Belangrijker: `question_text` is PER SITE instelbaar, dus
   ELKE vaste labelkolom breekt zodra iemand een langere vraag invult ("Bij welke
   opleider volg je je opleiding?" past al niet op één regel binnen 12rem). Gestapeld
   is er geen maat die kan breken: de vraag mag zo lang zijn als nodig.

   Bewust GEEN eigen maatvoering: alleen `display:block` + volle veldbreedte. De
   marge/padding komt van de omliggende opmaak (.auth-form, en op het profiel de
   tab-regel hierboven, die op specificiteit wint). ⚠ De inline style.display-toggle
   van #source-other-row wint hiervan (inline > author) — dát blijft de manier waarop
   het "Anders"-veld verschijnt, het [hidden]-attribuut werkt hier niet (v22.88). */
.field-stacked { display: block; margin: 0.6rem 0; }
.field-stacked :is(select, input) { width: 100%; box-sizing: border-box; }

.return-state { font-size: 1.1rem; font-weight: 600; }
.return-provisioned { color: var(--color-primary); }
.return-failed, .return-cancelled { color: #b00020; }
.return-readers a { color: var(--color-primary); }

@media (max-width: 640px) {
  .app-nav ul { flex-wrap: wrap; }
  .app-content { margin: 1rem auto; }
}

/* --- shop: vaste promotiebanner (e-book) ---
   Staat tussen de <h1> en .shop, dus BOVEN zoek en facetten, en loopt over de volle
   breedte van de contentkolom (.app-content bepaalt die breedte; de banner voegt zelf
   geen max-width toe).

   TWEE REGELS IN EEN VLAK. De kaart van v23.254 was te hoog (kop, vier gestapelde
   bullets, knop), de haarlijn van v23.255 te kaal. Dit is de tussenvorm: er staat weer
   een vlak, maar de HOOGTE keert niet terug. Drie dingen doen dat werk:
     - binnenruimte op --space in plaats van --space-lg (0,85 tegen 1,25rem);
     - twee regels in plaats van zes, want regel 2 is één lopende zin geworden;
     - de regels dicht op elkaar: --space-xs ertussen en verder nul marge, want de
       <p>-standaardmarge van de browser zou er alleen al 1em boven én onder leggen.

   DE VORM IS DIE VAN DE ACTIEKAART OP /acties (.actie-card, verderop in dit bestand):
   accentstreep links, lichte vulling, haarlijn rondom, dezelfde hoek. Vier waarden zijn
   daar LETTERLIJK vandaan overgenomen — background --color-surface, border 1px solid
   --color-line, border-left 4px solid --color-accent, border-radius --radius. De
   redenering staat bij die kaart en geldt hier één op één: de site-accentkleur is op
   deze schermen verder nergens vlakvullend aanwezig, dus een streep van 4px maakt het
   blok los van wat eromheen staat zónder een tweede achtergrondkleur te introduceren.

   DE HAARLIJN OP DE ANDERE DRIE ZIJDEN IS ER SINDS v23.261, EN DAT DRAAIT v23.260 TERUG.
   Die versie liet hem bewust weg om de banner lichter te houden dan de kaart. In de
   browser pakte dat anders uit: --color-surface is een LICHT vlak, en zonder rand dreef
   het op de paginakleur in plaats van erop te liggen. De vulling had geen begrenzing en
   de bovenkant van de banner viel weg. Het gewichtsverschil met de kaart zit al in de
   binnenruimte (--space tegen --space-lg) en in twee regels tegen een hele kaart; dat
   hoefde de rand niet óók te dragen. Schaduw heeft .actie-card niet, dus daar viel toen
   en nu niets over te beslissen.

   ⚠⚠ EN DE KLASSE ZELF IS NIET HERGEBRUIKT. De waarden staan gekopieerd op
   .shop-promo, niet als tweede gebruiker onder .actie-card. Dat is met opzet: twee
   schermen aan één selector bewegen stil mee, en dan verandert een tik aan de
   actiepagina ook de webshop. Wie de kaart daar herziet moet hier dus BEWUST langs.

   --color-tint (de oude vulling, de merk-tint van badges en icon-chips) is hiermee
   vervallen. Nog steeds geen nieuwe kleur- of maatliteral: alle waarden zijn tokens,
   op de 4px van de accentstreep na — en dat getal is niet verzonnen maar overgenomen,
   want de streep moet exact even dik zijn als die op /acties.

   ⚠ REGEL 2 IS INHOUD, GEEN BIJSCHRIFT, EN DRAAGT DAAROM GEEN ENKELE FONT-DECLARATIE.
   Hij stond tot v23.258 op --font-size-meta, de bijschrift-maat van .order-date en
   .ebook-meta. Dat las verkeerd: op een lichte skin viel de regel weg tegen het vlak,
   terwijl het juist de vier voordelen zijn die de banner te vertellen heeft.
   Die declaratie is WEG en er is niets voor in de plaats gekomen — geen font-size en
   geen color — zodat de regel maat én kleur van de body erft, precies als gewone tekst
   op de pagina. Wie hier ooit weer een maat neerzet maakt er weer een bijschrift van.

   Er is trouwens nooit een color op deze banner gestaan, ook niet op .shop-promo: de
   grijzere indruk kwam volledig van die kleinere maat tegen het vlak.

   HET VERSCHIL TUSSEN DE TWEE REGELS IS DAARMEE ALLEEN NOG GEWICHT. Regel 1 staat vet
   op de body-maat, regel 2 mager op dezelfde maat. Dat is genoeg hiërarchie voor twee
   regels, en het kost nog steeds geen enkel verzonnen getal.

   DOORLOPEN OP EEN SMAL VENSTER. Regel 2 is een gewone <p> zonder white-space- of
   flex-regel, dus hij breekt vanzelf op de spaties rond de middenpunten en zakt naar
   een volgende regel. Wie hier ooit nowrap of een flexrij aan toevoegt breekt dat. */
.shop-promo {
  background: var(--color-surface);
  /* ⚠ DEZE TWEE REGELS STAAN IN DEZE VOLGORDE OMDAT DE VOLGORDE HET RESULTAAT BEPAALT.
     `border` is een shorthand en zet ALLE VIER de zijden, dus ook links. Staat hij ná
     border-left, dan overschrijft hij de accentstreep en is de linkerkant 1px lijnkleur
     in plaats van 4px accent — stil, want de CSS blijft geldig en er valt niets uit.
     Eerst de vier zijden, dan links overschrijven. Precies zoals .actie-card het doet. */
  border: 1px solid var(--color-line);
  border-left: 4px solid var(--color-accent);
  border-radius: var(--radius);
  padding: var(--space);
  /* Alleen de LINKERkant wijkt af: tegen de 4px-streep aan is --space te krap en plakt
     de tekst aan de rand. --space-lg geeft er lucht en is een bestaande stap; boven,
     rechts en onder blijft --space precies zoals het stond, en de marge eronder ook. */
  padding-left: var(--space-lg);
  margin: 0 0 var(--space-lg);
}
.shop-promo-kop { margin: 0 0 var(--space-xs); font-weight: 700; }
/* margin 0 BLIJFT: zonder die nul legt :where(.app-content) :where(p) er 1em onder. */
.shop-promo-sub { margin: 0; }

/* --- shop 6a: zoek / facets / lijst / paginering --- */
.shop { display: flex; flex-direction: column; gap: 1rem; }
.shop-search { position: relative; } /* anker voor de absolute suggest-dropdown (6c) */
.shop-search label { display: block; font-size: 0.85rem; color: var(--color-muted); margin-bottom: 0.25rem; }
.shop-search-row { display: flex; }
.shop-search-input {
  flex: 1; min-width: 0; padding: 0.6rem 0.8rem; font-size: 1rem;
  border: 1px solid var(--color-muted); border-right: 0;
  border-radius: 8px 0 0 8px;
  background: var(--color-background); color: var(--color-text);
}
.shop-search-go {
  display: flex; align-items: center; justify-content: center;
  padding: 0 0.9rem; cursor: pointer;
  border: 1px solid var(--color-muted); border-radius: 0 8px 8px 0;
  background: var(--color-primary); color: #fff;
}
.shop-search-go svg { width: 1.15rem; height: 1.15rem; }
.shop-body { display: grid; grid-template-columns: 220px 1fr; gap: 1.5rem; align-items: start; }

.facet-sidebar { display: flex; flex-direction: column; gap: 1rem; }
.facet-group { background: var(--color-subtle); border-radius: 8px; padding: 0.6rem 0.8rem; }
.facet-dim {
  margin: 0 0 0.4rem; font-size: 0.78rem; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--color-muted);
}
.facet-list { list-style: none; margin: 0; padding: 0; }
.facet-node .facet-list { padding-left: 0.9rem; }
/* baseline i.p.v. center: bij een label dat over meer regels loopt horen vinkje en
   telgetal op de EERSTE tekstregel te staan, niet op de middelste. */
.facet-item { display: flex; align-items: baseline; gap: 0.4rem; padding: 0.15rem 0; cursor: pointer; font-size: 0.9rem; }
.facet-item .facet-label { flex: 1; }
.facet-count { color: var(--color-muted); font-size: 0.8rem; }
.facet-empty { color: var(--color-muted); font-size: 0.9rem; }

.results-main { min-width: 0; }
.results-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 0.75rem; }
.result-count { color: var(--color-muted); font-size: 0.9rem; }
.sort-wrap { display: flex; align-items: center; gap: 0.4rem; }
.sort-wrap label { font-size: 0.85rem; color: var(--color-muted); }
.shop-sort {
  padding: 0.4rem 0.5rem; border: 1px solid var(--color-muted); border-radius: 6px;
  background: var(--color-background); color: var(--color-text);
}

.filter-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.25rem 0.6rem; background: var(--color-subtle);
  border-radius: 999px; font-size: 0.85rem;
}
.chip-x { border: 0; background: none; cursor: pointer; font-size: 1rem; line-height: 1; color: var(--color-muted); padding: 0; }
.chip-x:hover { color: var(--color-text); }
.chip-clear { border: 0; background: none; cursor: pointer; color: var(--color-primary); font-size: 0.85rem; text-decoration: underline; }

.shop-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; }
.shop-grid .shop-status { grid-column: 1 / -1; }
.shop-status { padding: 2rem 0; text-align: center; color: var(--color-muted); }

.product-card {
  display: flex; flex-direction: column; overflow: hidden;
  border: 1px solid var(--color-line); border-radius: 10px;
  background: var(--color-background);
}
/* Een boekcover snijd je nooit bij en je letterboxt hem niet: de KOLOM bepaalt de
   breedte, de cover bepaalt zijn eigen hoogte. Geen vaste hoogte op de box en geen
   achtergrond — de wrapper sluit strak om de afbeelding. De width/height-attributen
   op de <img> (zie cover.js) geven de browser de ratio vóór de laadslag, dus de grid
   verspringt niet. Covers met een afwijkende ratio worden vanzelf iets hoger/lager;
   de kaart-body schuift mee, de kaartbreedte niet. */
.card-cover-wrap { display: block; overflow: hidden; }
.card-cover { display: block; width: 100%; height: auto; }
/* Placeholder heeft geen afbeelding om de hoogte aan te ontlenen: die zet zijn eigen
   ratio (~de coververhouding) en houdt het grijze vlak, anders verspringt de grid op
   missende covers. */
.card-cover--ph {
  display: flex; width: 100%; aspect-ratio: 17 / 24;
  background: var(--color-subtle); color: var(--color-muted);
  align-items: center; justify-content: center;
}
.card-cover--ph svg { width: 38%; height: 38%; opacity: 0.5; }
.card-body { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.7rem; flex: 1; }
/* Het badge-blok van de productkaart draagt TWEE soorten badge: vorm (E-book / Gedrukt)
   en levering (Direct / "Leverbaar (enkele dagen)"). Die stonden op één wrappende rij, dus
   of de levering onder de vorm kwam hing af van hoe breed het label toevallig was. Nu is
   .card-badges een KOLOM van regels: de vorm-badges zitten samen in .card-badges-vorm (één
   regel, onderling naast elkaar), de levering-badge is een eigen kind en staat daarmee
   altijd op de regel eronder — ook als hij zou passen.
   - align-items: flex-start, want een kolom-flexkind rekt anders tot de volle kaartbreedte
     en dan wordt het grijze badge-vlak een balk. De badges zelf blijven ongemoeid.
   - de gap is dezelfde 0,3rem als voorheen, en een flex-gap valt alleen TUSSEN bestaande
     kinderen: een kaart zonder levering-badge krijgt dus geen lege regel erbij.
   - .card-badges-vorm houdt flex-wrap als vangnet voor een extreem smalle kolom; in de
     grid (kolom min. 180px) passen "E-book · gratis" + "Gedrukt" naast elkaar. */
.card-badges { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; }
.card-badges-vorm { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.card-title { font-size: 0.95rem; margin: 0; line-height: 1.25; }
.card-subtitle { font-size: 0.82rem; color: var(--color-muted); margin: 0; }
.card-price { margin: 0.2rem 0 0; font-weight: 600; }
.card-price--free { color: var(--color-primary); }
.price-original { text-decoration: line-through; color: var(--color-muted); font-weight: 400; margin-right: 0.4rem; }
.price-sale { color: #b00020; }
.product-card .add-to-cart { margin-top: auto; width: 100%; text-align: center; font-size: 0.9rem; padding: 0.5rem; }
.card-error { margin: 0.3rem 0 0; font-size: 0.8rem; color: #b00020; }
.card-success { margin: 0.3rem 0 0; font-size: 0.8rem; color: #1a7f37; }

.badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 4px; font-size: 0.72rem; font-weight: 600; }
.badge-vorm { background: var(--color-subtle); color: var(--color-muted); }
/* Vorm-badges (Gedrukt / E-book / "E-book · gratis") zijn klikbare deeplinks naar de
   detailpagina met die vorm voorgeselecteerd. Geen link-underline/linkkleur; subtiele
   hover-lift + merk-tint als affordance. Alleen de <a>-variant beweegt — de defensieve
   <span>-fallback (vorm zonder slug) blijft statisch. */
a.badge-vorm {
  text-decoration: none;
  color: var(--color-muted);
  transition: transform 120ms ease, background-color 120ms ease;
}
a.badge-vorm:hover, a.badge-vorm:visited, a.badge-vorm:focus {
  text-decoration: none;
  color: var(--color-muted);
}
a.badge-vorm:hover { transform: translateY(-1px); background: var(--color-tint); }
.badge-sale { background: #b00020; color: #fff; }
.badge-avail { background: var(--color-subtle); color: var(--color-muted); }
.badge-avail--available, .badge-avail--digital { color: #1a7f37; }
.badge-avail--sold_out { color: #b00020; }
.badge-avail--pre_order { color: #9a6700; }
/* Docentexemplaar-badge in de cart-lade/kassa: merk-tint + primary, hetzelfde
   token-paar als de positieve statuslabels — geen hardcoded kleuren. */
.badge-teacher { background: var(--color-tint); color: var(--color-primary); }

.pager { display: flex; flex-wrap: wrap; gap: 0.3rem; justify-content: center; margin-top: 1.25rem; }
.pager button {
  min-width: 2rem; padding: 0.35rem 0.5rem; cursor: pointer;
  border: 1px solid var(--color-muted); border-radius: 6px;
  background: var(--color-background); color: var(--color-text);
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.pager button.is-current { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.pager button[disabled] { opacity: 0.4; cursor: default; }
.pager-gap { padding: 0.35rem 0.25rem; color: var(--color-muted); }

/* v23.46 — `text-decoration: none` ERBIJ, EN IN DIT BLOK (niet als losse regel verderop).
   Deze knop is een <a> op zeven plekken in de student-frontend, allemaal binnen .app-content,
   en viel daardoor terug op de prose-basislaag bovenin dit bestand
   (`:where(.app-content) :where(a) { text-decoration: underline }`, specificiteit nul). Het
   resultaat was een omlijnde knop MET een streep onder z'n label: een rand, een radius, een
   vulling én een onderstreping. .pay-btn had dat probleem niet — die zet `none` al in z'n
   eigen blok. Precies díe asymmetrie is wat hier wordt weggenomen.
   ⚠ DE DECLARATIE HOORT HIER EN NIET 800 REGELS VERDEROP. De fout was dat iemand die dit
   blok leest niet kon zien dat de knop onderstreept zou zijn; een reparatie op een andere
   plek in het bestand houdt dat probleem in stand. Zo staat het bij .pay-btn ook.
   ⚠ GEEN ONDERSTREPING-BIJ-HOVER. v23.45 zette die wél, gescoopt op .empty-state, omdat de
   knop daar anders "niets" leek te krijgen. Bij nader inzien geeft de gedeelde
   .btn-secondary:hover-regel hieronder al drie aanwijzingen tegelijk — randkleur, vulling én
   tekstkleur naar de merkkleur, plus een lichte lift — en dat is meer dan een streep. Een
   hover-streep hier zou bovendien een NIEUWE asymmetrie maken in de plaats van de oude: de
   primaire .pay-btn onderstreept bij hover namelijk niet. Een knop is een knop, in rust en
   bij hover. De .empty-state-uitzondering van v23.45 is hiermee vervallen en verwijderd. */
.btn-secondary {
  padding: 0.5rem 1rem; cursor: pointer; margin-top: 0.75rem;
  border: 1px solid var(--color-muted); border-radius: 6px;
  background: var(--color-background); color: var(--color-text);
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
              transform 0.15s ease;
}

/* Hover voor de OMLIJNDE knoppen (.btn-secondary, pager, qty-stepper). Ze deelden al
   exact dezelfde basis — rand in --color-muted, achtergrond in --color-background — dus
   ook één hover: rand + tekst naar de merkkleur, met een lichte tint als vulling. Ze
   blijven bewust outline (geen vol vlak): dat is voorbehouden aan de primaire .pay-btn,
   anders verdwijnt de rangorde tussen "Lezen" en "Naar de webshop".
   .is-current in de pager is al vol gevuld en wordt uitgezonderd — die zou anders bij
   hover juist lichter worden i.p.v. donkerder. */
.btn-secondary:not([disabled]):hover,
.pager button:not([disabled]):not(.is-current):hover,
.qty-stepper button:not([disabled]):hover {
  border-color: var(--color-primary);
  background: var(--color-tint);
  color: var(--color-primary);
}
/* Alleen de losse actie-knop lift mee; de pager- en stepper-knopjes zitten in een rij
   en zouden daar onrustig van gaan wippen. */
.btn-secondary:not([disabled]):hover { transform: translateY(-1px); }
.btn-secondary:not([disabled]):active { transform: translateY(0); }

@media (max-width: 720px) {
  .shop-body { grid-template-columns: 1fr; }
}

/* aantal-stepper per cart-regel: regel wordt kolom (hoofdrij + faal-slot) */
.cart-row--editable { flex-direction: column; align-items: stretch; gap: 0.3rem; }
/* Twee-regelige rij voor de smalle lade (~380px): regel 1 = titel over de volle breedte
   (wrapt vrij), regel 2 = stepper links · prijs rechts · × uiterst rechts. Grid met vaste
   kolommen lijnt stepper/prijs/× van élke rij op dezelfde x uit; doordat de titel z'n
   eigen rij heeft, groeit alleen die rij bij een lange titel — regel 2 springt niet.
   Grid op .cart-row-main omdat de vier cellen daar de directe kinderen van zijn. */
.cart-row-main {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "name name name" "stepper price remove";
  align-items: center; column-gap: 0.75rem; row-gap: 0.4rem;
}
/* Titel één stap kleiner: de lade is een controle-surface, de titels zijn lang
   ({vak} ({traject}) – {soort}) en wrappen anders naar 2-3 regels. line-height 1.3 houdt
   een gewrapte titel netjes. GEEN afkappen/ellipsis: het onderscheidende deel staat
   achteraan ("– Werkboek" vs "– Antwoorden"), dus de titel wrapt vrij en blijft volledig. */
.cart-row-name { grid-area: name; min-width: 0; font-size: 0.9rem; line-height: 1.3; }
/* v23.276 — het vorm-label achter de titel, in dezelfde vetdruk als kassa scherm 1.
   ⚠ DEZE REGEL IS NODIG OMDAT DE LADE BUITEN .app-content HANGT. De prose-basislaag zet
   vetdruk op 600 via `:where(.app-content) :where(strong, b)` (regel 490), en cart.js hangt
   z'n lade bewust aan <body> (layout/cart.js:81) zodat een routewissel hem niet meesloopt —
   dus die selector raakt hem niet en de <strong> zou op de UA-default (700) staan. Naast
   elkaar op één scherm, wat bij een open lade op de kassa letterlijk gebeurt, is dat een
   zichtbaar verschil in hetzelfde woord. Alleen de vetdruk wordt gelijkgetrokken; de rest
   van de prose-laag hoort de lade niet te raken, dus geen .app-content op de lade zelf.
   `.cart-row-name` wordt op precies één plek uitgezonden (layout/cart.js), dus deze regel
   raakt niets anders. */
.cart-row-name strong { font-weight: 600; }
/* v23.343 — DE DRUKREGEL ONDER DE TITEL IN DE LADE:
       Editie 2025/2026 - 1e druk - 9789083566986
   Eigen regel (display:block binnen de titel-span) zodat hij ONDER de titel valt en niet
   achter het vorm-label aanschuift; gedempt en kleiner, want het is bijschrift en geen
   tweede titel. Zelfde demping en maat als de tegenhanger op de kassa (.line-meta), zodat
   een open lade naast het kassa-overzicht niet twee verschillende bijschrift-stijlen laat
   zien.

   NAGEBOUWD EN NIET GEDEELD, net als de rest van deze lade: de kassa bouwt een
   tabelcel (.line-desc) en deze lade een grid-cel, dus de klassen kunnen niet dezelfde
   zijn. Wat gedeeld MOET blijven is de bron van de tekst, en die is gedeeld - beide lezen
   dezelfde payload-velden.

   DE HOOFDLETTER STAAT HIER EN NIET IN DE DATA. `pricing.edition_label` levert bewust
   kleine letters, want datzelfde veld belandt midden in een factuurregel; op een eigen
   regel hoort de zin met een hoofdletter te beginnen. Zelfde oplossing als v23.79 voor
   .line-meta, en om dezelfde reden op een BLOK-element: ::first-letter slaat op een
   inline box niet aan.

   GEEN nowrap op het geheel. Het ISBN alleen zou niet mogen afbreken, maar dat is hier
   het laatste stuk van een regel die toch al bij de scheidingstekens mag afbreken; een
   nowrap over de hele zin zou de lade op een smal scherm horizontaal laten scrollen. */
.cart-row-name .cart-line-meta {
  display: block; color: var(--color-muted); font-size: 0.85em; font-weight: 400;
}
.cart-row-name .cart-line-meta::first-letter { text-transform: uppercase; }
.cart-row-main .qty-stepper { grid-area: stepper; justify-self: start; }
.cart-row-price { grid-area: price; text-align: right; }
/* Geval C (alles gratis docentexemplaar): badge LINKS, "Gratis" RECHTS in de prijs-cel —
   zelfde reden-links/bedrag-rechts-idioom als de docent-subrijen (geval D). */
.cart-row-price--badge {
  display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
}
.cart-row-main .remove { grid-area: remove; }
/* Docent-split (geval D): twee weergave-subrijen onder de hoofdrij, label links ·
   bedrag rechts. Puur weergave — de stepper/× blijft op .cart-row-main. Gedempt en
   iets kleiner zodat de hoofdrij de controle-surface blijft. */
.cart-view .cart-row-split { display: flex; flex-direction: column; gap: 0.15rem; }
.cart-view .cart-row-splitline {
  display: flex; justify-content: space-between; gap: 0.75rem;
  font-size: 0.82rem; color: var(--color-muted);
}
.cart-view .cart-row-splitvalue { text-align: right; white-space: nowrap; }
.qty-stepper { display: inline-flex; align-items: center; gap: 0.4rem; flex: 0 0 auto; }
.qty-stepper button {
  min-width: 1.8rem; padding: 0.2rem 0.5rem; line-height: 1; cursor: pointer;
  border: 1px solid var(--color-muted); border-radius: 6px;
  background: var(--color-background); color: var(--color-text); font-size: 1rem;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.qty-stepper button[disabled] { opacity: 0.5; cursor: default; }
.qty-value { min-width: 1.4rem; text-align: center; }
.cart-row .remove {
  border: 0; background: none; cursor: pointer; padding: 0 0.2rem;
  font-size: 1.1rem; line-height: 1; color: var(--color-muted);
}
.cart-row .remove:hover { color: #b00020; }
.cart-row-error { margin: 0; font-size: 0.8rem; color: #b00020; }

/* Account — abonnementen-tab (M1-15c) */
/* v23.44 — .sub-card, .sub-card .sub-info EN .sub-card .sub-until STONDEN HIER EN ZIJN WEG.
   De abonnementsrij draagt sinds v23.44 het gedeelde kaart-recept van .ticket-row > a en de
   markup die deze drie classes zette bestaat niet meer (gemeten: ze kwamen alleen voor in
   account.js → loadSubscriptions en nergens anders in de repo). Dat is dode css, geen
   reservevorm, dus hij gaat eruit in plaats van te blijven staan als tweede abonnement-stijl
   waar niemand meer bij kan.
   Het bijschrift heet nu .sub-renewal en staat onderin dit bestand. Dat is niet alleen een
   hernoeming: deze regel droeg een LOSSE grijs-hex (een grijs dat niet met de skin meebeweegt,
   terwijl elk ander bijschrift op var(--color-muted) staat) en een LOSSE 0.875rem (een maat
   die van geen van de andere bijschriften was — die staan op --font-size-meta). Die hex kwam
   in dit bestand alleen hier voor en is er met dit blok dus helemaal uit.
   ⚠ GEEN HEX IN DIT COMMENTAAR. De kleurtoets in account-berichten.test.mjs leest de tekst
   van dit bestand vanaf het berichten-blok ONGEFILTERD, commentaar incluis — een hex hier
   noemen laat die test omvallen alsof er een nieuwe kleur is ingevoerd.
   ⚠ .sub-cancel BLIJFT: dat is de Opzeggen-knop, en die staat nog in de nieuwe rij. */
.sub-cancel {
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--c-border, #ccc);
  border-radius: 6px;
  background: #fff;
  cursor: pointer;
}
.sub-cancel:disabled { opacity: 0.6; cursor: default; }

/* --- boek-detail 6b: /boek/{slug} --- */
/* 400px: de cover is op 560px hoog ~396px breed — smaller en de kolom knelt hem af. */
.book-detail { display: grid; grid-template-columns: minmax(0, 400px) 1fr; gap: 1.5rem; align-items: start; }
.book-detail--loading, .book-detail--empty, .book-detail--error { display: block; }
.book-status { padding: 2rem 0; text-align: center; color: var(--color-muted); }
/* Anders dan de shop-kaart: hier houdt de detail-cover een vaste hoogte in een kolom die
   breed genoeg is (400px vs ~396px cover), dus letterboxt hij in de praktijk niet. */
.book-cover-wrap {
  height: 560px; background: var(--color-subtle); border-radius: 10px;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.book-cover-wrap .card-cover { height: 100%; width: auto; max-width: 100%; object-fit: contain; display: block; margin-inline: auto; }
.book-cover-wrap .card-cover--ph { width: 100%; height: 100%; aspect-ratio: auto; }
.book-main-col { min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; }
/* Terug-naar-webshop (?from): bewust een TEKSTLINK, geen knop — een tweede omkaderd
   element boven de titel zou met de In-winkelwagen-CTA concurreren. Gedempt (--color-muted)
   en kleiner dan de body, dus het leest als navigatie-affordance, niet als actie.
   De <p> staat in de flex-kolom (gap 0.75rem): eigen marges op 0 en alleen een kleine
   margin-bottom erbij, anders stapelt hij bovenop de gap en zakt de titel weg. */
.book-back { margin: 0 0 0.25rem; }
.book-back-link {
  display: inline-block; cursor: pointer;
  color: var(--color-muted); font-size: 0.9rem; text-decoration: none;
}
.book-back-link:hover { text-decoration: underline; }
.book-title { margin: 0; line-height: 1.2; }
/* GEPARKEERD: de ondertitel staat als uitprobeersel nu als eerste rij in het
   keuze-blok (zie renderKeuzeBlok), niet meer als los <p> onder de H1. Deze regel
   blijft staan zodat terugdraaien alleen een JS-wijziging is. Weghalen zodra het
   uitprobeersel definitief is. */
.book-subtitle { margin: 0; color: var(--color-muted); font-size: 0.95rem; }

/* Keuze-tabel (Beschikbaar/Druk/Vorm) en spec-tabel (Auteur/ISBN/…) delen ÉÉN grid,
   zodat de labelkolom zich instelt op het langste label van BEIDE en de waarden van
   allebei op dezelfde verticale lijn staan. Prijs+knop staan er in de opmaak tussenin
   en spannen daarom beide kolommen.
   De twee <dl>'s en hun rij-wrappers staan op display: contents; hun dt/dd zijn
   daardoor directe items van dít grid. Kolombreedte blijft max-content — volgt de
   inhoud, geen hardgezet getal dat op smal scherm knelt. */
.book-specs { display: grid; grid-template-columns: max-content 1fr; column-gap: 1rem; row-gap: 0.35rem; }
.book-specs .book-keuze,
.book-specs .book-meta,
.book-specs .keuze-row,
.book-specs .meta-row { display: contents; }
.book-specs dt { color: var(--color-muted); font-size: 0.9rem; }
/* min-width: 0 + overflow-wrap: een lange waarde (reeksnaam) mag krimpen en afbreken
   i.p.v. de kolom op te rekken en het grid te laten overlopen. */
.book-specs dd { margin: 0; font-size: 0.9rem; min-width: 0; overflow-wrap: break-word; }
/* Keuze-rijen dragen besturingselementen (select, pillen): die lijnen op de labelregel
   uit en krijgen iets lucht. Spec-rijen zijn platte tekst en lijnen bovenaan uit, zodat
   een meerregelige waarde (auteurs) niet los van z'n label zweeft. */
.book-specs .keuze-row > * { align-self: center; padding-block: 0.1rem; }
.book-specs .meta-row > * { align-self: start; }
.book-koop { grid-column: 1 / -1; margin: 0.6rem 0; display: flex; flex-direction: column; gap: 0.3rem; }

/* ===========================================================================
   v23.303 — DE BOEKHANDEL-KANT VAN DE PRODUCTPAGINA (deel A). STAP 3 VAN 4.
   ===========================================================================
   ⚠⚠ ELKE SELECTOR HIERONDER RAAKT UITSLUITEND MARKUP DIE ALLEEN VOOR EEN INGELOGDE
   BOEKHANDEL BESTAAT. Voor iedereen anders tekent book.js deze elementen niet
   (`renderBookshopPrices`/`renderBookshopChoices` geven "" zonder `product.bookshop`), dus
   er is geen regel hier die de bestaande productpagina kan raken. Er wordt bewust NIETS
   aan .book-koop, .card-price of .book-buy-wrap gewijzigd.

   De drie bedragen: eigen mini-tabel in dezelfde label-waarde-vorm als .book-meta, maar met
   een EIGEN grid — .book-koop is een flex-kolom en niet het .book-specs-grid, dus
   `display: contents` (het recept van .keuze-row/.meta-row) heeft hier geen grid om in te
   vallen. Gedempt en één stap kleiner: het zijn afgeleiden van de prijs erboven, niet de
   prijs zelf. */
.book-bookshop-prices {
  margin: 0.1rem 0 0.2rem; display: grid; grid-template-columns: max-content 1fr;
  column-gap: 1rem; row-gap: 0.15rem; font-size: 0.9rem;
}
.book-bookshop-prices .bookshop-price-row { display: contents; }
.book-bookshop-prices dt { color: var(--color-muted); }
.book-bookshop-prices dd { margin: 0; }

/* Aantal + leveringskeuze op één rij, wrappend op smal scherm. */
.book-bookshop-choices {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem;
  margin: 0.2rem 0;
}
.bookshop-qty-label { color: var(--color-muted); font-size: 0.9rem; }
/* Vaste, korte breedte: het veld draagt een aantal (1-3 cijfers) en hoort niet als een
   tekstveld over de kolom te lopen. */
.bookshop-qty {
  width: 5rem; padding: 0.3rem 0.5rem; border-radius: 6px;
  font-size: 0.9rem; font-family: inherit;
  border: 1px solid var(--color-muted); color: var(--color-text);
  background: var(--color-background);
}
.bookshop-qty:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.bookshop-delivery { display: flex; flex-wrap: wrap; align-items: center; gap: 0.15rem 0.7rem; }
.bookshop-delivery-title { color: var(--color-muted); font-size: 0.9rem; }
/* Het hele label is het klikdoel (radio + woord), zodat de trefzone niet het bolletje is. */
.bookshop-delivery-opt {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.9rem; cursor: pointer;
}

/* Editie-kiezer (v22.79): dropdown i.p.v. pillen — de labels zijn lang
   ("4e druk (2024/2025) (voorheen: …)") en horen niet als pil-rij over meerdere
   regels te breken. De vorm-toggle blijft wél pillen (twee korte opties).
   `width: auto` + max-width: de select groeit mee met het langste label maar loopt
   op smalle schermen niet buiten de kolom. */
.editie-select {
  width: auto; max-width: 100%; padding: 0.3rem 0.6rem; border-radius: 6px;
  font-size: 0.9rem; font-family: inherit;
  border: 1px solid var(--color-muted); color: var(--color-text); background: var(--color-background);
}
.editie-select:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }

.book-vorm { display: inline-flex; flex-wrap: wrap; gap: 0.4rem; }
.vorm-opt {
  padding: 0.3rem 0.8rem; border-radius: 999px; font-size: 0.9rem; text-decoration: none;
  border: 1px solid var(--color-muted); color: var(--color-text); background: var(--color-background);
}
.vorm-opt--active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.vorm-opt--disabled { opacity: 0.5; cursor: default; }

.book-detail .card-price { font-size: 1.3rem; }
/* De koopknop is de primaire actie en vult de breedte van het blok, maar met een
   max-width: de rechterkolom is op een breed scherm een paar honderd px breed en een
   CTA die dát helemaal overspant leest niet meer als knop. */
.book-buy-wrap { display: flex; flex-direction: column; align-items: stretch; gap: 0.3rem; max-width: 22rem; }
.book-buy { margin-top: 0; width: 100%; }
.book-success { margin: 0; font-size: 0.85rem; color: #1a7f37; }
.book-error { margin: 0; font-size: 0.85rem; color: #b00020; }

/* .book-meta heeft geen eigen grid meer: hij deelt het grid van .book-specs
   hierboven (display: contents), anders zouden de twee tabellen elk hun eigen
   labelbreedte berekenen en niet uitlijnen. */

.book-subjects { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.subject-tag {
  padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.82rem; text-decoration: none;
  background: var(--color-subtle); color: var(--color-text);
}
a.subject-tag:hover { color: var(--color-primary); }

.book-description { line-height: 1.5; }
.book-sample { margin: 0; }
.sample-link { color: var(--color-primary); }

@media (max-width: 640px) {
  .book-detail { grid-template-columns: 1fr; }
  .book-cover-col { max-width: 240px; }
  .book-cover-wrap { height: 420px; }
}

/* Smal scherm: label BOVEN waarde i.p.v. ernaast. De max-content-labelkolom zou op
   ~360px een kwart van de regel opeten, waardoor de dropdown en lange waarden
   ("Serie Vastgoed voor HBO en beroepspraktijk") in een te smalle restkolom knellen.
   Gestapeld houdt elke waarde de volle regelbreedte. Het gedeelde grid blijft staan —
   met één kolom is uitlijnen tussen de tabellen vanzelf een non-issue. */
@media (max-width: 30rem) {
  .book-specs { grid-template-columns: 1fr; row-gap: 0; }
  .book-specs .keuze-row > *, .book-specs .meta-row > * { align-self: start; }
  .book-specs dt { margin-top: 0.5rem; }
  .book-specs .editie-select { width: 100%; }
}

/* --- shop 6c: klikbare grid-kaart + suggest-dropdown --- */
.card-cover-link { display: block; }
.card-title a { color: inherit; text-decoration: none; }
.card-title a:hover { text-decoration: underline; }

.suggest-dropdown {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 40;
  background: var(--color-background);
  border: 1px solid var(--color-muted); border-top: 0;
  border-radius: 0 0 8px 8px; overflow: hidden;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.suggest-dropdown[hidden] { display: none; }
.suggest-item {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 0.7rem;
  text-decoration: none; color: var(--color-text);
}
.suggest-item:hover { background: var(--color-subtle); }
/* v23.341: 40x56 i.p.v. 32x42 — de rij draagt sinds deze versie twee tekstregels
   (titel + jaar, prijs + druk + vorm) en een knop; op 42px hoog stond de cover
   optisch te laag naast die twee regels. */
.suggest-cover-wrap {
  flex: 0 0 auto; width: 40px; height: 56px; overflow: hidden;
  background: var(--color-subtle); border-radius: 3px;
}
.suggest-cover { width: 100%; height: 100%; object-fit: cover; }
.suggest-cover--ph {
  display: flex; width: 100%; height: 100%; align-items: center; justify-content: center;
  color: var(--color-muted);
}
.suggest-cover--ph svg { width: 55%; height: 55%; opacity: 0.5; }
.suggest-text { display: flex; flex-direction: column; min-width: 0; }
.suggest-title { font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.suggest-price { font-size: 0.8rem; color: var(--color-muted); }

/* kop = gedempt kopje, niet klikbaar-ogend (spiegelt .facet-dim) */
.suggest-header {
  padding: 0.45rem 0.7rem 0.3rem; font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-muted);
}
/* afsluitrij = flex, gescheiden van de product-items (bovenrand): links de
   term-zoek, rechts de inklap-knop */
.suggest-foot { display: flex; align-items: stretch; border-top: 1px solid var(--color-line); }
.suggest-all {
  flex: 1 1 auto; text-align: left; cursor: pointer;
  padding: 0.5rem 0.7rem; font-size: 0.85rem;
  border: 0; background: var(--color-background); color: var(--color-accent);
}
.suggest-all:hover { background: var(--color-subtle); }
/* inklap-knop: rechts uitgelijnd, prominent (accent-kleur), klikbaar-ogend */
.suggest-collapse {
  flex: 0 0 auto; margin-left: auto; cursor: pointer;
  padding: 0.5rem 1rem; font-size: 1.15rem; line-height: 1;
  border: 0; background: var(--color-background); color: var(--color-accent);
}
.suggest-collapse:hover { background: var(--color-subtle); color: var(--color-primary); }

/* --- v23.341: +-knop, nul-hit-rij en toast in de suggest-dropdown --- */

/* +-knop: rechts in de rij (margin-left:auto duwt 'm tegen de rand), vierkant en
   groot genoeg om met een duim te raken. Hij staat BINNEN het rij-anker — de
   click-handler in shop.js vangt hem af, dit is puur de vorm. */
.suggest-add {
  flex: 0 0 auto; margin-left: auto;
  width: 32px; height: 32px; line-height: 1;
  background: var(--color-primary); color: #fff;
  border: 0; border-radius: 4px; cursor: pointer; font-size: 1.25rem;
}
.suggest-add:disabled { opacity: 0.55; cursor: default; }

/* nul-hit-rij: geen cover, dus geen flex-uitlijning met de rijen erboven nodig —
   één gedempte, cursieve regel met de catalogus-link erachter. */
.suggest-empty {
  padding: 0.55rem 0.7rem; font-size: 0.9rem;
  font-style: italic; color: var(--color-muted);
}
.suggest-empty a { color: var(--color-accent); white-space: nowrap; }

/* Toast: vaste positie rechtsboven, BOVEN de cart-lade (z-index 91) — de lade kan
   openstaan terwijl de bezoeker in de dropdown doorklikt, en dan moet de melding
   zichtbaar blijven. Zonder --visible is hij onzichtbaar én niet-aanklikbaar
   (pointer-events), zodat het lege element geen klikken van de pagina afvangt. */
.mpd-toast {
  position: fixed; top: 1rem; right: 1rem; z-index: 100;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: 0.75rem 1rem; border-radius: 6px;
  background: var(--color-primary); color: #fff;
  font-size: 0.9rem; line-height: 1.35;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);
  opacity: 0; transform: translateY(-0.4rem); pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease;
}
.mpd-toast--visible { opacity: 1; transform: translateY(0); }

/* --- add-to-cart / koop-knop hover-feedback (6c) — CSS-only, beide plekken --- */
.add-to-cart,
.book-buy {
  transition: background 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
.add-to-cart:not(:disabled):hover,
.book-buy:not(:disabled):hover {
  background: var(--color-accent); /* tint donkerder dan --color-primary */
  transform: translateY(-1px);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
.add-to-cart:not(:disabled):active,
.book-buy:not(:disabled):active {
  transform: translateY(0);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}

/* --- globale winkelwagen: nav-icoon + lade (layout/cart.js) --- */
/* Icoon + teller + chevron in de nav-balk, ná "Mijn account". De lade zelf hangt aan
   <body> (buiten #app-content) en hergebruikt de BESTAANDE cart-styling: .cart-row,
   .cart-row--editable, .qty-stepper, .cart-total, .pay-btn — daar is niets aan veranderd. */
.nav-cart-btn {
  /* Ruimere verticale padding = een fatsoenlijk tapbaar vlak op mobiel. De nav wordt
     hierdoor een paar px hoger (bewust: de gelijk-blijven-eis uit 4d99d23 is losgelaten).
     De uitlijning met de tekstlinks komt van align-items:center op .app-nav ul, niet van
     deze padding. */
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: none; border: 0; padding: 0.3rem 0.3rem; cursor: pointer;
  color: var(--color-text); font: inherit;
}
.nav-cart-btn:hover { color: var(--color-primary); }
.nav-cart-icon svg { width: 1.5rem; height: 1.5rem; display: block; }
.nav-cart-count {
  min-width: 1.15rem; padding: 0 0.3rem; border-radius: 999px;
  background: var(--color-primary); color: #fff;
  font-size: 0.75rem; font-weight: 600; line-height: 1.15rem; text-align: center;
}
/* Leeg mandje: de teller blijft staan (geen springende balk) maar wordt gedempt. */
.nav-cart-count--empty { background: var(--color-muted); }
/* Totaalbedrag naast de teller — zelfde waarde als de "Totaal"-regel in de lade. */
.nav-cart-total { font-size: 0.9rem; font-weight: 600; color: var(--color-text); white-space: nowrap; }
.nav-cart-chevron { font-size: 0.72rem; color: var(--color-muted); }

#cart-drawer-root[hidden] { display: none; }
.cart-drawer-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.35); z-index: 90;
}
.cart-drawer {
  position: fixed; top: 0; right: 0; z-index: 91;
  width: min(420px, 100vw);
  /* 100vh eerst als fallback; 100dvh = dynamische viewport, zodat de vaste voet niet
     achter de mobiele browserbalk valt. */
  height: 100vh; height: 100dvh;
  /* Flex-kolom: vaste kop + voet, alleen de body (.cart-view) scrolt. */
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--color-background); border-left: 1px solid var(--color-line);
  box-shadow: -2px 0 12px rgba(0, 0, 0, 0.12);
}
.cart-drawer-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.25rem 0.75rem;
}
/* Scrollende body. min-height:0 is essentieel: zonder krimpt een flex-item niet onder
   z'n inhoud en scrolt overflow-y niet (dan valt de voet buiten beeld). */
.cart-view {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 0 1.25rem;
}
/* Niet-scrollende voet met de Afrekenen-knop: dekkende achtergrond + scheidingslijn
   boven (haarlijn-stijl van a12a865), zodat de knop altijd zichtbaar onderin staat. */
.cart-drawer-foot {
  flex: 0 0 auto;
  padding: 0.75rem 1.25rem 1.5rem;
  border-top: 1px solid var(--color-line);
  background: var(--color-background);
}
.cart-drawer-title { margin: 0; font-size: 1.15rem; }
.cart-drawer-close {
  background: none; border: 0; cursor: pointer;
  font-size: 1.1rem; line-height: 1; color: var(--color-muted);
}
.cart-drawer-close:hover { color: var(--color-text); }
/* De Afrekenen-knop zit nu in .cart-drawer-foot (eigen padding), dus geen eigen
   marge meer; blok-breed zodat hij de volle voet vult. */
.cart-drawer .checkout-link { display: block; margin-top: 0; text-align: center; }

/* ============================================================================
   BERICHTEN — thread + ticketlijst (v23.24)
   ============================================================================
   Tot deze versie had GEEN ENKELE van de classes die berichten.js en
   util/ticket_row.js zetten een regel in dit bestand: .msg, .msg-student,
   .msg-support, .msg-meta, .msg-body, .thread, .ticket-list, .ticket-row,
   .ticket-subject, .ticket-date, .ticket-status, .ticket-flag — allemaal nul
   treffers. De twee nieuwste schermen van de site (/berichten en /berichten/{id})
   stonden dus als platte tekst mét opsommingsbolletjes op de pagina-achtergrond,
   terwijl ze functioneel juist het best gedekt zijn van allemaal.

   ENIGE UITZONDERING, en die blijft HIER ONGEMOEID: de bijlagen-groep
   (.msg-attachments / .att-*, hierboven bij de v23.18-sectie). Die is toen bewust
   klein gehouden "omdat de thread-bubbels zelf nog geen opmaak hebben" — dat klopt
   nu niet meer, maar de groep past zonder één wijziging in wat hieronder staat
   (gedempte knop, subtiel vlak, haarlijn, radius). Er is niets aan veranderd.

   GEEN TWEEDE STIJLTAAL. Alles hieronder hergebruikt patronen die al in dit
   bestand staan:
     - het kaart-patroon van .order-card (surface + haarlijn + var(--radius) +
       hover-lift met dezelfde color-mix-schaduw);
     - het badge-patroon van .order-status / .lic-badge (zelfde maten, nu via de
       tokens --font-size-badge / --pad-badge / --radius-badge);
     - de gedempte bijschrift-maat van .order-date / .line-vat
       (--font-size-meta, muted).
   Kleuren komen ALLEEN uit de bestaande tokens, dus beide schermen volgen de
   tenant-kleur uit /api/shell/skin.css. Nul nieuwe kleurwaarden. De vijf maat-
   tokens die hiervoor nodig waren staan in :root bij de ontwerptaal (v23.24).
   -------------------------------------------------------------------------- */

/* --- de thread ------------------------------------------------------------- */
.thread { margin: var(--space-lg) 0; }

/* DE AFSTAND TUSSEN TWEE BERICHTEN, en verder niets — de vorm van het bericht zelf staat
   in de v23.29-sectie onderaan dit bestand.

   Deze ene regel heeft alle drie de vormen van dit scherm meegemaakt en is telkens de
   drager van de groepering geweest: v23.24 zette er een inspringing bij een afzenderstreep
   neer, v23.28 maakte hem --space-xl (2rem) omdat de witruimte toen het enige was dat nog
   groepeerde. Sinds v23.29 doet de KAART dat, en dan hoort de tussenruimte juist krap te
   zijn: twee kaarten met 2rem ertussen vallen uit elkaar in losse blokken in plaats van
   samen een gesprek te vormen. --space-sm is dezelfde maat die de proef gebruikte en
   dezelfde die de cockpit-thread aanhoudt. */
.msg { margin-top: var(--space-sm); }

/* Afzender + tijd op één regel bovenaan, LINKS en gedempt: dit is BIJSCHRIFT bij de tekst
   eronder, niet de tekst zelf — zelfde maat en kleur als .order-date en .line-vat.

   v23.28 — was een flexregel die de tijd met margin-left:auto naar de rechterrand duwde.
   Op een kolom van 36rem zweefde die tijd bij een bericht van twee woorden een halve regel
   van de afzendernaam af; er was geen verband meer te zien tussen de twee. Nu staan ze
   naast elkaar als één zinnetje ("Klantenservice · 14:13"), dus geen flex, geen gap en
   geen wrap: de browser zet zelf een spatie tussen de inline-elementen. */
.msg-meta {
  margin-bottom: var(--space-xs);
  color: var(--color-muted); font-size: var(--font-size-meta);
}
.msg-time { white-space: nowrap; }

/* Een opgemaakt antwoord draagt z'n eigen <p>/<h3> uit de editor, en die krijgen van de
   prose-basislaag een ondermarge (`p { margin: 0 0 1em }`). De LAATSTE zou daarmee een gat
   tussen het bericht en de bijlagenknop trekken, de EERSTE zou de tekst van de meta-regel
   wegduwen. Alleen de buitenste kinderen; alles daarbinnen houdt z'n normale ritme. */
.msg-body > :first-child { margin-top: 0; }
.msg-body > :last-child { margin-bottom: 0; }

/* --- de ticketlijst -------------------------------------------------------- */
/* Bolletjes weg: dit is een lijst KAARTEN, geen opsomming. Twee dingen van de
   prose-basislaag moeten daarvoor uit — de `padding-left: 1.4em` op elke <ul> in
   #app-content én de accent-gekleurde ::marker. list-style:none haalt de tweede weg,
   padding:0 de eerste. */
.ticket-list { list-style: none; margin: var(--space-lg) 0; padding: 0; }
.ticket-row { margin: 0 0 var(--space-sm); }

/* DE HELE RIJ IS DE LINK. Het <a> uit ticket_row.js is hier een blok-flex over de volle
   breedte, dus er is geen dood gebied tussen het onderwerp en de statusbadge waar een klik
   niets doet — precies de plek waar iemand intuïtief mikt. De prose-basislaag geeft elke
   <a> in #app-content de linkkleur + onderstreping; een kaart hoort geen onderstreepte
   blauwe tekst te zijn, dus allebei terug naar de tekstkleur. */
/* ⚠ .ebook-item HANGT HIER MEE (v23.34), en dat is de reden dat deze regel een tweede
   selector heeft in plaats van een kopie verderop. De e-book-rij op /account is dezelfde
   soort rij als deze: titel + bijschrift links, één actie rechts. Ze deelt daarom het
   HELE recept — rand, radius, oppervlak, lift, randkleur — en scoopt alleen wat afwijkt
   (de eigen kolom .ebook-main en de badge, in het E-books-blok hierboven).
   De ebook-rij is een <div> en geen <a>: de actie is de Lezen-knop, niet de rij. Vandaar
   `.ebook-item` en niet `.ebook-item > a`, en vandaar :focus-within in plaats van
   :focus-visible verderop — een <div> krijgt zelf nooit focus, de knop erin wel. */
/* ⚠ .invoice-row EN .sub-row HANGEN HIER OOK MEE (v23.44), om exact dezelfde
   reden als .ebook-item hierboven. Daarmee zijn ALLE VIER de lijstschermen van /account nu
   dezelfde rij: berichten, e-books, facturen, abonnement. De factuurlijst was tot v23.44 het
   laatste <table> op /account en het abonnement een eigen .sub-card met eigen rand en eigen
   radius — twee vormen die niets deelden met de twee ernaast.
   Allebei <div> en geen <a>: de actie is de Downloaden-knop respectievelijk de Opzeggen-knop,
   niet de rij. Dus ook hier :focus-within verderop en geen :focus-visible. */
/* === DE GEDEELDE DRAGER (v23.56) ========================================================
   De vier lijstrijen hierboven en de vier PROFIELBLOKKEN (.account-card, zie het
   Profiel-blok eerder in dit bestand) tekenen hetzelfde vlak: oppervlak + haarlijn +
   radius. Die drie declaraties staan daarom hier, in EEN regel die ze allebei dragen.

   ⚠ WAAROM DE PROFIELBLOKKEN NIET HET HELE RIJ-RECEPT ERVEN, zoals .ebook-item en
   .invoice-row dat wel doen. Een lijstrij is een RIJ: `display:flex` met
   `align-items:baseline`, alles op een lijn, een actie rechts. Een profielblok is dat
   niet — het is een kop met daaronder een kolom velden en een knop. Zou .account-card
   de rij-regel erven, dan moest hij `display` en `align-items` meteen weer overschrijven,
   en dan deelt hij alleen nog de naam en niet het gedrag. Gedeeld is dus precies wat
   deelbaar IS (het vlak); de indeling houdt elk z'n eigen regel. Dat is ook wat er
   gebeurt als de radius of de lijnkleur ooit wijzigt: een plek, beide schermen.
   Geen vijfde kaartvariant erbij dus — .cat/.book, .order-card, .form-card en deze rij
   waren de vier die er al waren, en .account-card voegt er GEEN vorm aan toe. */
.ticket-row > a,
.ebook-item,
.invoice-row,
.sub-row,
.account-card {
  background: var(--color-surface);
  border: var(--line-hair) solid var(--color-line); border-radius: var(--radius);
}
.ticket-row > a,
.ebook-item,
.invoice-row,
.sub-row {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-xs) var(--space);
  padding: var(--space) var(--space-lg);
  color: var(--color-text); text-decoration: none;
  transition: border-color var(--anim-quick) ease, transform var(--anim-quick) ease,
              box-shadow var(--anim-quick) ease;
}
/* Hover: de lift + schaduw van .order-card, PLUS een merk-gekleurde rand. Die rand staat
   er met opzet naast de beweging, zodat de aanwijzing ook overblijft als de lift wegvalt. */
.ticket-row > a:hover,
.ebook-item:hover,
.invoice-row:hover,
.sub-row:hover {
  border-color: var(--color-primary);
  transform: translateY(var(--lift-y));
  box-shadow: var(--lift-shadow);
}
/* Toetsenbord: de globale `a:focus-visible`-regel (3px accent-outline) geldt hier al —
   die is NIET aangepast. Deze regel komt er bovenop zodat een rij mét focus dezelfde
   randkleur krijgt als onder de muis: de outline ligt óm de kaart, de rand markeert 'm
   ook van binnen, en hover en focus zien er dus hetzelfde uit. */
.ticket-row > a:focus-visible { border-color: var(--color-primary); }
/* De e-book-rij krijgt diezelfde markering zodra de Lezen-knop erin focus heeft: de knop
   houdt z'n eigen outline, de rij eromheen laat zien WELK boek dat is. */
.ebook-item:focus-within,
.invoice-row:focus-within,
.sub-row:focus-within { border-color: var(--color-primary); }

/* Onderwerp links en groeiend; datum, status en markering blijven rechts staan — dat volgt
   vanzelf uit de volgorde in ticket_row.js, daar is geen markup voor verplaatst.
   flex-basis 14rem zodat een lang onderwerp op een smal scherm een eigen regel pakt in
   plaats van de statusbadge van de rij af te duwen; min-width:0 laat 'm krimpen.
   ⚠ Die 14rem is BEWUST het enige losse getal in dit blok. Het is geen afstand maar een
   OMSLAGPUNT: de breedte waaronder déze ene rij afbreekt. Zelfde soort waarde als de
   min(560px, 100%) op .reply-form textarea en de min(420px, 100vw) op .cart-drawer, die
   hier ook als getal staan. Een token dat op precies één plek wordt gebruikt is een
   hernoeming en geen variabele — en zou suggereren dat er elders iets mee moet meebewegen. */
.ticket-subject { flex: 1 1 14rem; min-width: 0; font-weight: 600; }
.ticket-date { color: var(--color-muted); font-size: var(--font-size-meta); white-space: nowrap; }

/* Badge-patroon van .order-status / .lic-badge, dezelfde maten — maar nu via de tokens
   (--font-size-badge, --radius-badge) in plaats van de losse getallen die daar staan. Een
   vierde badge-vorm zou de schermen uit elkaar laten lopen; dit is geen nieuwe waarde. */
.ticket-status {
  font-size: var(--font-size-badge); font-weight: 700;
  letter-spacing: var(--track-badge); text-transform: uppercase; white-space: nowrap;
  padding: var(--pad-badge); border-radius: var(--radius-badge);
  background: var(--color-subtle); color: var(--color-muted);
}
/* Open = de MERK-kleur (tint + primary), zoals .lic-badge. Gesloten houdt het gedempte
   grijs van de basisregel hierboven: afgehandeld hoeft geen aandacht te trekken. */
.ticket-status-open { background: var(--color-tint); color: var(--color-primary); }
/* "wacht op antwoord" is een MEDEDELING, geen tweede status — dus geen tweede badge naast
   de eerste, maar een korte noot in dezelfde maat als de datum. In PRIMARY en niet in
   accent, om exact de reden die bij `.order-docs a` hierboven staat: brass op wit haalt
   3.0:1 en dat is te weinig voor tekst van deze grootte. */
.ticket-flag {
  color: var(--color-primary); font-size: var(--font-size-meta);
  font-weight: 600; white-space: nowrap;
}

/* --- datumscheiding + leesbreedte in de thread (v23.25) ---------------------
   Twee dingen die pas opvielen tóén er tijden in de thread stonden, allebei op dev in de
   browser gezien:

   (a) zeven berichten op één dag gaven zeven keer dezelfde datum onder elkaar. De dag
       staat nu één keer als scheidingsregel boven de groep (berichten.js → _threadHtml) en
       de berichten dragen alleen nog de klok.
   (b) de thread liep over de volle vensterbreedte. Met margin-left:auto op .msg-time stond
       de tijd daardoor tot een meter van de afzendernaam af en las de meta-regel niet meer
       als één regel — het probleem was dus niet de meta-regel maar de KOLOM eromheen.

   Deze regels staan LOSSTAAND onder het blok hierboven in plaats van erin: dit bestand
   wordt uitsluitend additief bijgewerkt, dus een bestaande regel uitbreiden zou 'm moeten
   herschrijven. Zelfde selectors, latere plek, geen conflicterende eigenschappen. */

/* De grens hoort op .thread en NIET op .msg-body: afzender en tijd staan buiten de body, en
   een grens dáár zou de meta-regel juist over de volle breedte laten staan — precies wat we
   wegnemen. Ook bewust niet op .account-panel: die draagt óók de andere accounttabs
   (bestellingen, facturen, adressen) en die willen hun tabelbreedte houden. */
.thread { max-width: var(--wrap-prose); }

/* De scheidingsregel: datum in het midden, haarlijn links en rechts. Flex met twee lege
   pseudo-elementen die de ruimte opeisen — zo blijft de tekst gecentreerd zonder vaste
   maten, ongeacht hoe lang de datum is. Gedempt en op bijschrift-maat: het is een
   wegwijzer, geen kop, en hij mag niet zwaarder wegen dan de berichten zelf. */
.thread-dag {
  display: flex; align-items: center; gap: var(--space);
  margin: var(--space-lg) 0 var(--space);
  color: var(--color-muted); font-size: var(--font-size-meta);
}
.thread-dag::before,
.thread-dag::after {
  content: ""; flex: 1 1 auto;
  border-top: var(--line-hair) solid var(--color-line);
}
/* De eerste scheiding staat direct onder de <h2> van het onderwerp; daar is de bovenmarge
   van .thread zelf al genoeg lucht. */
.thread-dag:first-child { margin-top: 0; }
.thread-dag span { white-space: nowrap; }

/* --- de thread als KAARTEN (v23.29) ----------------------------------------
   DE DERDE EN LAATSTE VORM VOOR DIT SCHERM, en de eerste die Jelte in de browser heeft
   goedgekeurd (als proef op /static/stijl.html, sectie 5b). Deze regels ZIJN die proef —
   de .p-*-namen zijn hier .msg-* geworden en het proefblok is verwijderd.

   WAT DE TWEE VORIGE VORMEN NIET OPLOSTEN. v23.26 gaf de helft van de berichten een
   gekleurd vlak en de andere helft een dun streepje: twee visuele talen door elkaar.
   v23.28 haalde beide weg en legde alles bij typografie en witruimte — geen omlijsting
   meer, naam onderdrukt bij een vervolg, 2rem tussen beurten tegen 0,5rem erbinnen. In de
   browser bleef het een lap tekst: zonder rand is er geen begin en geen eind van een
   bericht, en witruimte alleen leest niet als een grens.

   DE VORM DIE HET WEL DOET is die van de cockpit-ticketthread (ConversationThread.jsx),
   hier nagebouwd en niet gekopieerd — de cockpit is React met utility-classes.

     - BEIDE afzenders krijgen een kaart. Dat is het punt: de halve-halve van v23.26 was
       juist de onrust, en een kaart om álles heen geeft elk bericht dezelfde soort grens.
     - VASTE breedte 90%, geen max-width. Overgenomen uit de cockpit inclusief de reden:
       een maximum is een plafond en een kort bericht zou eronder blijven, waarmee het
       ritme alsnog stuk gaat. Nu vult elk bericht dezelfde 90%, kort of lang, en de
       overgebleven 10% maakt de links/rechts-uitlijning zichtbaar.
     - De TEKST blijft links uitgelijnd, ook in de kaart die rechts hangt. Niemand leest
       prettig rechts-uitgelijnd; het is het BLOK dat verschuift, niet de regel erin.

   ⚠ DE AS SPIEGELT T.O.V. DE COCKPIT, en dat is geen slordigheid maar de kern. In de
   cockpit is de BEHEERDER "ik": die hangt rechts met de merk-tint en de klant staat links.
   Aan de studentkant is de STUDENT "ik". Eén-op-één overnemen zou de student zichzelf als
   buitenstaander laten zien in zijn eigen gesprek. Dus omgekeerd:
     "Jij"            → rechts, lichte tint      (.msg-student)
     "Klantenservice" → links, oppervlak + rand  (.msg-support)

   UITLIJNEN MET margin-left:auto EN NIET MET EEN FLEX-RIJ. De proef zette elke kaart in
   een eigen rij-element met display:flex. Dat werkt, maar zou hier een extra knoop om elk
   bericht heen vragen — en dan verhuist `data-msg` naar dat element of raakt de
   bijlagen-repaint (die op `[data-msg]` de HELE knoop vervangt) een rij binnen een rij.
   Een blok van 90% met een automatische linkermarge hangt net zo goed rechts, zonder
   markup-wijziging en zonder die val. De reden die in de proef bij flex stond — dat
   text-align de TEKST zou verschuiven in plaats van het blok — geldt voor margin-auto
   sowieso niet.

   DE NAAM IS GEDEMPT, en dat is de ENIGE bewuste afwijking van de goedgekeurde proef.
   Daar stond "Klantenservice" nog in --color-primary, met in het commentaar de open vraag
   of dat niet één signaal te veel was nu de kaart het onderscheid al draagt. Jelte gaat
   daarin mee: de merkkleur op de naam is weg, allebei de afzenders houden het gedempte
   grijs van .msg-meta en de hele kopregel blijft bijschrift. Dat is ook wat de cockpit
   doet.

   DE DAGKOP BLIJFT ONGEWIJZIGD (browser-geverifieerd, v23.25) en dat is bewust een
   afwijking van de cockpit: die zet bij élk bericht de volledige datum, en bij zeven
   berichten op één dag staat die dan zeven keer onder elkaar.

   NUL NIEUWE KLEURWAARDEN: --color-tint / --color-tint-strong / --color-surface /
   --color-line zijn alle vier bestaande afgeleiden en volgen dus de skin per site.

   ⚠ MAAR DAARDOOR VALT HET PER SITE ANDERS UIT, want --color-surface doet in de twee
   paletten iets tegengestelds. Gemeten in de proef (kanaalwaarden bewust zonder hekje en
   zonder functienotatie: de kleurtoets in account-berichten.test.mjs leest dit bestand
   vanaf de berichten-kop, commentaar en al):
     platform      pagina 255/255/255 · Klantenservice 246/248/250 · Jij 237/243/253
                   -> beide kaarten donkerder dan de pagina, onderling ~6 uit elkaar; het
                      onderscheid leunt daar op de plaats en de randkleur.
     vastgoedboek  pagina 251/249/244 · Klantenservice 255/255/255 · Jij 233/235/230
                   -> de ene kaart lichter dan de pagina, de andere donkerder, ~22 uit
                      elkaar.
   Valt het op het platform te vlak uit, dan is --color-tint-strong op .msg-student de
   ingreep van één token (~12 uit elkaar). */
.msg {
  width: 90%;
  min-width: 0;
  padding: var(--space-sm) var(--space);
  border: var(--line-hair) solid var(--color-line);
  border-radius: var(--radius);
  /* Een lange URL of bestandsnaam heeft in een kaart van 90% minder ruimte dan in de volle
     kolom van de vorige vorm; zonder dit duwt hij de kaart uit de thread. */
  overflow-wrap: break-word;
}
.msg-support { background: var(--color-surface); }
/* margin-left:auto = rechts. De 10% die de kaart overlaat gaat naar links, en omdat de
   breedte vast is kan die marge niet wegvallen bij een kort bericht. */
.msg-student {
  margin-left: auto;
  background: var(--color-tint);
  border-color: var(--color-tint-strong);
}
/* De naam is het label van de kaart: zwaarder dan de tijd ernaast, maar in dezelfde
   gedempte kleur en op dezelfde maat als de rest van .msg-meta. GEEN eigen kleurregel per
   afzender meer (zie hierboven) — de kaart zegt al wie er aan het woord is. */
.msg-van { font-weight: 600; }
/* .msg-sep (de punt tussen naam en tijd) krijgt met opzet GEEN eigen regel: hij erft het
   gedempte grijs en het normale gewicht van .msg-meta, en staat daarmee bij de tijd in
   plaats van als derde element mee te doen. --color-line lag voor de hand maar is een
   LIJN-kleur (12% tekst over de achtergrond, op wit 228/229/229): een punt in die tint is
   niet meer te zien en dan kun je hem net zo goed weglaten. De class blijft wél staan —
   die draagt de aria-hidden en is de haak waarmee de tests toetsen dat er géén losse punt
   verschijnt zodra er niets naast de naam staat. */

/* De bijlagengroep is ONGEMOEID (.att-*, v23.18) en wordt hergebruikt zoals hij is. Binnen
   een kaart heeft hij wél een scheiding nodig, anders plakt de knop aan de tekst — de
   cockpit zet daar dezelfde haarlijn. */
.msg .msg-attachments {
  margin-top: var(--space-sm);
  padding-top: var(--space-xs);
  border-top: var(--line-hair) solid var(--color-line);
}

/* Boven de eerste kaart en direct onder een dagkop bepaalt de KOP de afstand. Met de
   krappe --space-sm van .msg is dat vandaag een garantie en geen reparatie: aangrenzende
   marges klappen samen en nemen de grootste, dus 0,5rem verliest sowieso van de 0,85rem
   ondermarge van .thread-dag. De regel blijft staan omdat die samenklap wegvalt zodra
   .thread ooit een flex- of grid-container wordt — en dan zou de kop lós van de groep
   komen te staan die hij aankondigt, wat precies de fout van v23.28 was. */
.thread .msg:first-child,
.thread-dag + .msg { margin-top: 0; }

/* --- de kolom van het antwoordformulier (v23.26) ----------------------------
   DE MELDINGSBALK LIEP UIT HET BEELD. "Bericht verstuurd" (.form-success) stond over
       de volle vensterbreedte terwijl de thread en het antwoordveld begrensd zijn — hij
       stak zichtbaar voorbij de rechterrand van het tekstvak.

   ⚠ NIET OP .form-success ZELF. Die class draagt óók de opslaan-bevestiging van de
   profiel- en adres-tab (account.js zet 'm daar via classList.add), en .auth-error zit op
   /login, /register, /activate, /verify, /binnenkort, /contact en de wachtwoord-resets. Een
   max-width op die twee classes zou dus zeven schermen versmallen die hier niet in scope
   zitten. De grens hangt daarom aan .reply-form — de vorm die berichten.js zet, en alleen
   daar (het antwoordveld in de thread en het compose-veld van /berichten/nieuw).

   ÉÉN GRENS, GEEN DRIE. De max-width staat op het FORMULIER en niet los op de twee
   meldingen: dan volgen het label, het tekstvak, de knop, de bevestiging én de
   foutmelding automatisch dezelfde kolom en kan er niets uit de pas gaan lopen. Dat is
   ook waarom het dezelfde var(--wrap-prose) is als .thread hierboven en geen tweede
   getal: de thread stond op 36rem (576px) en het tekstvak op 560px, en die 16px zag je
   als een scheve rechterrand tussen het laatste bericht en het veld eronder.

   v23.28 — de min(560px, 100%) die hierdoor werd overschreven stond nog als dode
   declaratie op .reply-form textarea (regel ~1084) en is nu weg. */
.reply-form { max-width: var(--wrap-prose); }
.reply-form textarea { width: 100%; }

/* === Financieel + Abonnement als lijstrijen (v23.44) ========================
   De laatste twee lijstschermen van /account. Financieel was een <table class="invoices-table">
   met vier koppen; Abonnement was een .sub-card met een eigen rand en een eigen radius.

   HET KAART-RECEPT STAAT HIER NIET — .invoice-row en .sub-row hangen in de selectorlijst van
   .ticket-row > a hierboven (v23.24-blok), naast .ebook-item. Rand, radius, oppervlak,
   hover-lift en de randkleur bij focus zijn dus gedeeld en niet nagebouwd. Hieronder staat
   alléén wat per rij AFWIJKT: de kolommen.

   ⚠ Die regels staan EERDER in dit bestand, dus alles hieronder dat met dezelfde
   specificiteit tegen ze in zou gaan WINT — omgekeerd aan het E-books-blok, waar de
   waarschuwing de andere kant op staat. Vandaar dat hier geen enkele eigenschap uit het
   kaart-recept herhaald wordt.

   ⚠ WAT ER NIET IS OPGERUIMD, en waarom: .invoices-table had NUL css-regels in dit bestand
   (gemeten: de class stond alleen in account.js en er zijn ook geen kale table/th/td-regels
   in shell.css). Er blijft dus geen dode css achter — de tabel was onopgemaakt. .pdf-pending
   BLIJFT staan: die class draagt óók de factuur-/creditnota-links op de Bestellingen-tab
   (account.js, .order-docs) en is daar ongemoeid. De .sub-card/.sub-info/.sub-until/
   .sub-cancel-regels hierboven: .sub-cancel leeft door (de knop in .sub-row draagt 'm), de
   andere drie zijn met dit blok dode css geworden en staan in het rapport.
   ⚠ EN: de noot bij .thread hierboven zegt dat .account-panel "de facturen" hun tabelbreedte
   moet laten houden. Dat gold voor de tabel die hier verdwijnt; voor Bestellingen en
   Adressen geldt het nog wél, dus die noot blijft juist waar het om gaat. */

/* v23.49 — .invoice-doc IS HET GROEIENDE ELEMENT VAN DE RIJ, en draagt type + nummer +
   verkoper als één regel tekst. Hier stond een `display: block`-versie voor de tweeregelige
   vorm van v23.47; die kolom (.invoice-main) is vervallen.

   HET NEEMT ALLE RUIMTE DIE DE DRIE VASTE ELEMENTEN RECHTS OVERLATEN. Dat is de reden dat de
   verkopersnaam hierbinnen staat en niet als vijfde flex-item ernaast: als eigen kolom zou
   hij met de datum, het bedrag en de knop om de breedte concurreren, en dan blijft er per rij
   een smalle strook over voor een naam als "XRS Human Capital BV".

   INKORTEN IN PLAATS VAN AFBREKEN OF DUWEN. De drie eigenschappen overflow/text-overflow/
   white-space zijn hier één geheel: samen zorgen ze dat een te lange regel eindigt op een
   ellipsis in plaats van (a) op een tweede regel te wrappen — dan is de rij weer twee regels
   hoog — of (b) de rij op te rekken en de knop naar rechts te duwen. `min-width: 0` is
   daarvoor NIET optioneel: een flex-item krimpt standaard niet onder zijn min-content-breedte,
   en zonder deze nul zou de tekst de rij alsnog verbreden en het bedrag met de knop
   wegdrukken. De volledige regel leeft in het title-attribuut (invoiceRowHtml), zoals bij
   .line-desc-text op de Bestellingen-tab.
   ⚠ MEETBEPERKING: jsdom heeft geen layout-engine, dus of en wanneer de ellipsis in beeld
   komt is hier niet te toetsen — alleen dát de vier eigenschappen samen op dit element staan.
   Zie het rapport bij v23.49. */
/* ⚠ flex-basis NUL EN NIET `auto`, en dat is het hele verschil tussen "de rijen staan gelijk"
   en "elke rij breekt op zijn eigen moment". Met `auto` is de flex-basis de INHOUD van dit
   element, en die verschilt per factuur: gemeten met de echte Public-Sans-advances is
   "Factuur 2026-9752 · Adveran BV" 248,7px en
   "Creditnota 2026-2491 · XRS Human Capital BV" 352,0px. Flexbox verdeelt items over regels
   op basis van die basis-maten — het krimpen gebeurt pas DAARNA, binnen een regel. Met `auto`
   wrapt de lange rij dus bij een breder venster dan de korte, en dan staan in één lijst rijen
   van twee verschillende hoogtes met de knop op twee verschillende plekken.
   Met basis 0 telt dit element bij die verdeling voor NUL mee: datum, bedrag en knop passen
   altijd samen op één regel zolang het venster dat toelaat, en de tekst krijgt precies wat er
   overblijft en kort in met de ellipsis. Elke rij breekt daarmee op exact hetzelfde moment,
   ongeacht de lengte van de verkopersnaam. */
/* v23.52 — HET NUMMER KAN NIET MEER WORDEN AFGEKAPT, en dat is de eigenlijke reparatie.
   Browser-gemeten klacht: op een smal venster stond er "2026-9…" met de datum er nog naast.
   De oorzaak was NIET dat de 30rem-query de datum niet pakte — die pakt hem wel, hij vuurde
   alleen te laat. Doorgerekend met de echte Public-Sans-advances op 17px (de body-maat; `rem`
   blijft 16px, want de font-size staat op body en niet op html):
       volle regel past                      vanaf ~808px
       nummer heel, naam kort in             808 → 595px
       NUMMER AFGEKAPT, datum nog in beeld   595 → 480px   <- wat Jelte zag
       datum + naam weg (de oude drempel)    onder 480px
   Er zat dus een band van ruim honderd pixels waarin het enige dat de klant nodig heeft — het
   nummer — werd weggeknipt terwijl de datum bleef staan.

   TWEE DINGEN ZIJN ER DAAROM VERANDERD. Dit blok doet het eerste: de ellipsis zat op de HELE
   titelregel, en die knipt aan het EIND. Zolang de regel breder is dan het nummer valt er
   alleen naam weg; wordt de regel smaller dan het nummer, dan knipt hij het nummer. Nu is
   .invoice-doc een eigen flex-rijtje waarin alleen de NAAM mag krimpen:
     - .invoice-type/.invoice-number/.invoice-sep staan op `flex: 0 0 auto` en krimpen nooit;
     - .invoice-seller draagt de ellipsis en slikt alle inkorting.
   ⚠ EN `min-width: 0` IS HIER WEG. Dat stond er om de ellipsis te laten werken, maar liet dit
   element ook onder zijn eigen inhoud krimpen — precies waardoor het nummer kon verdwijnen.
   Zonder die nul geldt `min-width: auto` = min-content, en min-content is hier het nummer
   (de naam telt voor nul mee, want die heeft z'n eigen min-width:0). Past de rij niet meer,
   dan WRAPT hij — een rij van twee regels is leesbaar, een half factuurnummer niet.
   ⚠ De flex-basis blijft 0: dat is wat sinds v23.49 voorkomt dat rijen met een lange
   verkopersnaam op een ander moment afbreken dan rijen met een korte.

   De GATEN komen van `gap` en niet meer van de spaties in de markup: binnen een flex-container
   worden tekstknoopjes die alleen witruimte bevatten niet gerenderd. Die spaties STAAN er nog
   wel in de DOM, en dat is met opzet — een voorleesprogramma en een kopieeractie lezen de
   tekst, niet de opmaak, en zonder die spaties zou daar "2026-9752Adveran BV" staan. */
.invoice-doc {
  flex: 1 1 0;
  display: flex; align-items: baseline; gap: var(--space-xs);
  font-weight: 600;
}
/* Alles vóór de naam is onkrimpbaar. Dit IS de garantie uit de klacht: het nummer blijft
   heel, wat er ook gebeurt. */
.invoice-type,
.invoice-number,
.invoice-sep { flex: 0 0 auto; white-space: nowrap; }
/* De verkopersnaam staat op DEZELFDE regel als het nummer maar is niet hetzelfde soort feit:
   het nummer is de identiteit, de naam een toelichting erbij. Vandaar terug naar normaal
   gewicht en de demping van de andere bijschriften in dit bestand — anders leest de rij als
   twee even zware titels naast elkaar. Geen eigen kleurwaarde: exact --color-muted, net als
   .invoice-date ernaast. */
/* De naam is het ENIGE dat krimpt en inkort — zie de noot bij .invoice-doc hierboven. */
.invoice-seller {
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 400; color: var(--color-muted);
}
/* Datum, gedempt en op dezelfde maat als .ticket-date en .ebook-meta. Blijft een EIGEN
   element op de rij (niet in .invoice-doc): hij mag niet met de verkopersnaam mee-inkorten,
   want een half afgekapte datum is onbruikbaar. white-space:nowrap houdt 'm heel. */
.invoice-date { color: var(--color-muted); font-size: var(--font-size-meta); white-space: nowrap; }
/* HET BEDRAG IS EEN EIGEN KOLOM. Rechts uitgelijnd binnen een vaste minimumbreedte, want
   "€ 49,95" en "€ 104,85" zijn niet even breed: zonder deze twee eigenschappen schuift de
   linkerrand van de Downloaden-knop per rij mee met het aantal cijfers, en dan staat de
   knoppenkolom van een factuurlijst zichtbaar scheef.
   ⚠ De 6rem is net als de 14rem van .ticket-subject een OMSLAGPUNT en geen afstand: het is
   de breedte van het langste bedrag dat we verwachten (vier cijfers plus centen, "€ 1.139,10"
   is de hoogste factuur op dev). Een langer bedrag rekt de kolom op in plaats van af te
   kappen — min-width, niet width.
   tabular-nums zet alle cijfers op dezelfde breedte, zodat de bedragen ook ONDERLING op de
   komma uitlijnen en niet alleen als blok rechts staan. */
.invoice-amount {
  min-width: 6rem; text-align: right; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Het abonnement-label groeit mee; dezelfde rol als .invoice-doc.
   v23.51 — TWEE DINGEN GEREPAREERD, allebei dezelfde als bij .invoice-doc, en allebei gemeten
   in plaats van aangenomen:

   (a) DE FLEX-BASIS WAS `auto` en is nu 0. Met `auto` is de basis de INHOUD, en de labels
       lopen hier sterker uiteen dan waar ook op /account: "Plus-abonnement" is 134,0px en
       "Vastgoedrekenen voor gevorderden" 271,2px (echte Public-Sans-advances). Flexbox
       verdeelt items over regels op die basis-maten en krimpt pas daarna, dus met `auto`
       wrapt een e-book-abonnement bij een breder venster dan een tier-abonnement — rijen van
       twee hoogtes in één lijst, met de Opzeggen-knop op twee plekken. Met basis 0 breekt
       elke rij op hetzelfde moment.
   (b) ER WAS GEEN OVERFLOW-BEHANDELING. `min-width: 0` liet het label wél krimpen, maar
       zonder overflow/text-overflow/white-space liep de tekst dan zichtbaar over de badge en
       de verlengdatum heen in plaats van in te korten. De volledige titel staat sinds v23.51
       in het title-attribuut (subRowHtml).

   ⚠ HIER VERDWIJNT NIETS OP EEN SMAL VENSTER, anders dan bij de factuurrij. Zie de
   30rem-query onderin dit bestand: "verlengt op <datum>" is niet redundant — bij een factuur
   staat het jaar al in het nummer, maar wanneer een abonnement wordt afgeschreven staat
   nergens anders. Wat hier inkort is het label, en dat heeft een title. */
.sub-label {
  flex: 1 1 0; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* "verlengt op <datum>" is bijschrift bij het label, geen tweede titel — zelfde demping en
   maat als .invoice-date hierboven. Dit verving .sub-until, dat op een losse grijs-hex en een
   losse 0.875rem stond: een grijs dat niet met de skin meebeweegt en een maat die van geen
   van de andere bijschriften in dit bestand was.
   ⚠ GEEN HEX IN DIT COMMENTAAR — zie de noot bij de verwijderde .sub-card hierboven: de
   kleurtoets leest dit bestand vanaf het berichten-blok ONGEFILTERD, commentaar incluis. */
.sub-renewal { color: var(--color-muted); font-size: var(--font-size-meta); white-space: nowrap; }
/* De Opzeggen-knop mag niet in de flex-rij zakken; zelfde ingreep als .ebook-item .pay-btn.
   De knop houdt verder z'n eigen maten (.sub-cancel hierboven, ongewijzigd). */
.sub-row .sub-cancel { margin-top: 0; }

/* --- de lege staat (v23.44) -------------------------------------------------
   GEEN KAART EN GEEN KADER, met opzet. De stijlpagina zei het al bij sectie 8: een omlijnd
   blok met "je hebt hier nog niets" erin leest als een foutmelding, terwijl er niets mis is.
   Wat deze bouwsteen doet is de twee dingen gelijktrekken die per tab uiteenliepen: de
   DEMPING (dit is een mededeling en geen kop, dus muted en niet de volle tekstkleur) en de
   plek van de ACTIE eronder.
   ⚠ Alleen de kleur en het ritme; geen background, geen border, geen padding. Voeg je die
   toe, dan is het alsnog het kader dat hier niet moet komen. */
.empty-state { color: var(--color-muted); margin: var(--space-lg) 0; }
/* De actie is een echte knop en moet daarom NIET meegedempt worden — dat is de enige weg
   vooruit op een leeg paneel. .btn-secondary brengt z'n eigen kleur mee; hier staat alleen
   de ruimte eronder. */
.empty-state .btn-secondary { margin-top: var(--space-sm); }

/* v23.46 — DE DRIE text-decoration-REGELS VAN v23.45 STONDEN HIER EN ZIJN WEG.
   v23.45 haalde de onderstreping van deze ene knop af met
       .empty-state .btn-secondary { text-decoration: none }
   plus een terugkeer bij :hover en :focus-visible. Dat was met opzet smal gescoopt: dezelfde
   onderstreepte knop stond op zes andere plekken en die waren browser-geverifieerd, dus ze
   mochten niet stil meebewegen.
   Sinds v23.46 draagt .btn-secondary zélf `text-decoration: none` (zie dat blok, met de
   volledige onderbouwing). Daarmee voegt de eerste regel hier NIETS meer toe — hij zou
   dezelfde waarde nog eens zetten op een hogere specificiteit, en dat leest bij de volgende
   ronde als "hier is iets bijzonders aan de hand" terwijl dat niet zo is.
   ⚠ EN DE HOVER-TERUGKEER IS BEWUST NIET MEEVERHUISD. Die maakte wél nog verschil — hij was
   de enige plek waar de streep terugkwam — maar dát verschil is precies wat weg moest: de
   knop in de lege staat gedroeg zich bij hover anders dan dezelfde knop op de zes andere
   schermen, en de primaire .pay-btn onderstreept bij hover ook niet. De aanwijzing komt nu
   overal van de gedeelde .btn-secondary:hover-regel: randkleur, vulling en tekstkleur naar de
   merkkleur, plus een lichte lift.
   Wat hier BLIJFT staan is de marge hierboven — die is ruimte en geen onderstreping. */

/* === de factuurrij op een smal venster (v23.51, drempel herzien in v23.52) =============================
   OPTIE A (besluit Jelte 30-07): onder de drempel verdwijnen de DATUM en de VERKOPERSNAAM
   visueel; wat overblijft is nummer · bedrag · knop.

   ⚠ DE DREMPEL WAS 30rem (480px) EN IS 40rem (640px) GEWORDEN. Dat is de tweede helft van de
   v23.52-reparatie, en de reden staat in het blok bij .invoice-doc: 480px was niet fout maar
   veel te laat. Doorgerekend met de echte fontmaten op 17px:
       nummer van een CREDITNOTA ("Creditnota 2026-2491", 178,7px) blijft heel MET datum
         in beeld vanaf ~595px;
       nummer van een gewone factuur ("2026-9752", 92,3px) vanaf ~501px.
   De oude drempel liet die twee banden dus open: tussen 595 en 480px stond er een afgekapt
   nummer met de datum er nog vrolijk naast. 40rem sluit ze allebei, want 640 > 595.
   ⚠ EN 40rem IS 640px — exact het breekpunt dat dit bestand al twee keer gebruikt (zie de
   twee `max-width: 640px`-query's hierboven). Een eigen getal ertussen zou de schermen op
   verschillende momenten laten kantelen.
   Met datum en naam weg staan er drie items en twee gaten in plaats van vier en drie: het
   vaste beslag valt van 326,4px naar 236,3px terug, en met de smallere bedrag-kolom hieronder
   blijft een gewone factuurrij tot ~377px één regel.

   ⚠ GEEN display:none EN GEEN [hidden]. De verkopersnaam is het antwoord op "van wie is deze
   factuur" en dat mag een voorleesprogramma niet kwijtraken; hij gaat visueel weg en blijft
   in de toegankelijkheidsboom. Voor de muis leeft hij door in het title-attribuut van
   .invoice-doc (zie invoiceRowHtml). Datzelfde geldt voor de datum: die is voor het oog
   redundant (het jaar staat in het nummer) maar niet voor iemand die de rij hóórt.

   ⚠ WAAROM DIT GEEN GENERIEKE .u-visually-hidden-KLASSE IS. Die zou hier niets opleveren: de
   verberging is VOORWAARDELIJK (alleen onder 30rem) en een class wordt onvoorwaardelijk in de
   markup gezet. Een generieke klasse zou dus ongebruikt in :root blijven staan én het recept
   zou hier alsnog een tweede keer staan — twee bronnen in plaats van één. Zodra een tweede
   scherm iets ONVOORWAARDELIJK visueel wil verbergen hoort dit recept eruit getrokken te
   worden; tot dan staat het op de enige plek die het gebruikt.
   Het recept zelf is het standaardrecept: uit de flow (absolute), teruggebracht tot 1px en
   weggeknipt. NIET `width:0;height:0` — dat haalt sommige voorleesprogramma's ook weg. */
@media (max-width: 40rem) {
  .invoice-date,
  .invoice-sep,
  .invoice-seller {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  /* Het bedrag houdt zijn rechtse uitlijning en een minimumbreedte — die is er om de
     linkerrand van de knop stil te houden — maar de 6rem van de brede weergave is hier te
     royaal. Gemeten: het breedste bedrag op dev is "€ 1.139,10" en dat is 70,3px, dus 4,5rem
     (72px) dekt het. Die 24px winst gaat naar het nummer, en dat is precies wat er op een
     telefoon van 390px nodig was om het nummer heel te houden.
     ⚠ min-width en niet width: een nog hoger bedrag rekt de kolom op i.p.v. af te kappen, en
     omdat élke rij in deze stand dezelfde minimumbreedte draagt blijft de knop op één lijn. */
  .invoice-amount { min-width: 4.5rem; }
}

/* --- /acties: het actieblok en het codeoverzicht ------------------------------
   DIT BLOK IS WÉL EEN KAART, en dat is precies de omgekeerde afweging van .empty-state
   hierboven. Daar leest een kader als een foutmelding omdat er niets is; hier is de kaart
   de reden dat de actie zich losmaakt van de lopende tekst eromheen. Vandaar de accentrand
   links: de site-accentkleur is op deze pagina verder nergens vlakvullend aanwezig, dus een
   streep van 4px zet het blok apart zonder een tweede achtergrondkleur te introduceren.
   Recept verder identiek aan .form-card (surface + haarlijn + --radius) — geen nieuwe
   kaartsoort, alleen een andere aanleiding. */
.actie-card {
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  border-left: 4px solid var(--color-accent);
  border-radius: var(--radius);
  padding: var(--space-lg);
  /* Weer een SYMMETRISCHE marge. De vorige ronde stond hier onder --space-xs om het
     codeoverzicht dat BUITEN de kaart hing er visueel tegenaan te trekken; nu de codes
     binnen de kaart staan draagt de kaart die samenhang zelf en is dat knijpwerk niet
     alleen overbodig maar verkeerd — het zou de kaart tegen de volgende sectie aan duwen. */
  margin: var(--space-lg) 0;
}
/* De actie zelf: groot en als eerste. Geen extra marge boven — de kaart geeft de ruimte al,
   en :first-child in .app-content krijgt elders al een marge-reset. */
.actie-kop { margin: 0; font-size: 1.45rem; }
/* De voorwaarden eronder, kleiner en gedempt: ze horen bij de actie en mogen hem niet
   overschreeuwen. Bullets blijven staan — het zijn losse uitzonderingen en die lezen als
   lijst beter dan als doorlopende zin. */
.actie-voorwaarden {
  margin: var(--space-sm) 0 0;
  padding-left: 1.2em;
  color: var(--color-muted);
  font-size: 0.88rem;
}
.actie-voorwaarden li { margin: 0.2em 0; }

/* De regel boven de codes, binnen de kaart. Draagt de kleur en de grootte van de gewone
   tekst en niet van de voorwaarden erboven: die zijn gedempt omdat het uitzonderingen zijn,
   terwijl dit aankondigt wat de klant hier komt halen.
   ⚠ NIET vet, en sinds de opleidersnamen eronder dat ook niet meer zijn draagt de hele
   kaart geen enkel vet niveau onder de kop. Dat is de bedoeling: elk vet stuk hierbinnen
   gaat als kopje lezen en maakt van het codeoverzicht weer een eigen sectie — precies wat
   er eerder uit moest. De bovenmarge zet deze regel los van de voorwaarden; eronder bijna
   niets, want de lijst hoort erbij. */
.actie-codes-kop { margin: var(--space) 0 0.25rem; }

/* Het overzicht: ÉÉN REGEL PER OPLEIDER, naam en code(s) naast elkaar, als sluitstuk van
   de kaart.
   ⚠ DEZELFDE LIJSTVORM ALS .actie-voorwaarden HIERBOVEN: bullets en dezelfde inspringing
   van 1.2em. De kaart draagt daarmee één opsommingsstijl in plaats van twee soorten lijst
   onder elkaar — een bulletloze lijst pal onder een lijst mét bullets leest als een ander
   soort inhoud, terwijl het hier gewoon de tweede opsomming van dezelfde kaart is. Blijft
   het getal 1.2em ergens veranderen, dan hoort het op beide plekken te veranderen.
   ⚠ Marge boven op 0: de afstand tot de regel erboven komt volledig uit de ondermarge van
   .actie-codes-kop, zodat er maar ÉÉN plek is die dat gat bepaalt. */
.actie-opleiders { margin: 0; padding-left: 1.2em; }
/* Regelafstand gelijk aan .actie-voorwaarden li, om dezelfde reden: één lijstritme in de
   kaart. De regels mogen dicht op elkaar — het zijn korte regels die als één blokje horen
   te lezen, niet als losse alinea's. */
.actie-opleider { margin: 0.2em 0; }
/* ⚠ .actie-opleider-naam DRAAGT BEWUST GEEN EIGEN OPMAAK MEER en heeft daarom geen regel.
   Hij was vet; dat is eraf. De naam hoeft zich niet los te vechten van de code erachter —
   de bullet en de dubbele punt leden de regel al, en vet erbij maakte er een kopje van
   boven een code die juist het opvallende deel is. De span blijft wél in de HTML staan: hij
   is de haak waarmee de tests naam en code uit elkaar houden, en inline is hij vanzelf.
   Zet hier dus geen display:block bij — dat duwt de codes weer naar een eigen regel. */
/* De code in dezelfde monospace als in het kassa-ballonnetje (.code-code): het is een reeks
   die de klant overtikt, en dan moet 0/O en 1/l uit elkaar te houden zijn. */
.actie-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ==========================================================================
   TABBLAD VOUCHERS (v23.309, inklapbaar per boek in v23.310)
   ==========================================================================
   Per boek een kaart (.account-card, het bestaande recept van de profiel- en adreskaart),
   met een kop die uitklapt en daaronder de codes. Er is hier bewust GEEN nieuw kaart-, badge- of
   rijrecept bijgekomen: alles wat er staat is afstand, uitlijning en de monospace van de
   code. Een vijfde kaartvorm naast .ebook-item, .ticket-row, .invoice-row en .sub-row zou
   het account op de vijfde tab anders laten lezen dan op de vier ernaast. */
.voucher-group + .voucher-group { margin-top: var(--space-sm); }
/* v23.315 — DE FLEX-KOP `.voucher-book-head` IS WEG, samen met "Kopieer beschikbare"
   (besluit Jelte). Hij bestond alleen om die knop naast de uitklapknop te houden; met één
   kind over zou hij een lege huls zijn die de volgende lezer laat zoeken naar het tweede
   element dat er niet meer is. De uitklapknop is nu het enige kind van de kaart.
   De uitklapknop is een REGEL en geen knopvorm: geen rand, geen vulkleur, geen eigen kleur
   — de titel moet lezen als de kaartkop die hij vervangt, en 45 knopvormen onder elkaar
   maken van dit scherm een bedieningspaneel. Alleen de aanwijzer verraadt dat je erop kunt
   klikken. `text-align: left` omdat een knop standaard centreert en een boektitel dat nooit
   hoort te doen. `display: flex` maakt hem blok-niveau en dus vanzelf zo breed als de kaart;
   de `flex: 1 1 12rem` die hier stond had alleen betekenis als flex-KIND van de weggevallen
   kop en is daarmee vervallen. */
.voucher-book {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 0 var(--space-sm);
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.voucher-book-title { font-weight: 600; }
/* Het driehoekje draait mee met aria-expanded. Een ::before op de KNOP en geen los element:
   het is decoratie, en een schermlezer heeft er niets aan naast de aria-toestand die er al
   staat. */
.voucher-book::before {
  content: "\25B8"; color: var(--color-muted); font-size: var(--font-size-meta);
}
.voucher-book[aria-expanded="true"]::before { content: "\25BE"; }
.voucher-book-body { margin-top: var(--space-sm); }
/* De telling ONDER de titel: BIJSCHRIFT, geen tweede kop. Dezelfde demping en maat als
   .ebook-meta en .order-date — één keuze voor elk metaregeltje in dit bestand.
   ⚠⚠ `flex-basis: 100%` — DE TELLING STAAT ALTIJD OP EEN EIGEN REGEL (v23.315, browser-
   gemeten door Jelte op "Publiekrecht Theorieboek (ebook)"). De kop is een flex-rij met
   `flex-wrap`, en dan beslist de LENGTE VAN DE TITEL waar de telling terechtkomt: een lange
   titel duwde hem naar de volgende regel, een korte liet hem erachteraan lopen. Vijf boeken
   onder elkaar lazen daardoor op twee verschillende manieren. Dit is dezelfde faalklasse als
   de standkolom die v23.312 met een tabel heeft rechtgezet: uitlijning die van de
   inhoudslengte afhangt, is geen uitlijning. Een volle basis dwingt de omslag ongeacht de
   titel; de row-gap staat op 0, dus hij komt strak onder de titel te staan. */
.voucher-tally {
  flex-basis: 100%; color: var(--color-muted); font-size: var(--font-size-meta);
}
.voucher-leeg { margin: 0; color: var(--color-muted); font-size: var(--font-size-meta); }
/* v23.315 — DE VIER FILTERVINKJES boven de codes van één boek. Een rij die mag omslaan: op
   een telefoon passen vier standwoorden met vinkje niet naast elkaar, en dan horen ze onder
   elkaar te vallen in plaats van ingedrukt te worden. Maat en demping van de metaregel
   hierboven — het is bediening bij de lijst, geen kop erboven.
   ⚠ DE HAARLIJN ERONDER scheidt de bediening van de codes; dezelfde --color-line als de
   celranden van de tabel eronder, zodat er geen tweede lijnsoort op dit scherm bijkomt. */
.voucher-filter {
  display: flex; flex-wrap: wrap; gap: 0.15rem var(--space-sm);
  margin-bottom: var(--space-sm); padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--color-line);
  font-size: var(--font-size-meta); color: var(--color-muted);
}
/* `.checkbox` brengt de uitlijning van vinkje en woord al mee; hier alleen de marge eruit,
   dezelfde ingreep als bij .voucher-switch-label hieronder.
   ⚠ DE HOOFDLETTER KOMT UIT CSS EN NIET UIT JAVASCRIPT. Het woord in de HTML is letterlijk
   het woord van de server (`state_label`) en is tegelijk de SLEUTEL waarop het filter
   matcht; hem in JavaScript verbouwen zou een tweede vorm van hetzelfde woord maken en het
   filter op zichzelf laten missen. Als zelfstandig label leest "Beschikbaar" beter dan
   "beschikbaar" — in de telling hierboven blijft het woord klein, want daar staat het
   midden in een zin ("1 beschikbaar").
   ⚠ `capitalize` EN NIET `::first-letter`: dat pseudo-element geldt niet wanneer het
   element met een replaced inline begint, en dat doet dit label — het vinkje staat vóór het
   woord. De regel zou stil niets doen. `text-transform` raakt alleen tekst en laat het
   vinkje ongemoeid; alle vier de standen zijn één woord, dus capitalize en "eerste letter"
   vallen hier samen. */
.voucher-filter-vink { margin: 0; cursor: pointer; text-transform: capitalize; }
.voucher-filter-vink input { margin-right: 0.3rem; }
/* ==========================================================================
   v23.312 — DE CODES STAAN IN EEN TABEL (was: een flex-rij per code)
   ==========================================================================
   Browser-gemeten door Jelte: het scherm was "hard en lomp", en de oorzaak was
   aanwijsbaar. In een flex-rij bepaalt de LENGTE van het standwoord waar de stand
   terechtkomt: "beschikbaar" duwde 'm verder naar rechts dan "verlopen", dus de kolom
   stond op elke regel ergens anders. Cellen die over rijen heen uitlijnen zijn precies
   wat een tabel is — met marges valt dat niet te repareren.

   ⚠ HET RECEPT IS DAT VAN .order-lines (de bestellingtabel op ditzelfde tabblad): haarlijn
   per cel in --color-line, dezelfde celpadding, table-layout: fixed, laatste rij zonder
   lijn. De CLASS is bewust een eigen: .order-lines draagt POSITIONELE kolomhaken
   (`td:nth-child(n+4) { width: 7rem }`, hierboven) die op de refund- en leverstatus van
   "Mijn bestellingen" zijn gemaat en hier de datum- en schuifjeskolom zouden raken.

   ⚠ DE KOLOMBREEDTES ZIJN GEMETEN, niet geschat — met fontTools op publicsans-700 (badge)
   en -400 (tabel), 0,7rem/0,9rem, root 16px, inclusief --track-badge en --pad-badge:
     BESCHIKBAAR 5,61rem + 18px pad = 6,74rem  ← de breedste van de vier standen
     INGEWISSELD 5,41rem → 6,54rem, UITGEGEVEN 5,01rem → 6,13rem, VERLOPEN 4,14rem → 5,27rem
     "12-08-2026" 4,90rem · "Kopieer" 3,25rem + 1,4rem knop-padding = 4,65rem
   Alles is border-box (regel 278), dus de celpadding van 0,6rem telt in de breedte mee. */
.voucher-table {
  width: 100%; margin: 0; font-size: 0.9rem; table-layout: fixed;
  border-collapse: collapse;
}
.voucher-table td {
  border-bottom: 1px solid var(--color-line); padding: 0.4rem 0.6rem 0.4rem 0;
  vertical-align: middle;
}
.voucher-table tr:last-child td { border-bottom: 0; }
/* Monospace op de CEL en niet alleen op de <code>, en dat is functioneel: `ch` is de
   breedte van een cijfer in het font van het element zelf. Op de cel gezet meet 19ch dus
   exact de 19 tekens van de code (vier blokjes plus drie koppeltekens), en schuift de
   kolom mee als het monospace-font van het systeem anders uitpakt. De 0,03em spatiëring
   van .voucher-code-text telt 19 keer mee; vandaar de opslag. */
.voucher-table .vc-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  width: calc(19ch + 19 * 0.03em + 0.6rem);
}
.voucher-table .vc-state { width: 7.5rem; }
.voucher-table .vc-date { width: 5.6rem; color: var(--color-muted); }
/* De schuifjeskolom krijgt GEEN breedte: hij vangt de speling op, en hij is ook de plek
   waar een weigering komt te staan (die mag afbreken over twee regels). */
.voucher-table .vc-copy { width: 5.5rem; padding-right: 0; }
/* Monospace om precies dezelfde reden als .actie-code en .code-code hierboven: dit is een
   reeks die wordt overgetikt, en dan moeten 0/O en 1/l uit elkaar te houden zijn. Het
   alfabet van de code weert die tekens al (services/vouchers._CODE_ALPHABET) — dit is de
   tweede helft van diezelfde zorg. */
.voucher-code-text { font-weight: 600; letter-spacing: 0.03em; }
/* De kopieerknop is de bestaande omlijnde rij-actie (.btn-secondary) en geen nieuwe
   knopvorm; die brengt een margin-top van 0.75rem mee die 'm in de rij liet zakken.
   Zelfde ingreep en zelfde reden als `.ebook-item .btn-secondary` hierboven. Ook compacter:
   op volle maat oogt hij als een kassaknop naast twintig codes — dezelfde afweging als bij
   `.ebook-item .pay-btn`. */
.voucher-code .btn-secondary {
  margin-top: 0; padding: 0.25rem 0.7rem; font-size: var(--font-size-meta);
}
/* Het label om het schuifje krijgt geen eigen regel meer dan nodig: .checkbox brengt de
   uitlijning al mee, hier alleen de marge eruit zodat de cel één lijn houdt. */
.voucher-switch-label { margin: 0; }
/* De weigering van een schuifje, ONDER de bediening waar hij op sloeg. Gedempt noch
   alarmerend: het is een mededeling dat deze code niet meer te wijzigen is. */
.voucher-fout { margin: 0.2rem 0 0; font-size: var(--font-size-meta); color: var(--color-muted); }
/* ⚠ ONDER 640px VERVALLEN DE VASTE BREEDTES. Vijf gemaatte kolommen tellen op tot ruim
   30rem; op een telefoon (18rem kaartbreedte) zou `table-layout: fixed` ze evenredig
   inkrimpen en dan wordt de CODE afgeknipt — precies het ene ding dat leesbaar moet
   blijven, want hij wordt overgetikt. Met `auto` verdeelt de browser op inhoud; de
   kolommen blijven onderling uitgelijnd, want dat doet een tabel sowieso. 640px is het
   bestaande breekpunt van dit bestand (regel 2707), geen nieuw getal. */
@media (max-width: 640px) {
  .voucher-table { table-layout: auto; font-size: var(--font-size-meta); }
  .voucher-table .vc-code,
  .voucher-table .vc-state,
  .voucher-table .vc-date,
  .voucher-table .vc-copy { width: auto; }
}
/* ==========================================================================
   v23.315 — EEN DODE CODE ZAKT WEG, EN DE HELE REGEL DEMPT MEE
   ==========================================================================
   Browser-gemeten door Jelte op v23.312: INGEWISSELD stond even hard op het scherm als
   BESCHIKBAAR. Dat klopte niet met wat de regels betekenen — een ingewisselde of verlopen
   code is DOOD, er valt niets meer mee te doen, en hij hoort dus niet even veel aandacht te
   trekken als een code die nog uitgedeeld kan worden.

   ⚠ WAT HIER STOND DEKTE HET NIET. Tot deze versie dempte alleen `.voucher-code-text`, met
   als argument dat de stand ernaast leesbaar moest blijven. Sinds v23.312 is die stand een
   BADGE, en die badge draagt voor alle vier de standen dezelfde gedempte tint — dus juist
   het element dat "dood" moest uitdrukken zag er precies zo uit als bij een levende code.
   Besluit Jelte: de hele regel dempt mee.

   ⚠ OPACITY EN GEEN NIEUWE KLEUR. Dit palet heeft geen tweede grijs onder --color-muted, en
   er een introduceren zou betekenen dat elke lezer van dit bestand voortaan tussen twee
   grijzen moet kiezen. `opacity` dempt wat er staat zonder één kleurwaarde toe te voegen —
   inclusief de badge, de datum, het schuifje en de kopieerknop, en dat is precies "de hele
   regel". 0.62 is LETTERLIJK HET GETAL van `.checkout-card--done/--upcoming` hierboven, de
   andere plek in dit bestand waar een afgehandelde of nog-niet-actieve stap wegzakt; het is
   dezelfde vraag en dus hetzelfde antwoord, geen tweede drempel.

   ⚠ DE EXPLICIETE DEMPING VAN `.voucher-code-text` IS VERVALLEN en niet vergeten: hij stond
   op --color-muted, en muted × 0.62 zou de code dubbel dempen tot bijna onleesbaar. De code
   staat nu op de gewone tekstkleur en zakt met de regel mee — samen ongeveer waar de oude
   losse demping uitkwam, maar dan met de rest van de regel erbij.

   ⚠ DE POORT IS `data-voucher-state` (de TECHNISCHE sleutel) en niet het woord: opmaak hangt
   aan de stand, niet aan de copy. Het filter hierboven takt op `data-voucher-stand`; die twee
   attributen staan er allebei en juist daarom hoeft geen van beide iets te vertalen. */
/* v23.321 — `revoked` HOORT IN DEZELFDE LIJST. Ingetrokken is sinds die versie een echte
   stand (`services/vouchers._state`) en het is de deadste van allemaal: een besluit, geen
   datum die nog kan opschuiven. Dezelfde 0.62 en geen eigen waarde — het is dezelfde vraag
   ("hier valt niets meer te doen") en dus hetzelfde antwoord. De kopieerknop op zo'n regel
   staat bovendien op `disabled` (account.js `magKopieren`); de demping is het signaal, de
   uitgezette knop is de grens. */
.voucher-code[data-voucher-state="redeemed"],
.voucher-code[data-voucher-state="expired"],
.voucher-code[data-voucher-state="revoked"] { opacity: 0.62; }
/* De verlopen codes staan INGEKLAPT (besluit Jelte: niet weggooien, alleen uit het zicht).
   De samenvatting is een gewone gedempte regel en geen knop: hij opent een lijst, hij voert
   niets uit. */
.voucher-expired { margin-top: var(--space-sm); }
.voucher-expired > summary {
  cursor: pointer; color: var(--color-muted); font-size: var(--font-size-meta);
}
