/* v4 visie-lezer — documentvorm: één doorlopende pagina. De magazinevorm is 27-8 verwijderd (Tim). */

const VISIE = window.VISIE_CONTENT;
const PaginaBalk = window.PaginaBalk;
const VISIE_INGEKORT = window.VISIE_CONTENT_KORT || VISIE;
const visieByid = Object.fromEntries(VISIE.map((s) => [s.id, s]));

/* De lezer volgt de volgorde van het document zelf (volgorde.txt), min laag 1 en de
   walkthrough. Zo verschijnt een nieuw hoofdstuk automatisch, ook als het nog geen
   letter of nummer heeft. De samenvatting staat hier buiten omdat zij geen hoofdstuk
   is maar de opening: haar badges en titel vormen de documentkop, en haar tekst staat
   sinds 23-8 als korte versie daaronder. */
const BUITEN_DE_LEZER = new Set(['samenvatting', 'managementsamenvatting', 'walkthrough']);
const HOOFDSTUKKEN = VISIE.filter((s) => !BUITEN_DE_LEZER.has(s.id));
const HOOFDSTUKKEN_KORT = VISIE_INGEKORT.filter((s) => !BUITEN_DE_LEZER.has(s.id) && (s.html || '').trim());

/* ---- werkstand: hulpmiddel tijdens de herziening, geen onderdeel van de publicatie ----
   Zet WERKSTAND_ZICHTBAAR op false voordat dit live gaat.
   De inhoud komt uit richtlijnen/hoofdstukregister.md en wordt door
   `build.py site` meegeschreven. Hier dus niets bijhouden. */
const WERKSTAND_ZICHTBAAR = false;

/* ---- redactiemodus: wie erin komt, en wat bewaren betekent ----
   Tim typt hiermee correcties rechtstreeks in de lezer. Sinds 23-8 is bewaren geen
   voorstel meer maar publiceren: de correctie gaat naar de tekstlaag en iedereen
   die de pagina daarna opent, leest zijn versie.

   Wie erin komt. Lokaal achter serveer.py altijd, want wie bij die server kan, kan
   ook bij de bestanden; die server zet de vlag VIC_HERZIENING. Online alleen met
   het wachtwoord, één keer meegegeven als `?redactie=<wachtwoord>`, waarna het in
   deze browser blijft staan. Zonder dat wachtwoord is er geen balk, en het endpoint
   weigert ook een verzoek zonder de sleutel: de knop verbergen is de gemakkelijke
   helft, de server is de echte slotgracht. Uitzetten in deze browser gaat met
   `?redactie=` (leeg). */
const LAAG_PAD = '/api/tekstlaag';
/* Eén pad voor de vraagbaak, lokaal en online. serveer.py biedt hem ook aan onder
   deze naam, zodat wat lokaal werkt ook op Vercel werkt (24-8). */
const VRAAG_PAD = '/api/vraag';
const SLEUTELNAAM = 'vic-redactie-sleutel';

/* De sleutel staat op twee plekken, localStorage én een cookie van 180 dagen,
   en wordt uit beide teruggelezen. Reden (27-8): bij Tim verdween de balk bij
   elke ververs, dus zijn browser hield de localStorage niet vast. Valt de één
   weg, dan herstelt de ander hem. `?redactie=` (leeg) wist bewust allebei. */
function leesSleutelCookie() {
  try {
    const m = document.cookie.match(/(?:^|;\s*)vicRedactie=([^;]*)/);
    return m ? decodeURIComponent(m[1]) : '';
  } catch (e) { return ''; }
}
function schrijfSleutelCookie(waarde) {
  try {
    if (waarde) document.cookie = 'vicRedactie=' + encodeURIComponent(waarde) + ';path=/;max-age=' + (60 * 60 * 24 * 180) + ';SameSite=Lax';
    else document.cookie = 'vicRedactie=;path=/;max-age=0;SameSite=Lax';
  } catch (e) {}
}
function redactieSleutel() {
  let uit = null;
  try { uit = new URLSearchParams(location.search).get('redactie'); } catch (e) {}
  if (uit !== null) {
    try {
      if (uit) localStorage.setItem(SLEUTELNAAM, uit);
      else localStorage.removeItem(SLEUTELNAAM);
    } catch (e) {}
    schrijfSleutelCookie(uit);
  }
  let s = '';
  try { s = localStorage.getItem(SLEUTELNAAM) || ''; } catch (e) {}
  if (!s) {
    s = leesSleutelCookie();
    if (s) { try { localStorage.setItem(SLEUTELNAAM, s); } catch (e) {} }
  } else {
    if (!leesSleutelCookie()) schrijfSleutelCookie(s);
  }
  return s;
}

const REDACTIE_BESCHIKBAAR = !!window.VIC_HERZIENING || !!redactieSleutel();

