/*
 * portal styling.
 *
 * APP LAYER ONLY. Every colour value lives in branding/colors.css, which is loaded before this
 * file; nothing here may declare a palette value. Load order:
 *     branding/colors.css  ->  branding/branding.css  ->  assets/app.css
 *
 * The huisstijl in one line: white canvas, black text, gold for section accents,
 * blue for actions/information/selection, light grey to lift a panel off the page.
 *   - gold   = section rules and the landing hover pulse
 *   - blue   = primary CTA, links, selected state, informational fills   (text/links use --brand-support-text;
 *              --brand-support itself is 2.52:1 and may only be a fill, border or icon)
 *   - grey   = structure: headers, panels, dividers, quiet chrome
 * A gold surface always carries --brand-on-accent (black). White on gold is 2.50:1 and unreadable.
 * Dutch-informal UX, sentence case, Lucide icons only.
 */

/* ===== Login ===== */
.app-login {
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: var(--brand-bg);
  padding: 24px;
}
.login-card {
  width: 100%;
  max-width: 360px;
  text-align: center;
  animation: app-card-in var(--brand-t-slow) var(--brand-ease-out) both;
}
/* Logo + tagline + fields settle in just after the card, so the eye lands in reading order. */
.login-logo {
  max-width: 220px; height: auto; margin-bottom: 12px;
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) .12s both;
}
.login-tagline {
  display: inline-block;
  white-space: nowrap;   /* a kicker pill must never wrap — condense the text instead */
  font-family: var(--brand-font-heading);
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--brand-tracking-kicker);
  /* Solid tint + gold-TEXT (4.52:1). --brand-accent-deep here was 3.09:1 and failed at 11px. */
  color: var(--brand-accent-text);
  background: var(--brand-accent-soft);
  padding: .45em 1.1em; border-radius: 999px;
  margin: 0 0 22px;
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) .18s both;
}
/* Uitleg op de inlogkaart (pin_herstel.php): één alinea onder de kicker. De kicker zelf mag niet
   afbreken en is dus kort; wat er verder gezegd moet worden staat hier, waar het wél mag doorlopen.
   Kleur staat er expliciet bij — dit is tekst op de witte kaart, precies waar grijs wegloopt. */
.login-uitleg {
  max-width: 34ch; margin: 0 0 22px;
  color: var(--brand-ink-muted); background: transparent;
  font-size: 14px; line-height: 21px;
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) .21s both;
}
.login-uitleg strong { color: var(--brand-ink); }

.login-form { animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) .24s both; }
.login-form { text-align: left; }
.login-form .field { margin-bottom: 16px; }
.login-form label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--brand-ink); }
.login-form input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  font: inherit;
  color: var(--brand-ink);
  background: var(--brand-white);
  transition: border-color var(--brand-t-fast) var(--brand-ease-out),
              box-shadow var(--brand-t-base) var(--brand-ease-out);
}
/* ONE FIELD, ONE COLOUR — same report as .deb-veld input:focus in debiteuren/debiteuren.css.
   The gold border (--brand-accent) plus the blue keyboard ring (--brand-support-text) painted two
   edges around one box, and the username field carries `autofocus`, so that double edge is the
   first thing the login screen shows. The border and the ring now use the same blue, and the glow
   goes off as soon as the ring appears. Blue and not gold: the ring is a non-text indicator and
   must reach 3:1 — support-text is 5.38:1 on white, the brand gold is 2.50:1.
   The soft glow stays as mouse decoration on :focus; see the note at .g-field:focus. */
.login-form input:focus { outline: none; border-color: var(--brand-support-text); box-shadow: 0 0 0 3px var(--brand-support-soft); }
/* Offset 0 and not the 2px used elsewhere: with a gap, the border and the ring read as two edges
   again — the same "double border" in one colour. At offset 0 they touch and paint one 3px edge. */
.login-form input:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 0; box-shadow: none; }
/* THE PIN FIELD IS MASKED BY JAVASCRIPT AND NOT BY THIS CSS (17-09-2026, corrected the same day).
 *
 * WHY. A browser must not offer to save the PIN. A four-digit PIN from a shared workplace does not
 * belong in the keychain of a private profile. There is no attribute that means "do not save", so
 * the only lever is to give the browser nothing it recognises as a credential. See login.php.
 *
 * `-webkit-text-security` WAS THE FIRST ATTEMPT AND IT FAILED ON SAFARI. WebKit reads that property
 * as "a password in disguise" and classifies the field as a credential anyway, so the offer came
 * back on iPhone and iPad. assets/login-pin.js now masks with bullet characters instead, and it
 * REMOVES the `.login-pin` class while doing so.
 *
 * SO THIS RULE IS THE NO-SCRIPT STATE ONLY. Without JavaScript the field stays `type="password"`
 * (see login.php) and this class never gets removed; the masking below then costs nothing and the
 * PIN is never readable on screen. The two spellings stay: the unprefixed form is the future
 * standard.
 *
 * This needs its own class and not `.login-form input`: the user name must stay readable. */
.login-pin {
  -webkit-text-security: disc;
          text-security: disc;
  /* Fixed-width digits, so four bullets do not dance while you type. */
  letter-spacing: .18em;
}

/* THE STATE THE SCRIPT SWITCHES TO. Same look, no text-security — that property is what Safari
   reads as a password field. The bullets are real characters in the value here. */
.login-pin-js {
  letter-spacing: .18em;
}

/* A failed login shakes once — faster to read than the text alone. */
.login-error {
  background: var(--brand-danger-soft);
  border: 1px solid var(--brand-danger);
  color: var(--brand-danger);
  padding: 10px 12px;
  border-radius: var(--brand-radius-sm);
  font-size: 13px;
  margin-bottom: 18px;
  text-align: left;
  animation: app-shake .4s var(--brand-ease-out) both;
}

/* ===== App shell ===== */
/* White bar, one gold rule underneath: the huisstijl's section line, doing the whole job that a
   coloured header bar used to do. Static — the never-sticky app rule stands. */
.app-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 24px;
  background: var(--brand-surface);
  border-bottom: 2px solid var(--brand-rule-accent);
  position: static; /* never sticky */
}
/* Full brand lockup (the wide logo variant) — carries the "Orthopedische Schoentechniek /
   Since 1978" text, so it needs a little more height than a wordmark to stay legible. */
.app-header img { width: 64px; height: auto; display: block; }
.app-header .header-home { display: inline-flex; align-items: center; border-radius: var(--brand-radius-sm); transition: opacity .12s ease; }
.app-header .header-home:hover { opacity: .75; }
.app-header .header-home:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 3px; }
.app-header .header-spacer { flex: 1; }
.app-header .header-user { color: var(--brand-ink-subtle); font-size: 13px; }
/* ===== Navigation in the header (see _header.php) =====
   The admin menu is a <details> — that is supposed to be closed until you open it. The mobile drawer
   is NOT: Chrome does not lay out the content of a closed <details> (measured: display flex, width
   0), so there the nav would be invisible on desktop. Hence a button + `.is-open`.
   FAIL-OPEN: the nav is only hidden when <html class="has-js"> is set. If the script never arrives,
   the navigation stands open instead of locked away. */

.app-header .header-drawer { display: contents; }   /* no box of its own on desktop */
.app-header .header-nav { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.app-header .header-burger { display: none; }       /* only below the breakpoint */
.app-header .btn-ghost.is-current { background: var(--brand-accent-soft); color: var(--brand-accent-text); }

/* ---- Admin menu ---- */
.app-header .header-menu { position: relative; }
.app-header .header-menu > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
}
.app-header .header-menu > summary::-webkit-details-marker { display: none; }
.app-header .header-menu[open] > summary { background: var(--brand-accent-soft); color: var(--brand-accent-text); }
.app-header .header-menu-caret { transition: transform var(--brand-t-fast) var(--brand-ease-out); }
.app-header .header-menu[open] .header-menu-caret { transform: rotate(180deg); }
.app-header .header-menu-list {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
  min-width: 220px;
  display: flex; flex-direction: column;
  padding: 6px;
  background: var(--brand-surface);
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .14);
}
/* EEN REGEL IN EEN MENU KAN OOK EEN KNOP ZIJN (09-09-2026). De vraagbaak opent een paneel en de twee
   schakelaars zetten iets aan of uit; dat zijn geen bestemmingen, dus het zijn `<button>`s. Ze zien
   er precies zo uit als een link — anders leest één regel in een verder gelijke lijst als iets
   anders, terwijl het verschil de gebruiker niets aangaat. Vandaar ook het terugzetten van de
   knop-standaardwaarden: een `<button>` erft geen lettertype en staat standaard gecentreerd. */
.app-header .header-menu-list a,
.app-header .header-menu-list button {
  display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left;
  padding: 9px 12px; border: 0; border-radius: var(--brand-radius-sm);
  background: none; font: inherit;
  font-size: 14px; color: var(--brand-ink); text-decoration: none; white-space: nowrap;
  cursor: pointer;
}
.app-header .header-menu-list a:hover,
.app-header .header-menu-list button:hover { background: var(--brand-accent-soft); color: var(--brand-accent-text); }
.app-header .header-menu-list a:focus-visible,
.app-header .header-menu-list button:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: -2px; }
.app-header .header-menu-list a[aria-current="page"] { background: var(--brand-grey-100); font-weight: 600; }
/* Een schakelaar die AAN staat, zegt dat ook zonder dat je de tooltip opent. */
.app-header .header-menu-list button[aria-checked="true"],
.app-header .header-menu-list button[aria-pressed="true"] { color: var(--brand-accent-text); font-weight: 600; }

/* De naam bovenin het accountmenu. Geen knop en geen link — het is de bevestiging dat je als de
   juiste persoon bent ingelogd, en op een gedeelde werkplek is dat geen overbodige vraag. */
.app-header .header-menu-list .header-user {
  display: block; padding: 6px 12px 8px; margin-bottom: 4px;
  border-bottom: 1px solid var(--brand-grey-100);
  font-size: 12px; color: var(--brand-ink-subtle); white-space: nowrap;
}
.app-header .header-menu-list .header-user strong { display: block; font-size: 14px; color: var(--brand-ink); }

/* De vuller die de drie menu's naar rechts duwt (zie _header.php). Op de telefoon staat alles onder
   elkaar in de lade en heeft "rechts" geen betekenis — daar valt hij weg. */
.app-header .header-fill { flex: 1 1 auto; }

/* ===== Breakpoint: below 1060px the nav becomes a drawer =====
   HET GETAL IS OPNIEUW GEMETEN OP 09-09-2026, en het stond te laag. Er stond 900px, met als
   onderbouwing "de breedste nav is 696px, dus op 1024 (tablet liggend) past hij op één rij". Die
   696px klopte voor de planningpagina van toen; de breedste nav is `debiteuren/` en die meet
   **886px**. Met logo (64px), de padding (2×24px) en de tussenruimte erbij heeft de balk ~1040px
   nodig. Gemeten resultaat vóór deze wijziging: **drie rijen op élke breedte, ook op 1500px** — de
   claim in spec.md §1c is dus nooit waar geweest.
   Na het opsplitsen van de balk (specs/navigatie.md §V4) is het één rij tot 1100px en twee rijen
   daaronder. Twee rijen maakt de balk 104px hoog, en dat is exact het getal waarmee de drawer
   destijds voor 768px werd verdedigd. Dezelfde regel, dezelfde uitkomst: onder 1060px een lade.
   1060 en niet 1040: 20px marge, zodat één woord extra in een paginalink niet meteen een tweede rij
   kost. Boven 1060px zou verbergen alleen kosten — daar is de balk 60% vol. */
@media (max-width: 1060px) {
  .app-header { gap: 10px; padding: 10px 14px; position: relative; flex-wrap: nowrap; }
  .app-header img { width: 46px; }

  .app-header .header-burger {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; flex: 0 0 auto;   /* 44px = the common minimum for a touch target */
    cursor: pointer; border-radius: var(--brand-radius-sm);
    border: 1px solid var(--brand-grey-300); background: var(--brand-surface); color: var(--brand-ink);
  }
  .app-header .header-burger[aria-expanded="true"] { background: var(--brand-accent-soft); color: var(--brand-accent-text); }

  /* Only hide when there is script (fail-open, see above). */
  .has-js .app-header .header-drawer { display: block; }
  .has-js .app-header .header-drawer .header-nav { display: none; }
  .has-js .app-header .header-drawer.is-open .header-nav {
    display: flex; flex-direction: column; align-items: stretch; gap: 4px;
    position: absolute; left: 0; right: 0; top: 100%; z-index: 40;
    padding: 10px 14px 14px;
    background: var(--brand-surface);
    border-bottom: 1px solid var(--brand-grey-300);
    box-shadow: 0 12px 32px rgba(0, 0, 0, .14);
    max-height: calc(100vh - 70px); overflow-y: auto;   /* short phone: scroll, do not cut off */
  }
  .app-header .header-nav .btn { justify-content: flex-start; width: 100%; padding: 11px 12px; font-size: 15px; }
  /* In de lade staat alles onder elkaar; er is geen "rechts" om iets naartoe te duwen. Een vuller die
     wél rekt zou een gat van een halve schermhoogte tussen de paginalinks en de menu's maken. */
  .app-header .header-fill { display: none; }

  /* Inside the drawer the admin menu expands in the flow; a floating panel on top of a drawer is
     unusable on a phone. */
  .app-header .header-menu { width: 100%; }
  .app-header .header-menu > summary { width: 100%; justify-content: flex-start; padding: 11px 12px; font-size: 15px; }
  .app-header .header-menu-list {
    position: static; min-width: 0; box-shadow: none; border: 0;
    padding: 2px 0 2px 14px; background: none;
  }
  .app-header .header-menu-list a,
  .app-header .header-menu-list button { padding: 10px 12px; font-size: 15px; }
}

/* Fluid shell: use the screen that's actually there. The cap scales with the viewport instead of
   freezing at a fixed pixel width (it used to sit at 1220px on a 1920px screen, leaving ~700px
   unused while column headers wrapped). min() keeps a sane max on ultra-wide displays. */
.app-main { max-width: min(1800px, 96vw); margin: 0 auto; padding: 24px; }
/* On a phone 24px of gutter on both sides is 13% of the screen, and the content inside is what has
   to give it back — that is where labels start wrapping. 96vw + 14px leaves ~346px of content at
   390px instead of ~326px, which is the difference between a pill fitting and not. */
@media (max-width: 640px) { .app-main { max-width: 100%; padding: 16px 14px 24px; } }

/* ===== Tile grid on the start screen (start.php) =====
   The wrapper and intro that used to sit here belonged to processen.php, which is now a 301 to
   start.php; start.php brings its own .st-wrap/.st-intro. The grid and the card did move along and
   are still what draws every tile. */
.proc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.proc-card {
  display: flex; flex-direction: column;
  overflow: hidden;
  text-decoration: none;


  /* Cards stagger in; --proc-i is set per card in the markup. */
  animation: app-card-in var(--brand-t-slow) var(--brand-ease-out) both;
  animation-delay: calc(var(--proc-i, 0) * .07s);
}

/* Media band: an uploaded photo fills it; without one we show the card's Lucide icon on grey. */
.proc-card-media {
  height: 132px;
  background: var(--brand-grey-100);
  display: grid; place-items: center;
  border-bottom: 1px solid var(--brand-grey-200);
  overflow: hidden;
}
.proc-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.proc-card-media svg { width: 44px; height: 44px; stroke-width: 1.25; color: var(--brand-support-text); }
.proc-card:hover .proc-card-media svg { transform: scale(1.06); transition: transform var(--brand-t-base) var(--brand-ease-spring); }

.proc-card-body { padding: 18px 20px 6px; flex: 1; }
.proc-card-body h2 { font-size: 17px; margin-bottom: 6px; }
.proc-card-body p { margin: 0; color: var(--brand-ink-subtle); font-size: 14px; line-height: 1.5; }
.proc-card-foot { padding: 12px 20px 18px; }
.proc-card-cta { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; color: var(--brand-support-text); }
.proc-card-cta .app-icon { transition: transform var(--brand-t-base) var(--brand-ease-out); }
.proc-card:hover .proc-card-cta .app-icon { transform: translateX(3px); }

/* A not-yet-built process stays visible but inert — the roadmap without pretending it works. */
.proc-card.is-disabled { opacity: .62; cursor: not-allowed; box-shadow: none; }
.proc-card.is-disabled:hover { transform: none; box-shadow: none; border-color: var(--brand-grey-200); }
.proc-card.is-disabled .proc-card-media { filter: grayscale(1); }

/* ===== Admin: process-card editor =====
   Each card is a <details>: collapsed on load (a page of open forms is unreadable once there are
   several cards), expanding on click. Native element = works without JS. */
.card-edit {
  margin-bottom: 16px;
  overflow: hidden;
}
.card-edit:hover { border-color: var(--brand-accent); }
/* Open = selected state, and that is blue's job. */
.card-edit[open] { border-color: var(--brand-support); box-shadow: var(--brand-shadow); }
/* A placeholder (no pipeline yet) is visually distinct from a real process card. */
.card-edit.is-placeholder { border-style: dashed; }
.card-edit.is-placeholder[open] { border-style: solid; }

.card-edit-head {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 18px;
  cursor: pointer;
  list-style: none;                 /* hide the default disclosure triangle */
  transition: background var(--brand-t-fast) var(--brand-ease-out);
}
.card-edit-head::-webkit-details-marker { display: none; }
.card-edit-head:hover { background: var(--brand-grey-100); }
.card-edit[open] .card-edit-head { border-bottom: 1px solid var(--brand-grey-200); background: var(--brand-grey-100); }
.card-edit-head-text { flex: 1; min-width: 0; }
.card-edit-toggle {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  font-size: 13px; font-weight: 600; color: var(--brand-support-text);
}
.card-edit-toggle .app-icon { transition: transform var(--brand-t-base) var(--brand-ease-out); }
.card-edit[open] .card-edit-toggle .app-icon { transform: rotate(180deg); }
.card-edit[open] .card-edit-toggle-text { display: none; }
/* The expanded form body. Hidden while collapsed: a <details> whose content carries an
   animation with `both` fill can still be laid out (and stay measurable/tabbable) even when the
   element is closed — so the collapse is stated explicitly here, and the entrance animation is
   scoped to [open] only. */
.card-edit-body { display: none; padding: 18px; }
.card-edit[open] .card-edit-body {
  display: block;
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) both;
}

.card-edit-thumb {
  width: 52px; height: 52px; flex: 0 0 auto;
  border-radius: var(--brand-radius-sm);
  background: var(--brand-support-soft);
  display: grid; place-items: center; overflow: hidden;
}
.card-edit-thumb img { width: 100%; height: 100%; object-fit: cover; }
.card-edit-thumb svg { width: 24px; height: 24px; color: var(--brand-support-text); stroke-width: 1.5; }
.card-edit-key { font-size: 12px; color: var(--brand-ink-subtle); margin-top: 2px; }

