/* The shell's CSS (finding 7, 2026-09-22): moved out of
   templates/cotton/layout/base.html so the browser can cache it -- the inline
   version was invalidated on every request by its CSP nonce. What stayed inline
   there is per-tenant/per-language: the brand-colour :root block and the three
   translated .planperson content: rules -- see that template's residual
   <style> block, right before this file's <link>. */

        /* styleguard:off -- token definitions are the source and may be raw */
        /* Admin skin (admin portal): the same app shell, a different brand-colour house style
           (slate/graphite; family via core.colour derivation). :root[data-skin] has
           higher specificity than :root, so this overrides the default/tenant --brand
           as soon as data-skin="admin" is set on <html>. */
        :root[data-skin="admin"] {
            --brand: #37414f; --brand-dark: #2a323d; --brand-action: #58697f; --brand-tint: #f1f2f3;
        }
        :root {
            /* Navigation chevrons as a vector mask (currentColor) instead of text-glyph arrows.
               One source, per direction; used by the disclosure/dropdown carets. */
            --chev-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
            --chev-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 15l6-6 6 6'/%3E%3C/svg%3E");
            --chev-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6l6 6-6 6'/%3E%3C/svg%3E");
            --chev-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 6l-6 6 6 6'/%3E%3C/svg%3E");
            --chev-updown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 10l4-4 4 4M8 14l4 4 4-4'/%3E%3C/svg%3E");
            /* === Design tokens -- see docs/ontwerpsysteem.html ===
               Brand = tenant-variable (family derived from --brand); the rest is fixed. */
            /* Semantic (vast). The four fixed text/status colours were darkened on
               2026-08-19 (JP: "volg de norm") so they meet the 4.5:1 that §3G's WCAG 2.2 AA
               asks for -- measured, not estimated, over 21 screens in a real browser:
               --text-muted 3.48 -> 5.19 on white and 4.63 on the palest tint it sits on,
               --danger 4.19 -> 4.68 on its own tint, --warning 3.15 -> 4.63,
               --success 4.04 -> 4.72. Each was the *token*, not the rules using it: one
               value carried every hint, label, chip and notice that failed. */
            --success: #1b7842; --success-dark: #166437; --success-tint: #d5f5e3;
            --danger: #b33528; --danger-tint: #fadbd8; --danger-field: #fdf3f2;
            --warning: #935f0b; --warning-tint: #fdebd0;
            /* Calendar-only colours (data-viz; used by the shift calendar and nothing else —
               easy to tweak here, and no other screen feels it). The generic values
               (--success/--danger/--brand-tint/--card/--txt-*) are reused where they match exactly. */
            --cal-highlighted: #eef5fb; --cal-today-bg: #eaf3fa; --cal-panel: 230px;
            --cal-full-text: #1e6b3f; --cal-full-bg: #eef9f2; --cal-full-border: #bfe3cd;
            --cal-full-headbg: #e3f4ea; --cal-full-headborder: #c2e6d0; --cal-slot-border: #a9dcbd; --cal-external-2: #aee3c4;
            --cal-under-text: #943126; --cal-under-border: #f0cbc5; --cal-under-headbg: #fbe7e4; --cal-under-headborder: #f2c9c3;
            --cal-empty-bg: rgba(192,57,43,0.06);
            --ux-mark: #f6b93b; /* review-aid: markering "nog UX-aandacht nodig" (tijdelijk) */
            /* Neutral & surfaces (vast) */
            --text: #2c3e50; --text-muted: #646f70; --text-inverse: #ffffff;
            --bg: #f4f6f7; --card: #ffffff; --surface-soft: #fafbfc; --surface-muted: #f6f8fa;
            --border: #e3e8ec; --border-strong: #d5dbdb;
            /* Data table (vast) */
            --row-even: #f7fafc; --row-hover: #edf4f9; --col-hover: #e3ecf2; --col-filtered: #d2e6f7;
            /* Overlay (vast) */
            --shadow: rgba(0,0,0,0.12); --shadow-strong: rgba(0,0,0,0.28); --divider-on-brand: rgba(255,255,255,0.15);
            --scrim: rgba(22,24,29,0.4);
            /* Typografie — modulaire type-scale (§3A), basis 15px */
            --txt-xs: 0.6875rem; --txt-sm: 0.8125rem; --txt-base: 0.9375rem; --txt-lg: 1.125rem; --txt-xl: 1.375rem;
            /* Spacing — 4px-basis, 8px-ritme (§3B) */
            --sp-1: 0.25rem; --sp-2: 0.5rem; --sp-3: 0.75rem; --sp-4: 1rem; --sp-5: 1.5rem; --sp-6: 2rem;
            /* Radius (§3B) */
            --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-pill: 999px;
            /* Height of the fixed app header (topbar); also the sticky offset of the sidebar */
            --head-h: 52px;
            /* The two sizes an icon control may have (principle 9, decision JP 2026-08-20).
               Measured that day, the small controls came out at four sizes -- 30, 32, 26 and
               19px -- and only two of those had a reason. Each of these does:

                 `--control-h`      a control that stands on its own: a card head, a toolbar,
                                    the app header. The size is a free choice, so it is fixed
                                    here rather than per rule.
                 `--control-h-row`  a control inside a table row, which has to stay within the
                                    row height or the row grows around it.

               A third kind takes no token on purpose: an inline marker (`.infobtn`) has to
               run with the line height of the text beside it, so a fixed box is the wrong
               answer there. That is also what keeps it conforming -- WCAG 2.2 SC 2.5.8 asks
               24x24, and an inline target is exempt exactly while it is bounded by the
               surrounding text. */
            --control-h: 1.9rem;
            --control-h-row: 1.6rem;
            /* One height for a field (decision JP 2026-09-17, 1a). The composition walk
               measured a text field at 25px on its own, 27px as a select and 28px inside a
               field grid -- three heights for one kind of control, none of them chosen. The
               field-grid row already stood at 28px for the read/edit swap not to jump, so
               that is the value; the colour swatch keeps its own box as a named variant. */
            --field-h: 28px;
            /* Height of the datatable toolbar; sticky offset of the column headers (they sit just
               below the sticky toolbar). One row: field height + vertical padding + border. */
            --tablebar-h: 3.1rem;
            /* Container-breedtes (§3B) — begrenzen form/kaart/content; data blijft fluid */
            --w-sm: 440px; --w-md: 600px; --w-lg: 780px; --w-xl: 1000px;

            /* Aliases that point at another token. The rest of this block was
               old-Dutch-name -> new token, and that migration is done as of 2026-08-07:
               --kaart/--tekst/--rand/--primair are themselves now already named --card/
               --text/--border/--brand. After the rename those rules read `--card:
               var(--card)`, and a custom property that points at itself is invalid:
               the card lost its background. */
            --brand-light: var(--brand-action);
            --background: var(--bg);
            --text-soft: var(--text-muted);
        }
        /* styleguard:on */
        * { box-sizing: border-box; }
        /* App frame: the header (head) and sidebar (side) are fixed; only the content area
           (main-content) scrolls -- vertically and, with a wide table, horizontally. That way the
           frame never shifts along. body fills the screen exactly and does not scroll itself. */
        body { margin: 0; font-family: "Segoe UI", system-ui, sans-serif; font-size: var(--txt-base); background: var(--background); color: var(--text); display: grid; grid-template-columns: 200px 1fr; grid-template-rows: var(--head-h) 1fr; grid-template-areas: "head head" "side main-content"; height: 100vh; overflow: hidden; }
        /* Vaste app-header (topbar) — brand, sticky; draagt logo + globale acties + user-menu (geen navigatie: principe 4) */
        .appheader { grid-area: head; position: sticky; top: 0; z-index: 60; background: var(--brand); color: var(--text-inverse); display: flex; align-items: center; gap: var(--sp-3); padding: 0 var(--sp-4); height: var(--head-h); box-shadow: 0 2px 6px var(--shadow); }
        .app-brand { display: flex; align-items: center; gap: var(--sp-2); color: var(--text-inverse); text-decoration: none; }
        .app-brand-text { font-size: var(--txt-lg); margin: 0; font-weight: 700; }
        .appheader-right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
        .appheader .head-icon { width: 1.2rem; height: 1.2rem; }
        a.headerbutton { text-decoration: none; }
        /* No shell navigation (the auth page type): the grid keeps reserving the 200px
           column and the header row otherwise, which leaves an empty gutter beside a
           login card. One area, and `main` scrolls the page itself. */
        body[data-chrome="none"] { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: "main-content"; }
        body[data-chrome="none"] main { padding: var(--sp-6) var(--sp-4); }
        aside { grid-area: side; background: var(--brand); color: var(--text-inverse); display: flex; flex-direction: column; position: sticky; top: var(--head-h); height: calc(100vh - var(--head-h)); overflow-y: auto; }
        .navbutton { display: none; background: none; border: 0; color: var(--text-inverse); padding: var(--sp-1) var(--sp-2); cursor: pointer; align-items: center; }
        aside nav { display: flex; flex-direction: column; padding: var(--sp-2) 0; flex: 1; }
        aside nav a { display: block; color: var(--text-inverse); text-decoration: none; padding: var(--sp-2) var(--sp-4); border-left: 3px solid transparent; font-weight: 600; }
        aside nav a:hover { background: var(--brand-dark); }
        aside nav a.subitem { padding: var(--sp-1) var(--sp-4) var(--sp-1) var(--sp-6); font-size: var(--txt-sm); opacity: 0.85; font-weight: 400; }
        aside nav a.active, aside .user a.config-subitem.active { background: var(--brand-dark); border-left: 3px solid var(--text-inverse); opacity: 1; }
        summary.navhead { padding: var(--sp-2) var(--sp-4) var(--sp-1); font-weight: 600; font-size: var(--txt-sm); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.75; color: var(--text-inverse); cursor: pointer; list-style: none; }
        summary.navhead::-webkit-details-marker { display: none; }
        /* Disclosure-caret als vector-chevron (mask, currentColor) i.p.v. tekst-glyph-pijl */
        summary.navhead::after, .profile > summary::after {
            content: ""; display: inline-block; width: 0.72em; height: 0.72em; vertical-align: -0.08em;
            background-color: currentColor;
            -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
            -webkit-mask-position: center; mask-position: center;
            -webkit-mask-size: contain; mask-size: contain; }
        summary.navhead::after { margin-left: var(--sp-1); opacity: 0.7; -webkit-mask-image: var(--chev-right); mask-image: var(--chev-right); }
        details[open] > summary.navhead::after { -webkit-mask-image: var(--chev-down); mask-image: var(--chev-down); }
        .config-menu { display: flex; flex-direction: column-reverse; }
        .config-menu > summary.navhead::after { -webkit-mask-image: var(--chev-up); mask-image: var(--chev-up); }
        .config-menu[open] > summary.navhead::after { -webkit-mask-image: var(--chev-down); mask-image: var(--chev-down); }
        .config-menu a { display: block; }
        /* Pinned views below a module: the list link becomes a collapsible
           subgroup header; the shortcuts are indented one level deeper. */
        details.viewgroup > summary.viewgroup-head { list-style: none; display: flex; align-items: center; cursor: pointer; }
        details.viewgroup > summary.viewgroup-head::-webkit-details-marker { display: none; }
        details.viewgroup > summary.viewgroup-head::after {
            content: ""; display: inline-block; width: 1.75rem; height: 1.75rem; flex: none; margin-right: var(--sp-2); opacity: 0.6;
            background-color: currentColor; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
            -webkit-mask-position: center; mask-position: center; -webkit-mask-size: 0.72em; mask-size: 0.72em;
            -webkit-mask-image: var(--chev-right); mask-image: var(--chev-right); }
        details.viewgroup[open] > summary.viewgroup-head::after { -webkit-mask-image: var(--chev-down); mask-image: var(--chev-down); }
        details.viewgroup > summary.viewgroup-head:hover { background: var(--brand-dark); }
        details.viewgroup a.view-shortcut { padding-left: calc(var(--sp-6) + var(--sp-3)); }
        .user a.config-subitem { padding: var(--sp-1) var(--sp-4) var(--sp-1) var(--sp-6); font-size: var(--txt-sm); opacity: 0.85; border-left: 3px solid transparent; }
        /* Header dropdowns: bell + user menu (native <details>, floating below the button) */
        .notif, .profile { position: relative; }
        .notif > summary, .profile > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: var(--sp-2); color: var(--text-inverse); }
        .notif > summary::-webkit-details-marker, .profile > summary::-webkit-details-marker { display: none; }
        /* 2rem until 2026-08-20, and that was the one size with no reason behind it: the
           header has room to spare, so the bell was simply 2px larger than every other icon
           control in the app. Now the standard size. */
        .headerbutton { position: relative; width: var(--control-h); height: var(--control-h); border-radius: var(--radius-md); border: 1px solid var(--divider-on-brand); background: transparent; color: var(--text-inverse); display: inline-flex; align-items: center; justify-content: center; }
        .notif > summary:hover .headerbutton, .profile > summary:hover { background: var(--brand-dark); }
        .notif-badge { position: absolute; top: -5px; right: -5px; min-width: 1rem; height: 1rem; padding: 0 var(--sp-1); border-radius: var(--radius-pill); background: var(--danger); color: var(--text-inverse); font-size: var(--txt-xs); font-weight: 700; display: flex; align-items: center; justify-content: center; line-height: 1; }
        .profile > summary { padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-1); border-radius: var(--radius-pill); border: 1px solid var(--divider-on-brand); }
        .profile > summary::after { opacity: 0.8; -webkit-mask-image: var(--chev-down); mask-image: var(--chev-down); }
        .profile .avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--brand-light); display: inline-flex; align-items: center; justify-content: center; font-size: var(--txt-sm); font-weight: 600; flex-shrink: 0; }
        .profile .name { font-size: var(--txt-sm); }
        .header-panel { position: absolute; right: 0; top: calc(100% + var(--sp-1)); color: var(--text); min-width: 15rem; z-index: 70; overflow: hidden; }
        /* The chosen language: a state on the item, not a class of its own
           (the modifier/attribute model of principle 3). */
        .profile .subitem[aria-current] { font-weight: 600; background: var(--surface-soft); }
        .profile a.subitem, .profile button.subitem { display: block; width: 100%; text-align: left; color: var(--text); text-decoration: none; padding: var(--sp-2) var(--sp-3); font-size: var(--txt-sm); background: none; border: 0; font-family: inherit; cursor: pointer; }
        .profile a.subitem:hover, .profile button.subitem:hover { background: var(--brand-tint); }
        /* Notificaties-paneel */
        .notif-panel { width: 21rem; max-width: calc(100vw - var(--sp-4)); }
        .notif-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); font-weight: 600; font-size: var(--txt-sm); }
        .notif-list { max-height: 60vh; overflow-y: auto; }
        .notif-item { display: block; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); border-left: 3px solid transparent; color: var(--text); text-decoration: none; }
        .notif-item:last-child { border-bottom: 0; }
        .notif-item:hover { background: var(--brand-tint); }
        .notif-item.unread { background: var(--surface-soft); border-left-color: var(--brand-light); }
        .notif-item--error { border-left-color: var(--danger); }
        .notif-item--attention { border-left-color: var(--warning); }
        .notif-title { display: block; font-weight: 600; font-size: var(--txt-sm); }
        .notif-text { display: block; font-size: var(--txt-sm); color: var(--text-soft); }
        .notif-time { display: block; font-size: var(--txt-xs); color: var(--text-soft); }
        .notif-empty { padding: var(--sp-5) var(--sp-3); text-align: center; color: var(--text-soft); font-size: var(--txt-sm); }
        .user { padding: var(--sp-2) 0; border-top: 1px solid var(--divider-on-brand); font-size: var(--txt-base); }
        .user a.config-subitem { color: var(--text-inverse); text-decoration: none; }
        .user a.config-subitem:hover { opacity: 1; background: var(--brand-dark); }
        main { grid-area: main-content; padding: var(--sp-4) var(--sp-5); min-width: 0; min-height: 0; overflow: auto; }
        .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-3); }
        h1 { font-size: var(--txt-xl); }
        h2 { font-size: var(--txt-lg); }

        /* Blocks: data that belongs together, each in a frame of its own */
        .blockgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--sp-2) var(--sp-5); align-items: start; }
        /* Flat block (§3C): no box-in-box; grouping via an h3 heading with a thin rule.
           max-width caps a form's width even with a single, unlabelled block (JP
           2026-09-20): a lone block in .blockgrid's auto-fit stretches to the full card
           otherwise, since auto-fit hands unclaimed space to whatever is there. */
        .block { padding: 0; max-width: var(--w-md); }
        .block h3 { margin: 0 0 var(--sp-2); border-bottom: 1px solid var(--border); padding-bottom: var(--sp-1); }
        .cardfoot { margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border); color: var(--text-soft); font-size: var(--txt-sm); }

        /* Tabellen: subtiel en compact, consistent op alle pagina's */
        table { width: 100%; border-collapse: collapse; font-size: var(--txt-sm); }
        th, td { text-align: left; padding: var(--sp-1) var(--sp-2); border-bottom: 1px solid var(--border); }
        thead th { background: var(--brand-tint); color: var(--brand); font-weight: 600; font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.03em; border-bottom: 2px solid var(--brand-light); white-space: nowrap; }
        thead th:first-child { border-top-left-radius: 6px; }
        thead th:last-child { border-top-right-radius: 6px; }
        tbody tr:nth-child(even) td { background: var(--row-even); }
        tbody tr:hover td { background: var(--row-hover); }
        th.sortable { cursor: pointer; user-select: none; }
        th.sortable:hover { background: var(--col-hover); }
        .sortmarker { margin-left: var(--sp-1); color: var(--brand-light); font-size: var(--txt-xs); }

        /* Dragging columns (order) and resizing their width. The wrapper is no longer a
           scroll context (the page scrolls, not the table) -- it only serves as the width reference
           for the column fit. That way the sticky column headers keep sticking to the page. */
        .tablewrap { min-width: 0; }
        /* Environment screen: the variables are split into seven sections, and every section
           is a table of its own. Auto layout sizes each one on its own content, so WAARDE
           landed at a different x in every section and the eye had nothing to run down.
           Fixed widths give the four columns one place across all of them (approved by JP
           2026-08-18). Percentages rather than pixels, because the card width follows the
           viewport. */
        .envtable { table-layout: fixed; width: 100%; }
        /* The description is no longer a column but an infotip component beside the name
           (approved by JP 2026-08-18). As a column it cost a fifth of the width on every
           row while it is read on maybe three: measured over the 32 variables in the
           register the median explanation is 64 characters, and PORTAL_HOST -- the one that
           genuinely needs explaining -- is 285. That width goes to the value and to a column
           of its own for the chips, which used to wrap under the name at whatever point the
           column happened to end. */
        /* Five columns since 2026-08-20: the status and its explanation used to share one
           cell, and the column was then as wide as its longest sentence on every row -- so
           the one column you want to run your eye down was the one you could not. The
           status column now holds a badge and nothing else, which is why it is the narrowest
           of the five. */
        .envtable th:nth-child(1), .envtable td:nth-child(1) { width: 24%; }
        .envtable th:nth-child(2), .envtable td:nth-child(2) { width: 13%; }
        .envtable th:nth-child(3), .envtable td:nth-child(3) { width: 33%; }
        .envtable th:nth-child(4), .envtable td:nth-child(4) { width: 12%; }
        .envtable th:nth-child(5), .envtable td:nth-child(5) { width: 18%; }
                /* Column headers stick vertically just below the sticky toolbar (§datatable-frame). `position:
           sticky` is also the positioning context for the resize handle (previously `relative`). */
        table.datatable thead th { position: sticky; top: var(--tablebar-h); z-index: 3; }
        /* The corner (sticky top as well as sticky left) has to stay over the headers that scroll along. */
        table.datatable thead th.selectcolumn, table.datatable thead th.actioncolumn { z-index: 4; }
        /* Handle inside the column border (right: 0, not -4px): otherwise the handle of the
           last column stuck out 4px beyond the table and that pulled in a tiny horizontal scrollbar. */
        .columnhandle { position: absolute; top: 0; right: 0; width: 8px; height: 100%; cursor: col-resize; z-index: 5; }
        .columnhandle:hover { background: var(--brand-light); opacity: 0.4; }
        th.dragging { opacity: 0.45; }
        th.droptarget { box-shadow: inset 3px 0 0 var(--brand-light); }
        td a { text-decoration: none; }
        td a:hover { text-decoration: underline; }

        a { color: var(--brand-light); }
        .button { display: inline-block; background: var(--brand-light); color: var(--text-inverse); border: 0; border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-4); cursor: pointer; text-decoration: none; font-size: var(--txt-base); }
        .button:hover { background: var(--brand); text-decoration: none; }
        .button--secondary { background: var(--text-muted); }
        .button--danger { background: var(--danger); }
        .button--small { padding: var(--sp-1) var(--sp-2); font-size: var(--txt-sm); }
        .button:disabled { opacity: 0.45; cursor: default; }
        .button--icon { width: var(--control-h); height: var(--control-h); padding: 0; font-size: var(--txt-base); line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
        .button--success { background: var(--success); }
        .button--success:hover { background: var(--success-dark); }
        .button svg { width: 1em; height: 1em; flex: none; }  /* label-knop met icoon (bv. dialoog-bevestig); .button--icon overruled hieronder */
        .button--icon svg { width: 16px; height: 16px; }
        .head-icon { width: 1em; height: 1em; vertical-align: -0.125em; }  /* inline icon in a heading/nav, scales with the text */
        /* styleguard:off — mail-/factuureditor (uitzonderingszone, §5a) */
        /* Gedeelde sjabloon-editorklassen (mail- en factuureditor) */
        .paletteblock { border:1px solid var(--border-strong); background:#fff; border-radius:6px; padding:0.5rem 0.55rem; cursor:grab; font-size:var(--txt-sm); user-select:none; }
        .paletteblock:hover { background:#eef3f7; border-color:var(--brand-light); }
        .paletteblock[aria-disabled="true"], .paletteblock[aria-disabled="true"]:hover { opacity:0.45; cursor:default; background:#f2f4f6; border-color:var(--border-strong); }
        .panel-label { display:block; font-size:var(--txt-xs); font-weight:600; margin:0.4rem 0 0.15rem; }
        .panel-input { width:100%; font-size:var(--txt-sm); padding:0.25rem 0.35rem; border:1px solid var(--border); border-radius:4px; }
        .panel-number { width:3.4rem; font-size:var(--txt-sm); padding:0.25rem 0.3rem; border:1px solid var(--border); border-radius:4px; }
        .panel-button { flex:1; font-size:var(--txt-sm); padding:0.2rem 0; border:1px solid var(--border-strong); border-radius:4px; background:#fff; color:inherit; cursor:pointer; }
        .panel-button--active { background:var(--brand-light); color:#fff; }
        .toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:3px; margin-bottom:0.55rem; }
        .tb-button { flex:0 0 auto; box-sizing:border-box; min-width:1.9rem; height:1.7rem; display:inline-flex; align-items:center; justify-content:center; gap:2px; padding:0 0.4rem; font-size:var(--txt-sm); line-height:1; border:1px solid var(--border-strong); border-radius:4px; background:#fff; color:inherit; cursor:pointer; }
        .tb-button:hover { background:#eef1f4; }
        .tb-button.active { background:#e4eef7; border-color:var(--brand, #1a5276); color:var(--brand, #1a5276); }
        .tb-button svg { width:15px; height:15px; }
        .panel-link { border:0; background:none; color:var(--text-soft); cursor:pointer; font-size:var(--txt-xs); padding:0; }
        .panel-section { border:1px solid var(--border); border-radius:8px; background:#fafbfc; padding:0.1rem 0.6rem 0.5rem; margin-bottom:0.6rem; }
        .panel-section:last-child { margin-bottom:0; }
        .panel-section > h4 { margin:0 0 0.4rem; padding:0.4rem 0 0.35rem; font-size:var(--txt-xs); text-transform:uppercase; letter-spacing:0.05em; font-weight:700; color:var(--brand); border-bottom:1px solid var(--border); }
        /* styleguard:on */
        /* The sticky formatting panel of the block editors (<ui.blockpanel>). Deliberately
           OUTSIDE the exception zone above: this is chrome around the canvas, not the
           document being edited, so it is token-driven and guarded like the rest of the
           house style. Until 2026-08-16 it was four inline styles in a partial. */
        .panel-box { flex: 0 0 255px; position: sticky; top: var(--sp-2); overflow: hidden; font-size: var(--txt-sm); }
        .panel-head { background: var(--surface-muted); border-bottom: 1px solid var(--border); padding: var(--sp-2); display: flex; justify-content: space-between; align-items: center; }
        .panel-box .panel-head h4 { margin: 0; }  /* wins over the base `.panel h4` top margin */
        .panel-body { padding: var(--sp-2); }
        /* The transparent border is there so every variant is the same height: the neutral
           one below needs a real border and would otherwise sit 2px taller than the rest. */
        /* `font-weight: 400` so a chip looks the same wherever it lands (2026-08-18). It
           had no weight of its own and inherited one, so the same badge came out bold inside
           a <strong> and normal in a table cell. A component class that changes appearance
           with its surroundings is not a component. */
        /* No room above and below (decision JP 2026-08-20, looking at the four forms side
           by side): the pill was reading as too heavy for what it says. The height comes
           from the line height of the text instead. Zero rather than 2px because the
           spacing scale starts at `--sp-1` and there is nothing smaller -- a literal 2px
           would be raw spacing, which the styleguard refuses and rightly so. */
        .badge { padding: 0 var(--sp-2); border-radius: var(--radius-pill); font-size: var(--txt-sm); font-weight: 400; border: 1px solid transparent; }
        /* The variant is a meaning, never a status code (richtlijn-0002 §4):
           green = good/active, orange = needs attention, red = exclusively error or
           destructive, grey = ended/inactive/off. An ended team is not an error. */
        .badge--positive { background: var(--success-tint); color: var(--success); }
        .badge--warning { background: var(--warning-tint); color: var(--warning); }
        .badge--negative { background: var(--danger-tint); color: var(--danger); }
        /* Outlined, unlike the three above: they carry a tinted background that reads as a
           chip on its own, while `--surface-soft` on a white card is a 1% difference and so
           read as loose grey words rather than as chips (JP 2026-08-18, on the environment
           screen). The other three keep their tint; only the neutral one needs an edge. */
        .badge--neutral { background: var(--surface-muted); color: var(--text-soft); border-color: var(--border); }
        .notice { padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-md); margin-bottom: var(--sp-3); }
        .notice--positive { background: var(--success-tint); color: var(--success); }
        .notice--negative { background: var(--danger-tint); color: var(--danger); }
        .notice--warning { background: var(--warning-tint); color: var(--warning); }
        /* Review aid: screens that still need UX attention (nav item, superuser-only).
           The second selector is not a duplicate but the config menu, which colours its
           own items with `.user a.config-subitem` -- two classes, so it outweighs a bare
           `a.to-migrate` and the marking was invisible there. Written the same way as the
           `active` rule above, which solves the same clash. Found by the uniformity guard
           on 2026-08-20, once the sidebar's navigation items became a kind of object. */
        a.to-migrate, aside .user a.config-subitem.to-migrate { font-style: italic; color: var(--ux-mark); }
        a.to-migrate::after, aside .user a.config-subitem.to-migrate::after { content: " •"; color: var(--ux-mark); }
        .status-ok { color: var(--success); }
        .status-error { color: var(--danger); }
        .fielderror { display: block; font-size: var(--txt-sm); color: var(--danger); }
        input.invalid, select.invalid { border-color: var(--danger); background: var(--danger-field); }
        /* Status colour wins over the muted base text class (§3E: a status speaks through colour, not through a glyph). */
        .hint.status-ok { color: var(--success); }
        .hint.status-error { color: var(--danger); }
        /* Empty table: no rows and no column headers, only a soft notice. The softness sits
           on the `.hint` around the sentence, not on the cell (2026-08-19): grey is the
           colour of explanation, and a table cell that carries it is a second appearance of
           an object that already has one. Same pixels on screen, one owner less. */
        tr.row-empty td { border: 0; padding: var(--sp-2) var(--sp-1); }
        table:has(> tbody > tr.row-empty:only-child) thead { display: none; }
        .cardhead { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
        /* line-height var(--control-h) gives the heading the same line box as an icon button, so align-items:center lands the title on the same spot whether or not the head carries a button. Without it the taller button row dropped the heading ~3px and the same page type sat at two heights (measured 16-09, tab-model track). */
        .cardhead h1, .cardhead h2, .cardhead h3, .tablebar h1 { margin: 0; line-height: var(--control-h); }
        /* A heading row without a heading (a tab side whose name the tab already carries, with only a plus button): the actions keep their place on the right. Until 17-09 the region rendered an empty h3 there, and the composition walk measured it as a heading 15px lower than every other. */
        .cardhead > :first-child:not(h1):not(h2):not(h3) { margin-left: auto; }
        tr[data-href] { cursor: pointer; }
        tr[data-href]:hover td { background: var(--brand-tint); }
        .text-soft { color: var(--text-soft); }
        .text-small { font-size: var(--txt-sm); }
        [hidden] { display: none !important; }
        /* Empty date field: no dd/mm/jjjj placeholder text until focus or entry
           (a plain text colour works in all browsers, including Firefox/Safari) */
        input[type="date"].date-empty:not(:focus) { color: transparent; }
        form p { margin: var(--sp-2) 0; }
        /* One appearance, and the colour is part of it (2026-08-19). The rule set weight and
           size but no colour, so a label took the colour of whatever it stood in: soft in a
           record grid and a setting row, dark in a column picker, brand blue in a filter
           panel, black in a dialog -- four appearances of one object, measured by
           `core/tests_uniformity.py`. Soft is the §3D field-label appearance the setting row
           was already told to follow, so that is the one that wins. */
        label { display: block; font-weight: 600; font-size: var(--txt-sm); color: var(--text-soft); margin-bottom: var(--sp-1); }
        summary { cursor: pointer; }
        /* `--txt-sm`, and that is the whole story for a field (2026-08-19). It used to be
           `--txt-base` here, and then six rules downstream set it back to `--txt-sm` for the
           setting row, the table bar, the filter panel, the option row, the record grid and
           the calendar side -- so 13px was the real size everywhere and 15px only survived
           where nobody had written an override yet (the parameter dialog). Six copies of one
           value: the base rule owns it now and the six are gone. */
        input, select, textarea { padding: var(--sp-1) var(--sp-2); border: 1px solid var(--border-strong); border-radius: var(--radius-md); font: inherit; font-size: var(--txt-sm); color: var(--text); }
        input:not([type=checkbox]):not([type=radio]):not([type=color]):not([type=file]), select { min-height: var(--field-h); }
        select { min-width: 8.5rem; }  /* an empty choice must not let the dropdown shrink */
        input[type="checkbox"], input[type="radio"] { accent-color: var(--brand-light); }
        /* One on/off value is a switch and not a bare checkbox (decision JP 2026-09-18, 7a);
           the ui.toggle component is the only thing that writes these classes. The control
           underneath stays a checkbox, so the keyboard, the form post and `cleaned_data` are
           unchanged and only the appearance moves to .toggle-track. Hence `opacity: 0` on the
           input and never `display: none`: an input that is not displayed is not focusable, and
           the switch would be unreachable without a mouse (WCAG 2.1.1). A row of checkboxes where
           you pick several out of a set (the roles) is a choice out of a row and not one value,
           and keeps its checkboxes -- the ui.field component makes that distinction.

           Do not write a component tag in this comment. Cotton reads this file for tags before
           the browser ever sees a comment marker, so a `c-` tag inside /* ... */ is parsed as a
           real component and the shell dies with "Unclosed tag" on every screen at once. Cost
           here: ten errored tests whose own templates were fine. */
        .toggle { position: relative; display: inline-flex; align-items: center; min-height: var(--field-h); cursor: pointer; }
        .toggle input[type="checkbox"] { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
        /* The four numbers of the switch only make sense together: track 40x24 with a 1px border
           leaves 38x22 inside, a 14px knob with --sp-1 around it fills that height exactly, and
           the travel is 38 - 22 = 16px. Change one and the other three need recomputing. */
        .toggle-track { width: 40px; height: 24px; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); background: var(--card); transition: background 0.12s, border-color 0.12s; }
        .toggle-track::after { content: ""; display: block; width: 14px; height: 14px; margin: var(--sp-1); border-radius: var(--radius-pill); background: var(--border-strong); transition: transform 0.12s, background 0.12s; }
        .toggle input:checked + .toggle-track { background: var(--brand-action); border-color: var(--brand-action); }
        .toggle input:checked + .toggle-track::after { background: var(--card); transform: translateX(16px); }
        .toggle input:focus-visible + .toggle-track { outline: 2px solid var(--brand-action); outline-offset: 2px; }
        /* The read-only switch (decision JP 2026-09-20) has no <input>, so its state lives on
           the switch itself -- aria-checked, read by assistive tech, doubles as the hook these
           two rules need. Same colours as the editable switch: a read-only value stays at full
           contrast, unlike a disabled one (Carbon Design System's read-only-states pattern). */
        .toggle--readonly { cursor: default; }
        .toggle--readonly[aria-checked="true"] > .toggle-track { background: var(--brand-action); border-color: var(--brand-action); }
        .toggle--readonly[aria-checked="true"] > .toggle-track::after { background: var(--card); transform: translateX(16px); }
        :focus-visible.toggle--readonly > .toggle-track { outline: 2px solid var(--brand-action); outline-offset: 2px; }
        .selectsearch { display: block; margin-bottom: var(--sp-1); }
        /* The searchable pick list (the `ui.combobox` component, 2026-09-18). The native
           select keeps the value and keeps submitting; what it gives up is its own appearance, so
           `.combovalue` has to carry the field look itself -- the rules above bind to
           `select`, and by then the select is clipped away. Clipped, not hidden: a
           `required` select that is `display:none` makes the browser refuse to submit at
           all, because it cannot focus the control it wants to report on. */
        .combobox { position: relative; display: inline-block; min-width: 8.5rem; }
        .form .combobox, form p .combobox { display: block; width: 100%; max-width: var(--w-sm); }
        .combovalue { width: 100%; min-height: var(--field-h); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); text-align: left; padding: var(--sp-1) var(--sp-2); border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--card); font: inherit; font-size: var(--txt-sm); color: var(--text); cursor: pointer; }
        .combovalue::after { content: ""; width: 0.9em; height: 0.9em; flex: none; background-color: var(--text-soft); -webkit-mask: var(--chev-down) no-repeat center / contain; mask: var(--chev-down) no-repeat center / contain; }
        .combobox:has(select:required) .combovalue { border-left: 3px solid var(--brand-light); }
        .combopanel { position: fixed; z-index: 40; padding: var(--sp-2); min-width: 220px; max-height: 300px; overflow-y: auto; }
        .combolist { display: flex; flex-direction: column; }
        .comboitem { display: block; width: 100%; text-align: left; border: 0; background: none; padding: var(--sp-1) var(--sp-2); font-size: var(--txt-sm); cursor: pointer; border-radius: var(--radius-sm); color: var(--text); white-space: nowrap; }
        .comboitem:hover, .comboitem.active { background: var(--brand-tint); }
        .comboitem[aria-selected="true"] { font-weight: 600; color: var(--brand); }
        .comboempty { margin: var(--sp-1) var(--sp-2); }
        input:required, select:required, textarea:required { border-left: 3px solid var(--brand-light); }  /* verplicht veld */
        /* Our text fields run on type="search" (keeps password-manager icons out, incl.
           Nordpass; see the script). Neutralise the search-specific styling so they look
           exactly like ordinary input fields. Real search fields (without data-textfield) untouched. */
        input[data-textfield] { -webkit-appearance: textfield; appearance: textfield; }
        input[data-textfield]::-webkit-search-cancel-button,
        input[data-textfield]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; display: none; }
        .form input, .form select, .form textarea, form p input, form p select, form p textarea { width: 100%; max-width: var(--w-sm); }
        .hint { font-size: var(--txt-sm); color: var(--text-soft); }
        /* The value of an environment variable on the env screen: monospace, a touch
           smaller, a soft surface with a border -- so a host name or an endpoint reads as
           a *value* rather than as prose. Approved by JP 2026-08-16. Until then those
           values sat in a bare <code>, which has no rule in this stylesheet at all and so
           leaned on the browser default: not reuse but the third category principle 3
           rules out. */
        /* `color` set here on purpose (uniformity walk, 2026-09-17): a machine value is
           something you read character by character, so it does not take the softness
           of a hint it happens to sit in. Without it the zone in the intro of the
           sending-domain tab came out soft grey beside the same value dark in the table. */
        .envvalue { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--txt-sm); color: var(--text); background: var(--surface-muted); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0 var(--sp-1); word-break: break-all; }
        /* A secret shows a mask plus its last four characters. The mask is soft and spaced
           so the eye goes to the tail, which is the part that answers the question: *which*
           key is set. Approved by JP 2026-08-18, who asked for a mask instead of the
           sentence "eindigt op ..." that stood there first. */
        .mask { color: var(--text-soft); letter-spacing: .08em; }
        /* The copyfield component -- a value plus the button that copies it. `flex-start`
           and not `center`: the value may wrap over six lines, and a button centred against
           that floats halfway down a block whose first line it belongs to. */
        .copyfield { display: inline-flex; align-items: flex-start; gap: var(--sp-1); max-width: 100%; }
        /* Quiet like `.infobtn`, and for the same reason: this hands you a value that is
           already in front of you, it does not act on a record. */
        .copybtn { background: none; border: 0; padding: var(--sp-1) 0 0; margin: 0; cursor: pointer; color: var(--text-soft); line-height: 0; border-radius: var(--radius-sm); }
        .copybtn:hover, .copybtn:focus-visible { color: var(--brand-action); }
        /* The only confirmation there is. A clipboard write leaves nothing on the screen,
           so without this the button answers a click with nothing at all. */
        .copybtn[data-copied] { color: var(--success); }
        .copyicon { width: 15px; height: 15px; }
        /* The infotip component -- info icon plus the explanation that hangs under it.
           `position: relative` here is what the panel is positioned against, so the
           component needs nothing of its surroundings. */
        .infotip { position: relative; display: inline; }
        /* At rest a touch darker than the border (--text-soft): visible enough to find,
           quiet enough to bear 32 times over (JP 2026-08-18: "te druk" about the first,
           heavier version, then "wel graag ietsje zichtbaarder" about the faintest one). */
        .infobtn { background: none; border: 0; padding: 0 0 0 var(--sp-1); margin: 0; cursor: pointer; color: var(--text-soft); vertical-align: -3px; line-height: 0; border-radius: 50%; }
        .infobtn:hover, .infobtn:focus-visible, .infobtn[aria-expanded="true"] { color: var(--brand-action); }
        .infoicon { width: 15px; height: 15px; }
        /* Floating, not in the flow: in the flow every hover would shift the table.
           A full line below the row, at JP's request 2026-08-18 -- closer than that it stuck
           to the bottom edge of its own row and read as part of it instead of as an
           explanation about it.
           The surface itself comes from `.panel`, like every other floating surface in the
           app; what stays here is what genuinely differs: where it hangs, how wide it may
           get, and that it is hidden until it is asked for. */
        /* `fixed`, not `absolute` (2026-08-19): an absolutely positioned panel grows out of
           the page from an icon near the right edge and adds to the scroll width, so the
           whole screen gains a horizontal scrollbar the moment it opens. Fixed coordinates
           can be clamped inside the window, which is what `positionFloater` does for the
           header filter and the row menu as well. Top/left are set from JavaScript; the
           values here are the position before the first hover. */
        .rowinfo { position: fixed; left: 0; top: 0; z-index: 30; display: none; width: max-content; max-width: 42ch; color: var(--text); padding: var(--sp-2) var(--sp-3); font-size: var(--txt-sm); line-height: 1.45; }
        .infobtn:hover + .rowinfo, .infobtn:focus-visible + .rowinfo, .rowinfo[data-pinned] { display: block; }
        /* Escape closes it while the mouse and the focus stay where they are (WCAG 1.4.13,
           "Content on Hover or Focus" -- hover content has to be dismissible without moving
           either). Without this rule Escape unpinned it and the focus rule above put it
           straight back, measured in Chromium: a button keeps :focus-visible after a click.
           The marker is cleared as soon as the pointer or the focus leaves, so the next
           hover shows it again. Same specificity as the rules above and placed after them,
           which is what makes it win -- no !important needed. */
        .infobtn + .rowinfo[data-dismissed] { display: none; }
        /* Pinned reads differently from hovered on purpose: it stays after the mouse leaves,
           and without a mark that looks like the page is stuck. */
        .rowinfo[data-pinned] { border-color: var(--brand-action); }
        /* Inline editing (§3D): a region with .editable shows unmistakably in edit mode
           ([data-editing]) that you are entering data -- tinted background +
           accent border. Colours via brand colour tokens, so it shifts along per club. */
        .editable[data-editing] { background: var(--brand-tint); box-shadow: inset 4px 0 0 var(--brand-action); }
        /* §3F -- repeated inline compositions centralised (tokens inside, no sprawl).
           A semantics-leaning hybrid: component classes for compositions, a small
           utility layer for truly atomic presentation. Catalogue: docs/ontwerpsysteem.html §3F. */
        .row { display: flex; gap: var(--sp-2); }                                   /* horizontale groep, standaard tussenruimte */
        .row--middle { align-items: center; }                                        /* met .row: verticaal gecentreerd */
        .row-between { display: flex; justify-content: space-between; align-items: center; }  /* uiteinden, geen gap */
        .flex-field { flex: 1; min-width: 180px; }                                    /* meegroeiend invoerveld in een .row */
        .content-sm { max-width: var(--w-sm); }                                        /* leeskolom smal */
        .content-md { max-width: var(--w-md); }                                        /* leeskolom middel */
        .content-lg { max-width: var(--w-lg); }                                        /* leeskolom breed */
        .centered { margin: var(--sp-6) auto; }                                    /* centre the block horizontally, rhythm above and below */
        .field-wide { width: 220px; }                                                 /* vaste, bredere veldmaat */
        .full-width { width: 100%; }
        /* Inline aankruisvakje + label op één regel (generiek) */
        .tickbox { display: flex; align-items: center; gap: var(--sp-1); cursor: pointer; }
        .right { text-align: right; }
        .nowrap { white-space: nowrap; }
        .no-margin { margin: 0; }
        .actionform { display: inline; margin: 0; }                                    /* inline actie-formulier (één knop) */
        /* §3F extended -- margin scale, row variants, alignment, field widths, components.
           Full catalogue + purpose: docs/ontwerpsysteem.html §3F. One canonical margin scale
           (`.mt-*`/`.mb-*`, token-mapped); the old semantic aliases `.mt-2`(→ mt-2),
           `.mb-3`(→ mb-3) and `.mt-0`(→ mt-0) have been absorbed into it (2026-07-30). */
        .mt-0 { margin-top: 0; }
        .mt-1 { margin-top: var(--sp-1); } .mt-2 { margin-top: var(--sp-2); } .mt-3 { margin-top: var(--sp-3); }
        .mt-4 { margin-top: var(--sp-4); }
        .mb-2 { margin-bottom: var(--sp-2); } .mb-3 { margin-bottom: var(--sp-3); }
        .ml-2 { margin-left: var(--sp-2); }
        .text-middle { text-align: center; }
        .vline-bottom { vertical-align: bottom; }
        .inline-block { display: inline-block; }
        .row--tight { gap: var(--sp-1); }                                             /* met .row: kleinere tussenruimte */
        .row--loose { gap: var(--sp-3); }                                              /* met .row: grotere tussenruimte */
        .row--bottom { align-items: flex-end; }                                        /* met .row: op de onderlijn */
        .row--start { align-items: flex-start; }
        .row--wrap { flex-wrap: wrap; }
        .field-time { width: 6.2rem; }                                                 /* tijd-invoer */
        .field-narrow { max-width: 150px; }                                              /* smalle invoer-cap */
        .field-md { width: 190px; }                                                    /* middelbrede invoer */
        .addrow { display: flex; gap: var(--sp-2); align-items: flex-end; flex-wrap: wrap; }  /* labeltjes+velden+bevestigknop om een detailrecord toe te voegen (§3D) */
        .plaintext { white-space: pre-wrap; font-family: inherit; }                /* platte-tekst-weergave (mailbody) */
        .subcard { border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-3); }  /* bordered sub-card inside a card */
        /* Brand colour preview (House style settings) -- WYSIWYG mock of the app look. */
        .brand-swatch { width: 3rem; height: 2.4rem; padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-md); cursor: pointer; }
        .hs-preview { display: flex; border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; min-height: 150px; }
        .brand-sidebar { width: 132px; flex: none; background: var(--brand); color: var(--text-inverse); padding: var(--sp-2) var(--sp-3); font-size: var(--txt-sm); }
        .brand-navitem { margin-top: var(--sp-1); padding: var(--sp-1) var(--sp-2); opacity: 0.85; }
        .hs-navitem--active { margin-top: var(--sp-2); opacity: 1; background: var(--brand-dark); border-left: 3px solid var(--text-inverse); border-radius: var(--radius-sm); }
        .brand-content { flex: 1; min-width: 0; padding: var(--sp-3) var(--sp-4); background: var(--card); }
        /* §3F — resterende (deels enkel-gebruik) klassen i.p.v. gemarkeerde one-offs */
        .icon-row { display: inline-flex; gap: var(--sp-1); align-items: center; }  /* pictogram + tekst inline */
        .weight-semibold { font-weight: 600; }
        .expander { cursor: pointer; color: var(--text-soft); }                          /* details-summary */
        .expander--accent { color: var(--brand-action); font-weight: 600; }
        .scroll-x { overflow-x: auto; }
        .cell-tight { padding: var(--sp-1); }
        .cardfoot--flat { border-top: 0; padding-top: 0; }
        .divider-top { margin-top: var(--sp-3); border-top: 1px solid var(--border); padding-top: var(--sp-2); }  /* separator above a collapsed block */
        .statusline { display: block; min-height: 1.1em; }                               /* reserveert hoogte (geen sprong) */
        .plaintext--framed { background: var(--surface-soft); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-3); }
        .mail-frame { width: 100%; height: 60vh; border: 1px solid var(--border); border-radius: var(--radius-md); }  /* mailweergave-vak */
        .icon-sprite { position: absolute; }                                             /* verborgen sprite-houder */
        .field-min { min-width: 220px; }
        .flex-field--wide { flex: 2; min-width: 260px; }
        /* Toegankelijkheid (Fase 5, §3G, WCAG 2.2 AA). */
        /* Skip link (2.4.1): first tab stop, jumps to the main content. */
        .skip { position: absolute; left: var(--sp-2); top: -3rem; z-index: 200; background: var(--card);
            color: var(--brand); border: 1px solid var(--border); border-radius: var(--radius-md);
            padding: var(--sp-2) var(--sp-3); text-decoration: none; transition: top 0.15s; }
        .skip:focus { top: var(--sp-2); }
        /* Visible keyboard focus (2.4.7/2.4.11): one token ring on all interactive elements;
           on the dark sidebar a light ring for enough contrast. */
        :focus-visible { outline: 2px solid var(--brand-action); outline-offset: 2px; }
        aside :focus-visible { outline-color: var(--text-inverse); }
        /* Hidden visually only, still read aloud (for screenreader-only labels).

           On one line, deliberately: `core/cssguard.py` matches a rule per line, so a body
           broken across two makes the class read as having no rule at all. Harmless while
           nothing carried the name, and the searchable pick list is the first thing that
           does -- it puts `.sr-only` on the select it clips away. */
        .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; }
        /* Bewegingsvoorkeur respecteren (2.3.3): animaties/overgangen vrijwel uit. */
        @media (prefers-reduced-motion: reduce) {
            *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
                transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
        }
        /* Loading/busy feedback (spec 3H): the navigation bar on a real navigation or
           submit, and the busy state of the button that was pressed. Token-driven, and
           nearly off under prefers-reduced-motion.

           **Two rules stood here and are gone (decision JP 2026-08-31): a spinner and a
           skeleton placeholder.** They had no user anywhere in this repo -- no markup, no
           JavaScript -- and "unused" is not why they went. Four screens have had the
           waiting problem already (a contribution round generating documents, an import
           applying rows, a mailing going out, a test club being built) and all four
           reached for something else: a readout with numbers. A spinner says *something is
           happening* and those screens know more than that; a skeleton belongs to a screen
           that renders empty and fills in later, and ours render server-side, so that box
           never exists. What they wanted is the progress component in `cotton/ui/`.

           (That component is named in words. Django parses this file whole, comments
           included, and cotton claims a tag that looks like one -- writing it out here took
           ten rendering tests down, for the third time in two days.)

           The `@keyframes` went with them: one turned the spinner and the other slid the
           skeleton's gradient, and neither had a second user. */
        .loadbar { position: fixed; top: 0; left: 0; height: 3px; width: 0; background: var(--brand-light);
            z-index: 100; opacity: 0; transition: width 8s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s; }
        .loadbar.busy { width: 92%; opacity: 1; }
        [aria-busy="true"] { cursor: progress; }
        button[aria-busy="true"], .button[aria-busy="true"] { opacity: 0.7; }
        /* §3D record field grid: reading and editing share exactly the same grid -- the value cell
           shows the text (.read) in read mode and an input box in edit mode in that same
           spot. Both share the same box model, so toggling does not shift a pixel. */
        .fieldgrid { display: grid; grid-template-columns: 132px minmax(0, 1fr); align-items: center; row-gap: var(--sp-1); column-gap: var(--sp-2); }
        /* margin: 0 overrides the global `label` rule's margin-bottom below -- .fieldgrid's
           own row-gap already spaces the rows, and this is a real <label> since 2026-09-22 */
        .fieldgrid > .label { font-size: var(--txt-sm); font-weight: 600; color: var(--text-soft); min-height: var(--field-h); display: flex; align-items: center; margin: 0; }
        /* Multiline field: label at the top (on the first line) instead of centred,
           so it does not move along with the height difference read text ↔ textarea */
        .fieldgrid > .label.label--top { align-self: flex-start; min-height: 0; padding-top: var(--sp-1); }
        .fieldgrid > .value { min-width: 0; min-height: var(--field-h); display: flex; align-items: center; }
        /* max-width here is the field-level twin of .block's cap above: the same
           var(--w-sm) the old .form/"form p" rule already gave a plain Django-forms-as-
           paragraphs screen (login, password reset), now reaching a fieldgrid field too --
           that selector never matched one, since the field component (named in words, see
           the loadbar comment above for why) renders a .label/.value pair, not a paragraph
           (JP 2026-09-20). */
        .fieldgrid .read, .fieldgrid .value input:not([type=checkbox]):not([type=radio]), .fieldgrid .value select, .fieldgrid .value textarea {
            box-sizing: border-box; width: 100%; max-width: var(--w-sm); min-width: 0; min-height: var(--field-h); padding: var(--sp-1) var(--sp-2);
            border: 1px solid transparent; border-radius: var(--radius-sm); font: inherit; font-size: var(--txt-sm); line-height: 1.35; }
        /* Checkbox/radio stay small, on the left; the 28px row (label) centres it → no jump */
        .fieldgrid .value input[type=checkbox], .fieldgrid .value input[type=radio] { width: auto; margin: 0; }
        .fieldgrid .read { display: flex; align-items: center; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .fieldgrid .read:empty::before { content: "\2014"; color: var(--border-strong); }
        /* Meerregelig (textarea-veld): lees en textarea delen dezelfde min-hoogte → geen sprong */
        .fieldgrid .read.multiline { display: block; white-space: normal; min-height: 56px; }
        /* No colour of its own (2026-08-19): this was `--brand`, so a value turned blue the
           moment you started editing it while the read text beside it is `--text` -- and on
           the portal skin that same rule produced graphite where every other field on the
           same screen was browser-black. The base `input` rule owns the colour now, and
           read and edit modes finally show a value in the same colour. */
        .fieldgrid .value input, .fieldgrid .value select, .fieldgrid .value textarea { background: var(--card); border-color: var(--border-strong); }
        .fieldgrid .value textarea { min-height: 56px; resize: vertical; white-space: normal; }
        /* Required field -- blue left bar as an inset shadow, does not shift the text */
        .fieldgrid .value input:required, .fieldgrid .value select:required, .fieldgrid .value textarea:required { box-shadow: inset 3px 0 0 var(--brand-light); }
        .fieldgrid .fielderror, .fieldgrid .hint { grid-column: 2; }
        /* Multi-checkbox (e.g. roles) compact inline, so the row does not grow much */
        .fieldgrid .value ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); }
        .fieldgrid .value ul li label { font-weight: 400; display: inline-flex; align-items: center; gap: var(--sp-1); }
        /* Toggle (generic, also for <ul> multiwidgets): read mode shows .read and hides
           the input; edit mode the other way round. Read-only .read.fixed always stays. */
        .fieldgrid .value > :not(.read) { display: none; }
        .editable[data-editing] .fieldgrid .value > .read:not(.fixed) { display: none; }
        .editable[data-editing] .fieldgrid .value > :not(.read) { display: block; }

        /* Config settings page type (§3C): setting rows in .card-sections. On the left the name +
           explanation (.hint), on the right a control that differs per value type (field/colour/status).
           All token-driven -- no font or size of its own, the same txt and sp scale as the rest. */
        .settingrow { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-1) var(--sp-6); align-items: start; padding: var(--sp-2) 0; }
        /* Config region cards get the brand tint (like the record edit mode), but
           without the small accent border on the left -- config is always in 'edit' after all. */
        .config-regions .card { background: var(--brand-tint); }
        .setting-info { min-width: 0; }
        /* Label + input box follow the record field grid exactly (§3D input mode) for uniformity:
           small, semibold, soft label; --txt-sm input with --radius-sm. */
        .setting-info strong { display: block; font-size: var(--txt-sm); font-weight: 600; color: var(--text-soft); }
        .setting-control { display: flex; align-items: center; gap: var(--sp-2); justify-self: end; align-self: start; flex-wrap: wrap; }
        .setting-control input:not([type=color]), .setting-control select { border-radius: var(--radius-sm); }
        @media (max-width: 640px) { .settingrow { grid-template-columns: 1fr; } .setting-control { justify-self: start; } }
        .editable[data-editing] .fieldgrid .value > ul { display: flex; }
        .editable[data-editing] .fieldgrid .value > .toggle { display: inline-flex; }  /* the rule above sets display:block, which would flatten the switch */
        /* Show only in edit mode (e.g. the autosave hint); visibility (not display)
           so the space stays reserved in read mode → toggling shifts nothing */
        .editable:not([data-editing]) [data-edit-only] { visibility: hidden; }
        h3 { margin: var(--sp-3) 0 var(--sp-1); color: var(--brand); font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.05em; }
        h4 { margin: 0 0 var(--sp-1); font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); }

        /* Tabelbalk: filters, kolomkeuze, export, paginagrootte */
        /* Datatable frame: the toolbar sticks (on both axes) just below the app header, so it stays
           put when scrolling vertically and horizontally. The page is the only scroller (no
           table scrollbar). `.tablehead` wraps the toolbar; z-index below the app header (60). */
        .tablehead { position: sticky; top: 0; left: 0; z-index: 40; margin-bottom: var(--sp-3); }
        /* List page = fixed frame that fills the available height; only the rows area scrolls. The
           scrollbar sits right against the screen edge (= page level, not a framed table scroll).
           Title, tabs, toolbar, column headers (sticky top:0 inside the scroller) and foot sit outside
           the scroller and are therefore fixed -- no sticky-offset arithmetic (that differs per page), no JS.
           `:has()` switches this on only on list pages; main does not scroll there itself (one scrollbar). */
        /* The list page stays a real card (bg + border + radius, just like the other screens); the
           space around it comes from the padding on main. The card is a flex column that fills the height,
           only the scroller (`.tablewrap`) scrolls. The fixed head parts have side padding (text
           clear of the border); the scroller has no side padding so that (a) the rows run the full card
           width with the scrollbar neatly against the inner edge, and (b) the sticky-left columns stick
           to the edge instead of sideways-scrolled data showing through in a padding strip. */
        main:has(.card--list) { padding: var(--sp-4) var(--sp-5); overflow: hidden; }
        .card--list { height: 100%; display: flex; flex-direction: column; margin: 0; padding: 0; overflow: hidden; }
        .card--list > .cardhead { flex: 0 0 auto; padding: var(--sp-3) var(--sp-4) var(--sp-2); margin: 0; }
        .card--list .tabs { flex: 0 0 auto; padding: 0 var(--sp-4); margin: 0; }
        /* `>`, because this is a flex-child rule for the list card's own layout, not a rule
           about hints. Since the empty-row notice carries `.hint` (2026-08-19) a descendant
           selector would indent that sentence inside the table as well. */
        .card--list > .hint { flex: 0 0 auto; padding: 0 var(--sp-4); margin: 0 0 var(--sp-2); }
        /* `relative` (not `static`): the bulk action bar is an overlay with `inset: 0` and must
           stay inside the toolbar -- with `static` it escapes to the whole screen. */
        .card--list .tablehead { position: relative; flex: 0 0 auto; padding: 0 var(--sp-4) var(--sp-2); margin: 0; }
        .card--list .tablewrap { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 0; }
        .card--list .tablefoot { position: static; flex: 0 0 auto; margin: 0; border-left: 0; border-right: 0; border-radius: 0; }
        .card--list table.datatable thead th { top: 0; }
        /* The bulk action bar (overlay, `inset: 0`) by default fills the entire `.tablehead` -- including
           the side/bottom padding that separates the toolbar from the card edge/table. That made the
           bar wider + taller than the search bar it replaces. Inset it with the same padding so that
           it covers exactly the `.tablebar` (no jump, equal size). */
        .card--list .actionbar { inset: 0 var(--sp-4) var(--sp-2) var(--sp-4); }
        .tablebar { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; background: var(--surface-soft); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-2); min-height: var(--tablebar-h); }
        .tablebar .space { flex: 1; }
        .tablebar input, .tablebar select { background: var(--card); padding: var(--sp-1) var(--sp-2); }
        .filterclear { font-size: var(--txt-sm); color: var(--text-soft); text-decoration: none; align-self: center; white-space: nowrap; }
        .filterclear:hover { color: var(--danger); }
        /* Tabelknoppen: losse knoppen (Kolommen/Export/Rijen/Weergave) met eigen flyout */
        .tablebuttons { display: flex; gap: var(--sp-2); align-items: center; margin-left: auto; }
        .tablebutton { position: relative; }
        .tablebutton summary { list-style: none; cursor: pointer; height: var(--control-h); display: inline-flex; align-items: center; }
        .tablebutton summary::-webkit-details-marker { display: none; }
        /* The raised panel: a white surface floating over the page. Five places open one
           -- the two toolbar popovers, the column filter, the row menu and the block
           editors' settings panel -- and until 2026-08-16 all five wrote these four
           declarations out by hand. What stays per place is what genuinely differs there:
           how it is positioned, its padding, its width. */
        .panel { background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--radius-md); box-shadow: 0 4px 14px var(--shadow); }
        .tablebutton .panel { position: absolute; right: 0; top: calc(100% + 4px); padding: var(--sp-2) var(--sp-3); z-index: 20; min-width: 220px; }
        .tablebutton .panel:has(.segments) { min-width: 120px; }  /* rijen-per-pagina: alleen getallen */
        .panel h4 { margin: var(--sp-1) 0 var(--sp-1); font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); }
        .tablebutton .panel select { width: 100%; font-size: var(--txt-sm); padding: var(--sp-1) var(--sp-1); }
        .result { font-size: var(--txt-sm); color: var(--text-soft); white-space: nowrap; align-self: center; }
        /* Header filters: small funnel on the right in the columnhead, options in a floating panel.
           Left = header text + sorting, right = filter: two separate click targets. */
        /* No transform here: that would anchor the floating (fixed) filter panel to the icon */
        .headerfilter { position: absolute; right: 7px; top: 50%; margin-top: -9px; }
        .headerfilter summary { list-style: none; cursor: pointer; color: var(--text-muted); display: flex; padding: var(--sp-1); }
        .headerfilter summary::-webkit-details-marker { display: none; }
        .headerfilter summary:hover { color: var(--brand); }
        .headerfilter.active summary { color: var(--brand-light); }
        thead th:has(.headerfilter) { padding-right: var(--sp-5); }
        /* Actief filter: hele kopcel duidelijk gemarkeerd */
        thead th:has(.headerfilter.active) { background: var(--col-filtered); box-shadow: inset 0 -2px 0 var(--brand-light); }
        /* Sortable header: subtle ↕ on hover (as long as it is not sorted on yet) */
        th.sortable:hover:not(:has(.sortmarker))::after { content: ""; display: inline-block; width: 0.9em; height: 0.9em; margin-left: var(--sp-1); vertical-align: -0.15em; background-color: var(--brand-light); -webkit-mask: var(--chev-updown) no-repeat center / contain; mask: var(--chev-updown) no-repeat center / contain; }
        .headerfilter .panel { position: fixed; padding: var(--sp-2) var(--sp-2); z-index: 40; min-width: 160px; max-height: 280px; overflow-y: auto; text-transform: none; font-weight: 400; letter-spacing: normal; }
        .headerfilter .panel input[type="text"], .headerfilter .panel input[type="search"] { width: 170px; padding: var(--sp-1) var(--sp-2); }
        .filteroption { display: flex; gap: var(--sp-2); align-items: center; margin: var(--sp-1) 0; white-space: nowrap; cursor: pointer; }
        .filteroption input { width: auto; }
        /* Optielijst in flyouts (bijv. weergaven) */
        .optionbutton { display: block; width: 100%; text-align: left; border: 0; background: none; padding: var(--sp-1) var(--sp-2); font-size: var(--txt-sm); cursor: pointer; border-radius: var(--radius-sm); color: var(--text); white-space: nowrap; }
        .optionbutton:hover { background: var(--brand-tint); }
        .optionbutton.active, .segments button.active { background: var(--brand-tint); box-shadow: inset 3px 0 0 var(--brand-light); color: var(--brand); font-weight: 600; }
        .optionok { border: 0; background: none; cursor: pointer; width: 1.5rem; height: 1.5rem; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); flex: none; }
        .optionok svg { width: 13px; height: 13px; }
        .optionrow input { flex: 1; min-width: 0; padding: var(--sp-1) var(--sp-2); }
        .optionok { background: var(--success); color: var(--text-inverse); }
        .optionok:hover { background: var(--success-dark); }
        .optionslot { display: flex; align-items: center; justify-content: center; gap: var(--sp-1); width: 100%; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); background: none; color: var(--text-soft); font-size: var(--txt-sm); padding: var(--sp-1) var(--sp-2); cursor: pointer; margin-top: var(--sp-1); }
        .optionslot:hover { border-color: var(--brand-light); color: var(--brand); background: var(--brand-tint); }
        .optionslot svg { width: 13px; height: 13px; }
        /* Views panel: per-row icon actions (star (default), pin (pin down), pencil, trash)
           and the draft ("not saved") markers. */
        .option-icon { border: 0; background: none; cursor: pointer; width: 1.5rem; height: 1.5rem; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); color: var(--text-soft); flex: none; }
        .option-icon:hover { background: var(--brand-tint); color: var(--text); }
        .option-icon.on { color: var(--brand); }
        .option-icon.success { color: var(--success); }
        .vw-defaulticon { width: 1rem; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--brand); }
        .vw-defaulticon svg { width: 13px; height: 13px; }
        /* Kebab (⋮) in a view row the same size as the other row icons (1.5rem/13px). */
        /* The size override that used to sit here (1.5rem) is gone: a row in the views panel
           is a row, so its menu takes `--control-h-row` from the rule above like every other
           one. It also crossed the WCAG 2.2 SC 2.5.8 minimum by nothing at all -- 24x24 exact
           -- where the standing row size clears it. The glyph stays smaller: the panel is
           narrow and the icon carries less work there than in a table row. */
        .optionrow .rowmenu > summary svg { width: 13px; height: 13px; }
        .option-icon svg { width: 13px; height: 13px; }
        .draft-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--warning); margin-left: var(--sp-1); vertical-align: middle; }
        .tablebutton summary.changed { position: relative; }
        .tablebutton summary.changed::after { content: ""; position: absolute; top: 1px; right: 1px; width: 7px; height: 7px; border-radius: 50%; background: var(--warning); box-shadow: 0 0 0 1.5px var(--card); }
        /* styleguard:off — takenkalender/dynamisch (uitzonderingszone, §5a) */
        /* Takenkalender: raster ma–zo met taakkaartjes in bezettingskleur */
        /* Continuous week planner: the weeks stacked in one scrollable block */
        /* Diensttype-filter: kleurchips om types te tonen/verbergen */
        .calfilter { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-bottom: 0.55rem; }
        .calfilter-label { font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-soft); margin-right: 0.15rem; }
        .calchip { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.2rem 0.6rem; border-radius: var(--radius-pill); border: 1px solid var(--border-strong); font-size: var(--txt-sm); font-family: inherit; line-height: 1.4; cursor: pointer; text-decoration: none; color: var(--text-soft); background: var(--card); }
        .calchip .calchip-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--chipcolour); flex: none; opacity: 0.4; }
        .calchip.on { color: var(--card); background: var(--chipcolour); border-color: var(--chipcolour); }
        .calchip.on .calchip-dot { background: var(--card); opacity: 1; }
        .calchip:not(.on) { text-decoration: line-through; opacity: 0.75; }
        .calchip:hover { filter: brightness(0.97); }
        /* Planpaneel: kandidaten gegroepeerd per bron-groep, met diensttype erboven */
        .plansection + .plansection { margin-top: 0.6rem; }
        .plansection { border-radius: 6px; padding: 0.15rem 0.25rem; transition: opacity 0.2s, box-shadow 0.2s; }
        .plansection-types { display: flex; flex-wrap: wrap; gap: 0.25rem 0.55rem; margin-bottom: 0.15rem; }
        .plansource { display: inline-flex; align-items: center; gap: 0.3rem; font-size: var(--txt-sm); font-weight: 600; color: var(--text); }
        .plansource .calchip-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--chipcolour); flex: none; }
        /* Unavailability indicator next to a name in the scheduling panel (status dot instead of emoji) */
        .dot-unavailable { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--danger); flex: none; }
        .plansection-head { margin: 0 0 0.3rem; font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--brand); border-bottom: 1px solid var(--border); padding-bottom: 0.15rem; }
        /* Slot aangeklikt: sectie met sleepbare personen licht op, de rest dimt */
        .plansection.highlighted { box-shadow: 0 0 0 2px var(--brand-light); background: var(--cal-highlighted); }
        .plansection.dimmed { opacity: 0.35; }
        .slotbar.chosen { outline: 2px solid var(--brand-light); outline-offset: 1px; }
        /* Slots of the clicked person stand out; the rest dims */
        .slotbar.fromperson { outline: 3px solid var(--brand-light); outline-offset: 1px; box-shadow: 0 0 0 6px rgba(41,128,185,0.4); z-index: 1; }
        .caltask.dimmed { opacity: 0.3; }
        .planperson.chosen { box-shadow: inset 3px 0 0 var(--brand-light); background: var(--cal-highlighted); }
        /* While dragging: shifts this person is not allowed on are dimmed */
        .caltask.no-target { opacity: 0.3; }
        /* Scheduled member who is not in the group(s) of the shift */
        .slotbar.task.external { background: repeating-linear-gradient(45deg, var(--success-tint) 0 5px, var(--cal-external-2) 5px 10px); }
        .slotbar.task.external .who { font-weight: 700; }
        /* Status per person for the clicked time window */
        .planperson[data-status="free"] { box-shadow: inset 3px 0 0 var(--success); }
        .planperson[data-status="busy"], .planperson[data-status="away"] { opacity: 0.4; }
        .planperson[data-status] .personhead::after { font-size: 0.66rem; white-space: nowrap; margin-left: 0.3rem; }
        .calendar-card { padding: 0.4rem; overflow: hidden; position: relative; }
        .calscroll-buttons { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); z-index: 5; display: flex; flex-direction: column; gap: 6px; }
        .calscroll-buttons button { width: 32px; height: 32px; border: 1px solid var(--border-strong); background: rgba(255,255,255,0.92); border-radius: 50%; cursor: pointer; color: var(--brand); font-size: 0.9rem; line-height: 1; box-shadow: 0 1px 5px rgba(0,0,0,0.2); }
        .calscroll-buttons button:hover { background: var(--card); color: var(--brand-light); }
        .calscroll-buttons button:active { background: var(--brand-tint); }
        /* No visible scrollbar; navigating is done with the up/down buttons and the
           arrow keys. */
        .calendar-scroll { position: relative; overflow-y: auto; overflow-x: auto; height: calc(100vh - 190px); min-height: 360px; scrollbar-width: none; -ms-overflow-style: none; }
        .calendar-scroll::-webkit-scrollbar { width: 0; height: 0; }
        .calendar-head { position: sticky; top: 0; z-index: 3; display: grid; gap: 4px; background: var(--card); padding-bottom: 3px; border-bottom: 1px solid var(--border); }
        .calweek { display: grid; gap: 4px; padding: 4px 0; transition: grid-template-columns 0.35s ease; }
        .calweek + .calweek { border-top: 1px solid var(--border); }
        .calweek.current { background: var(--cal-highlighted); box-shadow: inset 3px 0 0 var(--brand-light); border-radius: 6px; }
        .calday { background: var(--card); border: 1px solid var(--border); border-radius: 6px; min-height: 92px; padding: 0.25rem 0.3rem; display: flex; flex-direction: column; gap: 3px; }
        .calday.weekend { background: var(--surface-muted); }
        .calday.today { border-color: var(--brand-light); box-shadow: inset 0 0 0 2px var(--brand-light); background: var(--cal-today-bg); }
        .caldate { display: flex; justify-content: space-between; align-items: center; font-size: 0.82rem; font-weight: 700; color: var(--text); padding-bottom: 2px; border-bottom: 1px solid var(--border); }
        .calday.today .caldate { color: var(--brand); border-bottom-color: var(--brand-light); }
        .caldate .calday-name { text-transform: uppercase; font-size: 0.7rem; font-weight: 700; color: var(--text-soft); letter-spacing: 0.03em; }
        .calday.today .caldate .calday-name { color: var(--brand-light); }
        .caldate .plusbutton { text-decoration: none; color: var(--text-soft); padding: 0 4px; border-radius: 3px; font-weight: 700; }
        .caldate .plusbutton:hover { background: var(--brand-tint); color: var(--brand); }
        .caltask { display: block; font-size: 0.73rem; padding: 0.14rem 0.35rem; border-radius: 4px; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid; }
        .caltask.full { background: var(--success-tint); color: var(--success); border-color: var(--success); }
        .caltask.understaffed { background: var(--danger-tint); color: var(--cal-under-text); border-color: var(--danger); }
        .caltask:hover { filter: brightness(0.96); text-decoration: none; }
        .caltask.target { outline: 2px dashed var(--brand-light); outline-offset: 1px; }
        /* Verticale tijdas per dag: diensten als blok op tijdshoogte, hoogte = duur */
        .timeaxis { position: relative; margin-top: 3px; }
        .timeaxis .hourline { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--border); pointer-events: none; }
        .timeaxis .hourline span { position: absolute; left: 0; top: -0.52rem; font-size: 0.56rem; color: var(--text-soft); background: rgba(255,255,255,0.75); padding: 0 1px; z-index: 1; }
        /* Block track: leaves room on the left for the hour labels so they do not fall behind the blocks */
        .timeaxis .timeaxis-track { position: absolute; left: 16px; right: 1px; top: 0; bottom: 0; }
        /* Dienstblok: getinte cel per status; de verticale slot-balken vallen erbinnen */
        .timeaxis .caltask { position: absolute; box-sizing: border-box; display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--border-strong); border-left: 3px solid var(--brand-light); border-radius: 3px; min-height: 15px; overflow: hidden; padding: 0; }
        .timeaxis .caltask.full { border-left-color: var(--success); border-color: var(--cal-full-border); background: var(--cal-full-bg); }
        .timeaxis .caltask.understaffed { border-left-color: var(--danger); border-color: var(--cal-under-border); background: var(--danger-field); }
        .timeaxis .caltask .blockhead { flex: none; font-size: 0.7rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 3px; border-bottom: 1px solid var(--border); }
        .timeaxis .caltask.full .blockhead { color: var(--cal-full-text); background: var(--cal-full-headbg); border-bottom-color: var(--cal-full-headborder); }
        .timeaxis .caltask.understaffed .blockhead { color: var(--cal-under-text); background: var(--cal-under-headbg); border-bottom-color: var(--cal-under-headborder); }
        .timeaxis .caltask .slots { display: flex; flex: 1 1 auto; min-height: 0; gap: 2px; padding: 2px; box-sizing: border-box; }
        .timeaxis .caltask .slot { position: relative; flex: 1 1 0; min-width: 0; }
        .slotbar { position: absolute; left: 3px; right: 3px; display: flex; flex-direction: column; justify-content: space-between; text-align: center; border-radius: 2px; font-size: 0.68rem; line-height: 1.08; overflow: hidden; padding: 0 2px; box-sizing: border-box; }
        .slotbar .time { font-size: 0.6rem; opacity: 0.75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .slotbar .who { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .slotbar.task { background: var(--success-tint); color: var(--cal-full-text); border: 1px solid var(--cal-slot-border); }
        .slotbar.empty { border: 1px dashed var(--danger); background: var(--cal-empty-bg); color: var(--cal-under-text); }
        /* Child records in the task list: the staffing per place, below the shift */
        .childtoggle { border: 0; background: none; cursor: pointer; color: var(--text-soft); padding: 0 5px 0 0; font-size: 0.72rem; }
        .childtoggle:hover { color: var(--brand); }
        .childrow td.kind { font-size: 0.8rem; color: var(--text-soft); background: var(--surface-soft); padding-left: 1.8rem; }
        .childrow td.kind.empty { color: var(--cal-under-text); font-style: italic; }
        /* Planning panel beside the calendar: the people to drag in */
        .calcontent { display: flex; gap: 0.8rem; align-items: flex-start; flex-wrap: wrap; }
        .calside { width: var(--cal-panel); flex: none; background: var(--surface-muted); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 0.5rem 0.6rem; position: sticky; top: 0.6rem; max-height: calc(100vh - 190px); overflow-y: auto; }
        .calside h4 { font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-soft); }
        .planperson { background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 0.22rem 0.45rem; margin: 0.25rem 0; font-size: var(--txt-sm); cursor: grab; }
        .planperson:active { cursor: grabbing; }
        .planperson .personhead { display: flex; justify-content: space-between; align-items: center; gap: 0.3rem; }
        .planperson .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .planperson .meta { display: flex; gap: 0.2rem; align-items: center; flex: none; }
        /* styleguard:on */
        /* Paginator: one integrated block « ‹ 1–100 of 824 › » on the right in the bar */
        /* Datatable footer (§datatable-frame): sticks (on both axes) at the bottom; rows-per-page +
           count on the left, the pager on the right. That keeps navigation + count always in view, also when scrolling. */
        .tablefoot { position: sticky; bottom: 0; left: 0; z-index: 40; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-2); background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-1) var(--sp-3); margin-top: var(--sp-2); }
        .tablefoot .foot-left { display: flex; align-items: center; gap: var(--sp-3); }
        /* Rows-per-page sits at the bottom left → the panel opens upwards and to the right (otherwise it
           falls below/outside the viewport, or -- with `right: 0` -- to the left behind the sidebar). */
        .tablefoot .tablebutton .panel { top: auto; bottom: calc(100% + 4px); right: auto; left: 0; }
        /* Pager: kale chevron-icoonknoppen (ghost, sprite-iconen), rustig; lichten op bij hover. */
        .paginator { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--txt-sm); white-space: nowrap; color: var(--text-soft); }
        .paginator a, .paginator .off { width: 1.6rem; height: 1.6rem; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); text-decoration: none; color: var(--text-soft); }
        .paginator a:hover { background: var(--surface-soft); color: var(--brand); }
        .paginator .off { opacity: 0.4; }
        .paginator svg { width: 1rem; height: 1rem; }
        .paginator .pagenumber { padding: 0 var(--sp-1); font-variant-numeric: tabular-nums; }
        /* Embedded ("flat") datatable in a detail card (tableframe with embedded): a short
           list that hangs off one record is not a full-height list page, so none of the three
           list stickies apply -- there is no scroll to hold headers under, and no toolbar. The
           footer also sheds its box and becomes a quiet count line. The `table.datatable`
           header rule above is (0,1,3), so the flat override doubles the class to win. */
        table.datatable.datatable--flat thead th { position: static; }
        .tablefoot.tablefoot--flat { position: static; border: 0; background: transparent; padding: var(--sp-2) var(--sp-1) 0; margin-top: 0; }
        .columnlist { max-height: 340px; overflow-y: auto; }
        /* Toasts: notices stack at the top centre. Colour code: green = succeeded, orange =
           warning, red = error. Green goes by itself; orange and red stay until closed
           (decision JP 2026-09-17, 1a), so those two take pointer events for their close
           button while the stack itself stays click-through. */
        .toaststack { position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); z-index: 100; pointer-events: none; }
        .toast { display: flex; align-items: center; gap: var(--sp-3); color: var(--text-inverse); padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md); font-size: var(--txt-base); font-weight: 600; box-shadow: 0 4px 16px var(--shadow-strong); max-width: min(var(--w-sm), calc(100vw - var(--sp-4))); overflow-wrap: anywhere; text-align: center; opacity: 0; transform: translateY(-12px); transition: opacity 0.3s, transform 0.3s; }
        .toast.visible { opacity: 1; transform: none; }
        .toast--positive { background: var(--success); }
        .toast--warning { background: var(--warning); pointer-events: auto; }
        .toast--negative { background: var(--danger); pointer-events: auto; }
        .toast button { flex: none; width: var(--control-h); height: var(--control-h); display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: var(--radius-sm); background: none; color: inherit; cursor: pointer; }
        .toast button:hover { opacity: 0.75; }
        .toast button svg { width: 1rem; height: 1rem; }
        /* Rows-per-page option list: vertical, from top to bottom */
        .segments { display: flex; flex-direction: column; min-width: 110px; }
        .segments button { border: 0; background: none; padding: var(--sp-1) var(--sp-2); font-size: var(--txt-sm); cursor: pointer; color: var(--text); text-align: left; border-radius: var(--radius-sm); }
        .segments button:hover:not(.active) { background: var(--brand-tint); }
        /* Exportopties met bestandsicoontje */
        .exportoption { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--txt-sm); text-decoration: none; color: var(--text); padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-sm); }
        .exportoption:hover { background: var(--brand-tint); text-decoration: none; }
        .exportoption svg { flex: none; }
        .columnrow { display: flex; gap: var(--sp-1); align-items: center; padding: 0 var(--sp-1); border-radius: var(--radius-sm); }
        .columnrow:hover { background: var(--brand-tint); }
        .columnrow button { border: 0; background: none; cursor: pointer; padding: 0 var(--sp-1); color: var(--text-soft); font-size: var(--txt-sm); line-height: 1.4; }
        .columnrow button:hover { color: var(--brand); }
        .columnrow label { display: flex; gap: var(--sp-2); margin: var(--sp-1) 0; white-space: nowrap; flex: 1; cursor: pointer; }
        .columnrow input { width: auto; }
        .optionrow { display: flex; align-items: center; gap: var(--sp-1); }
        .optionrow .optionbutton { flex: 1; }
        .columngrip { cursor: grab; color: var(--text-soft); font-size: var(--txt-xs); letter-spacing: -1px; padding: 0 var(--sp-1) 0 0; user-select: none; }
        .columnrow.dragging { opacity: 0.45; }
        .columnrow.droptarget { box-shadow: inset 0 2px 0 var(--brand-light); }

        /* Tabbladen (instellingen-/beheerdeel) */
        /* Line tabs (§3C): underline style, calm for a data-rich record.
           One row (no wrap; NN/g: multiple rows break spatial memory);
           horizontal scrolling on a narrow screen. */
        .tabs { display: flex; gap: var(--sp-4); border-bottom: 1px solid var(--border); margin-bottom: var(--sp-4); flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
        /* One gap above the tab row, whatever sits there: after a heading row that gap is the row's own margin, after a key-fields line (a record) it was nothing -- measured 17-09 by the composition walk, 0px on six record sides against 8px on nine config screens. The list card keeps its flush row (rule above). */
        .card > .tabs { margin-top: var(--sp-2); }
        .cardhead + .tabs { margin-top: 0; }
        .tabs::-webkit-scrollbar { height: 0; }
        .tabs a { padding: var(--sp-2) 0; text-decoration: none; color: var(--text-soft); font-size: var(--txt-base); border-bottom: 2px solid transparent; margin-bottom: -1px; }
        .tabs a.active { color: var(--brand); font-weight: 600; border-bottom-color: var(--brand-light); }
        .tabs a:hover:not(.active) { color: var(--brand); }

        /* Rechtenmatrix (rollen): onderdelen horizontaal, rollen verticaal */
        .rightsmatrix .columnhead { height: 120px; vertical-align: bottom; padding: var(--sp-1) var(--sp-1); }
        .rightsmatrix .columnhead span { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; display: inline-block; }
        .rightsmatrix .rightscell { text-align: center; white-space: nowrap; padding: var(--sp-1) var(--sp-1); }
        .rights { display: inline-block; width: 15px; height: 15px; line-height: 15px; text-align: center; border-radius: var(--radius-sm); font-size: var(--txt-xs); font-weight: 700; margin: 0 var(--sp-1); }
        .rights--view { background: var(--brand-tint); color: var(--brand); }
        .rights--add { background: var(--success-tint); color: var(--success); }
        .rights--change { background: var(--warning-tint); color: var(--warning); }
        .rights--delete { background: var(--danger-tint); color: var(--danger); }
        .rights--all { background: var(--brand); color: var(--text-inverse); }
        /* A text token and not a border token: the letter in this chip says *which* right
           is missing, so it has to be readable. `--border-strong` gave 1.35:1 against the
           chip background where AA asks 4.5:1, over 243 chips on the roles screen -- found
           on 2026-08-20, the day that screen came into the contrast walk. `--text-muted`
           measures 5.01:1 and still reads as switched off. */
        .rights--off { background: var(--surface-soft); color: var(--text-muted); }

        /* Bulk action bar = overlay inside the sticky toolbar (§datatable-frame): with an active
           selection it covers the toolbar so nothing shifts (no separate row below it). */
        .actionbar { position: absolute; inset: 0; display: none; flex-wrap: wrap; gap: var(--sp-2); align-items: center; background: var(--brand-tint); border: 1px solid var(--brand-light); border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-3); font-size: var(--txt-sm); z-index: 1; }
        .tablehead[data-selection] .actionbar { display: flex; }
        .tablehead[data-selection] .tablebar { visibility: hidden; }
        .actionbar .counter { color: var(--brand); font-weight: 600; white-space: nowrap; }
        .actionbar .bar-rule { width: 1px; align-self: stretch; background: var(--brand-light); opacity: 0.4; }
        .actionbar .bar-right { margin-left: auto; display: flex; gap: var(--sp-2); align-items: center; }
        /* Actions menu inside the action bar: reuses the ⋮ rowmenu (positioning + closing
           in the base.html JS), but with a label trigger instead of the bare ⋮ circle. */
        /* Bulk action menu = icon-only ⋮ (principle 9: action buttons carry no text), the same size as the
           other action bar icon buttons (.button--icon, 1.9rem). Title/aria-label give "Actions" as the hint. */
        /* The bulk menu carries `.rowmenu` for its behaviour but sits in the toolbar, not in
           a row -- so it takes the standing size and not the row size. */
        .rowmenu.bulkmenu > summary { width: var(--control-h); height: var(--control-h); padding: 0; border-radius: var(--radius-md); border: 1px solid var(--border-strong); background: var(--card); color: var(--text-soft); }
        .rowmenu.bulkmenu > summary:hover, .rowmenu.bulkmenu[open] > summary { border-color: var(--brand-light); background: var(--card); color: var(--text); }
        .rowmenu.bulkmenu > summary svg { width: 16px; height: 16px; }
        .rowmenu .panel .panel-rule { height: 1px; background: var(--border); margin: var(--sp-1) 0; padding: 0; }
        /* Parameter dialog for bulk actions that need input (e.g. membership type + date). The
           fields belong to the list form via form=, so submitting sends the whole selection. */
        /* Same flat styling as #confirmdialog: one padded surface, no header border or tinted
           footer bar -- that makes both dialogs one component family (2026-07-30). */
        /* `color` explicitly, because a <dialog> is not a normal descendant: while open it
           renders in the top layer, so it does not inherit `body`'s colour and falls back to
           the browser default black. That is why the heading in here measured black while the
           same heading is `--text` on every other screen (2026-08-19). */
        .paramdialog { border: 0; color: var(--text); border-radius: var(--radius-lg); padding: var(--sp-4) var(--sp-5); width: min(360px, 92vw); box-shadow: 0 10px 34px var(--shadow-strong); }
        .paramdialog::backdrop { background: var(--scrim); }
        .paramdialog h2 { margin: 0 0 var(--sp-4); }
        .paramdialog .dlg-body { display: flex; flex-direction: column; gap: var(--sp-3); }
        .paramdialog .paramfield { display: flex; flex-direction: column; gap: var(--sp-1); }
        .paramdialog .paramfield input, .paramdialog .paramfield select { width: 100%; }
        .paramdialog .dlg-foot { margin-top: var(--sp-4); display: flex; justify-content: flex-end; gap: var(--sp-2); }
        /* Structural columns [selection][⋮]: fixed width, sticky-left so they stay in
           view while scrolling horizontally (the background is opaque so nothing
           shows through underneath). */
        /* Fixed width via min=max (content-independent, also in table-layout:auto) +
           zero side padding: the rendered width is exactly 2rem, so the sticky-left of
           the actioncolumn (2rem) lines up precisely with the selectcolumn and no data
           column disappears under the opaque background. The table.datatable prefix carries
           enough weight to override the generic `thead th { position: relative }` (resize
           handle context) -- otherwise the head was shifted relatively instead of sticky and
           the first data column dropped out. The content (checkbox/⋮) is centred → no side padding. */
        table.datatable td.selectcolumn, table.datatable th.selectcolumn,
        table.datatable td.actioncolumn, table.datatable th.actioncolumn {
            box-sizing: border-box; width: 2rem; min-width: 2rem; max-width: 2rem;
            padding-left: 0; padding-right: 0; text-align: center;
            position: sticky; z-index: 2; background: var(--card); }
        table.datatable td.selectcolumn, table.datatable th.selectcolumn { left: 0; }
        /* The actioncolumn is either the first column (no selection → left 0) or comes right
           after the selectcolumn (then indented by 2rem). The selectcolumn is namely
           conditional in _datatabletable.html: lists without bulk selection (e.g.
           users) only have the action column. Without this split the action
           head slid 2rem over the first data head. */
        table.datatable td.actioncolumn, table.datatable th.actioncolumn { left: 0; }
        table.datatable td.selectcolumn + td.actioncolumn,
        table.datatable th.selectcolumn + th.actioncolumn { left: 2rem; }
        tr[data-href]:hover td.selectcolumn, tr[data-href]:hover td.actioncolumn { background: var(--brand-tint); }
        /* ⋮ rowmenu: floating panel (position: fixed, placed in JS) so the
           overflow of the tablewrap does not clip it -- same approach as the headerfilter. */
        .rowmenu { display: inline-block; }
        /* Subtle, borderless ⋮ icon (no button look); hover gives a soft circle */
        .rowmenu > summary, .rowmenu-empty { list-style: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; width: var(--control-h-row); height: var(--control-h-row); border-radius: var(--radius-pill); color: var(--text-soft); }
        .rowmenu > summary svg, .rowmenu-empty svg { width: 18px; height: 18px; }
        .rowmenu > summary:hover, .rowmenu[open] > summary { background: var(--surface-soft); color: var(--text); }
        .rowmenu-empty { opacity: 0.4; cursor: default; }
        .rowmenu > summary::-webkit-details-marker { display: none; }
        .button--icon[aria-disabled="true"] { opacity: 0.4; cursor: default; }
        .rowmenu .panel { position: fixed; padding: var(--sp-1); z-index: 40; min-width: 180px; display: flex; flex-direction: column; }
        .rowmenu .panel form { margin: 0; }
        .rowmenu .panel a, .rowmenu .panel button, .rowmenu .panel span { display: flex; align-items: center; gap: var(--sp-2); width: 100%; box-sizing: border-box; font-size: var(--txt-sm); text-align: left; text-decoration: none; color: var(--text); padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-sm); background: none; border: 0; cursor: pointer; font-family: inherit; }
        .rowmenu .panel a:hover, .rowmenu .panel button:hover { background: var(--brand-tint); }
        .rowmenu .panel .button--danger { color: var(--danger); }
        .rowmenu .panel [aria-disabled="true"] { opacity: 0.45; cursor: default; }
        .rowmenu .panel [aria-disabled="true"]:hover { background: none; }
        .rowmenu .panel svg { flex: none; width: 1rem; height: 1rem; }

        /* Narrow screens (tablet portrait/mobile): the sidebar becomes a collapsible top bar,
           flyouts appear below the heading instead of next to it */
        @media (max-width: 800px) {
            body { grid-template-columns: 1fr; grid-template-areas: "head" "main-content"; }
            .navbutton { display: inline-flex; }
            /* Sidebar = slide-out drawer below the header; the hamburger toggles .open.
               height:auto (instead of the inherited calc(100vh…)) lets top+bottom decide
               the height against the REAL viewport -- otherwise the drawer gets too tall on
               mobile (100vh counts the browser chrome), does not scroll and its bottom
               (Instellingen) falls away. With top/bottom + overflow-y:auto the drawer scrolls. */
            aside { position: fixed; top: var(--head-h); left: 0; bottom: 0; height: auto; width: 15rem; max-width: 80vw; transform: translateX(-100%); transition: transform 0.2s ease; z-index: 55; box-shadow: 0 6px 20px var(--shadow-strong); }
            aside.open { transform: translateX(0); }
            .profile .name { display: none; }
            main { padding: var(--sp-3); }
            /* Do not hang the header dropdowns (bell + user menu) off the button (it sits
               indented from the edge → the panel falls off screen on the left), but clamp
               them under the header across the full width with a margin. */
            .appheader .header-panel { position: fixed; top: var(--head-h); left: var(--sp-2); right: var(--sp-2); width: auto; min-width: 0; max-width: none; }
        }
        /* Responsive datatable → cards (phone). On a narrow screen every row of the
           shared datatable component folds into a card: the head disappears and
           every cell stacks with its column label in front of it (`data-label` on the <td>,
           filled from `cel.label`). Wide lists no longer run off screen. Row click
           (`data-href`), the selection checkbox and the ⋮ action menu keep working.
           Dense matrices (the permissions matrix) run on a custom template with scroll-x
           and are deliberately left out here. */
        @media (max-width: 640px) {
            /* Kop toegankelijk verbergen (elk label staat nu per cel) */
            table.datatable thead, table.envtable thead { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; }
            table.datatable, table.datatable tbody, table.envtable, table.envtable tbody { display: block; width: auto; }
            /* Row = card. Flex-wrap: the selection checkbox and ⋮ share the card header line
               (checkbox left, ⋮ right); the data cells each take 100% and stack below it,
               with their column label in front. */
            table.datatable tr, table.envtable tr { display: flex; flex-wrap: wrap; align-items: center; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--card); margin-bottom: var(--sp-2); padding: 0 var(--sp-3); }
            /* Zebra/hover-achtergrond uit: de kaart ís al de rij-afbakening */
            table.datatable tbody tr:nth-child(even) td, table.datatable tbody tr:hover td, table.envtable tbody tr:nth-child(even) td, table.envtable tbody tr:hover td { background: transparent; }
            table.datatable td, table.envtable td { flex: 1 1 100%; border: none; border-bottom: 1px solid var(--border); padding: var(--sp-1) 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); text-align: right; }
            /* Empty cell = no value: drop the whole field line (otherwise label noise on the card).
               `:empty` does not count the ::before content, so the label alone does not keep it
               "filled"; selection/action cells have a checkbox/menu and stay. */
            table.datatable td:empty, table.envtable td:empty { display: none; }
            table.datatable tr td:last-child, table.envtable tr td:last-child { border-bottom: 0; }
            table.datatable td::before, table.envtable td::before { content: attr(data-label); flex: 0 0 33%; text-align: left; color: var(--brand); font-weight: 600; font-size: var(--txt-xs); text-transform: uppercase; letter-spacing: 0.03em; }
            /* Selection + ⋮ on the card header line: natural width, no label, no
               bottom border; the ⋮ is pushed to the right with an auto margin. */
            table.datatable td.selectcolumn, table.datatable td.actioncolumn, table.envtable td.selectcolumn, table.envtable td.actioncolumn { flex: 0 0 auto; position: static; border-bottom: 0; }
            table.datatable td.selectcolumn::before, table.datatable td.actioncolumn::before, table.envtable td.selectcolumn::before, table.envtable td.actioncolumn::before { content: none; }
            table.datatable td.actioncolumn, table.envtable td.actioncolumn { margin-left: auto; }
            /* Lege tabel: notice op volle breedte, zonder label-kolom, links */
            table.datatable tr.row-empty td, table.envtable tr.row-empty td { flex: 1 1 100%; justify-content: flex-start; text-align: left; }
            table.datatable tr.row-empty td::before, table.envtable tr.row-empty td::before { content: none; }
        }
        /* Touch screens: dragging (column order/width) is off, so hide the
           handles that belong to it */
        /* Desktopwerk (sjabloon-editor, kalenderplanner): op kleine of
           aanraakschermen vervangen door een notice */
        .desktop-notice { display: none; }
        @media (max-width: 999px), (pointer: coarse) {
            .desktop-only { display: none !important; }
            .desktop-notice { display: block; }
        }
        @media (pointer: coarse) and (not (any-pointer: fine)) {
            .columnhandle, .columngrip { display: none; }
        }
        @media print { aside, .tablebar, .actionbar, .button, .paginator { display: none !important; } body { height: auto; overflow: visible; } main { padding: 0; overflow: visible; } }

        /* ===== DOCUMENTATION VIEWER (docs-*) =====================================
           Chrome around the documentation viewer, NOT part of the app's design
           system. Separate on purpose (agreement JP 2026-08-09): these classes are
           registered in DOCS_CLASSES, not in CLASSES, they all carry the `docs-`
           prefix, and they may only be used by the viewer templates. The guard in
           core/tests_vocabulary.py holds all three of those.

           What belongs here is the *reading environment* (navigating, finding your
           place). What a document looks like on the inside is the document's own
           business -- it carries its own house style, see
           docs/documentatiestandaard.html paragraph 4B. */
        .docs-layout { display: flex; gap: var(--sp-4); height: calc(100vh - var(--head-h) - 2*var(--sp-4)); }
        .docs-outline { flex: 0 0 20rem; overflow-y: auto; padding-right: var(--sp-2); border-right: 1px solid var(--border); }
        .docs-outline[hidden] { display: none; }
        .docs-outline-item { display: block; padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-sm); color: var(--text-soft); font-size: var(--txt-sm); line-height: 1.35; text-decoration: none; border-left: 2px solid transparent; }
        .docs-outline-item:hover { background: var(--row-even); color: var(--text); }
        .docs-outline-item--sub { padding-left: var(--sp-4); font-size: var(--txt-xs); }
        .docs-outline-item--leaf { padding-left: calc(2 * var(--sp-4)); font-size: var(--txt-xs); }
        .docs-outline-item--active { color: var(--brand); border-left-color: var(--brand); font-weight: 600; }
        .docs-frame { flex: 1 1 auto; display: block; width: 100%; height: 100%; border: 0; }
        @media (max-width: 999px), (pointer: coarse) { .docs-outline { display: none; } }