function Werkstand({ h }) {
  const w = h.werkstand;
  if (!WERKSTAND_ZICHTBAAR || !w) return null;
  const nummer = w.nummer && w.nummer !== '—' ? ' · wordt hoofdstuk ' + w.nummer : '';
  const ronde = w.ronde && w.ronde !== '—' ? ' · ronde ' + w.ronde : '';
  return (
    <p style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: 11, color: 'var(--text3)',
                margin: '-6px 0 14px', letterSpacing: '.01em' }}>
      content/{h.id}.md{nummer}{ronde} · <Md tekst={w.stand}></Md>
    </p>
  );
}

/* ---- skelet: de grote lijn vóór de woorden ----
   Komt uit richtlijnen/skelet.md en wordt door `build.py site` meegeschreven.
   Staat boven de tekst zodat de koppeling tussen plan en tekst zichtbaar is.
   Verdwijnt met dezelfde schakelaar als de werkstandregel. */
const VORM_KLEUR = {
  tekst: 'var(--text3)', tabel: 'var(--blue, #3a5a9b)', lijst: 'var(--blue, #3a5a9b)',
  kaarten: 'var(--blue, #3a5a9b)', fiche: 'var(--accent2)', kader: 'var(--amber, #b8860b)',
  beeld: 'var(--accent2)', '—': 'var(--text3)',
};

/* Het skelet is Markdown, dus `**vet**` en `code` stonden letterlijk in de lezer.
   Dit maakt ze zichtbaar als opmaak: het is de tekst die beoordeeld moet worden. */
function Md({ tekst }) {
  if (!tekst) return null;
  const delen = String(tekst).split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
  return <>{delen.map((d, i) => {
    if (d.startsWith('**') && d.endsWith('**')) return <strong key={i}>{d.slice(2, -2)}</strong>;
    if (d.startsWith('`') && d.endsWith('`')) return <code key={i} style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: '.92em' }}>{d.slice(1, -1)}</code>;
    return d;
  })}</>;
}

function Skelet({ h }) {
  const blokken = h.skelet;
  if (!WERKSTAND_ZICHTBAAR || !blokken || !blokken.length) return null;
  return (
    <div style={{ margin: '0 0 22px', padding: '14px 16px', background: 'var(--bg3)',
                  border: '1px solid rgba(0,0,0,.06)', borderLeft: '3px solid var(--accent2)',
                  borderRadius: 'var(--radius)' }}>
      {blokken.map((b) => (
        <div key={b.nummer} style={{ marginBottom: 10 }}>
          <p style={{ margin: '0 0 8px', fontFamily: 'var(--font-sans)', fontSize: 11,
                      textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--accent2)',
                      fontWeight: 600 }}>
            skelet · {b.deel ? b.deel + ' · ' : ''}{b.nummer} — {b.titel}{b.stand ? ' · ' + b.stand : ''}
          </p>
          {b.kern && (
            <p style={{ margin: '0 0 10px', fontSize: 13, lineHeight: 1.6, color: 'var(--text)' }}>
              {b.kern}
            </p>
          )}
          {b.secties.map((s) => (
            <div key={s.kop} style={{ margin: '0 0 8px' }}>
              <p style={{ margin: '0 0 2px', fontSize: 12.5, fontWeight: 600 }}><Md tekst={s.kop}></Md></p>
              {s.elementen.map((e, i) => (
                <p key={i} style={{ margin: '0 0 1px', paddingLeft: 12, fontSize: 12.5,
                                    lineHeight: 1.5, color: 'var(--text2)' }}>
                  · <Md tekst={e.t}></Md>
                  <span style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: 10.5,
                                 color: VORM_KLEUR[e.v] || 'var(--text3)', marginLeft: 6 }}>
                    [{e.v}{e.h ? ' · ' + e.h : ''}]
                  </span>
                  {e.n && <span style={{ fontSize: 11, color: 'var(--text3)' }}> <Md tekst={e.n}></Md></span>}
                </p>
              ))}
            </div>
          ))}
          {b.noot && <p style={{ margin: '4px 0 0', fontSize: 11.5, fontStyle: 'italic',
                                 color: 'var(--text3)' }}><Md tekst={b.noot}></Md></p>}
        </div>
      ))}
    </div>
  );
}

/* ---- delen: Deel I tot V boven de hoofdstukken ----
   Het deel komt uit richtlijnen/skelet.md via `build.py site`. Een deelkop
   verschijnt bij het eerste hoofdstuk van dat deel, in de inhoudsopgave en
   in de tekst. Hoofdstukken zonder deel (de wachtkamer) slaan hem over. */