/* ── smalle schermen ──────────────────────────────────────────────────────
   Zelfde faalvorm als in beheer/berichten.css: een blok met `flex: 0 0 auto` naast tekst met
   `min-width: 0` geeft nooit toe, dus de tekst krijgt wat overblijft. Gemeten op 390px: de kop is
   324px breed, daarvan gaan er 36 naar padding, 52 naar de miniatuur, 28 naar twee gaps en 85 naar
   "Bewerken ⌄" — er bleef 123px over voor de titel, en die brak in 104px over DRIE regels
   ("UWV werkschoenen aanvraag"). Twee andere kaarten braken over twee.
   Een rij die geen rij meer kan zijn, houdt op er een te zijn: onder 560px zakt "Bewerken" naar een
   eigen regel, rechts uitgelijnd. De titel houdt dan 222px over en de langste (213px) past op één
   regel. Meet je hier ooit opnieuw: de langste titel moet onder .card-edit-head-text passen, niet
   onder de kop als geheel. */
@media (max-width: 560px) {
  .card-edit-head { flex-wrap: wrap; }
  .card-edit-head-text { flex: 1 1 auto; }
  .card-edit-toggle { flex: 1 0 100%; justify-content: flex-end; }
}
/* The "new placeholder card" form — always open, so it does not use the <details> chrome. */
.card-add { border: 2px dashed var(--brand-grey-300); border-radius: var(--brand-radius); padding: 18px; }

.card-edit input[type=file],
.card-add input[type=file] {
  width: 100%; padding: 10px 12px; font: inherit; font-size: 13px;
  border: 1px dashed var(--brand-grey-300); border-radius: var(--brand-radius-sm); background: var(--brand-white);
  color: var(--brand-ink);
}
.card-edit input[type=file]::file-selector-button,
.card-add input[type=file]::file-selector-button {
  font: inherit; font-size: 13px; margin-right: 10px; padding: 6px 12px; cursor: pointer;
  border: 1px solid var(--brand-grey-300); border-radius: var(--brand-radius-sm);
  background: var(--brand-grey-100); color: var(--brand-ink);
}

/* ===== Management overview (admin/overzicht.php) ===== */
/* ÉÉN KOLOM: DE KAARTEN STAAN ONDER ELKAAR (gevraagd 15-09-2026, met een schermafdruk erbij).
 *
 * Hier stond `repeat(auto-fit, minmax(min(340px, 100%), 1fr))`, en dat was goed gemeten toen er TWEE
 * processen waren: twee kaarten die samen de volle breedte vulden. Sinds de ordertrechter zijn het
 * er vijf, en dan legt auto-fit er drie op de eerste rij en twee op de tweede — met een kaart die
 * hoger is dan zijn buren omdat zijn naam over twee regels valt ("Regulier / maatschoenen"). Een
 * rafelrand die per soort order verspringt.
 *
 * ONDER ELKAAR LOST DAT STRUCTUREEL OP: elke soort krijgt dezelfde volle breedte en dezelfde hoogte,
 * de vier getallen staan onder elkaar in één kolom, en er komt er gewoon een regel bij als Grol een
 * soort toevoegt. Dat laatste is de reden dat dit geen tijdelijke keuze is — het aantal groeit.
 *
 * De oude afweging voor het geval iemand terug wil naar kolommen: met een VAST maximum (er stond
 * 560px) vullen de kaarten de breedte niet en eindigen ze links van het paneel eronder — gemeten
 * 156px rafelrand op 1400 en 348px op 1600. Gebruik dan 1fr, nooit een vast getal. */
.ov-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-bottom: 24px; }
/* Surface and interaction come from the shared branding card foundation. */
.ov-card {
  display: block; text-decoration: none;
  animation: app-rise-in var(--brand-t-slow) var(--brand-ease-out) backwards;
  animation-delay: calc(var(--ov-i, 0) * 70ms);
}
.ov-grid > .ov-card:nth-child(2) { --ov-i: 1; }
.ov-grid > .ov-card:nth-child(3) { --ov-i: 2; }
.ov-grid > .ov-card:nth-child(4) { --ov-i: 3; }
.ov-grid > .ov-card:nth-child(n + 5) { --ov-i: 4; }
.ov-card-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.ov-card-head h2 { font-size: 16px; }
.ov-card-cta { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--brand-support-text); white-space: nowrap; }
.ov-card-cta .app-icon { transition: transform var(--brand-t-base) var(--brand-ease-out); }
.ov-card:hover .ov-card-cta .app-icon { transform: translateX(3px); }
/* De vier getallen. Op een smal scherm verdelen ze de breedte; vanaf 700px krijgen ze een PLAFOND.
   Zonder dat plafond werd elk vakje een kwart van een kaart die sinds 15-09-2026 de volle breedte
   heeft — vier velden van 300px met één cijfer erin, en dan leest de rij niet meer als een rij. */
.ov-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }

@media (min-width: 700px) {
  .ov-kpis { grid-template-columns: repeat(4, minmax(0, 150px)); justify-content: start; }
}
.ov-kpi {
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
}
.ov-kpi-icon { width: 16px; height: 16px; stroke-width: 1.75; color: var(--brand-ink-subtle); margin-bottom: 4px; }
/* Same figure treatment as the dashboard KPI, one size down: heading face, tabular, tight — four
   tiles side by side only read as one instrument if their numbers share a column width. */
.ov-kpi-val {
  display: block; font-family: var(--brand-font-heading);
  font-size: 25px; font-weight: 700; line-height: 1.1;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums;
}
/* One line, always — a wrapped caption in a fixed tile reads as broken. If the container is too
   narrow for the word it truncates with an ellipsis (the title tooltip carries the full meaning). */
.ov-kpi-label {
  display: block; max-width: 100%;
  font-size: 11px; color: var(--brand-ink-subtle); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ov-kpi.is-warn .ov-kpi-icon { color: var(--brand-warning); }
.ov-kpi.is-breach .ov-kpi-icon { color: var(--brand-danger); }
.ov-kpi.is-warn { background: var(--brand-warning-soft); }
.ov-kpi.is-warn .ov-kpi-val { color: var(--brand-warning); }
.ov-kpi.is-breach { background: var(--brand-danger-soft); }
.ov-kpi.is-breach .ov-kpi-val { color: var(--brand-danger); }

/* Per-process status line under the KPI tiles: the verdict in words ("Alles op schema" /
   "1 over de norm"), so the green half of the scale is visible on the cockpit itself. */
.ov-status {
  display: flex; align-items: center; gap: 8px;
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--brand-rule);
  font-size: 13px; font-weight: 600;
}
.ov-status .app-icon { width: 16px; height: 16px; flex: 0 0 auto; }
.ov-status.is-ok { color: var(--brand-success); }
.ov-status.is-warn { color: var(--brand-warning); }
.ov-status.is-breach { color: var(--brand-danger); }

/* nowrap so the owner reads as one name, plus the ellipsis that has to come with it: without the
   overflow pair a long name simply runs off the right edge of the panel. */
.ov-attn-owner { color: var(--brand-ink-subtle); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

/* Severity legend — the scale, stated once. */
.ov-legend { display: flex; flex-wrap: wrap; gap: 18px; margin: 0 0 20px 2px; font-size: 12px; color: var(--brand-ink-subtle); }
.ov-legend-item { display: inline-flex; align-items: center; gap: 7px; }
.ov-legend .dot { width: 10px; height: 10px; border-radius: 999px; flex: 0 0 auto; }
.ov-legend .dot-ok { background: var(--brand-success); }
.ov-legend .dot-warn { background: var(--brand-warning); }
.ov-legend .dot-breach { background: var(--brand-danger); }

.ov-attention { display: flex; flex-direction: column; }
.ov-attn-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 13px; text-decoration: none; color: inherit;
  border-top: 1px solid var(--brand-rule);
}
.ov-attn-row:first-child { border-top: none; }
.ov-attn-row:hover { transform: translateX(3px); background: var(--brand-grey-100); }
/* Row-by-row reveal, 24ms apart — the same dense interval as the dashboard's attention strip. */
.ov-attention .ov-attn-row {
  animation: app-step-in var(--brand-t-base) var(--brand-ease-out) backwards;
  animation-delay: calc(.18s + var(--attn-i, 0) * 24ms);
}
.ov-attention .ov-attn-row:nth-child(2)  { --attn-i: 1; }
.ov-attention .ov-attn-row:nth-child(3)  { --attn-i: 2; }
.ov-attention .ov-attn-row:nth-child(4)  { --attn-i: 3; }
.ov-attention .ov-attn-row:nth-child(5)  { --attn-i: 4; }
.ov-attention .ov-attn-row:nth-child(6)  { --attn-i: 5; }
.ov-attention .ov-attn-row:nth-child(7)  { --attn-i: 6; }
.ov-attention .ov-attn-row:nth-child(8)  { --attn-i: 7; }
.ov-attention .ov-attn-row:nth-child(n + 9) { --attn-i: 8; }
.ov-attn-row .req-code { font-weight: 600; color: var(--brand-support-text); }
.ov-attn-who { flex: 1; min-width: 0; }
.ov-attn-row .pill:last-child { margin-left: auto; }

/* Phone: the four parts stack instead of competing for one line.
   As one flex row this collapsed exactly where it hurts most — the two pills and the code will not
   shrink, so the only flexible part (customer — stage) was squeezed to ~100px and broke into one
   word per line, while the nowrap owner ran off the edge. Below this width there is no arrangement
   in which all four fit side by side, so the row becomes a small card:
     line 1: which process · which order      line 2: who · which step
     line 3: whose desk it is on              line 4: why it is here (the severity pill)
   Severity last and on its own line: it is the longest string and the reason you are reading. */
@media (max-width: 640px) {
  .ov-attn-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
    gap: 6px 8px;
  }
  .ov-attn-row > .pill:first-child { grid-column: 1; justify-self: start; }
  .ov-attn-row .req-code { grid-column: 2; justify-self: start; }
  .ov-attn-who { grid-column: 1 / -1; }
  .ov-attn-owner { display: block; margin-top: 2px; }
  .ov-attn-row .pill:last-child { grid-column: 1 / -1; justify-self: start; margin-left: 0; }
}

/* ===== Icon picker (admin: choose a card icon) ===== */
.label-hint { font-weight: 400; color: var(--brand-ink-subtle); }
.icon-trigger {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 10px 14px; font: inherit; text-align: left; cursor: pointer;
  border: 1px solid var(--brand-grey-300); border-radius: var(--brand-radius-sm); background: var(--brand-white);
  color: var(--brand-ink);
  transition: border-color var(--brand-t-fast) var(--brand-ease-out), box-shadow var(--brand-t-base) var(--brand-ease-out);
}
.icon-trigger:hover { border-color: var(--brand-accent); }
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
.icon-trigger:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft); }
.icon-trigger-preview {
  display: grid; place-items: center; width: 38px; height: 38px; flex: 0 0 auto;
  border-radius: var(--brand-radius-sm); background: var(--brand-support-soft); color: var(--brand-support-text);
}
.icon-trigger-preview svg { width: 20px; height: 20px; stroke-width: 1.75; }
.icon-trigger-name { flex: 1; font-size: 13px; color: var(--brand-ink-muted); }
.icon-trigger-cta { font-size: 13px; font-weight: 600; color: var(--brand-support-text); }

#iconSearch {
  width: 100%; padding: 11px 14px; font: inherit;
  border: 1px solid var(--brand-grey-300); border-radius: var(--brand-radius-sm);
  background: var(--brand-white); color: var(--brand-ink);
}
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
#iconSearch:focus { outline: none; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft); }
#iconSearch:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
.icon-count { font-size: 12px; color: var(--brand-ink-subtle); margin: -8px 0 12px; }
.icon-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 8px;
  max-height: 46vh; overflow-y: auto; padding-right: 4px;
}
.icon-cell {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 6px; font: inherit; cursor: pointer;
  border: 1px solid var(--brand-grey-200); border-radius: var(--brand-radius-sm); background: var(--brand-white);
  color: var(--brand-ink);
  transition: border-color var(--brand-t-fast) var(--brand-ease-out),
              background var(--brand-t-fast) var(--brand-ease-out),
              transform var(--brand-t-fast) var(--brand-ease-out);
}
.icon-cell:hover { border-color: var(--brand-accent); background: var(--brand-surface-alt); transform: translateY(-1px); }
.icon-cell:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
.icon-cell svg { width: 22px; height: 22px; stroke-width: 1.6; color: var(--brand-support-text); }
.icon-cell span {
  font-size: 10px; color: var(--brand-ink-subtle); line-height: 1.25; text-align: center;
  /* Long names must not stretch the cell — clamp to two lines. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  word-break: break-word;
}

/* ===== Crop dialog + result preview ===== */
.crop-hint { font-size: 13px; color: var(--brand-ink-subtle); margin: 0 0 14px; }
.crop-stage { max-height: 52vh; background: var(--brand-grey-100); border-radius: var(--brand-radius-sm); overflow: hidden; }
.crop-stage img { max-width: 100%; display: block; }
.crop-result { margin-top: 12px; }
.crop-result-label { display: block; font-size: 12px; color: var(--brand-ink-subtle); margin-bottom: 6px; }
.crop-result img {
  display: block; width: 100%; max-width: 330px; height: auto;
  border: 1px solid var(--brand-grey-200); border-radius: var(--brand-radius-sm);
}
.crop-result .btn { margin-top: 8px; }

/* An order code is one token: "GORD-000015" broken after the hyphen reads as two things, and it is
   the handle people quote on the phone. Every list that shows one relies on this. */
.req-code { white-space: nowrap; }

/* ===== Pills / badges =====
   Solid light fill, dark text. Never a translucent tint — rgba(brand,.16) over white is a pastel
   that no label can sit on. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  /* "4/5 d" and "11/9 d" have to line up when they sit under each other on a board column — a pill
     full of proportional digits reads as a different size every row. */
  font-variant-numeric: tabular-nums;
  background: var(--brand-support-soft);
  color: var(--brand-support-text);
}
/* ---- Severity scale: the "don't worry → worry" language (one system, reused everywhere) ----
   ok     green  — on track, comfortably within the step norm ("op schema")
   warn   amber  — approaching the norm (step in before it breaches)
   breach red    — over the norm
   These map 1:1 to request.alert_level from appReqDecorate(). Colour is used ONLY where it guides
   action; calm items that aren't monitored stay neutral (no pill). Restrained on purpose — the
   brand is rustig/medisch, so red/amber mean "look here", not decoration. */
.pill-ok    { background: var(--brand-success-soft); color: var(--brand-success); }
.pill-warn  { background: var(--brand-warning-soft); color: var(--brand-warning); }
.pill-alert { background: var(--brand-danger-soft);  color: var(--brand-danger); }
/* Beheerder marker in the Medewerkers list — neutral, not a status colour: it labels what the
   account IS, it is not a warning. */
.pill-admin { background: var(--brand-grey-100); color: var(--brand-ink); }
/* Visible only to a screen reader. Used to announce what a sighted user reads from context but a
   screen-reader user cannot — e.g. that a link opens in a new tab. Deliberately NOT display:none:
   that would hide it from assistive technology too, which is the opposite of the point. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Limited access in the Medewerkers list ("3 van 6 onderdelen"). Neutral, just like pill-admin: it
   describes a setting, it is not a warning. Solid fill, dark text — a translucent tint over white
   yields a pastel on which no label is legible. */
.pill-limited { background: var(--brand-grey-100); color: var(--brand-ink); }

/* Access modal: one line per tile, checkbox on the left. No columns — at six to ten tiles a list is
   faster to scan than a grid, and it does not break on a narrow screen. */
.cards-rights { display: flex; flex-direction: column; gap: 2px; margin: 10px 0; }
.cards-rights-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;   /* a tile name must never fall across two lines */
}
.cards-rights-row:hover { background: var(--brand-grey-050, var(--brand-grey-100)); }

/* De rechtenmodal sinds v65 (specs/toegang_financieel.md): een recht kan uitleg dragen, een einddatum
   en een reden. De rij blijft één regel breed; de uitleg staat eronder en mag wél wikkelen — dat is
   een zin, geen label. */
.cards-rights-row { align-items: flex-start; }
.cards-rights-row > span { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; }
.cards-rights-uitleg { margin: 0; font-size: 12px; line-height: 1.4; text-wrap: pretty; }

.cards-rights-kop {
  margin: 12px 0 2px;
  font-size: 12px;
  font-weight: 600;
  color: var(--brand-ink-soft, var(--brand-ink));
  text-transform: none;
}

.cards-rights-tot,
.cards-rights-reden {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--brand-ink-soft, var(--brand-ink));
  white-space: nowrap;      /* "tot" en "Waarom" zijn labels en wikkelen nooit */
}
.cards-rights-tot input { width: 104px; }
.cards-rights-reden { margin-top: 10px; }
.cards-rights-reden input { flex: 1 1 auto; min-width: 0; }

/* De functieknoppen bovenin de rechtenmodal (specs/navigatie.md §V3). Een rij die mag afbreken: bij
   drie functies past hij op één regel, bij zes op twee — en een knoplabel dat over twee regels valt
   is erger dan een tweede regel knoppen. */
.cards-functies { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 6px; }
.cards-functies .btn { white-space: nowrap; }

/* WELKE FUNCTIE JE NET KOOS, MOET JE ZIEN STAAN — ook nadat de focus weg is.
   `is-current` was hier eerst de enige markering, en die klasse is alleen gestyled BINNEN de balk
   (`.app-header .btn-ghost.is-current`). In een modal deed hij dus niets: je klikte op Aanmeter, de
   vinkjes sprongen om, en zodra je ergens anders klikte was er geen enkel spoor meer van welke set je
   had gekozen. Dan lees je twaalf vinkjes terug om te weten waar je bent.
   Vulling ÉN randkleur, want dit staat naast twee knoppen die er verder identiek uitzien. (`.btn`
   is al 600, dus font-weight voegt hier niets toe — gemeten, niet aangenomen.) */
.cards-functies .btn.is-current {
  background: var(--brand-accent-soft);
  border-color: var(--brand-accent-deep);
  color: var(--brand-accent-text);
  font-weight: 600;
}
.cards-functies-uitleg { margin: 0 0 8px; font-size: 12px; line-height: 1.4; text-wrap: pretty; }

/* "Niemand kan hier nog bij" op het medewerkersscherm. Bewust de ondersteunende kleur en geen rood:
   er is niets stuk en er gaat vandaag niets mis. Het is een constatering waar een mens iets mee moet,
   en rood bewaren we voor wat echt fout is — anders neemt niemand de volgende melding serieus. */
