// ============================================================================
// David Analytics — COMPONENT LIBRARY · COMPONENTEN  (BRON VAN WAARHEID)
// ----------------------------------------------------------------------------
// Dit bestand is de enige plek waar de gestandaardiseerde componenten worden
// gedefinieerd. Zowel de CRM als de Component Library-galerij laden dit bestand,
// dus een wijziging hier werkt overal door. Niet elders herdefiniëren.
//
// Stijl staat in ds/components.css. Componenten worden op `window.DAKit` gezet
// (namespace) én op `window` (voor bestaand gebruik in de CRM).
// ============================================================================
(function () {
  // ---- TextInput -----------------------------------------------------------
  // Standaard tekstveld. Eén component voor lezen/bewerken/disabled met een
  // vaste hoogte (42px), zodat wisselen tussen states niets laat verspringen.
  function TextInput({ value, onChange, type = 'text', placeholder, invalid, disabled, trailing, inline, autoFocus, align }) {
    return (
      <div className={`ff-input-wrap${trailing ? ' has-trailing' : ''}`}>
        <input
          className={`ff-input${inline ? ' ff-inline' : ''}${invalid ? ' invalid' : ''}`}
          type={type === 'email' ? 'email' : type === 'tel' ? 'tel' : 'text'}
          value={value == null ? '' : value}
          placeholder={placeholder}
          disabled={disabled}
          autoFocus={autoFocus}
          style={align ? { textAlign: align } : undefined}
          onChange={(e) => { if (!disabled) onChange(e.target.value); }}
        />
        {trailing && <span className="ff-input-trailing">{trailing}</span>}
      </div>
    );
  }

  // ---- ReadValue -----------------------------------------------------------
  // Lees-weergave met exact dezelfde footprint als TextInput. Ondersteunt
  // mailto:/tel: links via `type`.
  function ReadValue({ value, type }) {
    if (type === 'email' && value) return <a className="ff-value fv-link" href={`mailto:${value}`} title={value}>{value}</a>;
    if (type === 'tel' && value) return <a className="ff-value fv-link" href={`tel:${String(value).replace(/\s/g, '')}`} title={value}>{value}</a>;
    return <span className={`ff-value${value ? '' : ' empty'}`} title={value || ''}>{value || '—'}</span>;
  }

  // ---- InfoTip -------------------------------------------------------------
  // Klein (i)-icoon met een hover/focus-tooltip die uitlegt wat een veld doet.
  function InfoTip({ text, label }) {
    return (
      <span className="ff-infotip">
        <button type="button" className="ff-infotip-btn" aria-label={label || 'Meer informatie'}>
          <window.Icon name="info" size={14} />
        </button>
        <span className="ff-infotip-bubble" role="tooltip">{text}</span>
      </span>
    );
  }

  // ---- FormField -----------------------------------------------------------
  // Label boven het veld + optioneel `info` ((i)-tooltip) en `extra` (bijv. een
  // melding) achter het label. De labelhoogte is vast, zodat een melding de
  // uitlijning niet verstoort.
  function FormField({ label, info, extra, children, wide }) {
    return (
      <div className={`ff-field${wide ? ' ff-field-wide' : ''}`}>
        <span className="ff-label">
          {label}
          {info && <InfoTip text={info} />}
          {extra}
        </span>
        {children}
      </div>
    );
  }

  // ---- FormGrid ------------------------------------------------------------
  // Twee-koloms raster dat FormFields uitlijnt. `wide` op een veld laat het de
  // volle breedte beslaan.
  function FormGrid({ children }) {
    return <div className="ff-form">{children}</div>;
  }

  // ---- RecordLayout --------------------------------------------------------
  // Pagina-body voor detail-/recordschermen: een inhoudsgebied naast een
  // optionele smalle rail (acties, feiten, een tijdlijn). één grid, één gat —
  // zodat alle kaarten in beide kolommen op hetzelfde stramien uitlijnen.
  //   · columns   — aantal gelijke kolommen in het inhoudsgebied (1–3).
  //   · rail      — inhoud van de smalle zijkolom; laat weg voor volle breedte.
  //   · railWidth — vaste breedte (bv. '296px') of 'flex' (rail beweegt mee, 2fr/1fr).
  //   · railSide  — 'right' (standaard) of 'left'.
  //   · railStack — bij smal scherm: 'above' (standaard) of 'below' de inhoud.
  // De railbreedte en hét uniforme gat staan als tokens in ds/components.css
  // (--ff-rail / --ff-grid-gap). Op smalle schermen klapt de rail boven (of
  // onder) de inhoud en worden meerdere kolommen één kolom.
  function RecordLayout({ columns = 1, rail = null, railWidth, railSide = 'right', railStack = 'above', children }) {
    const hasRail = rail != null && rail !== false;
    const flex = railWidth === 'flex';
    const cls = ['ff-record'];
    if (hasRail) cls.push('has-rail');
    if (hasRail && railSide === 'left') cls.push('rail-left');
    if (hasRail && flex) cls.push('rail-flex');
    if (hasRail && railStack === 'below') cls.push('rail-stack-below');
    const style = (hasRail && railWidth && !flex) ? { '--ff-rail': railWidth } : undefined;
    const content = <div className={`ff-record-content ff-cols-${columns}`}>{children}</div>;
    const railEl = hasRail ? <div className="ff-record-rail">{rail}</div> : null;
    return (
      <div className={cls.join(' ')} style={style}>
        {railSide === 'left' ? railEl : null}
        {content}
        {railSide === 'right' ? railEl : null}
      </div>
    );
  }

  // ---- Flag ----------------------------------------------------------------
  // Kleine inline-melding (accent-gekleurd), bedoeld voor in het `extra`-slot van een
  // FormField — bijv. "Aangepast".
  function Flag({ icon = 'pencil', children }) {
    return (
      <span className="name-flag">
        {icon && <window.Icon name={icon} size={11} />}
        {children}
      </span>
    );
  }

  // ---- ResetButton ---------------------------------------------------------
  // Compacte actieknop, bedoeld voor het `trailing`-slot van een TextInput.
  function ResetButton({ onClick, label = 'Herstel', title }) {
    return (
      <button type="button" className="name-reset" onClick={onClick} title={title}>
        <window.Icon name="rotate-ccw" size={13} />{label}
      </button>
    );
  }

  // ---- Textarea ------------------------------------------------------------
  // Meerregelig tekstveld. Zelfde randstijl als TextInput; groeit met `rows`.
  function Textarea({ value, onChange, placeholder, rows = 4, invalid, disabled, inline, autoFocus }) {
    return (
      <textarea
        className={`ff-input ff-textarea${inline ? ' ff-inline' : ''}${invalid ? ' invalid' : ''}`}
        rows={rows}
        value={value == null ? '' : value}
        placeholder={placeholder}
        disabled={disabled}
        autoFocus={autoFocus}
        onChange={(e) => { if (!disabled) onChange(e.target.value); }}
      />
    );
  }

  // ---- Select --------------------------------------------------------------
  // Keuzelijst. `options` is een array van strings of { value, label }.
  // Gesloten ziet het er identiek uit als een veld; geopend toont het een
  // gestylede popover (geen native OS-dropdown) die de merkstijl volgt.
  function Select({ value, onChange, options = [], placeholder, invalid, disabled, inline, autoFocus }) {
    const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
    const [open, setOpen] = React.useState(false);
    const sel = opts.find((o) => String(o.value) === String(value));
    const close = () => setOpen(false);
    React.useEffect(() => {
      if (!open) return undefined;
      const onKey = (e) => { if (e.key === 'Escape') close(); };
      document.addEventListener('keydown', onKey);
      return () => document.removeEventListener('keydown', onKey);
    }, [open]);
    return (
      <div className={`ff-select-wrap${disabled ? ' disabled' : ''}${inline ? ' inline' : ''}${open ? ' open' : ''}`}>
        <button
          type="button"
          className={`ff-input ff-select${inline ? ' ff-inline' : ''}${invalid ? ' invalid' : ''}${sel ? '' : ' ff-empty'}`}
          disabled={disabled}
          autoFocus={autoFocus}
          aria-haspopup="listbox"
          aria-expanded={open}
          onClick={() => { if (!disabled) setOpen((o) => !o); }}
        >
          <span className="ff-select-label">{sel ? sel.label : (placeholder || '')}</span>
        </button>
        <span className="ff-select-caret"><window.Icon name="chevron-down" size={17} /></span>
        {open && (
          <React.Fragment>
            <div className="menu-backdrop" onClick={close} />
            <div className="ff-pop" role="listbox">
              {opts.map((o) => {
                const on = String(o.value) === String(value);
                return (
                  <button type="button" key={o.value} role="option" aria-selected={on}
                    className={`ff-pop-item${on ? ' on' : ''}`}
                    onClick={() => { onChange(o.value); close(); }}>
                    <span className="ff-pop-label">{o.label}</span>
                    {on && <window.Icon name="check" size={15} />}
                  </button>
                );
              })}
            </div>
          </React.Fragment>
        )}
      </div>
    );
  }

  // ---- DateField -----------------------------------------------------------
  // Datumveld (native datepicker), zelfde footprint als TextInput.
  // `value` is ISO (yyyy-mm-dd).
  function DateField({ value, onChange, invalid, disabled, inline }) {
    return (
      <input
        className={`ff-input ff-date${inline ? ' ff-inline' : ''}${invalid ? ' invalid' : ''}${value ? '' : ' ff-empty'}`}
        type="date"
        value={value == null ? '' : value}
        disabled={disabled}
        onChange={(e) => { if (!disabled) onChange(e.target.value); }}
      />
    );
  }

  // ---- MoneyField ----------------------------------------------------------
  // Bedragveld met €-prefix. `value` is een getal; `onChange` geeft een getal
  // ---- MoneyField ----------------------------------------------------------
  // Bedragveld met €-prefix en live Nederlandse opmaak tijdens het typen
  // ('.' duizendtallen, ',' decimaal). `decimals` schakelt centen in.
  // `value` is een getal (of null); `onChange` geeft een getal (of null) terug.
  function groupInt(d) {
    if (d == null || d === '') return '';
    return String(d).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
  }
  function fmtMoney(num, decimals) {
    if (num == null || num === '' || isNaN(num)) return '';
    return Number(num).toLocaleString('nl-NL', {
      minimumFractionDigits: decimals ? 2 : 0,
      maximumFractionDigits: decimals ? 2 : 0,
    });
  }
  // Maakt ruwe invoer schoon → { text: weergave, num: getal|null }.
  function sanitizeMoney(raw, decimals) {
    let s = String(raw).replace(/\./g, ''); // duizendtekens weg
    if (!decimals) {
      const digits = s.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
      return { text: groupInt(digits), num: digits === '' ? null : Number(digits) };
    }
    s = s.replace(/[^\d,]/g, '');
    const fc = s.indexOf(',');
    let intp, decp = null;
    if (fc >= 0) { intp = s.slice(0, fc); decp = s.slice(fc + 1).replace(/,/g, '').slice(0, 2); }
    else intp = s;
    intp = intp.replace(/^0+(?=\d)/, '');
    let text = groupInt(intp || (decp !== null ? '0' : ''));
    if (decp !== null) text += ',' + decp;
    let num = null;
    if (!(intp === '' && !decp)) num = Number((intp || '0') + (decp ? '.' + decp : ''));
    return { text, num };
  }
  function MoneyField({ value, onChange, decimals = false, placeholder, invalid, disabled }) {
    const inputRef = React.useRef(null);
    const [display, setDisplay] = React.useState(() => fmtMoney(value, decimals));
    // Sync vanuit een externe waardewijziging (niet onze eigen echo).
    React.useEffect(() => {
      const cur = sanitizeMoney(display, decimals).num;
      const want = value == null || value === '' ? null : Number(value);
      if (cur !== want) setDisplay(fmtMoney(value, decimals));
    }, [value, decimals]);
    const onInput = (e) => {
      if (disabled) return;
      const { text, num } = sanitizeMoney(e.target.value, decimals);
      setDisplay(text);
      onChange(num);
      const el = inputRef.current;
      if (el) requestAnimationFrame(() => { try { el.setSelectionRange(text.length, text.length); } catch (_) {} });
    };
    return (
      <div className={`ff-input-wrap ff-money${disabled ? ' disabled' : ''}`}>
        <span className="ff-money-prefix">€</span>
        <input
          ref={inputRef}
          className={`ff-input ff-money-input${invalid ? ' invalid' : ''}`}
          type="text"
          inputMode={decimals ? 'decimal' : 'numeric'}
          value={display}
          placeholder={placeholder || (decimals ? '0,00' : '0')}
          disabled={disabled}
          onChange={onInput}
        />
      </div>
    );
  }

  // ---- Toggle --------------------------------------------------------------
  // Aan/uit-schakelaar met optioneel label.
  function Toggle({ checked, onChange, label, disabled }) {
    return (
      <label className={`ff-toggle${disabled ? ' disabled' : ''}`}>
        <span className={`ff-toggle-track${checked ? ' on' : ''}`} role="switch" aria-checked={!!checked}
          onClick={() => { if (!disabled) onChange(!checked); }}>
          <span className="ff-toggle-thumb" />
        </span>
        {label && <span className="ff-toggle-label">{label}</span>}
      </label>
    );
  }

  // ---- Checkbox ------------------------------------------------------------
  function Checkbox({ checked, onChange, label, disabled }) {
    return (
      <label className={`ff-check${disabled ? ' disabled' : ''}`}>
        <span className={`ff-check-box${checked ? ' on' : ''}`}
          onClick={() => { if (!disabled) onChange(!checked); }}>
          {checked && <window.Icon name="check" size={13} />}
        </span>
        {label && <span className="ff-check-label">{label}</span>}
      </label>
    );
  }

  // ---- RadioGroup ----------------------------------------------------------
  // `options` is een array van strings of { value, label }.
  function RadioGroup({ value, onChange, options = [], disabled, inline }) {
    const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
    return (
      <div className={`ff-radio-group${inline ? ' inline' : ''}`}>
        {opts.map((o) => {
          const on = value === o.value;
          return (
            <label key={o.value} className={`ff-radio${disabled ? ' disabled' : ''}`}>
              <span className={`ff-radio-dot${on ? ' on' : ''}`}
                onClick={() => { if (!disabled) onChange(o.value); }} />
              <span className="ff-radio-label">{o.label}</span>
            </label>
          );
        })}
      </div>
    );
  }

  // ---- Autocomplete --------------------------------------------------------
  // Zoekveld met resultatenlijst. Werkt op twee manieren:
  //   · `options`      — een vaste lijst die client-side wordt gefilterd
  //                      (bijv. personen/bedrijven uit de CRM).
  //   · `fetchOptions` — async (query) => item[]  (bijv. een KVK-API).
  // `onChange(text)` houdt de invoertekst bij; `onSelect(item)` vuurt bij keuze.
  function Autocomplete({
    value, onChange, onSelect, options, fetchOptions,
    getLabel, getSub, getKey, renderOption, leading,
    placeholder, minChars = 1, emptyText = 'Geen resultaten', debounce = 240,
    disabled, invalid,
  }) {
    const [open, setOpen] = React.useState(false);
    const [results, setResults] = React.useState([]);
    const [loading, setLoading] = React.useState(false);
    const [hi, setHi] = React.useState(-1);
    const wrapRef = React.useRef(null);
    const reqRef = React.useRef(0);
    const tRef = React.useRef(null);
    // Unieke veldnaam → voorkomt dat de browser het als bekend contactveld
    // (naam/e-mail) herkent en zijn eigen autofill/contactenlijst opent.
    const nameRef = React.useRef('ac-' + Math.random().toString(36).slice(2, 10));
    // Safari/iCloud Keychain negeert autocomplete="off". Het veld start daarom
    // readonly (Safari biedt dan niets aan) en wordt pas bij focus bewerkbaar.
    const [ro, setRo] = React.useState(true);

    const label = getLabel || ((o) => (o && (o.label || o.name || o.title)) || '');
    const keyOf = (o, i) => (getKey ? getKey(o) : (o && o.id != null ? o.id : label(o))) || i;

    React.useEffect(() => {
      const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
      document.addEventListener('mousedown', onDoc);
      return () => document.removeEventListener('mousedown', onDoc);
    }, []);

    const runSearch = (q) => {
      if (!q || q.length < minChars) { setResults([]); setLoading(false); setOpen(false); return; }
      if (fetchOptions) {
        const token = ++reqRef.current;
        setLoading(true); setOpen(true);
        Promise.resolve(fetchOptions(q)).then((items) => {
          if (token !== reqRef.current) return;
          setResults(items || []); setLoading(false); setHi(items && items.length ? 0 : -1);
        }).catch(() => { if (token === reqRef.current) { setResults([]); setLoading(false); } });
      } else {
        const f = (options || []).filter((o) => label(o).toLowerCase().includes(q.toLowerCase()));
        setResults(f); setLoading(false); setOpen(true); setHi(f.length ? 0 : -1);
      }
    };

    const onInput = (e) => {
      const t = e.target.value;
      onChange(t);
      if (tRef.current) clearTimeout(tRef.current);
      if (fetchOptions) tRef.current = setTimeout(() => runSearch(t), debounce);
      else runSearch(t);
    };

    const choose = (item) => {
      if (onSelect) onSelect(item);
      onChange(label(item));
      setOpen(false); setResults([]);
    };

    const onKey = (e) => {
      if (!open && e.key === 'ArrowDown') { runSearch(value || ''); return; }
      if (e.key === 'ArrowDown') { e.preventDefault(); setHi((h) => Math.min(results.length - 1, h + 1)); }
      else if (e.key === 'ArrowUp') { e.preventDefault(); setHi((h) => Math.max(0, h - 1)); }
      else if (e.key === 'Enter') { if (open && results[hi]) { e.preventDefault(); choose(results[hi]); } }
      else if (e.key === 'Escape') { setOpen(false); }
    };

    return (
      <div className="ff-ac" ref={wrapRef}>
        <div className="ff-input-wrap">
          <input
            className={`ff-input ff-ac-input${invalid ? ' invalid' : ''}`}
            type="text"
            name={nameRef.current}
            autoComplete="off"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
            role="combobox"
            aria-autocomplete="list"
            aria-expanded={open}
            data-lpignore="true"
            data-1p-ignore="true"
            data-form-type="other"
            readOnly={ro}
            value={value == null ? '' : value}
            placeholder={placeholder}
            disabled={disabled}
            onChange={onInput}
            onKeyDown={onKey}
            onFocus={() => { setRo(false); if ((value || '').length >= minChars && results.length) setOpen(true); }}
            onBlur={() => setRo(true)}
          />
          <span className="ff-ac-ico">
            {loading ? <span className="ff-ac-loader" /> : <window.Icon name="search" size={16} />}
          </span>
        </div>
        {open && (
          <div className="ff-ac-menu">
            {loading && <div className="ff-ac-msg">Zoeken…</div>}
            {!loading && results.length === 0 && <div className="ff-ac-msg">{emptyText}</div>}
            {!loading && results.map((o, i) => (
              <div
                key={keyOf(o, i)}
                className={`ff-ac-opt${i === hi ? ' hi' : ''}`}
                onMouseEnter={() => setHi(i)}
                onMouseDown={(e) => { e.preventDefault(); choose(o); }}
              >
                {renderOption ? renderOption(o) : (
                  <React.Fragment>
                    {leading && <span className="ff-ac-lead">{leading(o)}</span>}
                    <span className="ff-ac-opt-main">
                      <span className="ff-ac-opt-title">{label(o)}</span>
                      {getSub && getSub(o) && <span className="ff-ac-opt-sub">{getSub(o)}</span>}
                    </span>
                  </React.Fragment>
                )}
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  // ---- NumberField ---------------------------------------------------------
  // Numeriek veld voor gewone getallen (jaar, m², percentage, looptijd) — niet
  // voor bedragen (gebruik MoneyField). In de formulier-variant tonen optionele
  // `prefix`/`suffix` (bv. '%', 'm²') binnen het veld; `inline` geeft de
  // bewerk-in-plaats-look. `value` is een getal (of ''), onChange geeft dat terug.
  function NumberField({ value, onChange, step = 1, min, max, decimals, prefix, suffix, placeholder, invalid, disabled, inline, autoFocus, align }) {
    const handle = (e) => {
      if (disabled) return;
      const raw = e.target.value;
      if (raw === '') { onChange(''); return; }
      const n = decimals ? parseFloat(raw) : Number(raw);
      onChange(isNaN(n) ? '' : n);
    };
    const input = (
      <input
        className={`ff-input ff-number-input${inline ? ' ff-inline' : ''}${invalid ? ' invalid' : ''}`}
        type="number" inputMode={decimals ? 'decimal' : 'numeric'}
        step={step} min={min} max={max}
        value={value == null ? '' : value}
        placeholder={placeholder} disabled={disabled} autoFocus={autoFocus}
        style={align ? { textAlign: align } : undefined}
        onChange={handle}
      />
    );
    if (inline) return <div className="ff-input-wrap ff-number">{input}</div>;
    return (
      <div className={`ff-input-wrap ff-number${prefix ? ' has-prefix' : ''}${suffix ? ' has-suffix' : ''}${disabled ? ' disabled' : ''}`}>
        {prefix && <span className="ff-affix ff-affix-pre">{prefix}</span>}
        {input}
        {suffix && <span className="ff-affix ff-affix-suf">{suffix}</span>}
      </div>
    );
  }

  // ---- SearchInput ---------------------------------------------------------
  // Zoekveld: leading zoek-icoon + invoer, optioneel een teller-pill rechts
  // (bv. 'X ongelezen') en een wis-knop. Voor vrije tekstfilters in toolbars,
  // mailboxen en de ⌘K-zoeker. (Voor een resultatenlijst → Autocomplete.)
  function SearchInput({ value, onChange, placeholder = 'Zoeken…', count, onClear, autoFocus, inputRef }) {
    return (
      <div className="ff-search">
        <span className="ff-search-ico"><window.Icon name="search" size={16} /></span>
        <input
          ref={inputRef} className="ff-search-input" type="text"
          value={value == null ? '' : value} placeholder={placeholder} autoFocus={autoFocus}
          onChange={(e) => onChange(e.target.value)}
        />
        {count != null && count !== '' && count !== false && <span className="ff-search-count">{count}</span>}
        {onClear && value ? (
          <button type="button" className="ff-search-clear" onClick={onClear} aria-label="Wissen"><window.Icon name="x" size={15} /></button>
        ) : null}
      </div>
    );
  }

  // ---- MultiSelect ---------------------------------------------------------
  // Meervoudige keuze tegen bestaande data: kies waarden uit `options`; elke
  // keuze wordt een chip. Compact — chips + een '+ Toevoegen' op één regel.
  function MultiSelect({ value = [], onChange, options = [], placeholder = 'Toevoegen…', getLabel, invalid, disabled }) {
    const arr = Array.isArray(value) ? value : [];
    const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
    const remaining = opts.filter((o) => !arr.includes(o.value));
    const labelOf = getLabel || ((v) => { const o = opts.find((x) => x.value === v); return o ? o.label : v; });
    const add = (v) => { if (v && !arr.includes(v)) onChange([...arr, v]); };
    const remove = (v) => onChange(arr.filter((x) => x !== v));
    const [open, setOpen] = React.useState(false);
    const close = () => setOpen(false);
    React.useEffect(() => {
      if (!open) return undefined;
      const onKey = (e) => { if (e.key === 'Escape') close(); };
      document.addEventListener('keydown', onKey);
      return () => document.removeEventListener('keydown', onKey);
    }, [open]);
    return (
      <div className={`ff-multi-box${invalid ? ' invalid' : ''}${disabled ? ' disabled' : ''}`}>
        {arr.map((v) => (
          <span className="ff-chip" key={v}>
            {labelOf(v)}
            {!disabled && <button type="button" className="ff-chip-x" onClick={() => remove(v)} aria-label="Verwijderen"><window.Icon name="x" size={12} /></button>}
          </span>
        ))}
        {!disabled && remaining.length > 0 && (
          <span className="ff-multi-add-wrap">
            <button type="button" className="ff-multi-add-mini" aria-haspopup="listbox" aria-expanded={open}
              onClick={() => setOpen((o) => !o)}>
              <window.Icon name="plus" size={12} />{placeholder}
            </button>
            {open && (
              <React.Fragment>
                <div className="menu-backdrop" onClick={close} />
                <div className="ff-pop ff-pop-multi" role="listbox">
                  {remaining.map((o) => (
                    <button type="button" key={o.value} role="option" className="ff-pop-item"
                      onClick={() => { add(o.value); close(); }}>
                      <span className="ff-pop-label">{o.label}</span>
                    </button>
                  ))}
                </div>
              </React.Fragment>
            )}
          </span>
        )}
      </div>
    );
  }

  // ---- FileField -----------------------------------------------------------
  // Sleep-en-neerzet zone + klik om bestanden te kiezen. `onFiles(File[])`
  // krijgt de gekozen bestanden; `accept`/`multiple`/`hint` configureren het.
  function FileField({ onFiles, accept, multiple = true, hint = 'Sleep bestanden hierheen of klik om te kiezen', disabled }) {
    const ref = React.useRef(null);
    const [over, setOver] = React.useState(false);
    const pick = (files) => { const f = [...(files || [])]; if (f.length && onFiles) onFiles(f); };
    return (
      <div
        className={`ff-file${over ? ' over' : ''}${disabled ? ' disabled' : ''}`}
        role="button" tabIndex={disabled ? -1 : 0}
        onClick={() => { if (!disabled && ref.current) ref.current.click(); }}
        onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && !disabled) { e.preventDefault(); ref.current.click(); } }}
        onDragOver={(e) => { e.preventDefault(); if (!disabled) setOver(true); }}
        onDragLeave={() => setOver(false)}
        onDrop={(e) => { e.preventDefault(); setOver(false); if (!disabled) pick(e.dataTransfer.files); }}
      >
        <span className="ff-file-ico"><window.Icon name="upload" size={24} /></span>
        <span className="ff-file-hint">{hint}</span>
        <input ref={ref} type="file" accept={accept} multiple={multiple} hidden
          onChange={(e) => { pick(e.target.files); e.target.value = ''; }} />
      </div>
    );
  }

  // ==========================================================================
  // CHROME — knoppen, badges, kaarten en menu's
  // Wrappen de bestaande app.css-klassen, zodat de look pixel-identiek blijft
  // maar de CRM via componenten (één API) bouwt i.p.v. losse classNames.
  // ==========================================================================

  // ---- Button --------------------------------------------------------------
  // Pill-actieknop. `variant`: primary (accent-CTA) · ghost (wit/rand) ·
  // approve (groen) · danger (rood) · outline-danger (rode rand). `size="sm"`
  // voor compact. `icon`/`trailingIcon` plaatsen een Lucide-icoon.
  function Button({ variant = 'primary', size, icon, iconSize, trailingIcon, children, className = '', type = 'button', ...rest }) {
    const cls = `btn btn-${variant}${size === 'sm' ? ' sm' : ''}${className ? ' ' + className : ''}`;
    return (
      <button type={type} className={cls} {...rest}>
        {icon && <window.Icon name={icon} size={iconSize || 17} />}
        {children}
        {trailingIcon && <window.Icon name={trailingIcon} size={iconSize || 17} />}
      </button>
    );
  }

  // ---- ChipButton ----------------------------------------------------------
  // Compacte knop met rand voor toolbars en kaartkoppen. `primary` maakt 'm
  // accent-gekleurd (bijv. Opslaan); `active` markeert een ingedrukte filter.
  function ChipButton({ primary, active, icon, iconSize, trailingIcon, children, className = '', type = 'button', ...rest }) {
    const cls = `chip-btn${primary ? ' primary' : ''}${active ? ' active' : ''}${className ? ' ' + className : ''}`;
    return (
      <button type={type} className={cls} {...rest}>
        {icon && <window.Icon name={icon} size={iconSize || 16} />}
        {children}
        {trailingIcon && <window.Icon name={trailingIcon} size={iconSize || 15} />}
      </button>
    );
  }

  // ---- IconButton ----------------------------------------------------------
  // Vierkante knop met enkel een icoon. `size="sm"` voor compact. Geef altijd
  // een `label` mee voor toegankelijkheid (wordt aria-label én title).
  function IconButton({ icon, size, iconSize, label, title, className = '', type = 'button', ...rest }) {
    const cls = `icon-btn${size === 'sm' ? ' sm' : ''}${className ? ' ' + className : ''}`;
    return (
      <button type={type} className={cls} aria-label={label} title={title != null ? title : label} {...rest}>
        <window.Icon name={icon} size={iconSize || (size === 'sm' ? 17 : 18)} />
      </button>
    );
  }

  // ---- Badge ---------------------------------------------------------------
  // Statuslabel met gekleurde stip. `tone`: info · success · warning · danger · neutral.
  function Badge({ tone = 'info', icon, iconSize, children, className = '', ...rest }) {
    return (
      <span className={`badge ${tone}${className ? ' ' + className : ''}`} {...rest}>
        {icon && <window.Icon name={icon} size={iconSize || 12} />}{children}
      </span>
    );
  }

  // ---- StatusDot -----------------------------------------------------------
  // Losse gekleurde stip zonder label (menu's, lijsten). `tone` als bij Badge.
  function StatusDot({ tone = 'neutral', className = '' }) {
    return <span className={`badge-dot ${tone}${className ? ' ' + className : ''}`} />;
  }

  // ---- Card / CardHead -----------------------------------------------------
  // Witte, zacht-afgeronde kaart. CardHead is de vaste kop: primair-gekleurd icoon + titel
  // + optionele acties (als children, rechts uitgelijnd).
  function Card({ children, className = '', ...rest }) {
    return <section className={`d-card${className ? ' ' + className : ''}`} {...rest}>{children}</section>;
  }
  function CardHead({ icon, iconSize, title, children }) {
    return (
      <header className="d-card-head">
        {icon && <window.Icon name={icon} size={iconSize || 17} />}
        {title && <h3>{title}</h3>}
        {children}
      </header>
    );
  }

  // ---- Menu / MenuItem -----------------------------------------------------
  // Dropdownmenu dat onder een trigger opent, met schermvullende backdrop die
  // de buitenklik opvangt. `trigger` is je knop-element (de onClick wordt
  // automatisch aangevuld om te openen). `children` mag een functie `(close)=>…`
  // zijn zodat een item het menu na de actie kan sluiten. `align`: right|left.
  function Menu({ trigger, align = 'right', className = '', menuStyle, children }) {
    const [open, setOpen] = React.useState(false);
    const close = () => setOpen(false);
    const triggerEl = React.cloneElement(trigger, {
      onClick: (e) => { if (trigger.props.onClick) trigger.props.onClick(e); setOpen((o) => !o); },
    });
    const panelStyle = { ...(align === 'left' ? { left: 0, right: 'auto' } : {}), ...(menuStyle || {}) };
    return (
      <span className={`ff-menu-wrap${className ? ' ' + className : ''}`} style={{ position: 'relative', display: 'inline-block' }}>
        {triggerEl}
        {open && (
          <React.Fragment>
            <div className="menu-backdrop" onClick={close} />
            <div className="row-menu" role="menu" style={panelStyle}>
              {typeof children === 'function' ? children(close) : children}
            </div>
          </React.Fragment>
        )}
      </span>
    );
  }
  function MenuItem({ icon, iconSize, danger, children, onClick, ...rest }) {
    return (
      <button type="button" className={`row-menu-item${danger ? ' danger' : ''}`} onClick={onClick} {...rest}>
        {icon && <window.Icon name={icon} size={iconSize || 15} />}{children}
      </button>
    );
  }

  // ==========================================================================
  // ORGANISMEN — volledige, terugkerende UI-structuren
  // Net als de velden en chrome wrappen deze de bestaande app.css-klassen, dus
  // de look blijft pixel-identiek; de CRM bouwt ze nu via één component-API.
  // ==========================================================================

  // ---- Table ---------------------------------------------------------------
  // De centrale datatabel (CSS-grid met subgrid). `columns` is een array van
  // { key, label, w?, type? }, waarbij `type` (money|number|date) de uitlijning
  // en celopmaak stuurt. `renderCell(col, row)` levert de celinhoud (anders
  // `col.render(row)` of `row[col.key]`). `onRowClick(row, i)` maakt rijen
  // klikbaar; `minWidth` overschrijft de min-breedte (0 = meekrimpen in smalle
  // contexten zoals de drawer/relatie-overzichten).
  function Table({ columns = [], rows = [], onRowClick, renderCell, rowKey, minWidth, className = '' }) {
    const grid = columns.map((c) => c.w || '1fr').join(' ');
    const isNum = (t) => t === 'money' || t === 'number';
    const style = { gridTemplateColumns: grid };
    if (minWidth != null) style.minWidth = minWidth;
    return (
      <div className={`tbl${className ? ' ' + className : ''}`} style={style}>
        <div className="tbl-head">
          {columns.map((c) => (
            <div key={c.key} className={`th ${isNum(c.type) ? 'num' : ''}`}>{c.label}</div>
          ))}
        </div>
        {rows.map((row, i) => (
          <div className="tbl-row" key={rowKey ? rowKey(row, i) : i}
            onClick={onRowClick ? () => onRowClick(row, i) : undefined}>
            {columns.map((c) => {
              let cls = 'td';
              if (c.type === 'money') cls += ' money';
              else if (c.type === 'number') cls += ' num';
              else if (c.type === 'date') cls += ' date';
              const content = renderCell ? renderCell(c, row) : (c.render ? c.render(row) : row[c.key]);
              return <div key={c.key} className={cls}>{content}</div>;
            })}
          </div>
        ))}
      </div>
    );
  }

  // ---- Drawer --------------------------------------------------------------
  // Slide-over paneel dat van rechts inschuift, met schermvullende scrim die de
  // buitenklik opvangt. `head`/`foot` zijn vrije slots (kop met titel/acties,
  // voet met knoppen); `children` is de scrollende body. `lockClose` negeert de
  // buitenklik (bv. tijdens bewerken). `width` overschrijft de standaard 460px.
  function Drawer({ onClose, lockClose, head, foot, width, ariaLabel = 'Detail', className = '', children }) {
    return (
      <div className="drawer-scrim" onClick={lockClose ? undefined : onClose}>
        <aside className={`drawer${className ? ' ' + className : ''}`} style={width ? { width } : undefined}
          onClick={(e) => e.stopPropagation()} role="dialog" aria-label={ariaLabel}>
          {head != null && head !== false && <header className="drawer-head">{head}</header>}
          <div className="drawer-body">{children}</div>
          {foot != null && foot !== false && <footer className="drawer-foot">{foot}</footer>}
        </aside>
      </div>
    );
  }

  // ---- Tabs / Tab ----------------------------------------------------------
  // Onderstreepte tabbalk van de detailpagina's. `Tabs` houdt de actieve waarde
  // bij (`value`/`onChange`) en kleurt het gekozen `Tab` automatisch. Een `Tab`
  // toont een icoon + label; children vullen een extra slot rechts in het label
  // (bv. een voortgangsdonut of badge).
  function Tabs({ value, onChange, className = '', children }) {
    return (
      <nav className={`detail-tabs${className ? ' ' + className : ''}`}>
        {React.Children.map(children, (ch) => {
          if (!ch) return ch;
          const active = ch.props.active != null ? ch.props.active : value === ch.props.id;
          return React.cloneElement(ch, {
            active,
            onSelect: ch.props.onSelect || (() => onChange && onChange(ch.props.id)),
          });
        })}
      </nav>
    );
  }
  function Tab({ icon, iconSize, label, active, onSelect, children }) {
    return (
      <button type="button" className={`dtab ${active ? 'active' : ''}`} onClick={onSelect}>
        {icon && <window.Icon name={icon} size={iconSize || 16} />}{label}{children}
      </button>
    );
  }

  // ---- Modal / ConfirmDialog -----------------------------------------------
  // `Modal` is de gecentreerde dialoog-shell (scrim + witte box). `ConfirmDialog`
  // is de kant-en-klare bevestiging: icoonbubbel + titel + tekst + actie-voet.
  // `iconTone` kleurt de bubbel (danger|info|success|warning). Geef de knoppen mee
  // via `actions`; de body via children.
  function Modal({ onClose, lockClose, role = 'dialog', ariaLabel, width, className = '', children }) {
    return (
      <div className="confirm-scrim" onClick={lockClose ? undefined : onClose}>
        <div className={`confirm-box${className ? ' ' + className : ''}`} style={width ? { width } : undefined}
          onClick={(e) => e.stopPropagation()} role={role} aria-label={ariaLabel}>
          {children}
        </div>
      </div>
    );
  }
  const MODAL_TONES = {
    danger: { bg: 'var(--danger-050)', color: 'var(--danger)' },
    info: { bg: 'var(--primary-050)', color: 'var(--primary-600)' },
    success: { bg: 'var(--success-050)', color: 'var(--success)' },
    warning: { bg: 'var(--accent-050)', color: 'var(--accent-600)' },
  };
  function ConfirmDialog({ onClose, lockClose, icon, iconTone = 'danger', title, actions, role = 'alertdialog', ariaLabel, children }) {
    const tone = MODAL_TONES[iconTone] || MODAL_TONES.info;
    return (
      <Modal onClose={onClose} lockClose={lockClose} role={role} ariaLabel={ariaLabel || title}>
        {icon && <window.IconBubble icon={icon} size={46} radius={14} bg={tone.bg} color={tone.color} iconSize={22} />}
        {title && <h3>{title}</h3>}
        {children}
        {actions && <div className="confirm-actions">{actions}</div>}
      </Modal>
    );
  }

  // ---- EmptyState ----------------------------------------------------------
  // Lege-staat-blok: primair-gekleurde icoonbubbel + titel + uitleg + optionele actie. Voor
  // lege tabbladen, lege lijsten en niet-gekoppelde records.
  function EmptyState({ icon, iconSize = 26, title, action, bubbleSize = 56, bubbleRadius = 16, className = '', children }) {
    return (
      <div className={`d-empty${className ? ' ' + className : ''}`}>
        {icon && <window.IconBubble icon={icon} size={bubbleSize} radius={bubbleRadius}
          bg="var(--primary-050)" color="var(--primary-600)" iconSize={iconSize} className="d-empty-ico" />}
        {title && <b>{title}</b>}
        {children && <p>{children}</p>}
        {action}
      </div>
    );
  }

  // ---- KPI's (kerncijfers) -------------------------------------------------
  // Twee tegels die dezelfde app.css-klassen wrappen, plus één grid-wrapper.
  //
  //  · KpiGrid  — de responsive grid om de tegels. variant:
  //      'default'  → 3-koloms `.kpi-grid` (Overzicht-tab)
  //      'compact'  → smallere strook `.kpi-grid.sub` (boven elke detailtab)
  //      'dashboard'→ auto-fit `.pdash-kpis` (dashboards, met StatTile)
  //  · Kpi      — `.kpi`-tegel: uppercase label, groot cijfer (tone kleurt het:
  //      success|warning|danger), optionele subtekst en een voortgangsbalk via `bar`
  //      (0–100). `wide` laat 'm de volle gridbreedte vullen. `value` mag een
  //      node zijn (bv. een getal met een InfoTip-hint).
  //  · StatTile — `.pkpi`-tegel voor dashboards: label + primaire IconBubble in de
  //      kop, daaronder een groot cijfer óf een badge (`badge` + `tone`) en een
  //      subregel.
  function KpiGrid({ variant = 'default', className = '', children }) {
    const base = variant === 'dashboard' ? 'pdash-kpis' : variant === 'compact' ? 'kpi-grid sub' : 'kpi-grid';
    return <div className={`${base}${className ? ' ' + className : ''}`}>{children}</div>;
  }
  function Kpi({ label, value, sub, tone, bar, wide, className = '', children }) {
    return (
      <div className={`kpi${wide ? ' wide' : ''}${className ? ' ' + className : ''}`}>
        <span className="kpi-label">{label}</span>
        <b className={`kpi-value${tone ? ' ' + tone : ''}`}>{value}</b>
        {sub && <span className="kpi-sub">{sub}</span>}
        {bar != null && <span className="kpi-bar"><i style={{ width: bar + '%' }}></i></span>}
        {children}
      </div>
    );
  }
  function StatTile({ label, value, icon, iconSize = 16, sub, badge, tone, className = '', children }) {
    return (
      <section className={`d-card pkpi${className ? ' ' + className : ''}`}>
        <div className="pkpi-top">
          <span className="pkpi-label">{label}</span>
          {icon && <span className="pkpi-ico"><window.Icon name={icon} size={iconSize} /></span>}
        </div>
        {badge
          ? <span className={`badge ${tone || ''} pkpi-badge`}>{value}</span>
          : <span className="pkpi-value">{value}</span>}
        {sub && <span className="pkpi-sub">{sub}</span>}
        {children}
      </section>
    );
  }

  // ---- Timeline ------------------------------------------------------------
  // De verticale activiteitenlijst (verbindingslijn + gekleurde icoonstip per
  // regel). `Timeline` rendert de `<ol>`; geef `items` mee voor de standaard
  // vorm ({ icon, tone, title, meta, time }) of `children` met eigen <li>'s voor
  // maatwerk (klikbare regels, hover-popovers). `TimelineItem` levert de inhoud
  // van één regel — de stip + tekst — en accepteert children als rechter-slot.
  function TimelineItem({ icon, iconSize = 15, tone, title, meta, time, children }) {
    return (
      <React.Fragment>
        <window.IconBubble icon={icon} size={30} iconSize={iconSize} className={`tl-dot ${tone || ''}`} />
        <div className="tl-body">
          <b>{title}</b>
          {(meta || time) && <span className="tl-meta">{meta ? meta + ' · ' : ''}{time}</span>}
        </div>
        {children}
      </React.Fragment>
    );
  }
  function Timeline({ items, className = '', children }) {
    return (
      <ol className={`timeline${className ? ' ' + className : ''}`}>
        {items
          ? items.map((t, i) => (
              <li className="tl-item" key={i}>
                <TimelineItem icon={t.icon} tone={t.tone} title={t.title} meta={t.meta} time={t.time} />
              </li>
            ))
          : children}
      </ol>
    );
  }

  // ==========================================================================
  // AANVULLENDE PATRONEN — paginatie · segmented · tooltip · toast
  // Pager en Segmented wrappen bestaande app.css-klassen (.pager / .seg-btn),
  // dus de look blijft identiek. Tooltip en Toast zijn nieuwe staples met eigen
  // stijl in ds/components.css.
  // ==========================================================================

  // ---- Pager ---------------------------------------------------------------
  // Paginatie voor de tabelvoet. `page` is 1-based; `pageCount` het totaal;
  // `onChange(n)` vuurt bij een geldige sprong. Toont vorige/volgende-pijlen en
  // genummerde knoppen; bij veel pagina's verschijnt een ellipsis (…) tussen het
  // begin, het venster rond de huidige pagina en het einde. `siblings` stelt in
  // hoeveel buren rond de huidige pagina zichtbaar blijven.
  function Pager({ page = 1, pageCount = 1, onChange, siblings = 1, className = '' }) {
    const go = (n) => { if (onChange && n >= 1 && n <= pageCount && n !== page) onChange(n); };
    const pages = [];
    const start = Math.max(2, page - siblings);
    const end = Math.min(pageCount - 1, page + siblings);
    pages.push(1);
    if (start > 2) pages.push('…');
    for (let n = start; n <= end; n++) pages.push(n);
    if (end < pageCount - 1) pages.push('…');
    if (pageCount > 1) pages.push(pageCount);
    return (
      <div className={`pager${className ? ' ' + className : ''}`}>
        <button type="button" className="pg nav" aria-label="Vorige" disabled={page <= 1}
          onClick={() => go(page - 1)}><window.Icon name="chevron-left" size={16} /></button>
        {pages.map((n, i) => (n === '…'
          ? <span key={'gap' + i} className="pg-gap" aria-hidden="true">…</span>
          : <button type="button" key={n} className={`pg${n === page ? ' active' : ''}`}
              aria-current={n === page ? 'page' : undefined} onClick={() => go(n)}>{n}</button>
        ))}
        <button type="button" className="pg nav" aria-label="Volgende" disabled={page >= pageCount}
          onClick={() => go(page + 1)}><window.Icon name="chevron-right" size={16} /></button>
      </div>
    );
  }

  // ---- Segmented -----------------------------------------------------------
  // Compacte schakelaar tussen een paar weergaven/varianten — een pill-track met
  // één actieve primaire knop. `options`: strings of { value, label, icon }.
  // `size="sm"` voor de compacte kop-variant. Voor 2–4 korte opties; bij meer of
  // langere labels → Select of Tabs.
  function Segmented({ value, onChange, options = [], size, ariaLabel, className = '' }) {
    const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
    return (
      <div className={`view-switch${size === 'sm' ? ' sm' : ''}${className ? ' ' + className : ''}`}
        role="tablist" aria-label={ariaLabel}>
        {opts.map((o) => {
          const on = String(o.value) === String(value);
          return (
            <button type="button" key={o.value} role="tab" aria-selected={on}
              className={`seg-btn${on ? ' on' : ''}`} onClick={() => { if (onChange) onChange(o.value); }}>
              {o.icon && <window.Icon name={o.icon} size={size === 'sm' ? 14 : 15} />}{o.label}
            </button>
          );
        })}
      </div>
    );
  }

  // ---- Tooltip -------------------------------------------------------------
  // Algemene hover/focus-tooltip om een willekeurig element. `label` is de tekst;
  // `side`: top (standaard) | bottom | left | right. Puur CSS-gestuurd: toont op
  // hover én op toetsenbordfocus binnen de wrapper. Voor de (i)-veldhint → InfoTip.
  function Tooltip({ label, side = 'top', children, className = '' }) {
    return (
      <span className={`ff-tip${className ? ' ' + className : ''}`} data-side={side}>
        {children}
        <span className="ff-tip-bubble" role="tooltip">{label}</span>
      </span>
    );
  }

  // ---- Toast ---------------------------------------------------------------
  // Tijdelijke melding. Roep `window.toast(...)` imperatief aan; één gemonteerde
  // <ToastHost/> (bij de app-root) toont en verbergt ze.
  //   toast('Opgeslagen')                              → neutrale (info) toast
  //   toast({ message, title?, tone, icon?, duration?, action? })
  //   tone: info | success | warning | danger   ·   action: { label, onClick }
  // `duration` 0 = blijft staan tot de gebruiker sluit. Geeft een id terug.
  const toastBus = (() => {
    let subs = [];
    let seq = 0;
    return {
      subscribe(fn) { subs.push(fn); return () => { subs = subs.filter((s) => s !== fn); }; },
      push(t) {
        const id = ++seq;
        const item = Object.assign(
          { id, tone: 'info', duration: 4000 },
          typeof t === 'string' ? { message: t } : (t || {})
        );
        subs.forEach((s) => s(item));
        return id;
      },
    };
  })();
  function toast(t) { return toastBus.push(t); }
  const TOAST_ICON = { info: 'info', success: 'check-circle', warning: 'alert-triangle', danger: 'alert-circle' };
  function ToastHost({ position = 'bottom-right' }) {
    const [items, setItems] = React.useState([]);
    React.useEffect(() => toastBus.subscribe((item) => {
      setItems((cur) => [...cur, item]);
      if (item.duration) setTimeout(() => {
        setItems((cur) => cur.filter((x) => x.id !== item.id));
      }, item.duration);
    }), []);
    const remove = (id) => setItems((cur) => cur.filter((x) => x.id !== id));
    return (
      <div className={`ff-toasts ${position}`} role="region" aria-live="polite" aria-label="Meldingen">
        {items.map((t) => (
          <div key={t.id} className={`ff-toast ${t.tone}`} role="status">
            <span className="ff-toast-ico"><window.Icon name={t.icon || TOAST_ICON[t.tone] || 'info'} size={18} /></span>
            <div className="ff-toast-body">
              {t.title && <b>{t.title}</b>}
              <span>{t.message}</span>
            </div>
            {t.action && (
              <button type="button" className="ff-toast-action"
                onClick={() => { if (t.action.onClick) t.action.onClick(); remove(t.id); }}>{t.action.label}</button>
            )}
            <button type="button" className="ff-toast-x" aria-label="Sluiten" onClick={() => remove(t.id)}>
              <window.Icon name="x" size={15} />
            </button>
          </div>
        ))}
      </div>
    );
  }

  const DAKit = { TextInput, ReadValue, Textarea, Select, NumberField, DateField, MoneyField, SearchInput, MultiSelect, FileField, Toggle, Checkbox, RadioGroup, Autocomplete, FormField, FormGrid, RecordLayout, Flag, ResetButton, InfoTip, Button, ChipButton, IconButton, Badge, StatusDot, Card, CardHead, Menu, MenuItem, Table, Drawer, Tabs, Tab, Modal, ConfirmDialog, EmptyState, KpiGrid, Kpi, StatTile, Timeline, TimelineItem, Pager, Segmented, Tooltip, Toast: toast, ToastHost };
  window.DAKit = DAKit;
  // Bestaande CRM-code gebruikt deze namen direct op window — behouden.
  Object.assign(window, DAKit);
  window.toast = toast;
})();