const EERSTE_VAN_DEEL = (() => {
  const uit = {};
  let vorig = null;
  for (const h of HOOFDSTUKKEN) {
    if (h.deel && h.deel !== vorig) { uit[h.id] = h.deel; vorig = h.deel; }
  }
  return uit;
})();

function DeelKop({ id, klein }) {
  const deel = EERSTE_VAN_DEEL[id];
  if (!deel) return null;
  const [nummer, titel] = deel.replace(/^Deel /, '').split(' — ');
  if (klein) {
    // In de inhoudsopgave draagt de deelkop alleen zijn nummer. De titel stond er
    // 23-8 nog bij, in kapitalen met letterafstand, en brak dan over twee regels;
    // daarmee woog de scheiding zwaarder dan de hoofdstukken eronder. De volle
    // titel staat boven het hoofdstuk in de tekst, waar de lezer haar leest.
    return (
      <p title={'Deel ' + nummer + ' — ' + titel}
         style={{ margin: '13px 0 5px', fontFamily: 'var(--font-sans)', fontSize: 9,
                  textTransform: 'uppercase', letterSpacing: '.12em', color: 'var(--text3)',
                  fontWeight: 700, paddingTop: 9, borderTop: '1px solid rgba(0,0,0,.07)' }}>
        Deel {nummer}
      </p>
    );
  }
  return (
    <p style={{ margin: '2.5rem 0 -0.5rem', paddingTop: '1.1rem',
                borderTop: '2px solid var(--accent2)', fontFamily: 'var(--font-sans)',
                fontSize: 12, textTransform: 'uppercase', letterSpacing: '.12em',
                color: 'var(--accent2)', fontWeight: 700 }}>
      Deel {nummer} <span style={{ color: 'var(--text2)', letterSpacing: '.06em' }}>· {titel}</span>
    </p>
  );
}

function PullQuote({ tekst }) {
  return <blockquote className="magh-pq">“{tekst}”</blockquote>;
}

/* De badges en de titel uit de samenvatting vormen de kop van het document. De tekst
   eronder is 'het verhaal' en heeft sinds 23-8 een eigen pagina, dus hier alleen de kop. */
const VISIE_KOP = (() => {
  const d = document.createElement('div');
  d.innerHTML = visieByid['samenvatting'].html;
  const meta = d.querySelector('.meta-row');
  const h1 = d.querySelector('h1');
  return { meta: meta ? meta.outerHTML : '', h1: h1 ? h1.outerHTML : '' };
})();


/* ---- de vraagbaak ----
   Het enige onderdeel van deze site dat iets kan zeggen wat wij niet hebben
   geschreven. Twee dingen houden dat in het gareel: de instructie in
   richtlijnen/vraagbaak-instructie.md, en de toetsset in toets-vraagbaak.py.
   Daaronder ligt zoeken als bodem: dat kan niets verzinnen en is altijd aan. */

function zoekTreffers(term) {
  const t = (term || '').trim().toLowerCase();
  if (t.length < 3) return [];
  const uit = [];
  for (const h of HOOFDSTUKKEN) {
    const kaal = (h.html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ');
    let i = kaal.toLowerCase().indexOf(t);
    let n = 0;
    while (i !== -1 && n < 3) {
      uit.push({ id: h.id, titel: h.titel, letter: h.letter,
                 fragment: kaal.slice(Math.max(0, i - 70), i + t.length + 90).trim() });
      i = kaal.toLowerCase().indexOf(t, i + t.length);
      n++;
    }
    if (uit.length > 24) break;
  }
  return uit;
}

/* Het model antwoordt in Markdown, dus zonder dit staan er sterretjes in de lezer.
   Dit maakt er opmaak van, en het maakt de hoofdstukverwijzingen klikbaar: een
   antwoord dat zegt waar het staat, hoort je daar ook te kunnen brengen. */
const NUMMER_NAAR_ID = (() => {
  const uit = {};
  for (const h of HOOFDSTUKKEN) {
    if (h.letter && /^\d{1,2}$/.test(h.letter)) uit[h.letter] = h.id;
  }
  return uit;
})();

/* Eén patroon dat de hele verwijzing pakt: "hoofdstuk 12" of "(1.2, 12.1)". Zo hoeft
   er niet naar de context vóór een getal gekeken te worden, en blijven gewone getallen
   als 2027, 2.300 en 200.000 met rust. Getest op vier soorten antwoord. */
const VERWIJZING = /((?:hoofdstuk|sectie)\s+\d{1,2}(?:\.\d{1,2})?|\((?:\d{1,2}(?:\.\d{1,2})?)(?:\s*,\s*\d{1,2}(?:\.\d{1,2})?)*\))/gi;

/* Naar de sectie zelf springen als die bestaat, anders naar het hoofdstuk.
   `lib_render.py` zet op elke kop die met een sectienummer begint een anker
   `s-12-1`, dus "(12.1)" brengt je bij die sectie en niet bij het hoofdstukbegin. */
function naarPlek(nummer, gaNaar) {
  const deel = String(nummer).split('.');
  if (deel.length === 2) {
    const el = document.getElementById('s-' + deel[0] + '-' + deel[1]);
    if (el) {
      window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 96, behavior: 'smooth' });
      return true;
    }
  }
  const id = NUMMER_NAAR_ID[deel[0]];
  if (id) { gaNaar(id); return true; }
  return false;
}