.wees-rechten {
  margin: 0 0 14px;
  padding: 12px 14px;
  border-left: 3px solid var(--brand-support-text);
  border-radius: var(--brand-radius-sm);
  background: var(--brand-grey-100);
  color: var(--brand-ink);
  font-size: 13px; line-height: 1.5; text-wrap: pretty;
}
.wees-rechten[hidden] { display: none; }

/* De uitleg onder het redenveld is een toelichting, geen inhoud: klein en gedimd, net als elke
   andere .form-help. Binnen deze modal erfde hij de body-grootte en las daardoor als een alinea. */
.cards-rights .form-help { font-size: 12px; line-height: 1.45; margin: 4px 0 0; }

/* Een recht dat je via een ander recht krijgt: aangevinkt, op slot, en zichtbaar anders. */
.cards-rights-row input:disabled { opacity: .55; }
.cards-rights-row:has(input:disabled) { opacity: .8; }
.cards-rights-row input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--brand-accent); flex: 0 0 auto; }
/* Alleen het vinkje is 16x16 — het datumveld ernaast is een gewoon tekstveld en werd hierdoor
   een onbruikbaar blokje van zestien pixels. */

/* Legacy aliases kept so older markup keeps rendering the same colour. */
.pill-wait  { background: var(--brand-warning-soft); color: var(--brand-warning); }
.pill-done  { background: var(--brand-success-soft); color: var(--brand-success); }
.pill-hoog  { background: var(--brand-danger-soft);  color: var(--brand-danger); }

/* ===== Worker view ===== */
/* Two panels (new request + recent list) side by side on wide screens; stacked on narrow. The
   recent list gets the wider column so the code + name + shoe type + stage pill fit on one line. */
/* Reading-comfort cap, not a hard pixel freeze: the intake column may grow with the viewport
   (fields were stuck at ~197px on a 1920px screen), the list takes the rest. */
.worker-wrap { max-width: min(1500px, 100%); margin: 0 auto; }
.worker-wrap .worker-cols {
  display: grid;
  grid-template-columns: minmax(420px, 34%) minmax(0, 1fr);
  gap: 24px; align-items: start;
}
.worker-wrap .worker-cols > .panel { margin-bottom: 0; }
@media (max-width: 1100px) { .worker-wrap .worker-cols { grid-template-columns: minmax(0, 380px) minmax(0, 1fr); } }
@media (max-width: 900px)  { .worker-wrap .worker-cols { grid-template-columns: minmax(0, 1fr); } }
/* DEPTH INSTEAD OF A BORDER (motion.css, .m-surface). A 1px grey line and a layered shadow both
   separate the panel from the canvas; only one of them reads as a surface you could pick up. The
   hairline stays as a 1px INSET ring in the shadow stack — it costs no layout box, so removing the
   border changes nothing about where the content sits, and it keeps an edge on screens (and
   projectors) that swallow a soft shadow. */
.panel {
  margin-bottom: 24px;
  animation: app-rise-in var(--brand-t-slow) var(--brand-ease-out) backwards;
}
/* Panels arrive one after the other, not as one block — the single technique that separates a page
   that was revealed from a page that was switched on. 60ms apart, capped at the fourth so a screen
   full of panels cannot turn into a slide show. Same interval family as motion.css's m-stagger. */
.worker-cols > .panel:nth-child(2),
.app-main > .panel:nth-of-type(2) { animation-delay: .06s; }
.app-main > .panel:nth-of-type(3) { animation-delay: .12s; }
.app-main > .panel:nth-of-type(n + 4) { animation-delay: .18s; }
/* On a phone 24px of panel padding sits inside 14px of page gutter: 76px of the 390px screen is
   margin before any content starts. */
@media (max-width: 640px) { }
/* balance, not pretty: when a heading has to break anyway, split it evenly rather than leaving one
   orphaned word on line two. */
/* h1 EN h2 met dezelfde maat, met opzet. Elk scherm draagt sinds 24-08-2026 zijn eigen naam als
   <h1> — de koptekstbalk is chroom en noemt de pagina niet (zie _header.php), dus zonder dit begon
   iedere pagina bij <h2> en had een schermlezer geen enkel aanknopingspunt. Alleen het NIVEAU is
   veranderd, niet het uiterlijk: een h1 die ineens twee keer zo groot is, zou een toegankelijkheids-
   verbetering zijn die eruitziet als een ontwerpfout. Panelen verderop op dezelfde pagina blijven
   h2 en zien er identiek uit. */
.panel h1, .panel h2 { font-size: 18px; margin-bottom: 4px; text-wrap: balance; }
.panel .panel-sub { color: var(--brand-ink-subtle); font-size: 14px; margin: 0 0 20px; }

/* A panel heading with its action(s) on the right: <div class="panel-head"><div>title+sub</div><button>.
   The class was used in seven pages and styled in NONE of them, so the action fell BELOW the sub text
   and, because .panel-sub owns the only bottom margin, sat flush against the content underneath —
   the "+ Verrichting" button in catalogus was touching the KLANTAFSPRAKEN label. The margin moves to
   the head itself so a head with no action keeps exactly the same 20px it had before.
   Same shape as .ord-head / .doc-meta-head; those stay as they are (they carry extra local rules). */
.panel .panel-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.panel .panel-head > div:first-child { flex: 1 1 320px; min-width: 0; }
.panel .panel-head .panel-sub { margin-bottom: 0; text-wrap: pretty; }
/* Never let an action wrap onto a second line — shorten the label instead. */
.panel .panel-head > .btn,
.panel .panel-head > form,
.panel .panel-head > .deb-search,
.panel .panel-head > .deb-hoofdacties { flex: 0 0 auto; }
.panel .panel-head > .btn { white-space: nowrap; }

/* ---- bedragen dicht (specs/toegang_financieel.md §3a) -----------------------------------------
   `.app-geld` staat om élk bedrag op het scherm. Staat `geld-verborgen` op <html> — gezet vóór de
   eerste verf, zie appHeadAssets() — dan komt er een rij bolletjes overheen te liggen.
   OVERHEEN EN NIET IN PLAATS VAN, met opzet. `display: none` of een kortere tekst laat de kolom
   springen zodra je hem opent, en dan verschuift de hele lijst onder je handen. Het element houdt
   dus zijn eigen breedte; alleen de inkt gaat eruit.

   `color: transparent` en niet `visibility: hidden`, want de ::after moet er nog overheen kunnen
   liggen. `user-select: none` erbij: zonder dat sleep je met de muis zo de onzichtbare cijfers naar
   het klembord, en dan is het bolletje theater. Dit is presentatie, geen beveiliging — dat blijft
   het omzetrecht, dat het bedrag al bij de server tegenhoudt. */
html.geld-verborgen .app-geld {
  position: relative;
  color: transparent;
  user-select: none;
}
html.geld-verborgen .app-geld::after {
  content: "\2022\2022\2022\2022\2022";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  /* Bedragen staan rechts uitgelijnd, dus de bolletjes ook: anders verspringt de leesrand van de
     kolom zodra je hem dichtklapt. `inherit` volgt de tegel, die links uitlijnt. */
  justify-content: inherit;
  letter-spacing: 2px;
  /* Bewust gedempt en niet zwart: het is geen waarde maar de mededeling dat er een waarde is.
     --brand-ink-subtle haalt AA op --brand-surface; zie tests/contrast/. */
  color: var(--brand-ink-subtle);
}
/* De KPI-tegel lijnt links uit en zet het bedrag op 30px; daar hoort een grotere stip bij. */
html.geld-verborgen .kpi-val.app-geld::after { justify-content: flex-start; font-size: 24px; }
/* Een cel in een lijst lijnt rechts uit. `.deb-bedrag` staat er niet meer bij: dat was de kolom
   Bedrag in de uitklap van de {clienten}lijst, en die is op 09-09-2026 weggehaald. Het prijsboek
   draagt die klasse nog wel, maar zonder `.app-geld` — daar hoort geen masker. */
html.geld-verborgen .ord-bedrag.app-geld::after { justify-content: flex-end; }

/* ---- intake wizard (worker "Nieuwe aanvraag" form: klant -> schoentype -> overzicht) ---- */
.wiz-steps {
  display: flex; gap: 4px; list-style: none; margin: 0 0 20px; padding: 4px;
  background: var(--brand-grey-100); border-radius: var(--brand-radius-sm);
}
.wiz-step-tab {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 9px 8px; border-radius: var(--brand-radius-sm); font-size: 13px; font-weight: 600;
  white-space: nowrap;
  color: var(--brand-ink-subtle); cursor: pointer;
  transition: background var(--brand-t-fast) var(--brand-ease-out),
              color var(--brand-t-fast) var(--brand-ease-out),
              box-shadow var(--brand-t-base) var(--brand-ease-out),
              transform var(--brand-t-base) var(--brand-ease-out);
}
.wiz-step-tab:hover { color: var(--brand-ink); background: var(--brand-white); }
/* Phone: the number moves above the label instead of beside it. Side by side the three tabs need
   ~325px of content that cannot shrink (flex:1 still floors at min-content, and the label is
   nowrap), so the strip ran off a 320px screen. Stacked they need ~215px — and both the number,
   which is what carries the done-state, and the whole word survive. */
@media (max-width: 420px) {
  .wiz-steps { gap: 2px; }
  .wiz-step-tab { flex-direction: column; gap: 4px; padding: 8px 4px; font-size: 12px; min-width: 0; }
}
.wiz-step-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 600;
  background: var(--brand-grey-200); color: var(--brand-ink-muted);
}
/* The step you are on is a selection, so it is blue — black on brand blue is 8.32:1. The tab itself
   is a raised chip: it sits ABOVE the grey track rather than being a lighter patch of it, which is
   what makes advancing through the wizard read as movement. */
.wiz-step-tab.active {
  background: var(--brand-white); color: var(--brand-ink);
  box-shadow: var(--brand-shadow), inset 0 0 0 1px var(--brand-grey-200);
}
.wiz-step-tab.active .wiz-step-num { background: var(--brand-support); color: var(--brand-on-support); }
.wiz-step-tab.is-done .wiz-step-num { background: var(--brand-success); color: var(--brand-on-dark); animation: app-pop var(--brand-t-base) var(--brand-ease-spring); }
.wiz-step-num { transition: background var(--brand-t-base) var(--brand-ease-out), color var(--brand-t-base) var(--brand-ease-out); }
/* Each pane slides in as you advance, so the wizard reads as forward movement. */
.wiz-pane { animation: app-pane-in var(--brand-t-base) var(--brand-ease-out) both; }
.wiz-nav { display: flex; justify-content: space-between; align-items: center; margin-top: 22px; }
.wiz-summary { margin-bottom: 18px; }
.wiz-summary .req-detail-row { display: flex; gap: 8px; font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--brand-rule); }
.wiz-summary .req-detail-row .k { color: var(--brand-ink-subtle); min-width: 130px; }

.form-row { margin-bottom: 18px; }
.form-row label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--brand-ink); }
/* Help text under a field (rename modal): explains a consequence, not a validation error. */
.form-row .form-help { margin: 6px 0 0; font-size: 12px; line-height: 1.45; color: var(--brand-ink-subtle); }
.form-row input[type=text],
.form-row input[type=email],
.form-row input[type=tel],
.form-row textarea,
/* De toelichting bij de intakecheck staat NIET in een .form-row en kreeg daardoor niets van deze
   opmaak mee — hij viel terug op het standaardvak van de browser (dikke ingezette rand, eigen
   lettertype). Toegevoegd aan dezelfde regel in plaats van hem te kopiëren, zodat de twee niet
   uit elkaar kunnen lopen. */
.ic-note {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  font: inherit;
  color: var(--brand-ink);
  background: var(--brand-white);
}
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
.form-row input:focus, .form-row textarea:focus, .ic-note:focus {
  outline: none; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft);
}
.form-row input:focus-visible, .form-row textarea:focus-visible, .ic-note:focus-visible {
  outline: 2px solid var(--brand-support-text); outline-offset: 2px;
}
.form-row textarea, .ic-note { min-height: 72px; resize: vertical; }

/* Invalid field + its inline message. Colour is never the only signal — the message states the
   problem in words, and aria-invalid carries it to screen readers. */
.form-row input.is-invalid,
.form-row select.is-invalid,
.form-row textarea.is-invalid,
.ic-note.is-invalid,
.detail-edit input.is-invalid {
  border-color: var(--brand-danger);
  background: var(--brand-danger-soft);
}
.form-row input.is-invalid:focus,
.form-row textarea.is-invalid:focus,
.ic-note.is-invalid:focus,
.detail-edit input.is-invalid:focus {
  border-color: var(--brand-danger);
  box-shadow: 0 0 0 3px var(--brand-danger-soft);
}
.field-error {
  display: flex; align-items: center; gap: 6px;
  margin-top: 6px;
  font-size: 12px;
  color: var(--brand-danger);
  animation: app-rise-in var(--brand-t-fast) var(--brand-ease-out) both;
}
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 560px) { .form-grid { grid-template-columns: 1fr; } }

/* new/existing toggle */
.choice-row { display: flex; gap: 10px; }
.choice-row .choice {
  flex: 1;
  text-align: center;
  cursor: pointer;
  font-weight: 600;
  white-space: nowrap;
}
.choice-row .choice.active { border-color: var(--brand-support); background: var(--brand-support-soft); color: var(--brand-ink); }

/* shoe-type pick cards (pizza-style) */
.shoe-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.shoe-card {
  cursor: pointer;
  text-align: center;
}
/* Selecting a type pops once — the confirmation you picked it, without a separate message. */
.shoe-card.active {
  border-color: var(--brand-support); background: var(--brand-support-soft);
  animation: app-pop var(--brand-t-base) var(--brand-ease-spring);
}
.shoe-card .shoe-icon { color: var(--brand-support-text); margin-bottom: 8px; transition: transform var(--brand-t-base) var(--brand-ease-spring); }
.shoe-card:hover .shoe-icon, .shoe-card.active .shoe-icon { transform: scale(1.1); }
.shoe-card .shoe-icon svg { width: 26px; height: 26px; stroke-width: 1.5; }
/* A type name does not fit on one line in a 182px card ("Semi-orthopedische veiligheidsschoen" is
   35 characters), so here a wrap is genuinely unavoidable rather than sloppy copy. `balance` makes
   the browser spread the words evenly over the lines instead of leaving one word stranded on the
   last one. */
.shoe-card .shoe-label { font-size: 13px; font-weight: 600; color: var(--brand-ink); text-wrap: balance; }
/* Second layer (shaft height) sits a touch tighter than the type grid, under its own label. */
.shaft-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); margin-top: 4px; }

/* admin: shoe-type management (admin/schoentypes.php) */
.st-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 26px; }
/* One row per shoe type: name, toggle and both buttons side by side. It used to be two forms
   stacked (edit + activate), which cost half a screen height per type and made the Deactiveren
   button read as if it belonged to the next row. The forms stay separate — otherwise "Opslaan"
   would submit the toggle along with it — but now sit in the same flex row. */
.st-row {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px;
}
.st-row.is-inactive { background: var(--brand-surface-alt); opacity: .8; }
/* Fit the fields to the panel, including on desktops where the shell caps its width. */
.st-row-edit {
  display: flex; align-items: flex-end; gap: 14px;
  flex: 1 1 100%; flex-wrap: wrap; min-width: 0;
}
.st-label-field { display: flex; flex-direction: column; gap: 4px; flex: 1 1 240px; min-width: 180px; }
.st-field-cap { font-size: 12px; font-weight: 600; color: var(--brand-ink-subtle); white-space: nowrap; }
/* The input itself carries `.g-field` from branding.css — the shared field definition. This held
   only `width: 100%`, and that is exactly how this field was left sitting on the browser default
   unnoticed: a square box with a thin grey border, among nothing but rounded brand fields. So do
   not put a border/radius/padding here — then the field definition would exist in two places. */
.st-label-field .g-field { width: 100%; }
.st-check { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; color: var(--brand-ink); cursor: pointer; user-select: none; white-space: nowrap; }
/* Deep blue so the browser's white checkmark stays legible in the checked box. */
.st-check input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--brand-support-deep); cursor: pointer; }
/* Now sits INSIDE the row, right next to Opslaan — no longer on a line of its own below it. */
.st-row-toggle { display: flex; align-items: center; flex: 0 0 auto; margin: 0; }
/* De drie korte velden per voorziening (v87): kleur, icoon en checklist. Ze krijgen NIET de 420
   pixels van het naamveld — een kleurkeuze is één woord, en met vijf velden van die breedte past de
   rij op geen enkel scherm. */
.st-label-field.st-smal { flex: 0 1 150px; min-width: 120px; }
.st-add { border: 2px dashed var(--brand-grey-300); border-radius: var(--brand-radius); padding: 18px; }
.st-add h3 { font-size: 15px; margin-bottom: 12px; }
.st-add-row { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.st-proc { padding: 16px 0; border-top: 1px solid var(--brand-grey-200); }
.st-proc:first-of-type { border-top: 0; padding-top: 4px; }
.st-proc-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.st-proc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px 20px; }
/* HIER MAG EEN NAAM WEL OVER TWEE REGELS. `.st-check` staat op `nowrap`, en dat klopt op een rij met
   korte teksten — maar "Orthopedische voorzieningen aan confectieschoenen (OVAC)" past in geen
   enkele kolom van 240 pixels en liep dwars over zijn buurman heen. Twee namen over elkaar heen is
   erger dan één naam op twee regels: overlappende tekst leest als een kapot scherm. */
.st-proc-grid .st-check { align-items: flex-start; white-space: normal; }
.st-proc-grid .st-check input[type=checkbox] { flex: 0 0 auto; margin-top: 1px; }
.st-proc-grid .st-check span { text-wrap: pretty; }

/* ===== Workflow editor (beheer/processen.php) ===== */
/* One card per step. Density is the whole problem here: a pipeline is 10-12 steps and each carries
   six fields, so a naive stacked form is four screens of scrolling and the ORDER — the thing this
   screen exists to control — becomes invisible. Hence a header line you can scan (number, name,
   tools) with the fields in a grid underneath. */
.wf-procs { display: flex; flex-wrap: wrap; gap: 8px; }
/* A panel that is a problem report. `.panel` carries no border any more (its hairline lives in the
   shadow stack), so the red edge has to be stated as a ring here — a `border-color` alone would
   have coloured a border that no longer exists and this warning would have lost its outline. */
.wf-issues {
  border-color: var(--brand-danger);
  background: var(--brand-danger-soft);
  box-shadow: inset 0 0 0 1px var(--brand-danger), var(--brand-shadow);
}
.wf-issues h2 { color: var(--brand-danger); }
.wf-issues h2::after { background: var(--brand-danger); }
.wf-issues ul { margin: 0; padding-left: 20px; color: var(--brand-ink); font-size: 14px; line-height: 1.6; }

.wf-step {
  border: 1px solid var(--brand-grey-200);
  border-radius: var(--brand-radius-sm);
  background: var(--brand-surface);
  padding: 12px 16px 14px;
  margin-bottom: 10px;
}
.wf-step.is-inactive { background: var(--brand-surface-alt); opacity: .75; }

/* ---------- Reordering by dragging ----------
   The grip is the ONLY drag handle: the card carries a <details> and selectable text, and making the
   whole thing draggable would fight both. It is a character, not a Lucide icon, for the same reason
   as the ▲/▼ buttons — a control that disappears when a script fails is no control. */
.wf-grip {
  flex: 0 0 auto;
  /* --brand-grey-300 is a BORDER token: as ink on white it is 1.48:1, so the only drag handle on
     this screen was invisible until the pointer was already on it — discoverable only by accident.
     --brand-ink-subtle is 5.33:1, past the 3:1 floor for a non-text control, and still the app's
     quiet caption grey so it does not shout over the step title. Hover goes to full black. */
  color: var(--brand-ink-subtle);
  font-size: 15px; line-height: 1;
  padding: 2px 2px;
  user-select: none;                 /* dragging the grip must never start a text selection */
  cursor: grab;
}
.wf-step:hover .wf-grip { color: var(--brand-ink); }
.wf-grip:active { cursor: grabbing; }

/* The card being dragged: faded, so the gap it leaves behind is what the eye follows. */
.wf-step.is-dragging { opacity: .45; }
/* Where it will land. A gold rule, because gold is this app's marker for a line and a section
   edge — the drop position IS a line. */
.wf-step.is-drop-target { box-shadow: 0 -2px 0 0 var(--brand-accent); }

.wf-draghint { margin: -4px 0 12px; font-size: 12px; color: var(--brand-ink-subtle); }

/* Reduced motion is hardcoded off in the internal apps (see spec), so no transition is added here
   that would need suppressing. */

.wf-step-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Tabular numerals + a fixed box: the step numbers line up as a column, which is what makes a
   reorder readable at a glance instead of something you have to re-count. */
.wf-step-nr {
  flex: 0 0 auto; min-width: 26px; height: 26px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--brand-grey-100);
  font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--brand-ink);
}
.wf-step-name { font-size: 15px; min-width: 0; }
.wf-step-tools { display: flex; align-items: center; gap: 4px; margin-left: auto; flex: 0 0 auto; }
.wf-step-tools form { margin: 0; }
.wf-step-tools .btn[disabled] { opacity: .3; pointer-events: none; }

/* Collapsed by default, because 12 steps × six fields is four screens of form and the order scrolls
   away. The summary is not a bare "Bewerken": it repeats the step's settings in words, so the common
   question ("staat deze op zichtbaar? hoe lang duurt hij?") is answered without opening anything. */
.wf-step-details > summary {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  margin-top: 8px; padding: 4px 0 0 36px;   /* 36px = step-number column, so the text lines up */
  cursor: pointer; list-style: none;
  font-size: 13px; color: var(--brand-ink-subtle);
}
.wf-step-details > summary::-webkit-details-marker { display: none; }
.wf-summary-text { min-width: 0; }
/* The affordance. Underlined rather than coloured alone — "you can click this" must not depend on
   colour perception. */
.wf-summary-cue {
  margin-left: auto; flex: 0 0 auto; white-space: nowrap;
  font-weight: 600; color: var(--brand-support-text);
  text-decoration: underline; text-underline-offset: 2px;
}
.wf-cue-close, .wf-step-details[open] > summary .wf-cue-open { display: none; }
.wf-step-details[open] > summary .wf-cue-close { display: inline; }
.wf-step-details > summary:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; border-radius: var(--brand-radius-sm); }

/* Two wide fields on the top row, the two day-fields next to each other below — the pairing is the
   point: doorlooptijd (what the customer is told) and norm (when we chase) are easy to confuse, so
   they sit side by side with their own hint under each. */
.wf-step-form {
  display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr));
  gap: 12px 20px; margin-top: 12px;
}
.wf-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.wf-field-wide { grid-column: 1 / -1; }
.wf-field .g-field { width: 100%; }
.wf-cap { font-size: 12px; font-weight: 600; color: var(--brand-ink-subtle); white-space: nowrap; }
/* One caption is a sentence rather than a label. `nowrap` would push it off a phone screen, so this
   one may break — and `pretty` decides where, instead of leaving an orphan word on line two. */
.wf-cap-long { white-space: normal; text-wrap: pretty; }
.wf-opt { font-weight: 400; }
/* A hint is a sentence and may wrap; `pretty` keeps the last line from being one orphan word. */
.wf-hint { font-size: 12px; color: var(--brand-ink-muted); margin: 0; text-wrap: pretty; }
/* A number field plus its unit on ONE line — "dagen" under the box would read as a second field. */
.wf-days { display: flex; align-items: center; gap: 8px; }
.wf-days .g-field { width: 90px; flex: 0 0 auto; }
.wf-unit { font-size: 13px; color: var(--brand-ink-subtle); white-space: nowrap; }
.wf-flags {
  grid-column: 1 / -1;
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 20px;
  padding-top: 4px; border-top: 1px solid var(--brand-grey-200);
}
.wf-flags .btn { margin-left: auto; }

/* Branches of a decision, set apart so they read as belonging to the step rather than following it. */
.wf-choices {
  margin-top: 12px; padding: 12px 14px;
  border-left: 3px solid var(--brand-support);
  background: var(--brand-surface-alt);
  border-radius: var(--brand-radius-sm);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
}
.wf-choice-row { display: flex; align-items: center; gap: 10px; width: 100%; flex-wrap: wrap; }
.wf-choice-row .g-field { flex: 1 1 200px; min-width: 0; }
.wf-arrow { color: var(--brand-ink-subtle); flex: 0 0 auto; }

.wf-add { border: 2px dashed var(--brand-grey-300); border-radius: var(--brand-radius); padding: 18px; margin-top: 20px; }
.wf-add h3 { font-size: 15px; margin-bottom: 12px; }
.wf-add-row { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.wf-add-row .wf-field-wide { flex: 1 1 320px; grid-column: auto; }

/* The preview borrows the customer's own .track-step styling, so it cannot drift visually from the
   page it previews. Only the surroundings differ — no card, no animation on an admin screen. */
.wf-preview { max-width: 560px; }
.wf-preview .track-step { animation: none; }

@media (max-width: 900px) {
  .wf-step-form { grid-template-columns: 1fr; }
  .wf-step-tools { margin-left: 0; width: 100%; }
}

/* worker request list */
.req-list { display: flex; flex-direction: column; gap: 10px; }
.req-item {
  display: flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
}
/* The list reveals row by row — twelve rows at 24ms is a third of a second and it is the single
   thing that separates "a list appeared" from "a list was revealed". */
.req-list .req-item {
  animation: app-step-in var(--brand-t-base) var(--brand-ease-out) backwards;
  animation-delay: calc(var(--req-i, 0) * 24ms);
}
.req-list .req-item:nth-child(2)  { --req-i: 1; }
.req-list .req-item:nth-child(3)  { --req-i: 2; }
.req-list .req-item:nth-child(4)  { --req-i: 3; }
.req-list .req-item:nth-child(5)  { --req-i: 4; }
.req-list .req-item:nth-child(6)  { --req-i: 5; }
.req-list .req-item:nth-child(7)  { --req-i: 6; }
.req-list .req-item:nth-child(8)  { --req-i: 7; }
.req-list .req-item:nth-child(9)  { --req-i: 8; }
.req-list .req-item:nth-child(10) { --req-i: 9; }
.req-list .req-item:nth-child(n + 11) { --req-i: 10; }
.req-item .req-code { font-weight: 600; color: var(--brand-support-text); font-size: 13px; min-width: 78px; text-decoration: underline; text-decoration-color: var(--brand-grey-200); text-underline-offset: 2px; }
.req-item:hover .req-code { text-decoration-color: var(--brand-support); }
.req-item .req-name { flex: 1; font-weight: 600; min-width: 0; }
.req-item .req-shoe { color: var(--brand-ink-subtle); font-size: 12px; white-space: nowrap; }
.req-item.is-mine { border-left: 3px solid var(--brand-support); }
.req-item .req-mine { font-size: 11px; font-weight: 600; color: var(--brand-support-text); background: var(--brand-support-soft); padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
/* On narrow screens the row wraps to two lines so nothing gets crushed. */
@media (max-width: 900px) {
  .req-item { flex-wrap: wrap; gap: 4px 10px; }
  .req-item .req-name { flex: 1 1 100%; order: 2; }
  .req-item .req-code { order: 1; }
  .req-item .req-shoe { order: 3; white-space: normal; overflow-wrap: anywhere; }
  .req-item .pill { order: 4; margin-left: auto; }
}

.mail-hint {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 16px;
  background: var(--brand-support-soft);
  border: 1px dashed var(--brand-support);
  border-radius: var(--brand-radius-sm);
  color: var(--brand-ink);
  font-size: 13px;
  margin-bottom: 20px;
  transition: background var(--brand-t-base) var(--brand-ease-out),
              border-color var(--brand-t-base) var(--brand-ease-out),
              box-shadow var(--brand-t-base) var(--brand-ease-out),
              transform var(--brand-t-base) var(--brand-ease-out);
}
/* It is a link to another screen, so it behaves like every other clickable surface: it lifts. */
.mail-hint:hover { border-style: solid; transform: translateY(-2px); box-shadow: var(--brand-shadow); }
.mail-hint:active { transform: translateY(1px) scale(.995); transition-duration: .08s; }
.mail-hint .app-icon { color: var(--brand-support-text); transition: transform var(--brand-t-base) var(--brand-ease-out); }
/* The arrow nudges toward the destination on hover. */
.mail-hint:hover [data-lucide="arrow-right"] { transform: translateX(3px); }

/* ===== Dashboard ===== */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
/* Shared KPI material comes from branding.css; this block owns the figure layout and entrance. */
.kpi {
  position: relative;
  overflow: hidden;
  animation: app-rise-in var(--brand-t-slow) var(--brand-ease-out) backwards;
}
/* Staggered so the row resolves left-to-right rather than snapping in as a block. */
.kpi:nth-child(1) { animation-delay: .04s; }
.kpi:nth-child(2) { animation-delay: .10s; }
.kpi:nth-child(3) { animation-delay: .16s; }
.kpi:nth-child(n + 4) { animation-delay: .22s; }
/* A gold hairline that draws itself across the top on hover. */
.kpi::after {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--brand-accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--brand-t-slow) var(--brand-ease-out);
}
.kpi:hover::after { transform: scaleX(1); }
.kpi.kpi-wait::after { background: var(--brand-warning); }
/* The figure lands a beat AFTER its tile: the card arrives, then the number counts the eye in. The
   value is what the room reads from six metres, so it takes the heading face, display size and
   tabular figures — 20 and 37 then share a column width and the row reads as one instrument.
   `backwards`, never `both`: a finished `transform: none` still resolves to a real matrix in
   Chrome, which would make every tile a stacking context for good (see motion.css). */
.kpi .kpi-val {
  font-family: var(--brand-font-heading);
  font-size: 40px; font-weight: 700; color: var(--brand-ink);
  line-height: 1; letter-spacing: -.035em; font-variant-numeric: tabular-nums;
  animation: app-kpi-in .42s var(--brand-ease-out) backwards;
  animation-delay: calc(.16s + var(--kpi-i, 0) * .06s);
}
.kpi:nth-child(2) .kpi-val { --kpi-i: 1; }
.kpi:nth-child(3) .kpi-val { --kpi-i: 2; }
.kpi:nth-child(n + 4) .kpi-val { --kpi-i: 3; }
/* The caption is the quiet half of the pair, and it stays on ONE line — a wrapped KPI caption in a
   fixed tile reads as broken. ink-muted (7.00:1) rather than subtle: it sits under a 40px figure
   and has to hold its own. */
.kpi .kpi-label { color: var(--brand-ink-muted); font-size: 13px; margin-top: 7px; white-space: nowrap; }
.kpi.kpi-wait .kpi-val { color: var(--brand-warning); }
/* Phone: number and caption side by side instead of stacked. One column is unavoidable at this
   width, and three stacked cards of 78px pushed the board itself below the fold; laid out as a row
   the same three cost ~160px total. The caption keeps its nowrap — it just has room for it now. */
@media (max-width: 560px) {
  .kpi-row { gap: 10px; margin-bottom: 18px; }
  .kpi { display: flex; align-items: baseline; gap: 10px; }
  .kpi .kpi-val { font-size: 26px; }
  .kpi .kpi-label { margin-top: 0; }
}

.alert-strip {
  background: var(--brand-warning-soft);
  border: 1px solid var(--brand-warning);
  border-radius: var(--brand-radius);
  box-shadow: var(--brand-shadow-sm);
  padding: 14px 18px;
  margin-bottom: 24px;
  animation: app-rise-in var(--brand-t-slow) var(--brand-ease-out) .2s backwards;
}
.alert-strip h3 { font-size: 14px; color: var(--brand-warning); display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.alert-strip .alert-item {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0;
  font-size: 13px;
  border-top: 1px solid var(--brand-rule);
  cursor: pointer;
  transition: transform var(--brand-t-fast) var(--brand-ease-out);
}
.alert-strip .alert-item:hover { transform: translateX(3px); }
/* The list that says what is going wrong reveals row by row instead of appearing as one block.
   24ms apart (motion.css's dense interval), capped at the eighth: a long attention list must not
   turn the strip into a wait. */
.alert-strip .alert-item {
  animation: app-step-in var(--brand-t-base) var(--brand-ease-out) backwards;
  animation-delay: calc(.26s + var(--al-i, 0) * 24ms);
}
.alert-strip .alert-item:nth-child(2) { --al-i: 1; }
.alert-strip .alert-item:nth-child(3) { --al-i: 2; }
.alert-strip .alert-item:nth-child(4) { --al-i: 3; }
.alert-strip .alert-item:nth-child(5) { --al-i: 4; }
.alert-strip .alert-item:nth-child(6) { --al-i: 5; }
.alert-strip .alert-item:nth-child(7) { --al-i: 6; }
.alert-strip .alert-item:nth-child(n + 8) { --al-i: 7; }
/* The bell pulses twice on load — it only renders when something actually needs attention. */
.alert-strip h3 .app-icon { animation: app-bell 1.6s var(--brand-ease-out) .4s 2; transform-origin: 50% 10%; }
.alert-strip .alert-item:first-of-type { border-top: none; }
.alert-strip .alert-item .req-code { font-weight: 600; color: var(--brand-support-text); text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 2px; }
.alert-strip .alert-item:hover .req-code { text-decoration-color: var(--brand-support); }
.alert-strip .alert-item .alert-who { min-width: 0; }
/* Phone: stack, for the same reason as .ov-attn-row — code and pill do not shrink, so the customer
   name is the only part that can, and it collapsed to one word per line. Same order as there. */
@media (max-width: 640px) {
  .alert-strip { padding: 12px 14px; }
  .alert-strip .alert-item { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; padding: 10px 0; }
  .alert-strip .alert-item .pill { justify-self: start; }
}

/* Keyboard focus ring for the click-to-open alert rows (they're role=button).
   `.card:focus-visible` stond hier ook; die kaarten hoorden bij het kolommenbord en dat is op
   17-09-2026 verwijderd (specs/orderfunnel.md §9d-bis). */
.alert-strip .alert-item:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
.alert-empty { color: var(--brand-ink-subtle); font-size: 13px; }


/* ===== Modal ===== */
.modal-backdrop {
  position: fixed; inset: 0;
  /* No token for a translucent scrim; plain black at 40% is the neutral one. */
  background: rgba(0, 0, 0, .4);
  display: grid; place-items: center;
  padding: 24px;
  z-index: 50;
  /* Backdrop fades + blurs the app behind it; the dialog scales up from just under centre. */
  animation: app-fade-in var(--brand-t-base) var(--brand-ease-out) both;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.modal-backdrop[hidden] { display: none; }
.modal {
  background: var(--brand-surface);
  border-radius: var(--brand-radius);
  box-shadow: var(--brand-shadow-lg);
  width: 100%;
  /* Was a hard 520px, which clipped long e-mail addresses while ~880px of screen sat empty.
     Grows with the viewport, still capped for readable line lengths. */
  max-width: min(720px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
  animation: app-modal-in var(--brand-t-base) var(--brand-ease-spring) both;
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px 22px;
  border-bottom: 1px solid var(--brand-grey-200);
}
.modal-head h2 { font-size: 17px; }
.modal-body { padding: 22px; }
.modal-foot {
  display: flex; gap: 10px; justify-content: flex-end;
  padding: 16px 22px;
  border-top: 1px solid var(--brand-grey-200);
}
.modal .req-detail-row { display: flex; gap: 8px; font-size: 13px; padding: 4px 0; }
.modal .req-detail-row .k { color: var(--brand-ink-subtle); min-width: 130px; }

/* ── Technische gegevens op Beheer › Afzender ───────────────────────────────────────────────
   Een tweekolomstabel vraag/antwoord. Staat hier en niet in een modulebestand omdat
   beheer/mail.php geen eigen stylesheet heeft — de gedeelde laag is dan de enige plek (spec §1c). */
.mail-diag { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 14px; }
.mail-diag td { padding: 7px 0; border-bottom: 1px solid var(--brand-grey-100); vertical-align: top; }
/* De vraag krimpt niet weg onder een lang serverpad, en breekt niet middenin. */
.mail-diag td:first-child { width: 42%; padding-right: 16px; color: var(--brand-ink-muted); }
.mail-diag td:last-child { color: var(--brand-ink); }
/* Een pad of een functienaam mag afbreken: hij is soms langer dan de kolom en mag de tabel niet
   uit elkaar duwen. */
.mail-diag code { word-break: break-all; }

/* Het verzendlogboek deelt de tabel maar niet de kolomverdeling: links staat alleen een tijdstip
   met een uitkomst eronder, en 42% zou daar een halve tabel wit laten. */
.mail-log td:first-child { width: 22%; white-space: nowrap; }
.mail-log-ok  { color: var(--brand-support-deep); font-weight: 600; }
/* Rood, niet oranje: dit is het antwoord op "ging hij weg" en dat is ja of nee. */
.mail-log-nok { color: var(--brand-danger); font-weight: 600; }

/* Het vinkje voor de envelope-afzender. Eigen klasse en niet .st-check: die hoort bij het
   statusscherm, en twee schermen op één klasse betekent dat de een de ander verandert. */
.mail-check { display: inline-flex; align-items: center; gap: 8px; font-size: 15px;
              color: var(--brand-ink); background: var(--brand-surface); cursor: pointer; user-select: none; }
.mail-check input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--brand-support-deep); cursor: pointer; }
/* Vastgezet in config.local.php: hij mag te lezen zijn, maar niet uitnodigen tot klikken. */
.mail-check input[type=checkbox]:disabled { cursor: not-allowed; }
.mail-check:has(input:disabled) { color: var(--brand-ink-muted); cursor: not-allowed; }

/* ── Cliënt kiezen (de picker uit _debiteurveld.php + debiteurpicker.js) ────────────────────
   HIER EN NIET IN EEN MODULE. Stond tot 03-09-2026 in documenten/documenten.css; sinds
   archive/batch.php dezelfde picker gebruikt, zijn er twee modules die hem tekenen, en een module
   mag de stylesheet van een andere module niet laden (spec.md §1c). Ongewijzigd overgenomen. */

/* `center`, niet `flex-start`: de + is lager dan het invoerveld, dus bovenaan uitlijnen laat hem
   zichtbaar te hoog hangen. De suggestielijst hangt aan de container (position: absolute) en niet
   aan de uitlijning, dus die blijft gewoon onder het veld staan. */
.deb-pick-veld { position: relative; display: flex; gap: 8px; align-items: center; }
.deb-pick-veld input[type=text] { flex: 1 1 auto; }
.deb-pick-add { flex: 0 0 auto; }

/* De suggestielijst zweeft boven de rest van het formulier; hij mag nooit de knop eronder wegduwen
   terwijl je typt — dan verspringt de pagina onder je handen. */
.deb-pick-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 46px;
  z-index: 30;
  margin: 0;
  padding: 4px;
  list-style: none;
  max-height: 280px;
  overflow-y: auto;
  background: var(--brand-surface);
  color: var(--brand-ink);
  border: 1px solid var(--brand-grey-200);
  border-radius: var(--brand-radius);
  box-shadow: 0 10px 28px rgba(17, 24, 39, 0.14);
}

.deb-pick-item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 7px 10px;
  border-radius: var(--brand-radius-sm);
  cursor: pointer;
  white-space: nowrap;
}