function Verwijzing({ tekst, gaNaar }) {
  // binnen de verwijzing elk nummer klikbaar maken, de rest laten staan
  const delen = String(tekst).split(/(\d{1,2}(?:\.\d{1,2})?)/g);
  return <>{delen.map((d, i) => {
    const m = /^(\d{1,2})(?:\.\d{1,2})?$/.exec(d);
    if (!m || !NUMMER_NAAR_ID[m[1]]) return d;
    const diep = d.includes('.');
    return <span key={i} onClick={() => naarPlek(d, gaNaar)}
      title={diep ? 'ga naar deze paragraaf' : 'ga naar dit hoofdstuk'}
      style={{ cursor: 'pointer', color: 'var(--accent2)', fontWeight: 600,
               textDecoration: 'underline', textDecorationStyle: 'dotted' }}>{d}</span>;
  })}</>;
}

function AntwoordRegel({ tekst, gaNaar }) {
  const stukken = String(tekst).split(VERWIJZING);
  return <>{stukken.map((st, k) => {
    VERWIJZING.lastIndex = 0;
    if (VERWIJZING.test(st)) {
      VERWIJZING.lastIndex = 0;
      return <Verwijzing key={k} tekst={st} gaNaar={gaNaar}></Verwijzing>;
    }
    const delen = st.split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
    return <React.Fragment key={k}>{delen.map((d, i) => {
      if (d.startsWith('**') && d.endsWith('**')) return <strong key={i}>{d.slice(2, -2)}</strong>;
      if (d.startsWith('`') && d.endsWith('`')) return <code key={i} style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: '.92em' }}>{d.slice(1, -1)}</code>;
      return d;
    })}</React.Fragment>;
  })}</>;
}