.deb-pick-item:hover,
.deb-pick-item.is-actief { background: var(--brand-support-soft); color: var(--brand-ink); }

.deb-pick-naam { font-size: 14px; font-weight: 600; color: var(--brand-ink); }
/* Geboortedatum en GDEB-nummer: dit is waarmee je twee mensen met dezelfde naam uit elkaar houdt. */
.deb-pick-meta { font-size: 12px; color: var(--brand-ink-muted); }

.deb-pick-leeg,
.deb-pick-meer { padding: 8px 10px; font-size: 13px; color: var(--brand-ink-subtle); }

.deb-pick-gekozen {
  margin: 6px 0 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--brand-support-text);
}

.deb-dup-knop {
  display: block;
  width: 100%;
  margin: 4px 0 0;
  padding: 6px 8px;
  text-align: left;
  font: inherit;
  font-size: 13px;
  background: var(--brand-surface);
  color: var(--brand-ink);
  border: 1px solid var(--brand-grey-200);
  border-radius: var(--brand-radius-sm);
  cursor: pointer;
}

.deb-dup-knop:hover { border-color: var(--brand-support-dark); }

/* ── Versies: het wijzigingenregister (v68) ─────────────────────────────────────────────────
   Eén blok per wijziging. De code en de datum staan links en rechts van de titel op ÉÉN regel; de
   titel mag krimpen, de code en de datum nooit — een half afgebroken GCHG-nummer of datum is niet
   over te nemen in een gesprek. */
.ver-rij {
  padding: 12px 0;
  border-bottom: 1px solid var(--brand-grey-100);
}
.ver-rij:last-child { border-bottom: 0; }

.ver-kop { display: flex; align-items: baseline; gap: 10px; }
.ver-code {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--brand-ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ver-titel { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 15px; text-wrap: pretty; }
.ver-datum {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--brand-ink-muted);
  white-space: nowrap;
}

.ver-toelichting { margin: 6px 0 0; color: var(--brand-ink-subtle); text-wrap: pretty; }
.ver-raakt { margin: 4px 0 0; font-size: 13px; color: var(--brand-ink-subtle); }

/* Eén regel per soort oordeel: label, stand, wie/wanneer, en de knoppen. Wrapt als geheel op een
   smal scherm — dit is een rij bouwstenen, geen label dat op twee regels mag vallen. */
.ver-stand {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}
.ver-stand-label {
  flex: 0 0 8.5rem;
  font-weight: 600;
  font-size: 13px;
  white-space: nowrap;
}
.ver-stand .pill { white-space: nowrap; }
.ver-door  { font-size: 12px; color: var(--brand-ink-muted); white-space: nowrap; }
.ver-reden { font-size: 12px; color: var(--brand-ink-subtle); font-style: italic; }
.ver-knop  { display: inline; margin: 0; }
/* Staat er geen knop, dan staat hier de reden. Gedempt en klein: het is een antwoord, geen
   waarschuwing — er is niets mis, het hoort hier alleen niet te kunnen. */
.ver-geen  { font-size: 12px; color: var(--brand-ink-muted); font-style: italic; white-space: nowrap; }

/* Afkeuren klapt open omdat er een reden bij hoort. `summary` draagt de knopstijl, dus het ziet
   eruit als de knop ernaast — en het werkt zonder JavaScript. */
.ver-afkeur > summary { display: inline-block; cursor: pointer; list-style: none; }
.ver-afkeur > summary::-webkit-details-marker { display: none; }
.ver-afkeur-form {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
  padding: 10px;
  border-radius: 8px;
  background: var(--brand-grey-050);
}
.ver-afkeur-form input[type=text] { flex: 1 1 18rem; min-width: 0; }
.ver-afkeur-form label { font-size: 13px; white-space: nowrap; }

.ver-spoor { margin-top: 8px; }
.ver-spoor > summary { cursor: pointer; font-size: 13px; color: var(--brand-support-text); }
.ver-spoor ul { margin: 6px 0 0; padding-left: 1.2rem; font-size: 13px; color: var(--brand-ink-subtle); }
.ver-spoor li { margin: 3px 0; text-wrap: pretty; }

/* De uitleg waarom er geen knoppen staan. Geen waarschuwing — er is niets mis, het hoort zo. */
.ver-uitleg {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 0;
  color: var(--brand-ink-subtle);
  font-size: 13px;
  text-wrap: pretty;
}

/* "Klaar om af te ronden" (v67): de evaluaties waarvan de opvolgdatum is aangebroken. Een eigen
   paneel bovenaan en niet een filter in de lijst eronder — dit is werk dat vandaag mag, en dat moet
   je zien zonder ernaar te zoeken. */
.panel-opvolg { border-left: 4px solid var(--brand-accent); }
.opvolg-lijst { list-style: none; margin: 0; padding: 0; }
.opvolg-regel {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--brand-grey-100);
}
.opvolg-regel:last-child { border-bottom: 0; }
.opvolg-regel > a { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opvolg-code { flex: 0 0 auto; font-size: 12px; color: var(--brand-ink-muted); font-variant-numeric: tabular-nums; }
.opvolg-regel .pill { flex: 0 0 auto; white-space: nowrap; }

/* De formulieren die aan een order hangen (v67). Dezelfde scheidingslijn als de tijdlijn eronder,
   want het is hetzelfde soort blok: wat er met deze order is gebeurd, en wat er nog moet. */
.order-docs { margin-top: 18px; border-top: 1px solid var(--brand-grey-200); padding-top: 16px; }
.order-docs h3 { font-size: 14px; margin-bottom: 12px; }

/* Eén regel per formulier, en die regel blijft EEN regel: de naam mag krimpen (met een ellips als
   het echt niet past), de statuspil nooit — een pil die afbreekt leest als twee pillen. */
.order-doc {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--brand-grey-100);
}
.order-doc:last-child { border-bottom: 0; }
.order-doc > a {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--brand-support-text);
}
.order-doc-code {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--brand-ink-muted);
  font-variant-numeric: tabular-nums;
}
.order-doc .pill { flex: 0 0 auto; white-space: nowrap; }

/* Event history: dates, connected milestones and readable transitions. Motion settles after entry. */
.timeline { margin-top: 18px; border-top: 1px solid var(--brand-grey-200); padding-top: 16px; }
.tl-header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.timeline h3 { font-size: 14px; margin: 0; white-space: nowrap; }
.tl-count { font-size: 11px; color: var(--brand-ink-subtle); white-space: nowrap; }
.tl-list { list-style: none; margin: 0; padding: 0; }
.tl-item {
  --tl-tone: var(--brand-ink-subtle);
  position: relative; display: grid; grid-template-columns: 100px 32px minmax(0, 1fr);
  gap: 12px; padding-bottom: 14px; font-size: 13px; line-height: 20px;
}
.tl-item[data-kleur="blauw"] { --tl-tone: var(--brand-support-text); }
.tl-item[data-kleur="groen"] { --tl-tone: var(--brand-success); }
.tl-item[data-kleur="geel"] { --tl-tone: var(--brand-accent-text); }
.tl-item[data-kleur="rood"] { --tl-tone: var(--brand-danger); }
/* The connector ends at the next node, including variable-height notes. */
.tl-item:not(:last-child)::before, .tl-item:not(:last-child)::after {
  content: ""; position: absolute; left: 127px; top: 20px; bottom: -20px; width: 2px;
  background: var(--brand-grey-200); pointer-events: none;
}
.tl-item:not(:last-child)::after {
  background: color-mix(in srgb, var(--tl-tone) 45%, var(--brand-grey-200));
  transform-origin: top;
}
.tl-item.is-visible:not(:last-child)::after {
  animation: tl-connect .45s var(--brand-ease-out) both;
  animation-delay: calc(var(--tl-i) * 90ms + .15s);
}
.tl-item:last-child { padding-bottom: 0; }
.tl-node {
  z-index: 1; display: grid; place-items: center; width: 32px; height: 32px; margin-top: 4px;
  border: 1px solid color-mix(in srgb, var(--tl-tone) 35%, var(--brand-white)); border-radius: 50%;
  background: var(--brand-surface); color: var(--tl-tone);
}
.tl-item.is-visible .tl-node {
  animation: tl-arrive .4s var(--brand-ease-out) backwards; animation-delay: calc(var(--tl-i) * 90ms);
}
.tl-node svg { width: 15px; height: 15px; stroke-width: 2; }
.is-latest .tl-node {
  background: var(--tl-tone); border-color: var(--tl-tone); color: var(--brand-white);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--tl-tone) 12%, transparent);
}
.tl-item .tl-body {
  display: flex; flex-direction: column; align-items: stretch; gap: 6px; min-width: 0; padding: 10px 12px;
}
.tl-item.is-visible .tl-body {
  animation: tl-arrive .4s var(--brand-ease-out) backwards; animation-delay: calc(var(--tl-i) * 90ms + .08s);
}
.tl-item.is-latest .tl-body {
  --g-card-border: color-mix(in srgb, var(--tl-tone) 32%, var(--brand-grey-200));
  --g-card-bg: color-mix(in srgb, var(--tl-tone) 5%, var(--brand-surface));
}
.tl-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.tl-label { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 7px; min-width: 0; overflow-wrap: anywhere; }
.tl-label svg { width: 13px; height: 13px; flex: 0 0 auto; color: var(--brand-ink-subtle); }
.tl-from { color: var(--brand-ink-muted); }
.tl-to { font-weight: 600; }
.tl-latest { font-size: 10px; font-weight: 600; color: var(--tl-tone); white-space: nowrap; margin-left: auto; }
.tl-note { overflow-wrap: anywhere; text-wrap: pretty; }
.tl-when { padding-top: 7px; text-align: right; font-size: 11px; color: var(--brand-ink-muted); }
.tl-when time { display: flex; flex-direction: column; font-variant-numeric: tabular-nums; }
.tl-date { overflow-wrap: anywhere; }
.tl-time { color: var(--brand-ink-subtle); }
.tl-actor { display: flex; align-items: baseline; gap: 5px; font-size: 11px; color: var(--brand-ink-subtle); overflow-wrap: anywhere; }
.tl-actor svg { width: 12px; height: 12px; flex: 0 0 auto; align-self: center; }
.tl-actor span { min-width: 0; }
@keyframes tl-connect { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes tl-arrive { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 540px) {
  .tl-item { grid-template-columns: 32px minmax(0, 1fr); gap: 5px 10px; }
  .tl-node { grid-column: 1; grid-row: 1 / 3; margin-top: 0; }
  .tl-when { grid-column: 2; grid-row: 1; text-align: left; padding-top: 0; }
  .tl-when time { flex-direction: row; flex-wrap: wrap; gap: 8px; }
  .tl-item .tl-body { grid-column: 2; grid-row: 2; }
  .tl-item:not(:last-child)::before, .tl-item:not(:last-child)::after { left: 15px; top: 16px; bottom: -16px; }
}

.cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.cta-row.cta-advance { align-items: center; border-top: 1px solid var(--brand-grey-200); padding-top: 16px; }
.cta-row .cta-label { font-size: 13px; color: var(--brand-ink-subtle); margin-right: 2px; white-space: nowrap; }
/* One action row in the detail modal: advance buttons left, secondary actions right. Separated
   from the fields above by a rule so the modal reads as "data / what you can do with it". */
.cta-row.cta-actions {
  align-items: center; justify-content: space-between; gap: 10px;
  border-top: 1px solid var(--brand-grey-200); padding-top: 16px;
}
.cta-group { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* With no advance buttons (terminal/closed request) the secondary group must not jump left. */
.cta-group-secondary { margin-left: auto; }
/* Inline edit form inside the detail modal (own-order editing). Reuses .form-row / .form-grid. */
.detail-edit .form-row { margin-bottom: 12px; }
/* Child combinator, NOT a descendant `:first-of-type`: that also matched the first .form-row
   inside .form-grid (E-mail), pushing it 8px below Telefoon and breaking their shared baseline. */
.detail-edit > .form-row:first-of-type { margin-top: 8px; }
/* Grid cells sit on one baseline — no stray margin from rules meant for the outer column. */
.detail-edit .form-grid .form-row { margin-top: 0; }
/* The read-only rows at the top (stap/eigenaar) share the label column width of the fields below,
   so the whole modal reads as one column instead of two competing indents. */
.detail-edit .req-detail-row {
  padding: 4px 0 6px;
  border-bottom: 1px solid var(--brand-rule);
  margin-bottom: 8px;
}
.detail-edit .req-detail-row:last-of-type { margin-bottom: 16px; }
/* Selects inside the modal match the text inputs exactly (native controls otherwise render as
   an unstyled OS widget — see #ed_shoe in the worker modal). */
.detail-edit select,
.form-row select {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  font: inherit;
  color: var(--brand-ink);
  background: var(--brand-white);
  cursor: pointer;
  /* Custom chevron; `appearance:none` removes the OS arrow so it can't double up. A data URI
     cannot read a CSS variable, so the stroke is the URL-encoded (%23...) form of
     --brand-ink-subtle. Keep the two in step if that token ever moves. */
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 38px;
  transition: border-color var(--brand-t-fast) var(--brand-ease-out),
              box-shadow var(--brand-t-base) var(--brand-ease-out);
}
.detail-edit select:hover,
.form-row select:hover { border-color: var(--brand-accent); }
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
.detail-edit select:focus,
.form-row select:focus { outline: none; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft); }
.detail-edit select:focus-visible,
.form-row select:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
/* Dark-UI rule from the repo standards: state option colours explicitly, never inherit — a dark
   UA otherwise renders the list grey-on-grey. */
.detail-edit select option,
.form-row select option { background: var(--brand-white); color: var(--brand-ink); }
.detail-note { margin-top: 14px; font-size: 13px; color: var(--brand-ink); background: var(--brand-support-soft); padding: 10px 12px; border-radius: var(--brand-radius-sm); }
/* Assign/reassign owner row in the dashboard detail modal. */
.assign-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--brand-grey-200); }
.assign-row label { font-size: 13px; color: var(--brand-ink-subtle); white-space: nowrap; }
/* Inherits the chevron + focus treatment from the shared select rule above. Its box is matched to
   the .btn-sm beside it (6px vertical padding, 13px font, 1px border → same 34px height) so the
   dropdown and the "Toewijzen" button line up exactly. */
.assign-row select {
  flex: 1; min-width: 160px;
  padding: 6px 34px 6px 12px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  background-color: var(--brand-white);
  color: var(--brand-ink);
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
  cursor: pointer;
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  transition: border-color var(--brand-t-fast) var(--brand-ease-out),
              box-shadow var(--brand-t-base) var(--brand-ease-out);
}
.assign-row select:hover { border-color: var(--brand-accent); }
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
.assign-row select:focus { outline: none; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft); }
.assign-row select:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
.assign-row select option { background: var(--brand-white); color: var(--brand-ink); }
.modal.modal-narrow { max-width: 380px; }

/* ===== Opschonen (beheer/opschonen.php) =====
   Drie lijsten met dezelfde botten: pictogram · naam en uitleg · aantal rechts. Wat weg KAN heeft
   een vinkje ervoor, wat blijft een slotje — dezelfde vorm, zodat de twee panelen naast elkaar
   leesbaar zijn als één antwoord op "wat gebeurt er met mijn data".
   Het aantal staat rechts en krijgt tabulaire cijfers, zodat 16.926 en 3 onder elkaar uitlijnen en
   je met één blik ziet welk onderdeel groot is. Nooit afbreken: een getal op twee regels is geen
   getal meer. */
.clean-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
.clean-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  cursor: pointer;
}
.clean-row:has(input:checked) { border-color: var(--brand-danger); background: var(--brand-danger-soft); }
/* Leeg = niets te doen. Wel tonen ("die was al leeg" is een antwoord), niet aanklikbaar. */
/* STILLER MAAK JE MET KLEUR, NIET MET OPACITY.
 *
 * Hier stond `opacity: .55` op de hele rij. Opacity dimt alles even hard — ook de tekst die je nog
 * moet kunnen lezen. Gemeten op deze rij: .clean-desc #ADADAD = 2,24:1, .clean-note #BEA572 = 2,38:1
 * en .clean-count #A3A3A3 = 2,52:1, allemaal ver onder de 4,5:1 die tekst nodig heeft. En het is
 * geen uitgeschakeld onderdeel dat van die eis vrijgesteld is: de rij TOONT nog steeds hoeveel er
 * staat, en dat is precies de informatie waarvoor je hier bent.
 *
 * Nu doet de OMGEVING het stil-zijn: een grijzer vlak en een zachtere rand zeggen "hier valt niets
 * te doen", terwijl elk stuk tekst zijn eigen, gewoon leesbare kleur houdt. */
.clean-row.is-empty {
  cursor: default;
  background: var(--brand-grey-100);
  border-color: var(--brand-grey-200);
}
.clean-row.is-empty:hover { border-color: var(--brand-grey-200); background: var(--brand-grey-100); }
.clean-row.is-empty .clean-name { color: var(--brand-ink-muted); }   /* 7,00:1 op wit, 6,7:1 hier */
.clean-row.is-empty .clean-desc,
.clean-row.is-empty .clean-count { color: var(--brand-ink-subtle); } /* 5,33:1 op wit, 5,1:1 hier */
.clean-row > input { width: 16px; height: 16px; margin-top: 3px; accent-color: var(--brand-danger); flex: 0 0 auto; }
.clean-icon { flex: 0 0 auto; color: var(--brand-ink-subtle); line-height: 0; margin-top: 2px; }
.clean-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.clean-name { font-weight: 600; }
.clean-desc { color: var(--brand-ink-subtle); font-size: 13px; text-wrap: pretty; }
/* De consequentie die je pas achteraf ontdekt (mail komt terug, sjablonen blijven). Amber, want het
   stuurt een beslissing — het is geen fout. */
.clean-note {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--brand-warning);
  text-wrap: pretty;
}
.clean-count {
  flex: 0 0 auto;
  align-self: center;
  font-size: 13px;
  color: var(--brand-ink-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.clean-count strong { color: var(--brand-ink); font-size: 15px; }

.clean-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.clean-picked { color: var(--brand-ink-subtle); font-size: 13px; white-space: nowrap; }

.clean-safe, .clean-backups { display: flex; flex-direction: column; gap: 4px; }
.clean-safe-row, .clean-backup-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: var(--brand-radius-sm);
}
.clean-safe-row:nth-child(odd), .clean-backup-row:nth-child(odd) { background: var(--brand-surface-alt); }
.clean-safe-row > .app-icon, .clean-backup-row > .app-icon { color: var(--brand-success); flex: 0 0 auto; }
.clean-safe-name, .clean-backup-when { font-weight: 600; white-space: nowrap; }
.clean-safe-why {
  color: var(--brand-ink-subtle);
  font-size: 13px;
  flex: 1 1 auto;
  min-width: 0;
  text-wrap: pretty;
}
.clean-backup-when { font-variant-numeric: tabular-nums; }
/* Omvang en downloadknop schuiven samen naar rechts; de knop staat het verst weg omdat hij de
   actie is en de omvang alleen context. Beide nowrap — "Downloaden" op twee regels is geen knop. */
.clean-backup-size { margin-left: auto; color: var(--brand-ink-muted); font-size: 13px; white-space: nowrap; }
.clean-backup-row .btn { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 620px) {
  .clean-safe-why { flex-basis: 100%; }
  .clean-safe-row, .clean-backup-row { flex-wrap: wrap; }
}

/* De waarschuwing in de bevestiging. Rode tekst op de rode tint — beide uit colors.css, dus AA. */
.clean-warn {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 16px;
  padding: 10px 12px;
  border-radius: var(--brand-radius-sm);
  background: var(--brand-danger-soft);
  color: var(--brand-danger);
  font-size: 13px;
  text-wrap: pretty;
}
.clean-warn .app-icon { flex: 0 0 auto; margin-top: 1px; }
.clean-sum-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 0;
  border-bottom: 1px solid var(--brand-grey-200);
}
.clean-sum-name { font-weight: 600; }
/* Wat er precies weggaat, per tabel in mensentaal. Klein, maar aanwezig: het kopje "Aanvragen"
   zegt niet dat de tijdlijn meegaat, en dat is nou juist wat iemand wil weten vóór hij bevestigt. */
.clean-sum-parts { flex: 1 1 100%; color: var(--brand-ink-subtle); font-size: 12px; text-wrap: pretty; }

/* ===== Medewerkers panel (admin) ===== */
.workers-panel { margin-top: 24px; }
.worker-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.worker-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* STILLER MAAK JE MET KLEUR, NIET MET OPACITY — zie ook .clean-row.is-empty hierboven.
 *
 * Hier stond `opacity: .6` op de hele rij, en die rij is LEVEND: Activeren, Bewerken en Reset PIN
 * doen het gewoon. Een uitgeschakeld onderdeel is vrijgesteld van de contrasteis, dit niet. Gemeten
 * met de dimming erop: de pil "Inactief" #83A4B6 op #E8F1F6 = 2,32:1 en @gebruikersnaam #A6A6A6 =
 * 2,43:1. Het badge dat vertelt WAT er met deze rij aan de hand is, was daarmee het slechtst
 * leesbare element erin — precies verkeerd om.
 *
 * De rij wordt nu stil door zijn vlak (grijzer, zachtere rand) en door de naam één stap in te
 * houden. De pil gaat juist de andere kant op: neutraal grijs met zwarte tekst, want dat is de
 * mededeling. De pil-varianten (Beheerder, "3 van 6 onderdelen") blijven wat ze zijn — die zeggen
 * iets anders dan de status en horen niet op dezelfde kleur te vallen. */
.worker-row.is-inactive {
  background: var(--brand-grey-100);
  border-color: var(--brand-grey-200);
}
.worker-row.is-inactive .worker-name { color: var(--brand-ink-muted); }   /* 6,7:1 op grey-100 */
.worker-row.is-inactive .worker-user,
.worker-row.is-inactive .worker-contact { color: var(--brand-ink-subtle); }  /* 5,1:1 op grey-100 */
/* De kale .pill is de statusmelding "Inactief"; alles met een modifier (.pill-admin, .pill-limited)
   heeft een eigen betekenis en blijft ongemoeid. */
.worker-row.is-inactive .pill:not([class*="pill-"]) {
  background: var(--brand-grey-300);
  color: var(--brand-ink);
}
.worker-row .worker-name { font-weight: 600; }
.worker-row .worker-user { color: var(--brand-ink-subtle); font-size: 12px; }
/* Phone and e-mail: one line each, never wrapped mid-number. They are links (tel:/mailto:) but must
   not shout — the row is a list of people, not a list of addresses, so they read as the same quiet
   metadata as @username until you hover. */
.worker-row .worker-contact {
  color: var(--brand-ink-subtle);
  font-size: 12px;
  white-space: nowrap;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.worker-row .worker-contact:hover,
.worker-row .worker-contact:focus-visible { color: var(--brand-ink); border-bottom-color: currentColor; }
/* Wraps: an admin row carries five buttons (Bewerken · Reset PIN · Toegang · Maak beheerder ·
   Deactiveren) and they total ~510px, so on anything narrower they ran straight off the page. */
.worker-row .worker-actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
.add-worker { padding-top: 4px; }
.add-worker .btn { margin-top: 2px; }

/* ALLE VIJF DE VELDEN OP ÉÉN REGEL (15-09-2026, verzoek uit de review: "maak deze kaart compacter").
 *
 * Het waren twee regels van drie en twee, elk met 18 pixels marge eronder, in een paneel dat over de
 * volle breedte staat. Dat is een formulier van vijf korte velden dat een halve schermhoogte pakt
 * terwijl de rechterhelft leeg blijft.
 *
 * DE KOLOMBREEDTES DRAGEN DE BETEKENIS. De PIN houdt een paar cijfers, maar zijn kolom is met opzet
 * niet de smalste: de regel eronder ("minimaal 4 cijfers, geen voorspelbaar patroon") staat in dat
 * vak, en in een kolom van 120 pixels wordt die zeven regels hoog en bepaalt hij de hoogte van de
 * hele rij. Breed genoeg maken is hier dus KORTER, niet langer.
 *
 * DIE REGEL BLIJFT EEN BUURMAN VAN HET VELD, en niet losse tekst onder het formulier: hij hoort bij
 * de PIN en nergens anders bij. apps/local/tests/grol_toegang/pin_ui.sh leest hem als
 * `#nw_pin ~ .form-help` — haal hem uit deze `.form-row` en die controle valt om. */
.add-worker .form-grid {
  grid-template-columns: 1.1fr 1fr minmax(190px, .8fr) 1.1fr .9fr;
  gap: 12px;
}
/* De marge onder een veld is hier dubbelop: de `gap` van het raster doet dat al. */
.add-worker .form-grid .form-row { margin-bottom: 0; }
@media (max-width: 1100px) { .add-worker .form-grid { grid-template-columns: 1fr 1fr minmax(190px, .8fr); } }
@media (max-width: 760px) { .add-worker .form-grid { grid-template-columns: 1fr; } }
@media (max-width: 560px) {
  .worker-row .worker-actions { margin-left: 0; flex-basis: 100%; }
}

/* ===== Toast ===== */
.toast-wrap { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 100; display: flex; flex-direction: column; gap: 8px; }
/* PRESENTATION ONLY BELOW — DO NOT TOUCH THE `animation` LINE.
   The out-fade is scheduled purely by `animation-delay: 2.9s` with `fill-mode: both`, and the JS
   removes the node at 3.2s. Anything that forces that delay to 0 (a prefers-reduced-motion gate did
   exactly this) fires the end keyframe immediately and `both` pins opacity: 0 — every confirmation
   and every error in the app then lives its whole life invisible and reads as "the API is broken".
   See the long note at the bottom of this file. Change the padding, never the timing. */
.toast {
  background: var(--brand-ink);
  color: var(--brand-on-dark);
  padding: 12px 22px;
  border-radius: 999px;
  font-size: 13.5px;
  font-weight: 600;
  /* Layered: the app's deepest shadow plus a hairline of light on the top edge, so the pill reads
     as an object above the page rather than a dark rectangle printed on it. */
  box-shadow: var(--brand-shadow-lg), inset 0 1px 0 rgba(255, 255, 255, .18);
  /* Rises in with a slight overshoot, holds, then fades out just before JS removes it at 3.2s. */
  animation: app-toast-in var(--brand-t-base) var(--brand-ease-spring) both,
             app-toast-out var(--brand-t-base) var(--brand-ease-out) 2.9s both;
}
.toast.toast-error { background: var(--brand-danger); color: var(--brand-on-dark); }

/* ===== Empty state =====
   The screen a demo audience sees first, and for years it was one grey sentence floating in a white
   void. Now it is a composed slot: a quiet icon, the sentence in the app's own voice, and a dashed
   well that shows the shape of what is going to live here. The markup is unchanged — every
   `<div class="empty-state">…</div>` in the app gets this without touching a single page.
   The icon is drawn as a MASK, not an <img>: the shape comes from the URL, the colour from
   `currentColor`, so it can never disagree with the text next to it. Same Lucide "inbox" outline as
   the icon set, inlined for the same reason the select chevron is (a data URI cannot fail to load
   and cannot cost content if a script dies). Decorative — the sentence carries the meaning. */
.empty-state {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%;
  text-align: center;
  text-wrap: pretty;
  color: var(--brand-ink-muted);       /* 7.00:1 on white, 6.8:1 on the alt surface — AA either way */
  font-size: 14px; line-height: 1.5;
  padding: 34px 22px;
  border: 1px dashed var(--brand-grey-300);
  border-radius: var(--brand-radius);
  background: var(--brand-surface-alt);
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) backwards;
}
.empty-state::before {
  content: '';
  width: 30px; height: 30px; flex: 0 0 auto;
  background-color: currentColor;
  /* Decorative: the sentence under it carries the meaning, so this sits deliberately below text
     contrast — around 2:1 on the tinted well, the same weight as the empty-state icon in
     debiteuren.css. Visible from the back of a room, quiet enough not to become the message. */
  opacity: .5;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* An empty state that sits inside a panel that already has its own frame does not need a second
   one — there it is just the icon and the sentence. */
.pl-wrap .empty-state, .modal-body > .empty-state { border: 0; background: none; }

/* ===== Mail intake ===== */
.mi-status {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 16px; border-radius: var(--brand-radius-sm);
  font-size: 13px; margin-bottom: 20px;
  box-shadow: var(--brand-shadow-sm);
  animation: app-rise-in var(--brand-t-base) var(--brand-ease-out) backwards;
}
.mi-status .app-icon { flex: 0 0 auto; }
.mi-status-ok   { background: var(--brand-success-soft); color: var(--brand-success); border: 1px solid var(--brand-success); }
.mi-status-warn { background: var(--brand-warning-soft); color: var(--brand-warning); border: 1px solid var(--brand-warning); }
.mi-status-info { background: var(--brand-support-soft); color: var(--brand-ink); border: 1px dashed var(--brand-support); }
.mi-status a.btn { margin-left: auto; }

/* The mail-intake view gets more page width than the default shell — the inbox + form
   side-by-side need the room. */
.app-main.mailintake { max-width: 1500px; }

/* minmax(0, …) on both tracks, not a bare 1fr: a grid track's automatic minimum is min-content, and
   the inbox holds .mi-msg-snip with white-space: nowrap — one long mail preview then sets the
   column width and the whole page scrolls sideways (measured: 841px of track at a 390px viewport).
   The snippet's own overflow/ellipsis cannot save it, because the track grew to fit it first. */
.mi-cols { display: grid; grid-template-columns: minmax(420px, 1fr) minmax(0, 1.3fr); gap: 24px; align-items: start; }
@media (max-width: 980px) { .mi-cols { grid-template-columns: minmax(0, 1fr); } }

/* THE RIGHT-HAND VOID. Until you pick a message, `#miForm` is `hidden` and the wider of the two
   columns is nothing — on a 1400px screen that is half the page showing white. A pseudo-element of
   a grid container is itself a grid item, so the empty track can hold a placeholder that says what
   the space is for, in the same language as every other empty state in the app: quiet icon, one
   sentence, dashed well. It disappears the moment a message is chosen (`:has()`), and below the
   breakpoint — where the columns stack and it would only push the inbox down — it is not rendered
   at all.
   The sentence lives in CSS because this file may not edit the page; if that ever changes, move it
   into the markup as a real element and delete this rule rather than keeping both. */
@media (min-width: 981px) {
  .mi-cols:has(> .mi-form[hidden])::after {
    content: 'Kies links een e-mail. De gegevens uit het bericht komen hier alvast ingevuld te staan.';
    display: grid;
    place-content: center;
    align-self: stretch;
    min-height: 260px;
    padding: 40px;
    /* The same Lucide inbox outline as .empty-state, drawn 44px above the centred sentence. Stroke
       is the URL-encoded form of --brand-grey-300 (a data URI cannot read a custom property — same
       trick, and same caveat, as the select chevron): keep the two in step if that token moves. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 24 24' fill='none' stroke='%23D4D4D4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center calc(50% - 44px);
    text-align: center;
    text-wrap: pretty;
    font-size: 14px; line-height: 1.5;
    color: var(--brand-ink-muted);        /* 6.8:1 on the tinted well */
    /* background-COLOR, never the shorthand: `background:` here would reset the icon above to
       none — which is exactly what it did the first time. */
    background-color: var(--brand-surface-alt);
    border: 1px dashed var(--brand-grey-300);
    border-radius: var(--brand-radius);
    animation: app-rise-in var(--brand-t-slow) var(--brand-ease-out) .1s backwards;
  }
}

.mi-search { display: flex; gap: 8px; margin-bottom: 14px; }
.mi-search input[type=search] {
  flex: 1; min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm);
  font: inherit; color: var(--brand-ink); background: var(--brand-white);
}
/* Zie de notitie bij .g-field:focus in branding/branding.css: de gloed blijft de muisdecoratie,
   de leesbare ring hoort er op :focus-visible naast. */
.mi-search input[type=search]:focus { outline: none; border-color: var(--brand-accent); box-shadow: 0 0 0 3px var(--brand-accent-soft); }
.mi-search input[type=search]:focus-visible { outline: 2px solid var(--brand-support-text); outline-offset: 2px; }
.mi-more { margin-top: 12px; }