function Antwoord({ tekst, gaNaar }) {
  if (!tekst) return null;
  const blokken = String(tekst).trim().split(/\n{2,}/);
  return (
    <div style={{ margin: '14px 0 0', fontSize: 13.5, lineHeight: 1.68 }}>
      {blokken.map((b, i) => {
        const regels = b.split('\n');
        const lijst = regels.every((r) => /^\s*[-*\u2022]\s+/.test(r));
        if (lijst) {
          return (
            <ul key={i} style={{ margin: '0 0 .8rem', paddingLeft: '1.2rem' }}>
              {regels.map((r, j) => (
                <li key={j} style={{ marginBottom: '.3rem' }}>
                  <AntwoordRegel tekst={r.replace(/^\s*[-*\u2022]\s+/, '')} gaNaar={gaNaar}></AntwoordRegel>
                </li>
              ))}
            </ul>
          );
        }
        const kop = /^#{1,4}\s+/.test(b);
        if (kop) {
          return <p key={i} style={{ margin: '0 0 .5rem', fontWeight: 700 }}>
            <AntwoordRegel tekst={b.replace(/^#{1,4}\s+/, '')} gaNaar={gaNaar}></AntwoordRegel></p>;
        }
        return <p key={i} style={{ margin: '0 0 .8rem' }}>
          <AntwoordRegel tekst={b} gaNaar={gaNaar}></AntwoordRegel></p>;
      })}
    </div>
  );
}

function Vraagbaak({ gaNaar }) {
  const [open, zetOpen] = React.useState(false);
  // De startpagina laat dezelfde knop zien en opent hem hiermee, zonder dat er
  // een tweede vraagbaak naast deze komt te staan (Tim, 23-8).
  React.useEffect(() => {
    const aan = () => zetOpen(true);
    window.addEventListener('vic-vraagbaak-open', aan);
    return () => window.removeEventListener('vic-vraagbaak-open', aan);
  }, []);
  const [vraag, zetVraag] = React.useState('');
  const [bezig, zetBezig] = React.useState(false);
  const [antwoord, zetAntwoord] = React.useState('');
  const [fout, zetFout] = React.useState('');
  const treffers = React.useMemo(() => zoekTreffers(vraag), [vraag]);

  const stel = () => {
    if (!vraag.trim() || bezig) return;
    zetBezig(true); zetFout(''); zetAntwoord('');
    fetch(VRAAG_PAD, { method: 'POST', headers: { 'content-type': 'application/json' },
                        body: JSON.stringify({ vraag }) })
      .then((r) => r.json())
      .then((d) => { if (d.ok) zetAntwoord(d.antwoord); else zetFout(d.fout || 'het lukte niet'); })
      .catch(() => zetFout('geen verbinding met de vraagbaak'))
      .finally(() => zetBezig(false));
  };

  // De plaatsing zit in de CSS van index.html, niet hier: de knop staat onder de
  // inhoudsopgave en het paneel groeit naar links de marge in. Zo bedekt het de
  // leestekst niet meer (Tim, 23-8).
  if (!open) {
    return <div className="vb-knop" onClick={() => zetOpen(true)}>stel je vraag</div>;
  }
  return (
    <div className="vb-paneel">
      <div className="vb-kop">
        <p className="vb-titel">Stel je vraag</p>
        <span className="vb-sluit" onClick={() => zetOpen(false)}>&times;</span>
      </div>
      <p className="vb-uitleg">
        Stel een vraag over dit stuk. Het antwoord komt uit de tekst en zegt waar het staat.
        Staat iets er niet in, dan hoor je dat.
      </p>
      <textarea className="vb-veld" value={vraag} onChange={(e) => zetVraag(e.target.value)} rows={2}
        onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) stel(); }}
        placeholder="Waarom zouden juist deze boeren betaald worden?"></textarea>
      <div className="vb-regel">
        <span className={'vb-vraag' + (bezig ? ' bezig' : '')} onClick={stel}>
          {bezig ? 'aan het lezen…' : 'vraag'}
        </span>
        <span className="vb-hint">of Cmd+Enter</span>
      </div>
      {fout && <p className="vb-fout">{fout}</p>}
      {antwoord && <Antwoord tekst={antwoord} gaNaar={gaNaar}></Antwoord>}
      {treffers.length > 0 && (
        <div className="vb-treffers">
          <p className="vb-treffers-kop">waar die woorden staan ({treffers.length})</p>
          {treffers.slice(0, 6).map((t, i) => (
            <p key={i} className="vb-treffer" onClick={() => { gaNaar(t.id); zetOpen(false); }}>
              <strong style={{ color: 'var(--accent2)' }}>{t.letter ? t.letter + ' — ' : ''}{t.titel}</strong><br />…{t.fragment}…
            </p>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- documentvorm: één doorlopende pagina, alle hoofdstukken open ---- */

/* ---- redactiemodus: in de tekst typen en de wijzigingen opsturen ----
   Tim, 20-8. Waarom niet rechtstreeks in content/ schrijven: dan kan een
   wijziging van hem botsen met een schrijfactie van een sessie, en dan toont de
   diff in content/ niet meer exact wat er veranderde. Daarom een wachtrij:
   serveer.py schrijft naar feedback/redactie-<datum>.md, en een sessie past het
   toe. Zo maakt het niet uit wie er op dat moment bezig is.

   Twee dingen die de tekst beschermen. Zolang deze modus aan staat pauzeert het
   automatisch herladen (window.VIC_REDACTIE, opgevangen in index.html), en wat
   er getypt is staat in localStorage. Een herbouw kost dus niets. */
/* ---- de tekstlaag ----
   De gepubliceerde tekst komt uit content/*.md; de correcties die Tim in de lezer
   typt liggen daar als laag bovenop. Eén alinea is één regel in die laag: de tekst
   zoals zij gepubliceerd staat (`was`) en zoals hij haar wil (`wordt`). Leeg
   betekent: dit blok weglaten.

   Waarom de laag `was` bewaart en niet alleen de nieuwe tekst: zodra de correctie
   in content/ is bijgeschreven, staat er iets anders in de pagina dan `was`, en dan
   past de laag zichzelf niet meer toe. Zo dooft elke regel uit zodra hij in de bron
   is geland, en kunnen de twee niet stil uit elkaar lopen. Dat is precies het risico
   waarvoor `CLAUDE.md` de regel heeft dat de browser nooit in de bron schrijft.

   Wat de laag niet kan: inline opmaak binnen een alinea bewaren. Wie een alinea met
   vetgedrukte woorden of een fiche-verwijzing bewerkt, levert platte tekst op. Dat is
   dezelfde grens als in de redactiemodus zelf en zij is bewust: de laag gaat over
   woorden, de opmaak hoort in de bron. */

/* Elke alinea, lijstregel en tabelcel krijgt een sleutel: het bronbestand van het
   hoofdstuk plus zijn plaats daarin. Dus `scope#12`. Verschuift er iets in de bron,
   dan klopt de plaats niet meer — en daarom wordt er altijd óók op `was` getoetst. */
/* Wat bewerkbaar is en wat een sleutel krijgt, staat op één plek: alinea's,
   lijstregels, tabelcellen, en divs die zelf tekst dragen zonder blok-kinderen.
   Die laatste groep dekt de kaartvakken (.decision-card) én de accentblokken
   die hun tekst rechtstreeks in de div hebben staan; die waren tot 27-8 niet
   te bewerken (Tim, bij het groene WIS-blok in 11.2). */
function isTekstDiv(el) {
  if (el.tagName !== 'DIV') return false;
  if (el.querySelector('p, li, td, table, div, h1, h2, h3, h4, h5')) return false;
  return (el.innerText || '').trim().length > 0;
}
function redactieDoelen(sectie) {
  return [...sectie.querySelectorAll('[data-nt-sectie] p, [data-nt-sectie] li, [data-nt-sectie] td, [data-nt-sectie] div')]
    .filter((el) => el.tagName !== 'DIV' || isTekstDiv(el));
}
function blokkenMetSleutel() {
  const uit = [];
  for (const sectie of document.querySelectorAll('[data-hoofdstuk]')) {
    const id = sectie.getAttribute('data-hoofdstuk');
    redactieDoelen(sectie).forEach((el, i) => uit.push({ el, sleutel: id + '#' + i, hoofdstuk: id }));
  }
  return uit;
}

function pasLaagToe(blokken) {
  for (const { el, sleutel } of blokkenMetSleutel()) {
    const regel = blokken[sleutel];
    if (!regel) {
      // de regel is uit de laag gehaald: de gepubliceerde tekst hoort terug
      if (el.dataset.laagWas !== undefined) {
        el.textContent = el.dataset.laagWas;
        el.style.display = '';
        delete el.dataset.laagWas;
      }
      continue;
    }
    const gepubliceerd = el.dataset.laagWas !== undefined ? el.dataset.laagWas : el.innerText;
    if (String(gepubliceerd).trim() !== String(regel.was).trim()) continue;  // bron is bij
    el.dataset.laagWas = gepubliceerd;
    if (!String(regel.wordt).trim()) {
      el.style.display = 'none';
    } else {
      el.style.display = '';
      el.textContent = regel.wordt;
    }
  }
}

/* De laag wordt bij het openen opgehaald en daarna elke twintig seconden opnieuw.
   Dat tweede is wat "de hele wereld ziet het" waarmaakt zonder de pagina te
   herladen: wie het stuk open heeft staan, krijgt de correctie er binnen een halve
   minuut in. In redactiemodus staat de lus stil, anders overschrijft hij wat er op
   dat moment getypt wordt. */
function useTekstlaag(lijstlengte) {
  React.useEffect(() => {
    let gestopt = false;
    let versie = null;
    const haal = () => fetch(LAAG_PAD, { cache: 'no-store' })
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        if (!d || gestopt) return;
        if (d.versie !== versie) { versie = d.versie; pasLaagToe(d.blokken || {}); }
      })
      .catch(() => {});   // geen laag is geen fout: dan staat de gepubliceerde tekst er
    haal();
    const klok = setInterval(() => { if (!window.VIC_REDACTIE) haal(); }, 20000);
    return () => { gestopt = true; clearInterval(klok); };
  }, [lijstlengte]);
}

const REDACTIE_SLEUTEL = 'vic-redactie-wachtrij';

function laadWachtrij() {
  try { return JSON.parse(localStorage.getItem(REDACTIE_SLEUTEL)) || []; } catch (e) { return []; }
}
function bewaarWachtrij(rij) {
  try { localStorage.setItem(REDACTIE_SLEUTEL, JSON.stringify(rij)); } catch (e) {}
}

function RedactieBalk({ aan, zet, rij, zetRij }) {
  const [bezig, setBezig] = React.useState(false);
  const [melding, setMelding] = React.useState('');
  if (!REDACTIE_BESCHIKBAAR) return null;

  const versturen = () => {
    if (!rij.length || bezig) return;
    setBezig(true);
    fetch(LAAG_PAD, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'x-redactie-sleutel': redactieSleutel() },
      body: JSON.stringify({ wijzigingen: rij }),
    })
      .then((r) => r.json())
      .then((a) => {
        if (!a.ok) throw new Error(a.fout || 'onbekende fout');
        setMelding((a.bewaard || rij.length) + ' bewaard — iedereen leest dit nu zo');
        zetRij([]); bewaarWachtrij([]);
      })
      .catch((e) => setMelding('niet gelukt: ' + e.message))
      .then(() => setBezig(false));
  };

  const knop = (actief) => ({
    fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
    border: '1px solid ' + (actief ? 'var(--accent)' : 'rgba(0,0,0,.18)'),
    background: actief ? 'var(--accent-light)' : '#fff',
    color: actief ? 'var(--accent2)' : 'var(--text2)',
    borderRadius: 5, padding: '5px 12px',
  });

  return (
    <div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 60,
                  background: 'var(--bg)', borderTop: aan ? '2px solid var(--accent)' : '1px solid rgba(0,0,0,.10)',
                  boxShadow: '0 -2px 12px rgba(0,0,0,.06)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
                    maxWidth: 1020, margin: '0 auto', padding: '8px 20px' }}>
      <button style={knop(aan)} onClick={() => { zet(!aan); setMelding(''); }}>
        {aan ? '\u25be redactiemodus staat aan' : '\u270e redactiemodus'}
      </button>
      {aan && (
        <>
          {/* Bewaren staat links naast de redactiemodusknop en niet rechts: daar
              hangt de vaste vraagbaakknop overheen, en die bedekte hem (Tim, 26-8).
              De uitleg is naar de rechterkant verhuisd, want die hoeft niet
              klikbaar te zijn. */}
          <span style={{ fontSize: 12, fontWeight: 600, color: rij.length ? 'var(--accent2)' : 'var(--text3)' }}>
            {rij.length} gewijzigd
          </span>
          <button style={knop(!!rij.length)} onClick={versturen} disabled={!rij.length || bezig}>
            {bezig ? 'bewaren\u2026' : 'bewaren'}
          </button>
          {rij.length > 0 && (
            <button style={{ ...knop(false), fontWeight: 400 }}
              onClick={() => { if (confirm('Alle ' + rij.length + ' wijzigingen weggooien?')) { zetRij([]); bewaarWachtrij([]); location.reload(); } }}>
              weggooien
            </button>
          )}
          <span style={{ fontSize: 12, color: 'var(--text3)', flex: 1, minWidth: 180, paddingRight: 190 }}>
            klik in een alinea en typ; leegmaken haalt hem weg. Bewaren zet het voor
            iedereen online.
          </span>
        </>
      )}
      {melding && <span style={{ fontSize: 12, color: 'var(--text2)' }}>{melding}</span>}
      </div>
    </div>
  );
}

/* Maakt de alinea's van één hoofdstuk bewerkbaar en houdt de wijzigingen bij.
   Alleen blokken op het hoogste niveau van de sectie: wat in een fiche of een
   partial zit is niet een-op-een naar de bron te herleiden, en dat wordt als
   voorstel met context geregistreerd in plaats van als exacte vervanging. */
function useRedactie(aan, rij, zetRij) {
  React.useEffect(() => {
    window.VIC_REDACTIE = aan;
    // Dezelfde selectie als blokkenMetSleutel(), uit dezelfde functie — anders
    // wijzen de sleutels naar de verkeerde blokken. Sinds 27-8 doen ook divs
    // mee die zelf tekst dragen (kaartvakken, accentblokken zonder p erin).
    const blokken = [...document.querySelectorAll('[data-hoofdstuk]')].flatMap((s) => redactieDoelen(s));
    if (!aan) { blokken.forEach((b) => { b.removeAttribute('contentEditable'); b.style.outline = ''; }); return; }
    const opslag = new Map();
    // de sleutel per element, één keer berekend: tijdens het typen mag de plaats
    // niet opnieuw geteld worden, want een verborgen blok verschuift de telling
    const sleutels = new Map(blokkenMetSleutel().map((b) => [b.el, b]));
    const bij = (e) => {
      const el = e.target.closest('p, li, td, div');
      if (!el) return;
      const bij_start = opslag.get(el);
      const wordt = el.innerText.trim();
      if (bij_start === undefined || wordt === bij_start) return;
      const merk = sleutels.get(el);
      if (!merk) return;
      // `was` is de tekst zoals zij gepubliceerd staat, ook als de laag hier al
      // eerder iets over heen heeft gelegd. Anders wijst de regel na een tweede
      // correctie naar een tekst die in de bron nooit heeft bestaan.
      const was = el.dataset.laagWas !== undefined ? el.dataset.laagWas : bij_start;
      const exact = !el.closest('.fiche-popup') && !el.closest('table') && el.tagName === 'P';
      zetRij((oud) => {
        const zonder = oud.filter((w) => w.sleutel !== merk.sleutel);
        const nieuw = [...zonder, { sleutel: merk.sleutel, hoofdstuk: merk.hoofdstuk, was, wordt, exact }];
        bewaarWachtrij(nieuw);
        return nieuw;
      });
    };
    blokken.forEach((b) => {
      opslag.set(b, b.innerText.trim());
      b.setAttribute('contentEditable', 'true');
      b.style.outline = '1px dashed rgba(62,166,53,.35)';
      b.style.outlineOffset = '3px';
      b.addEventListener('blur', bij);
    });
    return () => blokken.forEach((b) => {
      b.removeAttribute('contentEditable'); b.style.outline = '';
      b.removeEventListener('blur', bij);
    });
  }, [aan, zetRij]);
}