.mi-list { display: flex; flex-direction: column; gap: 8px; }
.mi-msg {
  text-align: left; width: 100%;
  cursor: pointer; font: inherit;
  /* The inbox reveals message by message, top down — the same reading order as the list itself. */
  animation: app-step-in var(--brand-t-base) var(--brand-ease-out) backwards;
  animation-delay: calc(var(--mi-i, 0) * 24ms);
}
.mi-list .mi-msg:nth-child(2)  { --mi-i: 1; }
.mi-list .mi-msg:nth-child(3)  { --mi-i: 2; }
.mi-list .mi-msg:nth-child(4)  { --mi-i: 3; }
.mi-list .mi-msg:nth-child(5)  { --mi-i: 4; }
.mi-list .mi-msg:nth-child(6)  { --mi-i: 5; }
.mi-list .mi-msg:nth-child(7)  { --mi-i: 6; }
.mi-list .mi-msg:nth-child(8)  { --mi-i: 7; }
.mi-list .mi-msg:nth-child(9)  { --mi-i: 8; }
.mi-list .mi-msg:nth-child(n + 10) { --mi-i: 9; }
/* The selected message is a raised chip, not just a differently coloured rectangle. */
.mi-msg.active { border-color: var(--brand-support); background: var(--brand-support-soft); box-shadow: var(--brand-shadow-sm); }
.mi-msg-top { display: flex; justify-content: space-between; gap: 8px; }
.mi-msg-from { font-weight: 600; font-size: 13px; color: var(--brand-ink); }
.mi-msg-date { font-size: 11px; color: var(--brand-ink-subtle); flex: 0 0 auto; }
.mi-msg-subj { font-size: 13px; color: var(--brand-ink-muted); margin: 3px 0 2px; }
.mi-msg-snip { font-size: 12px; color: var(--brand-ink-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mi-form-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mi-src { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 999px; margin-left: 6px; vertical-align: middle; white-space: nowrap; }
.mi-src-mail   { background: var(--brand-support-soft); color: var(--brand-support-text); }
.mi-src-manual { background: var(--brand-grey-100); color: var(--brand-ink-muted); }

.mi-email-src { margin: 8px 0 18px; }
.mi-email-src summary { cursor: pointer; font-size: 13px; color: var(--brand-support-text); }
.mi-email-body { white-space: pre-wrap; font-size: 13px; color: var(--brand-ink); background: var(--brand-grey-100); border-radius: var(--brand-radius-sm); padding: 12px 14px; margin-top: 10px; max-height: 240px; overflow-y: auto; }

/* ===== Confirmation-email preview modal ===== */
.mail-preview-frame {
  width: 100%;
  height: 60vh;
  min-height: 380px;
  border: 1px solid var(--brand-grey-200);
  border-radius: var(--brand-radius-sm);
  background: var(--brand-white);
}
.mail-preview-to {
  display: flex; align-items: baseline; gap: 12px;
  font-size: 13px; color: var(--brand-ink-subtle); margin: 0 0 12px;
}
.mail-preview-to strong { color: var(--brand-ink); }
.modal.modal-wide { max-width: 680px; }
/* "Verstuurd op ..." rides on the recipient line, NOT in the button row: three buttons plus this
   note need ~717px and the modal is 680, so in the footer the note was clipped mid-word. Up here it
   shares a line with a short address and has all the room it needs. */
.mail-sent-note {
  margin-left: auto;
  font-size: 12px;
  color: var(--brand-ink-subtle);
  white-space: nowrap;
}

/* ===== De volgpagina — STAAT NIET MEER HIER =====
   De opmaak van de volgpagina (.app-track, .track-card, .track-step…) stond woord voor woord óók in
   `mijn/portal.css`, want de klantpagina mag dit stijlblad niet laden. Sinds 16-09-2026 staat die
   opmaak één keer, in `assets/track.css`, en laden beide pagina's dat bestand. Het scherm dat de
   regels hier nog nodig heeft is `beheer/processen.php` (het voorbeeld in de editor); dat linkt
   `assets/track.css` zelf. De drie animaties die erbij horen staan verderop in dit bestand, want die
   worden ook door interne schermen gebruikt. */

/* A short gold rule marks the section — the one decorative flourish the huisstijl allows. */
.panel > h1::after,
.panel > h2::after {
  content: ''; display: block; width: 30px; height: 3px; border-radius: 2px;
  background: var(--brand-rule-accent);
  margin-top: 7px;
}
/* ===== Page loader ===== */
/* Adapted from site/ (overlay at opacity 1 → JS adds .is-loaded once data is in → fade out), but
   Deliberately light: a small logo + spinner, no branded splash. A CSS failsafe hides it after 4s even if
   the JS never runs, so a script error can never leave the app stuck behind the overlay. */
.app-loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  /* Solid white: the canvas colour, so the app assembling behind it is simply not there yet. */
  background: var(--brand-white);
  opacity: 1;
  transition: opacity .4s ease;
  animation: app-loader-failsafe .3s ease 4s forwards;
}
body.is-loaded .app-loader { opacity: 0; pointer-events: none; }
@keyframes app-loader-failsafe { to { opacity: 0; visibility: hidden; pointer-events: none; } }

/* Hide the page content until the data is in, so the user never sees the UI assemble piece by piece
   behind the loader — the loader is all they see, then the finished page fades in. FAIL-SAFE: a CSS
   animation holds the content hidden, then force-reveals it at ~4s even if JS never adds .is-loaded,
   so a script error can't leave a blank page. The keyframe defines BOTH 0% (hidden, held for the
   first ~99%) and 100% (shown), so during the run the element is reliably invisible until the end.
   (The reduced-motion block below reveals instantly, no fade.) */
.app-main {
  animation: app-content-failsafe 4s linear forwards;
}
body.is-loaded .app-main {
  opacity: 1;
  transition: opacity .35s ease .05s;
  animation: none;
}
@keyframes app-content-failsafe {
  0%, 99% { opacity: 0; }
  100%    { opacity: 1; }
}

/* Het laadscherm schaalt mee met het venster.
 *
 * Het logo stond op 132px vast en dat is op een groot scherm een postzegel in het midden van een wit
 * vlak — het las als "er is iets misgegaan" in plaats van "hij komt eraan".
 *
 * `vmin` en niet `vw`: op een breed scherm zou 50vw een logo van bijna duizend pixels opleveren, en
 * op een laag venster zou het onder de vouw vallen. De kleinste van de twee afmetingen houdt hem in
 * beide richtingen heel. Op een telefoon is dat ruwweg de helft van de breedte — precies wat gevraagd
 * was; op een desktop wint de bovengrens.
 *
 * DIE BOVENGRENS IS GEEN SMAAK MAAR SCHERPTE. tenantLogo() levert `logo-black@2x.png` (846px breed),
 * dus tot ongeveer 420 CSS-pixels blijft hij ook op een retina-scherm scherp. Daarboven wordt hij
 * zichtbaar zachter en ziet een groter logo er slechter uit dan een kleiner. Verhoog dit dus niet
 * zonder een grotere bronafbeelding.
 *
 * Alles eronder rekent door op één variabele, zodat de verhoudingen op elk formaat kloppen. */
.app-loader-inner {
  --loader-logo: min(50vmin, 420px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--loader-logo) * .11);
}
.app-loader-inner img { width: var(--loader-logo); height: auto; }
.app-loader-spin {
  /* Geklemd: op een klein scherm mag het rondje niet onder de 22px zakken (dan leest het niet meer
     als beweging), op een groot scherm niet met het logo mee blijven groeien tot een tweede logo. */
  width: clamp(22px, calc(var(--loader-logo) * .13), 52px);
  height: clamp(22px, calc(var(--loader-logo) * .13), 52px);
  border: max(3px, calc(var(--loader-logo) * .012)) solid var(--brand-grey-200);
  border-top-color: var(--brand-accent);
  border-radius: 50%;
  animation: app-loader-spin .8s linear infinite;
}
@keyframes app-loader-spin { to { transform: rotate(360deg); } }

/* ===== Motion keyframes =====
   Shared vocabulary used across the app. Kept few and reused deliberately so every surface
   moves the same way — that consistency is what reads as "polished" rather than "animated". */

/* Content settling into place: up and in. The workhorse. */
@keyframes app-rise-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* A card arriving: slightly larger movement + a touch of scale. */
@keyframes app-card-in {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
/* Dialog entrance — starts just under centre and scales up to meet the backdrop. */
@keyframes app-modal-in {
  from { opacity: 0; transform: translateY(12px) scale(.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes app-fade-in { from { opacity: 0; } to { opacity: 1; } }
/* Wizard panes advance from the right. */
@keyframes app-pane-in {
  from { opacity: 0; transform: translateX(12px); }
  to   { opacity: 1; transform: translateX(0); }
}
/* Tracking-page steps: a smaller, quicker version of rise-in for list items. */
@keyframes app-step-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Toast: up from the bottom edge, then away again. */
@keyframes app-toast-in {
  from { opacity: 0; transform: translateY(16px) scale(.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes app-toast-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(8px); }
}
/* A KPI figure landing. It travels a fraction of its own height (em, so the distance scales with
   the number) and lands a beat after its tile — that pause is what counts the eye in. */
@keyframes app-kpi-in {
  from { opacity: 0; transform: translateY(.3em); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Selection confirmation — a single restrained pop, no bounce-back. */
@keyframes app-pop {
  0%   { transform: scale(1); }
  55%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}
/* Live indicator on the customer's current stage: the ring widens, the colour stays put. */
@keyframes app-pulse {
  0%, 100% { box-shadow: 0 0 0 4px var(--brand-support-soft); }
  50%      { box-shadow: 0 0 0 8px var(--brand-support-soft); }
}
/* Attention bell — a small tilt, not a jingle. */
@keyframes app-bell {
  0%, 60%, 100% { transform: rotate(0); }
  10%, 30%      { transform: rotate(-9deg); }
  20%, 40%      { transform: rotate(9deg); }
}
/* Failed login. */
@keyframes app-shake {
  0%, 100%     { transform: translateX(0); }
  20%, 60%     { transform: translateX(-5px); }
  40%, 80%     { transform: translateX(5px); }
}

/* ===== Entrances on the screens that have a loading overlay =====
   worker.php, mailintake.php and the admin dashboard hold an opaque white sheet over the page until
   the data is in. Anything server-rendered underneath would play its entrance BEHIND that sheet and
   simply be there when it lifts — the reveal spent on nobody. So on those screens the entrance is
   held and re-armed the moment `body.is-loaded` appears, exactly as motion.css does for the header.
   `:has()` is what tells a loader screen from a plain one; if it ever fails, the animation just runs
   early and the content is still visible — never the other way round.
   Only `animation-name` is touched, never the shorthand: the per-item stagger delays live in their
   own rules and a shorthand here would silently reset every one of them to zero. Everything the JS
   renders AFTER load (the board, the KPI row, the request list) is deliberately not in this list —
   it is inserted after the sheet has lifted and animates where you can see it. */
body:has(.app-loader) :is(.panel, .mi-status, .empty-state) { animation-name: none; }
body:has(.app-loader).is-loaded :is(.panel, .mi-status) { animation-name: app-rise-in; }
body:has(.app-loader).is-loaded .empty-state { animation-name: app-rise-in; }

/* ===== Print failsafe =====
   Everything above that starts at opacity 0 has to be on paper whether or not the browser chose to
   run its animation. This is NOT a motion preference (see the note below) — it is the guard that
   keeps a staggered board, a KPI row or a revealed list from printing as a blank page. Same
   failsafe as the one at the bottom of motion.css, for the elements this file animates. */
@media print {
  .panel, .kpi, .kpi .kpi-val, .ov-card, .alert-strip, .alert-strip .alert-item,
  /* `.board > *`, `.board-col .card` en `.col-empty` stonden hier ook, voor het kolommenbord.
     Dat is op 17-09-2026 verwijderd (specs/orderfunnel.md §9d-bis). */
  .tl-item, .req-list .req-item, .mi-msg, .mi-status,
  .ov-attention .ov-attn-row, .empty-state, .track-step, .login-card, .login-logo,
  .login-tagline, .login-form, .proc-card, .wiz-pane, .card-edit[open] .card-edit-body {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   UITGEZET, MET OPZET — `not all` matcht nooit. Niet weghalen, en zeker niet "herstellen".
   Dit blok stond aan en heeft drie keer geld en tijd gekost. De machine waarop deze app wordt
   beoordeeld meldt `prefers-reduced-motion: reduce` aan de browser (een Windows-instelling), terwijl
   de gebruiker dat juist NIET wil. Gevolg: elke animatie die hier werd toegevoegd werd stilletjes
   platgeslagen tot 0,01 ms, en drie keer op rij luidde de conclusie "ik zie geen verschil" — één keer
   over typende puntjes, één keer over een hele bewegings-pass in Jarvis, en één keer hier.
   Erger nog: het kon een boodschap volledig WISSEN. `.toast` plande zijn uitfade met
   `animation-delay: 2.9s` en `fill-mode: both`; die delay naar 0 forceren liet het eind-keyframe
   meteen vuren en pinde `opacity: 0`. Elke melding in deze app — fouten én bevestigingen — werd
   aangemaakt, leefde 3,6 seconden onzichtbaar en werd weer opgeruimd. Wat je zag was niets, en dat
   las als "de API doet het niet".
   Dit is een intern werkscherm, geen publieke site. `site/` mag `prefers-reduced-motion` wél volgen
   voor decoratieve beweging; hier niet. Zelfde regel als Jarvis, dat hem afdwingt met een selftest.
   Wil je hier ooit toch beweging temperen, doe het dan per element en nooit met een sterretje.
   ───────────────────────────────────────────────────────────────────────────────────────────── */
@media not all {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  /* Kill hover/press displacement outright — duration alone would still move these. */
  .card:hover, .kpi:hover, .shoe-card:hover, .req-item:hover,
  .btn-primary:hover, .alert-strip .alert-item:hover,
  .btn:active, .card:active, .shoe-card:active { transform: none; }
  .kpi:hover::after { transform: scaleX(1); }
  .shoe-card:hover .shoe-icon, .shoe-card.active .shoe-icon,
  .mail-hint:hover [data-lucide="arrow-right"] { transform: none; }
  /* The current-stage dot keeps its ring, just without the breathing. */
  .track-step-current .track-dot { box-shadow: 0 0 0 4px var(--brand-support-soft); }
  /* Loader/content failsafes: the overlay must still clear and content must still reveal, or a
     reduced-motion user gets a permanently blank page. These are function, not decoration. */
  .app-loader-spin { border-top-color: var(--brand-accent); }
  body.is-loaded .app-loader { display: none; }
  .app-main { animation: none; opacity: 1; }
  /* A toast is the app TALKING to you — same category as the failsafes above, not decoration.
     Its fade-OUT is scheduled purely by a 2.9s animation-delay, and the blanket rule forces every
     delay to 0ms, so that keyframe fired immediately and `both` pinned the end state (opacity: 0).
     The node was created, spent its whole life invisible, and was removed on schedule: every error
     and every confirmation in this app arrived as nothing at all. Killing the animation is the fix —
     the toast is simply there, and toast() still removes it on its own 3.6s timer. */
  .toast { animation: none; opacity: 1; transform: none; }
}


/* ═══ Shared building blocks for module pages ════════════════════════════
   Form, modal and list parts used by MORE THAN ONE module (planning/, catalogus/). They first lived
   in planning/planning.css; once catalogus/ needed them too, that would have meant one module
   loading the other's stylesheet. A module should only know its own CSS plus this shared layer.
   The `pl-` prefix is historical (planning) and deliberately not renamed: that touches PHP AND JS
   in two modules without anything getting better. */
/* [hidden] must beat EVERY display rule below.
   Without this a .pl-field or .av-savebar carrying the hidden attribute shows up anyway: a class
   with `display: grid/flex` has higher specificity than the browser rule for [hidden]. Visible
   consequence: the date picker sat in the week pattern and the save bar was permanently there, even
   without changes. */
[hidden] { display: none !important; }

/* ── modal content ──────────────────────────────────────────────────────── */

.pl-icon-btn {
  border: 0; background: transparent; cursor: pointer; padding: 6px;
  border-radius: 8px; color: var(--brand-ink-muted); display: inline-flex;
}
.pl-icon-btn:hover { background: var(--brand-grey-100); color: var(--brand-ink); }

.pl-form { display: grid; gap: 14px; }
.pl-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.pl-field { display: grid; gap: 5px; min-width: 0; }
.pl-field label { font-size: 13px; font-weight: 600; color: var(--brand-ink-muted); white-space: nowrap; }
.pl-field input, .pl-field select, .pl-field textarea {
  /* Uiterlijk komt van de basisregel bovenaan; hier staat alleen wat dit grid ervan afwijkt. */
  width: 100%;
}
/* De padding wordt hier wél herhaald omdat deze regel specifieker is dan de basis: zonder deze
   uitzondering zou `padding: 8px 10px` de ruimte voor de chevron opeten en liep de tekst eronder. */
.pl-field select { padding-right: 32px; }
.pl-field textarea { resize: vertical; min-height: 64px; }
.pl-field input:focus-visible, .pl-field select:focus-visible, .pl-field textarea:focus-visible {
  /* Same ring as the base field rule above — 5.38:1, not the 2.52:1 fill blue. */
  outline: 2px solid var(--brand-support-text); outline-offset: 1px; border-color: var(--brand-support);
}
.pl-field-error input, .pl-field-error select { border-color: var(--brand-danger); }
.pl-error {
  font-size: 12px; color: var(--brand-danger); display: flex; align-items: center; gap: 4px;
}
.pl-hint { font-size: 12px; color: var(--brand-ink-muted); }

.pl-modal-actions {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--brand-grey-200);
}
.pl-modal-actions .pl-spacer { margin-left: auto; }

/* Customer search */
.pl-search-results {
  border: 1px solid var(--brand-grey-200);
  border-radius: var(--brand-radius-sm);
  max-height: 168px; overflow: auto;
}
.pl-search-results:empty { display: none; }
.pl-search-hit {
  display: block; width: 100%; text-align: left; font: inherit; font-size: 13px;
  padding: 8px 10px; border: 0; background: transparent; cursor: pointer;
  border-bottom: 1px solid var(--brand-grey-100);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pl-search-hit:last-child { border-bottom: 0; }
.pl-search-hit:hover, .pl-search-hit:focus-visible { background: var(--brand-support-soft); }

.pl-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: 999px;
  background: var(--brand-support-soft); color: var(--brand-support-deep);
  font-size: 13px; font-weight: 600; white-space: nowrap;
}
.pl-chip button { border: 0; background: transparent; cursor: pointer; color: inherit; display: inline-flex; padding: 0; }

/* Detail view */
.pl-detail-grid { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: 14px; margin-bottom: 16px; }
.pl-detail-grid dt { color: var(--brand-ink-muted); white-space: nowrap; }
.pl-detail-grid dd { margin: 0; color: var(--brand-ink); min-width: 0; overflow-wrap: anywhere; }

/* The address of one appointment, with the route button (planning.js, spec §7.7).
   One line that may wrap, and two buttons that may NOT: `white-space: nowrap` on the buttons plus
   `min-width: 0` on the text keeps "Route" and "Wijzigen" on one line, however long the address is.
   A button label that falls over two lines is the most reported UX complaint in this suite. */
.pl-address-panel {
  margin-bottom: 16px; padding-top: 12px;
  border-top: 1px solid var(--brand-grey-200);
}
.pl-address-summary { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 14px; }
.pl-address-pin { color: var(--brand-ink-muted); flex: none; }
.pl-address-text { flex: 1 1 160px; min-width: 0; color: var(--brand-ink); overflow-wrap: anywhere; }
.pl-address-empty { color: var(--brand-ink-muted); }
.pl-address-summary .btn { white-space: nowrap; flex: none; }
.pl-address-editor { display: grid; gap: 12px; margin-top: 12px; }
.pl-address { display: grid; gap: 12px; }
.pl-address-actions { display: flex; justify-content: flex-end; }

.pl-timeline { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.pl-timeline li { display: grid; grid-template-columns: auto 1fr; gap: 10px; font-size: 13px; }
.pl-timeline time { color: var(--brand-ink-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Join the existing heading and list into one recessed history block. */
#plModalBody > h3.panel-sub:has(+ .pl-timeline),
#plModalBody > h3.panel-sub + .pl-timeline {
  background: var(--brand-surface-sunken);
  color: var(--brand-ink);
  padding-inline: var(--brand-space-4);
}
#plModalBody > h3.panel-sub:has(+ .pl-timeline) {
  margin: var(--brand-space-5) 0 0;
  padding-block: var(--brand-space-4) var(--brand-space-3);
  border-radius: var(--brand-radius) var(--brand-radius) 0 0;
  font-size: 14px;
  white-space: nowrap;
}
#plModalBody > h3.panel-sub + .pl-timeline {
  padding-bottom: var(--brand-space-4);
  border-radius: 0 0 var(--brand-radius) var(--brand-radius);
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--brand-space-3);
}
#plModalBody > h3.panel-sub + .pl-timeline > li {
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
}
#plModalBody > h3.panel-sub + .pl-timeline > li > span {
  min-width: 0;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
@media (max-width: 640px) {
  #plModalBody > h3.panel-sub:has(+ .pl-timeline),
  #plModalBody > h3.panel-sub + .pl-timeline {
    padding-inline: var(--brand-space-3);
  }
}

.pl-signal-list { display: grid; gap: 6px; margin: 0 0 14px; padding: 0; list-style: none; }
.pl-signal-list li {
  display: flex; align-items: center; gap: 8px;
  background: var(--brand-warning-soft); color: var(--brand-warning);   /* 5,31:1 */
  border-radius: var(--brand-radius-sm);
  padding: 8px 12px; font-size: 13px;
}

/* Slot finder and waiting list */
.pl-list { display: grid; gap: 8px; max-height: 380px; overflow: auto; }
.pl-list-item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  font: inherit; font-size: 14px; text-align: left; cursor: pointer;
}
/* Selectable item: the same blue selection affordance as .pl-search-hit — in this house blue is the
   colour of "this is what you pick", not of the chrome. */
.pl-list-item strong { white-space: nowrap; }
.pl-list-item .pl-grow { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-empty { color: var(--brand-ink-muted); font-size: 14px; padding: 20px 0; text-align: center; }


/* Module pages keep some air at the bottom; used by both planning/ and catalogus/. */
.pl-wrap { padding-bottom: 48px; }

/* GPH code (Gemeenschappelijke Prestatiecodelijst Hulpmiddelen) wherever a request is shown.
   Monospace and fully selectable on one click: it is a 12-digit reference that gets read aloud and
   typed over, and a proportional font makes it easy to lose your place mid-number. */
code.gph {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; letter-spacing: .02em; white-space: nowrap;
  background: var(--brand-grey-100); color: var(--brand-ink);
  padding: 1px 6px; border-radius: 4px; user-select: all;
}

/* ---------- Intake: gesprekspunten en de check bij een bestaande klant ----------
   Een informatievlak, geen waarschuwing: de medewerker doet niets fout, hij krijgt houvast. Vandaar
   de rustige steunkleur en geen rood of amber. */
.intake-hints {
  margin: 0 0 14px;
  padding: 12px 14px;
  border-radius: var(--brand-radius);
  background: var(--brand-support-soft);
  border-left: 3px solid var(--brand-support);
}
.intake-hints-head {
  margin: 0 0 6px;
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--brand-support-text);
}
.intake-hints-list { margin: 0; padding-left: 18px; }
.intake-hints-list li { font-size: 13px; line-height: 20px; color: var(--brand-ink); text-wrap: pretty; }

.intake-check { display: flex; flex-direction: column; gap: 14px; margin: 0 0 14px; }
.ic-q { display: flex; flex-direction: column; gap: 6px; }
.ic-label { margin: 0; font-size: 13px; font-weight: 600; color: var(--brand-ink); }
/* Three answers side by side; they wrap as a group on a narrow screen rather than one option
   dropping to a line of its own. */
.ic-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.ic-opt {
  cursor: pointer; white-space: nowrap;
  font-size: 13px; font-weight: 500;
  padding: 6px 14px; border-radius: 999px;
  border: 1px solid var(--brand-grey-300);
  background: var(--brand-surface); color: var(--brand-ink);
}
.ic-opt:hover { border-color: var(--brand-accent); }
/* The chosen answer is filled, not merely outlined: at a glance you must see WHICH of the three it
   was, from across a desk, while on the phone.
   The fill is the BLUE CTA gradient with a white label — the documented look for a filled pill
   (branding/README.md, "The primary button"). It was gold, which is the second-tier treatment and
   carries BLACK; white on gold is unreadable and gold is reserved for rules and section marks. */
.ic-opt.active {
  background: linear-gradient(135deg, var(--brand-cta-from) 0%, var(--brand-cta-to) 100%);
  border-color: transparent;
  color: var(--brand-on-cta, #fff);
  font-weight: 600;
}
.ic-note { font-size: 13px; }   /* the rest comes from the shared input rule above */

/* ── DE TABELKOP: ÉÉN BLAUW VOOR DE HELE APP (15-09-2026) ─────────────────────────────────────
 *
 * DE BLAUWDRUK IS DE {CLIENTEN}LIJST (`.deb-headrow` in debiteuren.css, gevraagd op 15-09-2026:
 * "blauw met witte letters zoals de knoppen, als een echte scheiding"). Daarna stond er in één app
 * twee keer een kolomkop: blauw op het ene scherm, grijs-op-grijs op alle andere. Glenn: *"from an
 * uniform style perspective ... if we do we need to do it across the Grol/ app"*.
 *
 * OP HET ELEMENT `thead th` EN NIET OP EEN KLASSE, met opzet. Een klasse moet je per tabel
 * aanzetten, en dan is de tabel die er volgend jaar bij komt weer grijs — precies het gat dat dit
 * verschil liet ontstaan. Zo doet elke tabel in de app vanzelf mee, en de schermen hoeven alleen
 * hun eigen indeling te beschrijven.
 *
 * HET VERLOOP KOMT UIT DEZELFDE TOKENS ALS `.btn-primary` en `.deb-headrow`: er is één blauw in de
 * app en geen tweede dat er bijna op lijkt. Verf hier nooit een eigen kleurcode.
 *
 * `background-color` LOS van `background-image`, om dezelfde reden als bij `.btn-primary`: verft
 * het verloop niet, dan ligt er onder deze witte letters nog steeds blauw en geen wit-op-wit.
 *
 * LET OP BIJ EEN NIEUWE KOP-REGEL IN EEN SCHERM-CSS: `.x thead th` is sterker dan deze regel. Zet
 * daar dus alleen indeling neer (breedte, uitlijning, `position: sticky`), nooit `background` of
 * `color` — dat is precies hoe een scherm weer uit de pas gaat lopen. */
/* HET VERLOOP LIGT OP DE RIJ EN NIET OP DE CEL (gemeld 15-09-2026, met een schermafdruk erbij).
   Een verloop op elke <th> begint in ELKE cel opnieuw: je ziet zes lichte-naar-donkere blokjes
   naast elkaar in plaats van één balk. Op de rij loopt het één keer over de hele breedte, precies
   zoals bij .deb-headrow. De cellen laten het dus dóór en verven zelf niets. */
thead tr {
  background-color: var(--brand-cta-to);
  background-image: linear-gradient(135deg, var(--brand-cta-from) 0%, var(--brand-cta-to) 100%);
}
thead th {
  background: transparent;
  color: var(--brand-on-cta);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  text-align: left;
  /* Een kolomkop mag nooit over twee regels breken: dat duwt de hele tabel omlaag. */
  white-space: nowrap;
}

/* Een kop die zelf een knop of een link is (sorteren) erft de witte letter. Zonder dit valt hij
   terug op het blauw van een link — blauw op blauw. */
thead th a,
thead th button { color: inherit; }

/* ── De functiematrix ──────────────────────────────────────────────────────────────────────────
 *
 * Staat HIER en niet in handboek.css, want twee schermen tonen hem: het beheerscherm naast de
 * knoppen die de vinkjes zetten, en het handboek met de uitleg eromheen. Eén opbouw
 * (beheer/functiematrix_lib.php), dus ook één opmaak. */

/* ── Samenvouwbare panelen op het beheerscherm ────────────────────────────────────────────────
 *
 * WAAROM ELK PANEEL DICHT BEGINT. Medewerkers laadde als vijf open lappen onder elkaar: de lijst,
 * een formulier met vijf velden, de functiematrix, de ziekmeldinstelling en dertig logregels. Je
 * komt er voor één ding, meestal voor één persoon, en de rest stond er evengoed.
 *
 * DE REGEL MOET HET ANTWOORD DRAGEN, anders is dit geen rust maar verstoppen. Elke kop noemt
 * daarom wat erachter zit: "9 medewerkers, 1 inactief", "12 in 30 dagen", de naam die je belt bij
 * ziekte. Wie alleen dát wilde weten, hoeft niet te klikken.
 *
 * EEN WAARSCHUWING GAAT NOOIT ACHTER EEN KLIK. Zie `.wees-rechten` op medewerkers.php: die staat
 * buiten elke samenvouwer. */
.mw-vouw > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 2px 0;
  font-size: 15px;
  color: var(--brand-ink);
  /* Het standaarddriehoekje eruit: het staat per browser ergens anders, en naast een pictogram van
     een vaste lijndikte valt dat verschil op. `.mw-vouw-pijl` neemt het over. */
  list-style: none;
}
.mw-vouw > summary::-webkit-details-marker { display: none; }
.mw-vouw > summary:focus-visible { outline: 2px solid var(--brand-accent); outline-offset: 2px; }
.mw-vouw[open] > summary { margin-bottom: 10px; }

/* Het pijltje draait mee. Zelfde beweging als bij `.card-edit`, zodat "dit klapt open" er op elk
   scherm van deze app hetzelfde uitziet. */
.mw-vouw-pijl {
  flex: none;
  color: var(--brand-ink-subtle);
  transition: transform var(--brand-t-base, .2s) var(--brand-ease-out, ease);
}
.mw-vouw[open] > summary .mw-vouw-pijl { transform: rotate(90deg); }

/* Het onderwerp-pictogram. In de steunkleur en niet in het zwart van de kop: het wijst de kop aan,
   het is niet zelf de kop. */
.mw-vouw-icoon { flex: none; color: var(--brand-support-text, var(--brand-ink-muted)); }

/* ── De legenda, die tegelijk het filter is ───────────────────────────────────────────────────
 *
 * De lijst staat op alfabet, dus de PLEK van een rij zegt niet meer wat voor account het is. Het
 * kleurmerk links op de rij doet dat, en dit rijtje vertelt wat de kleuren betekenen — én brengt je
 * met één klik naar die rijen. Eén ding in plaats van twee: een legenda die je één keer leest plus
 * een filterbalk die dezelfde drie woorden herhaalt.
 *
 * DE KLEUR IS NOOIT HET ENIGE SIGNAAL. Elke chip draagt zijn woord en zijn aantal, elke rij draagt
 * zijn pil. Wie kleuren niet onderscheidt, leest exact hetzelfde. */
.mw-legenda { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }

.mw-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px;
  border: 1px solid var(--brand-grey-300);
  border-radius: 999px;
  background: var(--brand-surface);
  color: var(--brand-ink);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.mw-chip:hover { border-color: var(--brand-ink-subtle); }
.mw-chip:focus-visible { outline: 2px solid var(--brand-accent); outline-offset: 2px; }

/* De ingedrukte chip krijgt het vlak van zijn eigen kleur, zodat je ziet WELK filter aan staat en
   niet alleen DAT er een filter aan staat. Zachte tint met zwarte tekst: de volle merkkleuren zijn
   vulling, geen tekstachtergrond (branding/colors.css). */
.mw-chip[aria-pressed="true"] { border-color: currentColor; }
.mw-chip-beheer[aria-pressed="true"]     { background: var(--brand-accent-soft);  color: var(--brand-ink); }
.mw-chip-medewerker[aria-pressed="true"] { background: var(--brand-support-soft); color: var(--brand-ink); }
.mw-chip-inactief[aria-pressed="true"]   { background: var(--brand-grey-100);     color: var(--brand-ink); }

/* Het kleurvlekje in de chip: precies dezelfde kleur als de rand op de rij eronder. Wijkt er één
   af, dan wijst de legenda naar iets wat er niet is. */
.mw-chip-vlek { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.mw-chip-beheer     .mw-chip-vlek { background: var(--brand-accent); }
.mw-chip-medewerker .mw-chip-vlek { background: var(--brand-support-text); }
.mw-chip-inactief   .mw-chip-vlek { background: var(--brand-grey-300); }

/* Het merk op de rij zelf. Een rand links en geen vlakvulling: de rij draagt al een achtergrond
   (inactief is grijs), en twee lagen kleur over elkaar maken allebei onleesbaar. */
.worker-row.mw-soort-beheer     { border-left: 4px solid var(--brand-accent); }
.worker-row.mw-soort-medewerker { border-left: 4px solid var(--brand-support-text); }
.worker-row.mw-soort-inactief   { border-left: 4px solid var(--brand-grey-300); }

/* De toelichting in de kop: kleiner en rustiger dan de titel, maar wel leesbaar — hij draagt het
   antwoord, dus hij mag geen grijs randje worden. Op één regel: dit is een kop. */
.mw-vouw-hint {
  color: var(--brand-ink-muted);
  font-size: 13px;
  font-weight: 400;
  white-space: nowrap;
}

/* Twee panelen onder elkaar in dezelfde sectie hebben een scheiding nodig, anders lezen de twee
   koppen als één lijstje zonder verband. */
.mw-vouw + .mw-vouw {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--brand-grey-200);
}

.fm-wrap { overflow-x: auto; margin: 0 0 12px; }

.fm-tabel {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  background: var(--brand-surface);
  color: var(--brand-ink);
}
.fm-tabel th,
.fm-tabel td {
  padding: 7px 10px;
  border-bottom: 1px solid var(--brand-grey-200);
  text-align: left;
  vertical-align: top;
}
/* Kleur en letter komen van de gedeelde `thead th`-regel hierboven. Hier staat alleen wat eigen is
   aan deze tabel. */
.fm-tabel thead th {
  /* De kolomkoppen zijn functienamen; die mogen nooit over twee regels breken. */
  white-space: nowrap;
}

/* DE FUNCTIEKOP STAAT BOVEN ZIJN EIGEN KOLOM (16-09-2026, verzoek uit de review: "center the
   headers as the content is also centered"). De cellen eronder zijn gecentreerd — "Ja" of "—" — en
   een kop links boven een gecentreerde kolom wijst bij een brede kolom naar niets.
   ALLEEN DEZE KOPPEN. "Onderdeel", "Nieuwe functie" en "Nu bij" staan boven een naam, een lege
   invulplek en een getal, en die staan alle drie links; die koppen blijven dus links. */
.fm-tabel thead th.fm-kop { text-align: center; }

/* De twee knoppen eronder zijn een flexrij: die volgt `text-align` niet en moet apart mee. */
.fm-kop .fm-kop-acties { justify-content: center; }
.fm-tabel code { font-size: 11px; color: var(--brand-ink-subtle); }

/* De cel IS het antwoord, dus hij moet ook op een zwart-witprint te lezen zijn: daarom het woord
   "Ja" en niet alleen een vulling. De vulling maakt de kolom scanbaar, het woord maakt hem
   leesbaar — haal het woord er niet uit. */
.fm-ja {
  background: var(--brand-success-soft);
  color: var(--brand-ink);
  font-weight: 600;
  text-align: center;
}
.fm-nee {
  color: var(--brand-ink-subtle);
  text-align: center;
}

/* De invulkolom. Wit en niet grijs: hierop schrijft iemand met een pen. */
.fm-leeg {
  background: var(--brand-surface);
  color: var(--brand-ink);
  min-width: 5rem;
}

/* Een onderdeel dat GEEN ENKELE functie uitdeelt. De volle rij vertelt dat er iets anders is, de
   gekleurde rand vertelt WAT — en die rand overleeft een zwart-witprint. */
.fm-wees td { background: var(--brand-warning-soft); color: var(--brand-ink); }
.fm-wees td:first-child { box-shadow: inset 3px 0 0 var(--brand-warning); }

/* ── De bedienbare matrix (alleen het beheerscherm) ───────────────────────────────────────────
 *
 * DE HELE CEL IS DE KNOP. Een vinkje in een raster van veertien bij vier is een muisdoel van twaalf
 * pixels tussen dertien identieke buren; de cel is er honderd. Het woord "Ja" blijft staan: een
 * vulling alleen overleeft geen zwart-witprint en geen kleurenblindheid. */
/* DE KNOP IS DE CEL, niet een blokje erin. De padding staat daarom op de knop en niet op de `td`:
   anders kleurt bij hover alleen het midden op en zie je een gele postzegel op een groen vlak. */
/* De ketting die een knop een tabelcel laat VULLEN. Een `td` heeft van zichzelf geen hoogte om
   tegen af te rekenen, dus `height: 100%` op de knop lost in niets op en blijft de knop zo hoog als
   zijn tekst — bij een rij van 55 pixels is dat een blokje van 34 met een rand eromheen die niet
   meekleurt. `height: 1px` op de tabel is de bekende truc: een tabel rekt altijd op tot zijn
   inhoud, dus het is in de praktijk een MINIMUM waar de percentages hieronder aan kunnen hangen.
   Haal je één regel uit deze vier weg, dan staat het blokje terug. */
.fm-tabel { height: 1px; }
.fm-tabel tbody tr { height: 100%; }
.fm-tabel td.fm-klik { padding: 0; height: 100%; }
.fm-klik .fm-cel { height: 100%; }

.fm-cel {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 34px;
  padding: 7px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
}
/* Hover op de HELE cel. Eén kleur voor aan en uit: het is de aanwijzer "hier klik je", en dat is
   dezelfde mededeling of er nu Ja of een streepje staat. Het woord in de cel blijft staan, dus je
   raakt de toestand niet kwijt terwijl je eroverheen gaat. */
.fm-cel:hover { background: var(--brand-accent-soft); color: var(--brand-ink); }
.fm-cel:focus-visible { outline: 2px solid var(--brand-accent); outline-offset: -2px; }
.fm-cel:disabled { cursor: progress; opacity: .6; }

/* De acties bij een kolomkop. Klein en in normale letters: de KOP is de functienaam, deze twee zijn
   gereedschap ernaast en mogen hem niet overstemmen. */
.fm-kop-acties { display: flex; gap: 6px; margin-top: 4px; }
.fm-kop-knop {
  padding: 1px 6px;
  border: 1px solid var(--brand-grey-300);
  border-radius: 999px;
  background: var(--brand-surface);
  color: var(--brand-ink-muted);
  font: inherit;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
  white-space: nowrap;
}
.fm-kop-knop:hover { border-color: var(--brand-accent); color: var(--brand-ink); }
.fm-kop-weg:hover { border-color: var(--brand-danger); color: var(--brand-danger); }
.fm-kop-knop[data-zeker="1"] {
  border-color: var(--brand-danger);
  background: var(--brand-danger-soft);
  color: var(--brand-ink);
}

.fm-hernoem { display: flex; gap: 4px; margin-top: 4px; }
.fm-hernoem input {
  width: 8rem;
  padding: 2px 6px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm, 6px);
  background: var(--brand-surface);
  color: var(--brand-ink);
  font: inherit;
  font-size: 12px;
  text-transform: none;
  letter-spacing: 0;
}

/* Toevoegen staat ONDER de tabel: je leest eerst wat er al is. */
.fm-nieuw {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  font-size: 13px;
}
.fm-nieuw label { font-weight: 600; color: var(--brand-ink); }
.fm-nieuw input {
  flex: 1 1 12rem;
  min-width: 10rem;
  padding: 6px 10px;
  border: 1px solid var(--brand-grey-300);
  border-radius: var(--brand-radius-sm, 8px);
  background: var(--brand-surface);
  color: var(--brand-ink);
  font: inherit;
}

.fm-legenda {
  margin: 0 0 10px;
  padding-left: 18px;
  font-size: 12px;
  line-height: 18px;
  color: var(--brand-ink-muted);
}
.fm-note {
  margin: 0 0 10px;
  font-size: 12px;
  line-height: 18px;
  color: var(--brand-ink-subtle);
}

@media (max-width: 640px) {
  .mw-vouw > summary { flex-wrap: wrap; }
  .mw-vouw-hint { flex-basis: 100%; white-space: normal; }
}

/* Detail values need the full phone width, especially email addresses and provision names. */
.modal .req-detail-row > span:last-child { min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 640px) {
  .modal .req-detail-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; padding: 7px 0; }
  .modal .req-detail-row .k { min-width: 0; }
}