function DocumentLezer({ kort }) {
  const LIJST = kort ? HOOFDSTUKKEN_KORT : HOOFDSTUKKEN;
  /* Het inklappen is er op 23-8 uit (Tim): je leest het of je leest het niet, en
     de korte versie staat bovenaan. Ook de opgeslagen inklapstand wordt opgeruimd,
     anders houdt een oude browser hoofdstukken dicht die niet meer dicht kunnen. */
  React.useEffect(() => { try { localStorage.removeItem('vic-v4-doc-open'); } catch (e) {} }, []);

  // sticky inhoudsopgave: welk hoofdstuk is in beeld?
  const [actief, setActief] = React.useState(LIJST[0] && LIJST[0].id);
  const [redactie, setRedactie] = React.useState(false);
  const [rij, zetRij] = React.useState(laadWachtrij);
  useRedactie(redactie, rij, zetRij);
  useTekstlaag(LIJST.length);
  // de balk staat vast onderaan: ruimte eronder, anders valt de laatste regel erachter
  React.useEffect(() => {
    if (!REDACTIE_BESCHIKBAAR) return;
    document.body.style.paddingBottom = '58px';
    return () => { document.body.style.paddingBottom = ''; };
  }, []);
  React.useEffect(() => {
    const ids = LIJST.map((h) => h.id);
    const onScroll = () => {
      let cur = ids[0];
      for (const id of ids) {
        const el = document.getElementById('doc-' + id);
        if (el && el.getBoundingClientRect().top <= 130) cur = id;
      }
      setActief(cur);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  // klik in inhoudsopgave: hoofdstuk openen en er soepel heen scrollen
  const gaNaar = (id) => {
    // Alle hoofdstukken staan open sinds 23-8, dus er hoeft niets uitgeklapt te
    // worden; hier stond nog een aanroep van de verdwenen inklapstand, en die liet
    // het scrollen stilzwijgend mislukken.
    const el = document.getElementById('doc-' + id);
    if (el) {
      window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 96, behavior: 'smooth' });
    }
  };

  return (
    <div data-screen-label="Visie document (doorlopend)">
      <PaginaBalk naam={kort ? 'ingekorte versie' : 'volledige versie'}
        sub={kort ? 'alle zestien hoofdstukken, zonder de bronnen en de naslag'
                  : 'het naslagwerk: met de cijfers, de bronnen en de afbakeningen'}></PaginaBalk>
      <div className="v4-doc-layout">
        <div className="v4-zijkolom">
          <nav className="v4-toc" aria-label="Inhoudsopgave">
            <p className="v4-toc-kop">inhoud</p>
            {LIJST.map((h) => (
              <React.Fragment key={h.id}>
                <DeelKop id={h.id} klein={true}></DeelKop>
                <a className={'v4-toc-link' + (actief === h.id ? ' actief' : '')} onClick={() => gaNaar(h.id)}>
                  <span className="v4-toc-letter">{h.letter || '·'}</span>
                  <span className="v4-toc-titel">{h.titel}</span>
                </a>
              </React.Fragment>
            ))}
          </nav>
        </div>
        <div className="v4-doc-body">
      <article className="v4-doc" style={{ marginBottom: 6 }}>
        {/* De drie badges (Visiedocument · Augustus 2026 · Veenweiden Innovatiecentrum)
            staan hier niet meer: de balk erboven zegt hetzelfde en zegt het preciezer
            (Tim, 24-8). Op de startpagina staan zij wel, want daar is geen balk. */}
        <Html html={VISIE_KOP.h1}></Html>
      </article>
      <article className="v4-doc">
        <RedactieBalk aan={redactie} zet={setRedactie} rij={rij} zetRij={zetRij}></RedactieBalk>
        <Vraagbaak gaNaar={gaNaar}></Vraagbaak>
        {LIJST.map((h) => (
          <section key={h.id} id={'doc-' + h.id} data-hoofdstuk={h.id}>
            <DeelKop id={h.id}></DeelKop>
            <h2><span>{h.letter ? h.letter + ' \u2014 ' : ''}{h.titel}</span></h2>
            <Werkstand h={h}></Werkstand>
            <Skelet h={h}></Skelet>
            <div className="v4-doc-inline">
              <Html html={h.html}></Html>
            </div>
          </section>
        ))}
      </article>        </div>
      </div>
    </div>
  );
}

/* ---- laag 1 ---- */

function VisieLaag1({ openHoofdstuk, kort }) {
  return <DocumentLezer kort={kort}></DocumentLezer>;
}

Object.assign(window, { VISIE, HOOFDSTUKKEN, VisieLaag1, visieByid, Vraagbaak });
