// Aanmeldformulieren (/meedoen). Twee ingangen:
//   tab "jongeren"  — Meld je aan / Meld mijn kind aan
//   tab "beheerder" — Word groepsbeheerder
// Versturen gaat via forms-config.js naar Google Sheets (Apps Script of Google Formulier).

/* ---------------- validatie ---------------- */
const bbIsEmail = (v) => /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(String(v).trim());
const bbIsVoornaam = (v) => {
  const s = String(v).trim();
  if (/[.]/.test(s)) return false;
  if (!/^[a-z\u00C0-\u024F]+(?:[ '\u2019-][a-z\u00C0-\u024F]+)*$/i.test(s)) return false;
  return s.replace(/[^a-z\u00C0-\u024F]/gi, "").length >= 3;
};
const BB_VOORNAAM_FOUT = "Vul je volledige voornaam in, dus geen voorletters of afkortingen.";
const BB_VOORNAAM_KIND_FOUT = "Vul de volledige voornaam van je kind in, dus geen voorletters of afkortingen.";
// Nederlands telefoonnummer: 10 cijfers beginnend met 0 (06 voor mobiel, of een
// netnummer), ook geschreven met +31, 0031, spaties, streepjes, punten of haakjes.
const bbIsPhone = (v) => {
  let s = String(v).trim().replace(/[\s\-().]/g, "");
  if (s.startsWith("+31")) s = "0" + s.slice(3);
  else if (s.startsWith("0031")) s = "0" + s.slice(4);
  return /^0[1-9][0-9]{8}$/.test(s);
};
const bbIsPostcode = (v) => /^[1-9][0-9]{3}\s?[a-z]{2}$/i.test(String(v).trim());
/* Werkgebied van Buiten Buddies: Haarlem en directe omgeving. Alleen de vier
   cijfers van de postcode tellen mee, de letters doen niet ter zake. */
const BB_WERKGEBIED_PC = [
  [2011, 2037], // Haarlem, alle wijken
  [2051, 2051], // Overveen
  [2061, 2061], // Bloemendaal
  [2063, 2063], // Spaarndam, gemeente Haarlem
  [2071, 2071], // Santpoort-Noord
  [2082, 2082], // Santpoort-Zuid
  [2100, 2106], // Heemstede
  [2111, 2111], // Aerdenhout
];
const bbInWerkgebied = (v) => {
  const n = parseInt(String(v).trim().slice(0, 4), 10);
  return BB_WERKGEBIED_PC.some(([van, tot]) => n >= van && n <= tot);
};
/* Een geldige postcode buiten het werkgebied. Zolang de bezoeker de nieuwsbrief
   niet aanvinkt, blokkeert dit het verzenden. */
const bbBuitenWerkgebied = (v) => !!String(v || "").trim() && bbIsPostcode(v) && !bbInWerkgebied(v);
const BB_BUITEN_WERKGEBIED = "Nog even geduld, we komen eraan! We zijn helaas nog niet actief in jouw buurt. Meld je hieronder aan voor de nieuwsbrief (zet het vinkje) en blijf op de hoogte van onze uitbreidingsplannen! Je kunt dan ook de postcode van jouw buurt laten staan en het formulier alsnog versturen.";
const BB_BUITEN_WERKGEBIED_OK = "We zijn nog niet actief in jouw buurt, dus we kunnen de aanmelding zelf nog niet verwerken. We vragen daarom alleen je eigen naam en e‑mailadres. We noteren je postcode als nieuw interessegebied en laten het weten zodra we in jouw buurt starten.";
const BB_BUITEN_WERKGEBIED_OK_BEHEERDER = "We zijn nog niet actief in jouw buurt, dus we kunnen je aanmelding zelf nog niet verwerken. We vragen daarom alleen je naam, je e‑mailadres en de keuze 'Ouder of andere vrijwilliger'. We noteren je postcode als nieuw interessegebied en laten het weten zodra we in jouw buurt starten.";

function bbValidateJongeren(v) {
  const e = {};
  /* Buiten het werkgebied met een vinkje voor de nieuwsbrief: dan slaan we alleen
     de gegevens van de ouder op, dus alleen die velden worden gecontroleerd. */
  const alleenOuder = bbBuitenWerkgebied(v.postcode) && !!v.updates;
  const ov = v.ouderVoornaam.trim();
  if (!ov) e.ouderVoornaam = "Vul je voornaam in.";
  else if (!bbIsVoornaam(ov)) e.ouderVoornaam = BB_VOORNAAM_FOUT;
  if (!v.ouderAchternaam.trim()) e.ouderAchternaam = "Vul je achternaam in.";
  if (!alleenOuder) {
    const kv = v.kindVoornaam.trim();
    if (!kv) e.kindVoornaam = "Vul de voornaam van je kind in.";
    else if (!bbIsVoornaam(kv)) e.kindVoornaam = BB_VOORNAAM_KIND_FOUT;
    if (!v.kindAchternaam.trim()) e.kindAchternaam = "Vul de achternaam van je kind in.";
    if (!String(v.kindLeeftijd).trim()) e.kindLeeftijd = "Vul de leeftijd van je kind in.";
    else {
      const nl = parseInt(String(v.kindLeeftijd).replace(/[^0-9]/g, ""), 10);
      if (!isNaN(nl) && nl > BB_LEEFTIJD_MAX) e.kindLeeftijd = BB_LEEFTIJD_MAX_FOUT;
    }
  }
  if (!v.postcode.trim()) e.postcode = "Vul je postcode in, zo weten we in welke buurt je woont.";
  else if (!bbIsPostcode(v.postcode)) e.postcode = "Vul een geldige postcode in, bijvoorbeeld 2012 AB.";
  else if (!bbInWerkgebied(v.postcode) && !v.updates) e.postcode = BB_BUITEN_WERKGEBIED;
  if (!alleenOuder) {
    if (!v.kindWhatsapp) e.kindWhatsapp = "Geef aan of je kind eigen WhatsApp heeft.";
    if (v.kindWhatsapp === "Ja") {
      if (!v.kindTelefoon.trim()) e.kindTelefoon = "Vul het telefoonnummer van je kind in.";
      else if (!bbIsPhone(v.kindTelefoon)) e.kindTelefoon = "Vul een geldig Nederlands telefoonnummer in van 10 cijfers, bijvoorbeeld 06-12345678.";
    }
  }
  if (!alleenOuder) {
    if (!v.ouderTelefoon.trim()) e.ouderTelefoon = "Vul je eigen telefoonnummer in.";
    else if (!bbIsPhone(v.ouderTelefoon)) e.ouderTelefoon = "Vul een geldig Nederlands telefoonnummer in van 10 cijfers, bijvoorbeeld 06-12345678.";
  } else if (v.ouderTelefoon.trim() && !bbIsPhone(v.ouderTelefoon)) {
    e.ouderTelefoon = "Vul een geldig Nederlands telefoonnummer in van 10 cijfers, bijvoorbeeld 06-12345678.";
  }
  if (!v.email.trim()) e.email = "Vul je e‑mailadres in.";
  else if (!bbIsEmail(v.email)) e.email = "Vul een geldig e‑mailadres in, bijvoorbeeld naam@voorbeeld.nl.";
  if (!alleenOuder && !v.toestemming) e.toestemming = "Zonder toestemming kunnen we je kind niet aanmelden.";
  return e;
}

function bbValidateBeheerder(v) {
  const e = {};
  const alleenOuder = bbBuitenWerkgebied(v.postcode) && !!v.updates;
  const bv = v.voornaam.trim();
  if (!bv) e.voornaam = "Vul je voornaam in.";
  else if (!bbIsVoornaam(bv)) e.voornaam = BB_VOORNAAM_FOUT;
  if (!v.achternaam.trim()) e.achternaam = "Vul je achternaam in.";
  if (!v.postcode.trim()) e.postcode = "Vul je postcode in, zo weten we om welke buurt het gaat.";
  else if (!bbIsPostcode(v.postcode)) e.postcode = "Vul een geldige postcode in, bijvoorbeeld 2012 AB.";
  else if (!bbInWerkgebied(v.postcode) && !v.updates) e.postcode = BB_BUITEN_WERKGEBIED;
  if (!v.rol) e.rol = "Geef aan wat op jou van toepassing is.";
  if (!alleenOuder) {
    if (!v.telefoon.trim()) e.telefoon = "Vul je telefoonnummer in.";
    else if (!bbIsPhone(v.telefoon)) e.telefoon = "Vul een geldig Nederlands telefoonnummer in van 10 cijfers, bijvoorbeeld 06-12345678.";
  } else if (v.telefoon.trim() && !bbIsPhone(v.telefoon)) {
    e.telefoon = "Vul een geldig Nederlands telefoonnummer in van 10 cijfers, bijvoorbeeld 06-12345678.";
  }
  if (!v.email.trim()) e.email = "Vul je e‑mailadres in.";
  else if (!bbIsEmail(v.email)) e.email = "Vul een geldig e‑mailadres in, bijvoorbeeld naam@voorbeeld.nl.";
  return e;
}

/* ---------------- versturen ---------------- */
/* Kenmerk per inzending, om een eventuele dubbele regel te kunnen herkennen. */
function bbInzendingId() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}

async function bbSubmitForm(formKey, values) {
  const cfg = window.BB_FORM_CONFIG || { mode: "demo" };
  const payload = Object.assign({}, values, {
    formulier: formKey,
    verstuurd: new Date().toISOString(),
    pagina: location.href,
    // Uniek nummer per inzending. Het Sheet gebruikt dit niet, maar het staat wel in
    // het verzoek, zodat een dubbele regel altijd te herkennen is aan een gelijk
    // nummer en aan de tijd, mocht dat ooit nog gebeuren.
    inzendingId: bbInzendingId(),
  });

  const klaar = await bbNaarSheets(cfg, payload);
  // De melding per e‑mail gaat na de opslag in Google Sheets en mag nooit een
  // geslaagde inzending laten mislukken.
  await bbMeldPerMail(cfg, formKey, values);
  await bbNieuwsbriefUitContact(cfg, formKey, values);
  if (formKey === "jongeren") await bbBeheerderUitAanmelding(cfg, values);
  return klaar;
}

/* Vinkt iemand op het contactformulier ook het hokje voor updates aan, dan komt
   die inschrijving hiermee als aparte regel in het tabblad Nieuwsbrief terecht.
   Alleen naar Google Sheets, dus zonder tweede e‑mail: het contactbericht meldt
   de inschrijving al op de regel 'Nieuwsbrief'. Staat het adres al in het tabblad,
   dan vult het script alleen 'Aanmelding via' aan en komt er geen dubbele regel. */
async function bbNieuwsbriefUitContact(cfg, formKey, values) {
  if (formKey !== "contact") return false;
  const aan = values.updates === true || values.updates === "true" || values.updates === "ja";
  if (!aan || !values.email) return false;
  try {
    await bbNaarSheets(cfg, {
      formulier: "nieuwsbrief",
      voornaam: values.voornaam || "",
      tussenvoegsel: values.tussenvoegsel || "",
      achternaam: values.achternaam || "",
      email: values.email,
      optIn: true,
      bron: "contactformulier",
      verstuurd: new Date().toISOString(),
      pagina: location.href,
    });
    return true;
  } catch (err) {
    return false;
  }
}

/* Woont iemand buiten het werkgebied en vinkt hij de nieuwsbrief aan, dan mag hij
   wel verzenden. We slaan dan uitsluitend de gegevens van de ouder op: een regel in
   het tabblad 'Nieuwe interesse gebieden' met de postcode en het soort aanmelding,
   en een regel in het tabblad Nieuwsbrief. Beide regels worden door één verzoek
   weggeschreven: twee verzoeken vlak na elkaar botsten op het slot dat het script
   op het Sheet zet, waardoor de tweede regel soms verloren ging. Gegevens van het
   kind gaan nooit mee. */
async function bbSubmitAlleenNieuwsbrief(formKey, values) {
  const cfg = window.BB_FORM_CONFIG || { mode: "demo" };
  const soort = formKey === "beheerder" ? "Groepsbeheerder aanmelding" : "Kind aanmelding";
  const gedeeld = {
    voornaam: values.ouderVoornaam || values.voornaam || "",
    tussenvoegsel: values.ouderTussenvoegsel || values.tussenvoegsel || "",
    achternaam: values.ouderAchternaam || values.achternaam || "",
    email: values.email || "",
  };
  const postcode = String(values.postcode || "").trim();
  const telefoon = values.ouderTelefoon || values.telefoon || "";
  const rol = formKey === "beheerder" ? (values.rol || "") : "";

  const klaar = await bbNaarSheets(cfg, Object.assign({}, gedeeld, {
    formulier: "interessegebied",
    postcode: postcode,
    soort: soort,
    rol: rol,
    telefoon: telefoon,
    // Het script schrijft hiermee ook meteen de regel in het tabblad Nieuwsbrief.
    optIn: true,
    bron: "nieuw interessegebied, postcode " + postcode,
    verstuurd: new Date().toISOString(),
    pagina: location.href,
  }));

  await bbMeldPerMail(cfg, "interessegebied", Object.assign({}, gedeeld, {
    postcode: postcode,
    soort: soort,
    rol: rol,
    telefoon: telefoon,
  }));

  return klaar;
}

/* Vinkt een ouder op het aanmeldformulier aan dat hij wil meedenken over een
   buurtapp, dan komt die persoon ook als aanmelding op het tabblad Groepsbeheerders
   te staan, los van de andere vinkjes. De melding per e‑mail gaat niet dubbel: de
   mail over de aanmelding van het kind vermeldt dit al op een eigen regel. */
async function bbBeheerderUitAanmelding(cfg, values) {
  if (!values.beheerderInteresse) return false;
  try {
    await bbNaarSheets(cfg, {
      formulier: "beheerder",
      voornaam: values.ouderVoornaam || "",
      tussenvoegsel: values.ouderTussenvoegsel || "",
      achternaam: values.ouderAchternaam || "",
      postcode: values.postcode || "",
      rol: "Ouder, via aanmelding kind",
      telefoon: values.ouderTelefoon || "",
      email: values.email || "",
      updates: !!values.updates,
      verstuurd: new Date().toISOString(),
      pagina: location.href,
    });
    return true;
  } catch (err) {
    return false;
  }
}

/* Melding per e‑mail via de eigen serverfunctie op Vercel. De serverfunctie bepaalt
   zelf wie de mail krijgt en welke velden erin mogen: de website stuurt alleen de
   ingevulde waarden mee. Lukt het niet, dan merkt de bezoeker daar niets van, want
   de inzending staat al in Google Sheets. */
async function bbMeldPerMail(cfg, formKey, values) {
  const url = (cfg && cfg.mailEndpoint) || "";
  if (!url) return false;
  if (location.protocol !== "http:" && location.protocol !== "https:") return false;
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ formulier: formKey, waarden: values }),
    });
    return res.ok;
  } catch (err) {
    return false;
  }
}

async function bbNaarSheets(cfg, payload) {
  if (cfg.mode === "appsscript" && cfg.appsScriptUrl) {
    const body = new FormData();
    Object.keys(payload).forEach((k) => body.append(k, payload[k]));
    /* Bewust met mode: no-cors, en dus zonder de respons te lezen.

       Waarom: een webapp van Apps Script antwoordt met een omleiding naar
       googleusercontent.com, en die omleiding stuurt geen CORS-kopregels mee. Een
       gewone fetch loopt daarop stuk met een foutmelding, terwijl het verzoek op de
       server al helemaal is verwerkt. Vingen wij die fout op en stuurden wij het
       verzoek opnieuw, dan kwam elke inzending twee keer in het Sheet: twee regels in
       het Logboek, en bij een aanmelding een tweede optelling bij 'Aantal kinderen'.

       Met no-cors gebeurt dat niet meer: er gaat precies één verzoek uit. De browser
       geeft ons geen inzicht in het antwoord, dus wij gaan uit van een geslaagde
       opslag. Dat kan hier, want de melding per e-mail loopt via onze eigen server en
       die meldt een storing wel. Gaat het verzoek helemaal niet weg, bijvoorbeeld
       zonder internet, dan gooit fetch alsnog een fout en proberen wij het één keer
       opnieuw. Dat is dan veilig, want het eerste verzoek is de deur niet uit. */
    try {
      await fetch(cfg.appsScriptUrl, { method: "POST", mode: "no-cors", body });
    } catch (err) {
      await fetch(cfg.appsScriptUrl, { method: "POST", mode: "no-cors", body });
    }
    return true;
  }

  if (cfg.mode === "googleform") {
    const gf = (cfg.googleForm || {})[payload.formulier] || {};
    if (!gf.formId) throw new Error("Geen Google Formulier ingesteld");
    const body = new FormData();
    Object.keys(gf.fields || {}).forEach((k) => {
      const entry = gf.fields[k];
      if (entry && payload[k] !== undefined && payload[k] !== "") body.append(entry, payload[k]);
    });
    await fetch("https://docs.google.com/forms/d/e/" + gf.formId + "/formResponse", {
      method: "POST", mode: "no-cors", body,
    });
    return true;
  }

  // demo
  await new Promise((r) => setTimeout(r, 700));
  console.log("[Buiten Buddies] demo-modus, niet verstuurd:", payload);
  return true;
}

/* ---------------- bouwstenen ---------------- */
/* De melding over het werkgebied begint met een opgewekte zin. Die staat vet,
   zodat meteen duidelijk is dat dit geen afwijzing is. */
function FoutTekst({ tekst }) {
  if (tekst === BB_BUITEN_WERKGEBIED) {
    const eind = tekst.indexOf("!") + 1;
    return <><strong>{tekst.slice(0, eind)}</strong>{tekst.slice(eind)}</>;
  }
  return tekst;
}

function Field({ id, label, hint, error, required, hideOpt, children }) {
  return (
    <div className={"bb-field" + (error ? " has-error" : "")}>
      <label htmlFor={id}>
        {label}
        {required ? <span className="bb-field__req" aria-hidden="true"> *</span> : (hideOpt ? null : <span className="bb-field__opt"> (optioneel)</span>)}
      </label>
      {children}
      {hint && <p className="bb-field__hint" id={id + "-hint"}>{hint}</p>}
      {error && (
        <p className="bb-field__error" id={id + "-error"}>
          <Icon name="close" size={14} /> <FoutTekst tekst={error} />
        </p>
      )}
    </div>
  );
}

/* De melding over het werkgebied is te lang voor een regel onder het invoerveld.
   Die verschijnt daarom als venster naast de postcode, over het formulier heen.
   Op desktop laat een klik ernaast, of Escape, de melding meteen verdwijnen. Op
   een aanraakscherm niet: daar tikt iemand snel per ongeluk naast het venster,
   en dan is de tekst weg voordat hij gelezen is. Op mobiel sluit alleen het
   kruisje, en Escape voor wie een toetsenbord heeft aangesloten. */
function PostcodeMelding({ id, onClose }) {
  const ref = useRef(null);
  useEffect(() => {
    const opAanraken = window.matchMedia("(hover: none), (max-width: 620px)").matches;
    const buiten = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const toets = (e) => { if (e.key === "Escape") onClose(); };
    if (!opAanraken) document.addEventListener("pointerdown", buiten, true);
    document.addEventListener("keydown", toets);
    return () => {
      document.removeEventListener("pointerdown", buiten, true);
      document.removeEventListener("keydown", toets);
    };
  }, [onClose]);
  return (
    <div className="bb-pcpop" id={id} ref={ref} role="alert">
      <span className="bb-pcpop__pijl" aria-hidden="true"></span>
      <button type="button" className="bb-pcpop__sluit" onClick={onClose} aria-label="Melding sluiten">
        <Icon name="close" size={16} />
      </button>
      <p className="bb-pcpop__kop">Nog even geduld, we komen eraan!</p>
      <p>We zijn helaas nog niet actief in jouw buurt.</p>
      <p>
        Meld je hieronder eerst aan voor de nieuwsbrief (zet het vinkje) en blijf op de hoogte van onze
        uitbreidingsplannen! Je kunt daarna ook de huidige postcode van jouw buurt laten staan, zonder
        foutmelding, en het formulier alsnog versturen.
      </p>
    </div>
  );
}

const BB_LEEFTIJD_MAX = 16;
const BB_LEEFTIJD_MAX_FOUT = "Leuk dat je meedoet, maar onze buurtgroepen zijn voor 10 tot 16 jaar.";

function TextInput({ id, value, onChange, onBlur, error, hint, type = "text", ...rest }) {
  return (
    <input
      id={id} name={id} type={type} value={value}
      onChange={(e) => onChange(e.target.value)} onBlur={onBlur}
      className={"bb-input" + (error ? " has-error" : "")}
      aria-invalid={error ? "true" : undefined}
      aria-describedby={[hint ? id + "-hint" : null, error ? id + "-error" : null].filter(Boolean).join(" ") || undefined}
      {...rest}
    />
  );
}

function RadioGroup({ id, options, value, onChange, error, label, hint, required }) {
  return (
    <fieldset className={"bb-fieldset" + (error ? " has-error" : "")}>
      <legend>
        {label}
        {required && <span className="bb-field__req" aria-hidden="true"> *</span>}
      </legend>
      <div
        className="bb-radio-row"
        role="radiogroup"
        aria-invalid={error ? "true" : undefined}
        aria-describedby={[hint ? id + "-hint" : null, error ? id + "-error" : null].filter(Boolean).join(" ") || undefined}
      >
        {options.map((o) => (
          <label key={o} className={"bb-radio" + (value === o ? " is-on" : "")}>
            <input
              type="radio" name={id} value={o} checked={value === o}
              onChange={() => onChange(o)}
            />
            <span>{o}</span>
          </label>
        ))}
      </div>
      {error && (
        <p className="bb-field__error" id={id + "-error"}>
          <Icon name="close" size={14} /> {error}
        </p>
      )}
    </fieldset>
  );
}

function CheckRow({ id, checked, onChange, error, hint, children }) {
  return (
    <div className={"bb-check" + (error ? " has-error" : "")}>
      <label htmlFor={id}>
        <input
          id={id} name={id} type="checkbox" checked={checked}
          onChange={(e) => onChange(e.target.checked)}
          aria-invalid={error ? "true" : undefined}
          aria-describedby={[hint ? id + "-hint" : null, error ? id + "-error" : null].filter(Boolean).join(" ") || undefined}
        />
        <span>{children}</span>
      </label>
      {hint && <p className="bb-check__hint" id={id + "-hint"}>{hint}</p>}
      {error && (
        <p className="bb-field__error" id={id + "-error"}>
          <Icon name="close" size={14} /> {error}
        </p>
      )}
    </div>
  );
}

/* Bij een fout springt de pagina naar de samenvatting bovenaan het formulier. Gaat
   het alleen om de postcodemelding, dan blijft de bezoeker staan waar hij was: die
   melding staat als venster naast het invoerveld al in beeld. */
function bbFocusSamenvatting(errs, ref) {
  const alleenPostcode = Object.keys(errs).every((k) => errs[k] === BB_BUITEN_WERKGEBIED);
  if (alleenPostcode) return;
  requestAnimationFrame(() => {
    if (ref.current) ref.current.focus();
  });
}

function ErrorSummary({ errors, summaryRef, order }) {
  /* De melding over het werkgebied verschijnt uitsluitend als venster naast het
     postcodeveld, nooit in de rode samenvatting bovenaan het formulier. */
  const alles = order.filter((k) => errors[k] && errors[k] !== BB_BUITEN_WERKGEBIED);
  if (!alles.length) return null;
  const tekstVan = (k) => errors[k];
  return (
    <div className="bb-form__summary" role="alert" tabIndex={-1} ref={summaryRef}>
      <strong>Er ontbreekt nog iets. Controleer {alles.length === 1 ? "dit veld" : "deze velden"}:</strong>
      <ul>
        {alles.map((k) => (
          <li key={k}>
            <a
              href={window.bbHref(k)}
              onClick={(e) => {
                e.preventDefault();
                const el = document.getElementById(k) || document.querySelector('[name="' + k + '"]');
                if (el) el.focus({ preventScroll: false });
              }}
            >
              {tekstVan(k)}
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}

function SendError({ onRetry }) {
  const mail = (window.BB_FORM_CONFIG && window.BB_FORM_CONFIG.fallbackEmail) || "info@buitenbuddies.org";
  return (
    <div className="bb-form__summary bb-form__summary--send" role="alert">
      <strong>Het versturen is niet gelukt.</strong>
      <p>
        Probeer het nog een keer, of stuur je aanmelding per e‑mail naar{" "}
        <a href={"mailto:" + mail}>{mail}</a>. Dan pakken we het handmatig op.
      </p>
      <Button variant="secondary" onClick={onRetry}>Opnieuw versturen</Button>
    </div>
  );
}

/* ---------------- confetti-regen bij een gelukte aanmelding ---------------- */
/* speed maakt de val trager of sneller, herhaal laat vallen deeltjes opnieuw
   bovenaan beginnen. Uit staat dat voor een korte eenmalige wolk. */
function ConfettiRain({ duration = 7000, count = 150, speed = 1, herhaal = true, start: startPos = "boven" }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches
        || document.documentElement.classList.contains("bb-rm-force")) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    // Staat het canvas in een eigen kader (zoals in het doneervenster), dan rekenen
    // we met dat kader. Bij het schermvullende canvas is dit gelijk aan het venster.
    const kader = canvas.getBoundingClientRect();
    const w = Math.round(kader.width) || window.innerWidth;
    const h = Math.round(kader.height) || window.innerHeight;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);

    const palette = ["#94B143", "#8DBB4E", "#6FB561", "#AFC600", "#135637", "#FDFDF5", "#FFFFFF"];
    const accent = "#FAD728";
    const rand = (a, b) => a + Math.random() * (b - a);
    const parts = [];
    for (let i = 0; i < count; i++) {
      const accentPart = Math.random() < 0.14;
      parts.push({
        x: rand(-20, w + 20),
        // 'direct' zet alle deeltjes vlak boven de rand, zodat ze meteen in beeld
        // vallen in plaats van eerst een lange aanloop van bovenaf te maken.
        y: startPos === "direct" ? rand(-h * 0.35, -10) : rand(-h, -20),
        vy: rand(1.7, 4.4) * speed,
        sway: rand(0.6, 2.1),
        swaySpeed: rand(0.008, 0.022),
        phase: rand(0, Math.PI * 2),
        size: rand(5, 11),
        color: accentPart ? accent : palette[Math.floor(Math.random() * palette.length)],
        shape: Math.random() < 0.5 ? "rect" : Math.random() < 0.5 ? "circle" : "ribbon",
        rotation: rand(0, Math.PI * 2),
        spin: rand(-0.13, 0.13),
        tilt: rand(0, Math.PI * 2),
        tiltSpeed: rand(0.03, 0.09),
      });
    }

    const start = performance.now();
    const FADE = duration - 2200;
    let raf;
    const frame = (now) => {
      const t = now - start;
      ctx.clearRect(0, 0, w, h);
      let alpha = 1;
      if (t > FADE) alpha = Math.max(0, 1 - Math.pow((t - FADE) / (duration - FADE), 2));

      for (const p of parts) {
        p.y += p.vy;
        p.phase += p.swaySpeed;
        p.x += Math.sin(p.phase) * p.sway;
        p.rotation += p.spin;
        p.tilt += p.tiltSpeed;
        if (p.y > h + 40) {
          if (herhaal) { p.y = rand(-160, -20); p.x = rand(-20, w + 20); }
          else p.y = h + 200;
        }

        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.translate(p.x, p.y);
        ctx.rotate(p.rotation);
        ctx.scale(Math.cos(p.tilt), 1);
        ctx.fillStyle = p.color;
        if (p.shape === "rect") ctx.fillRect(-p.size / 2, -p.size * 0.6, p.size, p.size * 1.35);
        else if (p.shape === "circle") { ctx.beginPath(); ctx.arc(0, 0, p.size * 0.45, 0, Math.PI * 2); ctx.fill(); }
        else ctx.fillRect(-p.size, -p.size * 0.18, p.size * 2, p.size * 0.36);
        ctx.restore();
      }

      if (t < duration) raf = requestAnimationFrame(frame);
      else ctx.clearRect(0, 0, w, h);
    };
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, [duration, count, speed, herhaal, startPos]);

  return <canvas ref={ref} className="bb-confetti-rain" aria-hidden="true" />;
}

function ThankYou({ title, body, go, extra, newsletter }) {
  const ref = useRef(null);
  // Na het versturen staat de bezoeker onderaan het formulier: spring terug naar
  // de bevestiging zodat de tekst direct leesbaar is. We meten daarbij de kaart
  // eromheen, zodat de donkergroene rand bovenaan mee in beeld komt en er nog een
  // kleine marge boven zit.
  useEffect(() => {
    if (!ref.current) return;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
      || document.documentElement.classList.contains("bb-rm-force");
    const kaart = ref.current.closest(".bb-form-card, .bb-cc__formwrap") || ref.current;
    const header = document.querySelector(".bb-header");
    const hoogte = header ? header.getBoundingClientRect().height : 72;
    const top = kaart.getBoundingClientRect().top + window.scrollY - hoogte - 15;
    window.scrollTo({ top: Math.max(0, top), behavior: reduceMotion ? "auto" : "smooth" });
  }, []);
  return (
    <div className="bb-thanks" role="status" ref={ref} data-screen-label="Bedankt">
      <ConfettiRain />
      <span className="bb-thanks__mark" aria-hidden="true"><Icon name="check" size={34} stroke={2.6} /></span>
      <h2>{title}</h2>
      <p>{body}</p>
      {extra && <p className="bb-thanks__extra">{extra}</p>}
      {newsletter && (
        <div className="bb-thanks__news">
          <span aria-hidden="true"><Icon name="mail" size={20} /></span>
          <p>
            {newsletter === "buurt" ? (
              <>
                Leuk dat je ook updates per e‑mail wil ontvangen. We houden je graag op de hoogte van
                Buiten Buddies in de buurt, maximaal één keer per maand.
              </>
            ) : (
              <>
                Leuk dat je ook updates per e‑mail wil ontvangen. We houden je graag op de hoogte van de
                aankomende activiteiten van Buiten Buddies voor jouw kind in de buurt, maximaal één keer
                per maand.
              </>
            )}
          </p>
        </div>
      )}
      <div className="bb-thanks__cta">
        <Button variant="primary" onClick={() => go("home")}>
          <Icon name="arrowLeft" /> Terug naar home
        </Button>
        <Button variant="secondary" onClick={() => go("hoe-werkt-het")}>Hoe werkt het?</Button>
      </div>
    </div>
  );
}

/* ---------------- eenmalige melding: leeftijdsgrens van WhatsApp ----------------
   WhatsApp hanteert in de Europese Unie een minimumleeftijd van dertien jaar voor een
   eigen account. Geeft een ouder aan dat het kind eigen WhatsApp heeft terwijl de
   opgegeven leeftijd daaronder ligt, dan brengen wij die voorwaarde eenmalig onder de
   aandacht.

   Dit is uitdrukkelijk geen blokkade: het formulier kan gewoon verstuurd worden en er
   verandert niets aan de velden. De ouder of verzorger maakt de afweging zelf en blijft
   daarvoor verantwoordelijk. Zodra de melding is weggeklikt, met het kruisje, met de
   knop, met Escape of door ernaast te klikken, komt hij niet meer terug zolang de ouder
   op het formulier bezig is. Verlaat hij de pagina en komt hij later opnieuw, dan wordt
   de melding wel weer getoond als aan beide voorwaarden is voldaan. Er wordt hiervoor
   niets in de browser opgeslagen. */

/* STAAT UIT sinds 1 september 2026, op verzoek van Sabrina. Zet deze schakelaar op
   true om de melding weer te laten verschijnen. De tekst en het venster hieronder zijn
   bewust bewaard gebleven, er hoeft dus niets herbouwd te worden. */
const BB_WA13_AAN = false;
const BB_WA13_MINIMUM = 13;
/* Moet de melding nu verschijnen? Alleen bij een geldige leeftijd onder de dertien in
   combinatie met eigen WhatsApp. Een leeg of half ingevuld veld doet niets. */
function bbWa13Nodig(leeftijd, watsapp) {
  if (watsapp !== "Ja") return false;
  const n = parseInt(String(leeftijd).replace(/[^0-9]/g, ""), 10);
  return !isNaN(n) && n > 0 && n < BB_WA13_MINIMUM;
}

function Wa13Glyph({ size = 26 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
      <path d="M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.46 1.32 4.96L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91C21.96 6.45 17.5 2 12.04 2zm5.8 14.09c-.25.7-1.45 1.34-2 1.38-.55.04-1.06.24-3.58-.75-3.04-1.2-4.9-4.36-5.05-4.57-.14-.2-1.17-1.6-1.17-3.06 0-1.45.76-2.17 1.03-2.47.27-.3.59-.37.79-.37.2 0 .4 0 .57.01.18.01.43-.07.67.51.25.6.83 2.05.9 2.2.07.15.12.32.02.52-.1.2-.15.32-.3.5-.15.17-.31.38-.44.51-.15.15-.3.31-.13.61.17.3.76 1.25 1.63 2.02 1.12.99 2.06 1.3 2.36 1.45.3.15.47.13.65-.08.17-.2.74-.86.94-1.16.2-.3.4-.25.67-.15.27.1 1.72.81 2.01.96.3.15.5.22.57.35.07.13.07.75-.18 1.45z"/>
    </svg>
  );
}

function WhatsAppLeeftijdModal({ voornaam, onSluit }) {
  const cardRef = useRef(null);
  const closeRef = useRef(null);
  useEffect(() => {
    const vorige = document.activeElement;
    if (closeRef.current) closeRef.current.focus();
    const onKey = (e) => {
      if (e.key === "Escape") { onSluit(); return; }
      if (e.key !== "Tab" || !cardRef.current) return;
      const items = cardRef.current.querySelectorAll("button, a[href]");
      if (!items.length) return;
      const eerste = items[0], laatste = items[items.length - 1];
      if (e.shiftKey && document.activeElement === eerste) { e.preventDefault(); laatste.focus(); }
      else if (!e.shiftKey && document.activeElement === laatste) { e.preventDefault(); eerste.focus(); }
    };
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("keydown", onKey);
      if (vorige && vorige.focus) vorige.focus();
    };
  }, [onSluit]);

  const naam = String(voornaam || "").trim();
  return (
    <div
      className="bb-modal"
      role="presentation"
      onMouseDown={(e) => { if (e.target.classList.contains("bb-modal")) onSluit(); }}
    >
      <div className="bb-modal__card bb-wa13" role="dialog" aria-modal="true" aria-labelledby="wa13-titel" ref={cardRef}>
        <span className="bb-wa13__mark" aria-hidden="true"><Wa13Glyph size={26} /></span>
        <h2 id="wa13-titel">Even iets over de leeftijdsgrens van WhatsApp</h2>
        <p>
          WhatsApp hanteert in de Europese Unie een minimumleeftijd van dertien jaar voor een
          eigen account. Dat is een voorwaarde van WhatsApp zelf en geen regel van Buiten Buddies.
        </p>
        <p>
          Je gaf aan dat {naam ? naam : "je kind"} eigen WhatsApp heeft, en de leeftijd die je hebt
          ingevuld ligt daaronder. Daarom brengen we het één keer onder je aandacht. Als ouder of
          verzorger maak je die afweging natuurlijk zelf, en die keuze laten we graag bij jou.
        </p>
        <p className="bb-wa13__rust">
          Je aanmelding gaat gewoon door en je hoeft niets aan te passen. Wil je liever dat alles
          via jou loopt, kies dan bij de vraag over WhatsApp voor &bdquo;Nee&rdquo;. Dan gebruiken we
          alleen jouw telefoonnummer en krijg jij de berichten uit de buurtapp.
        </p>
        <Button variant="primary" onClick={onSluit}>Dat is duidelijk, ik ga verder</Button>
        <button ref={closeRef} type="button" className="bb-modal__x" onClick={onSluit} aria-label="Sluiten">
          <Icon name="close" size={20} />
        </button>
      </div>
    </div>
  );
}

/* ---------------- formulier: ouders en jongeren ---------------- */
function SignupJongeren({ go }) {
  const [v, setV] = useState({
    ouderVoornaam: "", ouderTussenvoegsel: "", ouderAchternaam: "",
    kindVoornaam: "", kindTussenvoegsel: "", kindAchternaam: "",
    kindLeeftijd: "10", postcode: "",
    kindWhatsapp: "", kindTelefoon: "", ouderTelefoon: "", email: "",
    toestemming: false, updates: false, beheerderInteresse: false,
  });
  const [errors, setErrors] = useState({});
  const [status, setStatus] = useState("idle"); // idle | sending | senderror | sent
  const [alleenNieuwsbrief, setAlleenNieuwsbrief] = useState(false);
  const summaryRef = useRef(null);
  const formRef = useRef(null);
  // Onthoudt of het venster met de werkgebiedmelding is weggeklikt, zodat het niet
  // meteen weer verschijnt zodra de bezoeker het postcodeveld verlaat.
  const pcGesloten = useRef(false);
  const buitenGebied = bbBuitenWerkgebied(v.postcode);
  // Buiten het werkgebied met een vinkje voor de nieuwsbrief: dan hoeven alleen de
  // eigen gegevens ingevuld te worden, dus vervalt ook het sterretje bij telefoon.
  const alleenOuder = buitenGebied && !!v.updates;

  // Eenmalige melding over de leeftijdsgrens van WhatsApp. Weggeklikt is weggeklikt
  // zolang de ouder op dit formulier bezig is. Komt hij de pagina later opnieuw binnen,
  // dan begint dit vanzelf weer bij nul, want deze waarde leeft alleen zolang het
  // formulier in beeld is.
  const [wa13Open, setWa13Open] = useState(false);
  const wa13Gehad = useRef(false);
  /* De melding staat UIT, op verzoek van Sabrina (1 september 2026). Er wordt niet meer
     op de combinatie leeftijd en eigen WhatsApp gecontroleerd en er verschijnt dus geen
     venster meer. Het venster zelf en de tekst staan bewaard in WhatsAppLeeftijdModal
     hierboven, zodat het later in één handeling terug te zetten is: zet BB_WA13_AAN op
     true, dan werkt alles weer zoals het was. */
  useEffect(() => {
    if (!BB_WA13_AAN || wa13Gehad.current || alleenOuder) return;
    if (bbWa13Nodig(v.kindLeeftijd, v.kindWhatsapp)) setWa13Open(true);
  }, [v.kindLeeftijd, v.kindWhatsapp, alleenOuder]);
  const wa13Sluit = useCallback(() => {
    wa13Gehad.current = true;
    setWa13Open(false);
  }, []);

  const set = (k) => (val) => {
    setV((p) => Object.assign({}, p, { [k]: val }));
    if (errors[k]) setErrors((p) => { const n = Object.assign({}, p); delete n[k]; return n; });
  };
  /* Leeftijd: boven de zestien stopt het ophogen en verschijnt de melding direct,
     ook bij de pijltjestoetsen of de stepper van het invoerveld. */
  const setLeeftijd = (val) => {
    const n = parseInt(String(val).replace(/[^0-9]/g, ""), 10);
    if (!isNaN(n) && n > BB_LEEFTIJD_MAX) {
      setV((p) => Object.assign({}, p, { kindLeeftijd: String(BB_LEEFTIJD_MAX) }));
      setErrors((p) => Object.assign({}, p, { kindLeeftijd: BB_LEEFTIJD_MAX_FOUT }));
      return;
    }
    set("kindLeeftijd")(val);
  };
  const leeftijdToets = (ev) => {
    if (ev.key !== "ArrowUp" && ev.key !== "PageUp") return;
    const n = parseInt(String(v.kindLeeftijd).replace(/[^0-9]/g, ""), 10);
    if (!isNaN(n) && n >= BB_LEEFTIJD_MAX) {
      ev.preventDefault();
      setErrors((p) => Object.assign({}, p, { kindLeeftijd: BB_LEEFTIJD_MAX_FOUT }));
    }
  };
  /* De stepperpijltjes van het veld klemmen zelf op max, dus dan volgt geen wijziging
     en geen toetsaanslag. Een klik op het omhoogpijltje bij zestien, en scrollen omhoog
     in het veld, laten we daarom zelf de melding geven. */
  const leeftijdAlMax = () => {
    const n = parseInt(String(v.kindLeeftijd).replace(/[^0-9]/g, ""), 10);
    return !isNaN(n) && n >= BB_LEEFTIJD_MAX;
  };
  const leeftijdSpinner = (ev) => {
    const r = ev.currentTarget.getBoundingClientRect();
    if (ev.clientX > r.right - 34 && ev.clientY < r.top + r.height / 2 && leeftijdAlMax()) {
      setErrors((p) => Object.assign({}, p, { kindLeeftijd: BB_LEEFTIJD_MAX_FOUT }));
    }
  };
  const leeftijdScroll = (ev) => {
    if (ev.deltaY < 0 && leeftijdAlMax()) {
      setErrors((p) => Object.assign({}, p, { kindLeeftijd: BB_LEEFTIJD_MAX_FOUT }));
    }
  };
  const blur = (k) => () => {
    const e = bbValidateJongeren(v);
    setErrors((p) => {
      const n = Object.assign({}, p);
      // Is het venster met de werkgebiedmelding zojuist weggeklikt, dan mag het
      // verlaten van het postcodeveld die melding niet meteen terugzetten. Anders
      // is er een tweede klik nodig voordat het venster echt weg is.
      if (e[k] === BB_BUITEN_WERKGEBIED && pcGesloten.current) { delete n[k]; return n; }
      if (e[k]) n[k] = e[k]; else delete n[k];
      return n;
    });
  };
  /* De postcodemelding verschijnt meteen tijdens het typen, niet pas bij verzenden.
     Zo is direct duidelijk waarom het vinkje voor de nieuwsbrief er toe doet. */
  const setPostcode = (val) => {
    setV((p) => Object.assign({}, p, { postcode: val }));
    pcGesloten.current = false;
    setErrors((p) => {
      const n = Object.assign({}, p);
      delete n.postcode;
      if (bbBuitenWerkgebied(val) && !v.updates) n.postcode = BB_BUITEN_WERKGEBIED;
      return n;
    });
  };
  /* Zet iemand met een postcode buiten het werkgebied het vinkje voor de nieuwsbrief
     aan, dan vervallen op slag alle verplichte velden. Alle openstaande meldingen
     verdwijnen dan ook meteen, ook de rode samenvatting bovenaan, zodat het formulier
     weer neutraal oogt. Fouten komen pas terug bij het verlaten van een veld dat dan
     nog verplicht is, of bij het versturen. */
  const setUpdates = (val) => {
    setV((p) => Object.assign({}, p, { updates: val }));
    const buiten = bbBuitenWerkgebied(v.postcode);
    if (buiten && val) {
      setErrors({});
      // Het formulier klapt nu in tot een paar velden. Zonder deze sprong staat de
      // bezoeker ineens onder het formulier te kijken, dus brengen we hem terug naar
      // de eerste velden die hij moet invullen of controleren.
      requestAnimationFrame(() => {
        if (!formRef.current) return;
        const top = formRef.current.getBoundingClientRect().top + window.pageYOffset - 96;
        window.scrollTo(0, Math.max(0, top));
      });
      return;
    }
    setErrors((p) => {
      const n = Object.assign({}, p);
      if (val) { if (n.postcode === BB_BUITEN_WERKGEBIED) delete n.postcode; }
      else if (buiten) n.postcode = BB_BUITEN_WERKGEBIED;
      return n;
    });
  };
  /* Buiten het venster klikken laat zowel het venster als de rode regel bovenaan
     het formulier verdwijnen. */
  /* Kom je opnieuw in het postcodeveld, dan telt de controle weer: zolang het vinkje
     voor updates uit staat, verschijnt de melding elke keer opnieuw. */
  const focusPostcode = () => {
    pcGesloten.current = false;
    if (!bbBuitenWerkgebied(v.postcode) || v.updates) return;
    setErrors((p) => (p.postcode === BB_BUITEN_WERKGEBIED
      ? p
      : Object.assign({}, p, { postcode: BB_BUITEN_WERKGEBIED })));
  };
  const sluitPostcodeMelding = () => setErrors((p) => {
    pcGesloten.current = true;
    if (p.postcode !== BB_BUITEN_WERKGEBIED) return p;
    const n = Object.assign({}, p);
    delete n.postcode;
    return n;
  });
  const send = async () => {
    setStatus("sending");
    const alleen = bbBuitenWerkgebied(v.postcode);
    setAlleenNieuwsbrief(alleen);
    try {
      if (alleen) await bbSubmitAlleenNieuwsbrief("jongeren", v);
      else await bbSubmitForm("jongeren", v);
      setStatus("sent");
      if (window.bbGaEvent) window.bbGaEvent("formulier_verzonden", { formulier: alleen ? "kind aanmelden, nieuw gebied" : "kind aanmelden", nieuwsbrief: !!v.updates });
    }
    catch (err) { setStatus("senderror"); }
  };
  const onSubmit = (e) => {
    e.preventDefault();
    const errs = bbValidateJongeren(v);
    // Bij versturen telt de werkgebiedmelding weer mee, ook als het venster eerder
    // was weggeklikt: anders blijft het echte struikelblok onzichtbaar.
    pcGesloten.current = false;
    setErrors(errs);
    if (Object.keys(errs).length) {
      bbFocusSamenvatting(errs, summaryRef);
      return;
    }
    send();
  };

  if (status === "sent") {
    return alleenNieuwsbrief ? (
      <ThankYou
        go={go}
        title="Bedankt, je staat op de lijst!"
        body="We laten het weten zodra Buiten Buddies in jouw buurt start."
        extra="De aanmelding voor een buurtapp kunnen we nog niet verwerken, want daar zijn we nog niet actief."
        newsletter={true}
      />
    ) : (
      <ThankYou
        go={go}
        title="Gelukt, je aanmelding is binnen!"
        body="We nemen binnen een paar dagen contact op."
        extra="Binnenkort gaat je kind nieuwe vrienden in de buurt maken om mee naar buiten te gaan."
        newsletter={v.updates}
      />
    );
  }

  return (
    <React.Fragment>
    <form className="bb-form" onSubmit={onSubmit} noValidate ref={formRef}>
      <ErrorSummary
        errors={errors} summaryRef={summaryRef}
        order={["ouderVoornaam", "ouderAchternaam", "kindVoornaam", "kindAchternaam", "kindLeeftijd", "postcode", "kindWhatsapp", "kindTelefoon", "ouderTelefoon", "email", "toestemming"]}
      />

      <p className="bb-form__grouplabel">Ouder of verzorger</p>

      <div className="bb-form__row bb-form__row--name">
        <Field id="ouderVoornaam" label="Voornaam" error={errors.ouderVoornaam} required>
          <TextInput id="ouderVoornaam" value={v.ouderVoornaam} onChange={set("ouderVoornaam")} onBlur={blur("ouderVoornaam")} error={errors.ouderVoornaam} autoComplete="given-name" />
        </Field>
        <Field id="ouderTussenvoegsel" label="Tussenvoegsel" hideOpt>
          <TextInput id="ouderTussenvoegsel" value={v.ouderTussenvoegsel} onChange={set("ouderTussenvoegsel")} autoComplete="additional-name" />
        </Field>
        <Field id="ouderAchternaam" label="Achternaam" error={errors.ouderAchternaam} required>
          <TextInput id="ouderAchternaam" value={v.ouderAchternaam} onChange={set("ouderAchternaam")} onBlur={blur("ouderAchternaam")} error={errors.ouderAchternaam} autoComplete="family-name" />
        </Field>
      </div>

      {/* Buiten het werkgebied slaan we alleen de gegevens van de ouder op. Alles wat
          dan niet meer nodig is verdwijnt uit beeld, zodat er een kort en overzichtelijk
          formulier overblijft. Zodra er weer een postcode binnen het werkgebied staat,
          komt alles terug, met wat al ingevuld was. */}
      {!alleenOuder && (
        <>
          <p className="bb-form__grouplabel">Je kind</p>

          <div className="bb-form__row bb-form__row--name">
            <Field id="kindVoornaam" label="Voornaam" error={errors.kindVoornaam} required>
              <TextInput id="kindVoornaam" value={v.kindVoornaam} onChange={set("kindVoornaam")} onBlur={blur("kindVoornaam")} error={errors.kindVoornaam} />
            </Field>
            <Field id="kindTussenvoegsel" label="Tussenvoegsel" hideOpt>
              <TextInput id="kindTussenvoegsel" value={v.kindTussenvoegsel} onChange={set("kindTussenvoegsel")} />
            </Field>
            <Field id="kindAchternaam" label="Achternaam" error={errors.kindAchternaam} required>
              <TextInput id="kindAchternaam" value={v.kindAchternaam} onChange={set("kindAchternaam")} onBlur={blur("kindAchternaam")} error={errors.kindAchternaam} />
            </Field>
          </div>

          <div className="bb-form__row">
            <Field id="kindLeeftijd" label="Leeftijd kind" error={errors.kindLeeftijd} required>
              <TextInput id="kindLeeftijd" type="number" inputMode="numeric" min="1" max={BB_LEEFTIJD_MAX} value={v.kindLeeftijd} onChange={setLeeftijd} onKeyDown={leeftijdToets} onPointerDown={leeftijdSpinner} onWheel={leeftijdScroll} onBlur={blur("kindLeeftijd")} error={errors.kindLeeftijd} />
            </Field>
          </div>
        </>
      )}

      <div className="bb-pcwrap">
        <Field id="postcode" label="Postcode" hint="Zo koppelen we je kind aan de juiste buurt. Buiten Buddies is nu alleen actief in Haarlem en directe omgeving." error={errors.postcode === BB_BUITEN_WERKGEBIED ? "" : errors.postcode} required>
          <TextInput id="postcode" value={v.postcode} onChange={setPostcode} onFocus={focusPostcode} onBlur={blur("postcode")} error={errors.postcode} hint autoComplete="postal-code" placeholder="2012 AB" />
          {errors.postcode === BB_BUITEN_WERKGEBIED && (
            <PostcodeMelding id="postcode-error" onClose={sluitPostcodeMelding} />
          )}
        </Field>
      </div>
      {buitenGebied && v.updates && (
        <p className="bb-form__buiten" role="status">{BB_BUITEN_WERKGEBIED_OK}</p>
      )}

      {!alleenOuder && (
        <RadioGroup
          id="kindWhatsapp" label="Heeft je kind eigen WhatsApp?"
          options={["Ja", "Nee"]} value={v.kindWhatsapp} onChange={set("kindWhatsapp")}
          error={errors.kindWhatsapp} required
        />
      )}

      {!alleenOuder && v.kindWhatsapp === "Ja" && (
        <Field id="kindTelefoon" label="Telefoonnummer kind" hint="Alleen gebruikt om je kind in de buurtapp te zetten." error={errors.kindTelefoon} required>
          <TextInput id="kindTelefoon" type="tel" value={v.kindTelefoon} onChange={set("kindTelefoon")} onBlur={blur("kindTelefoon")} error={errors.kindTelefoon} hint placeholder="06-12345678" />
        </Field>
      )}

      <div className="bb-form__row">
        {!alleenOuder && (
          <Field id="ouderTelefoon" label="Telefoonnummer ouder" error={errors.ouderTelefoon} required>
            <TextInput id="ouderTelefoon" type="tel" value={v.ouderTelefoon} onChange={set("ouderTelefoon")} onBlur={blur("ouderTelefoon")} error={errors.ouderTelefoon} autoComplete="tel" placeholder="06-12345678" />
          </Field>
        )}
        <Field id="email" label="E‑mailadres" error={errors.email} required>
          <TextInput id="email" type="email" value={v.email} onChange={set("email")} onBlur={blur("email")} error={errors.email} autoComplete="email" placeholder="naam@voorbeeld.nl" />
        </Field>
      </div>

      {!alleenOuder && (
        <div className="bb-consent">
          <p>
            Ik geef toestemming dat mijn kind deelneemt aan Buiten Buddies en dat het telefoonnummer
            wordt gebruikt om buitenspeelafspraken tussen buurtkinderen via WhatsApp mogelijk te maken.
            Buiten Buddies deelt dit nummer niet met derden en verwijdert het nummer van mijn kind
            uit de eigen administratie zodra de buitenspeelkoppeling tot stand is gebracht. Buiten
            Buddies mag mij later benaderen met een korte, vrijwillige vragenlijst over de ervaringen
            van mijn kind. Als ouder blijf ik te allen tijde verantwoordelijk voor mijn kind en het gedrag van
            mijn kind tijdens het buitenspelen. Ook ga ik akkoord met de{" "}
            <a href={window.bbHref("disclaimer")} target="_blank" rel="noopener noreferrer">disclaimer</a>{" "}
            van Buiten Buddies.
          </p>
        </div>
      )}

      <div className="bb-checks">
        {!alleenOuder && (
          <CheckRow id="toestemming" checked={v.toestemming} onChange={set("toestemming")} error={errors.toestemming}>
            Ik heb de toestemmingstekst en de disclaimer gelezen en ga akkoord <span className="bb-field__req" aria-hidden="true">*</span>
          </CheckRow>
        )}

        <CheckRow
          id="updates" checked={v.updates} onChange={setUpdates}
          hint="Buiten Buddies verstuurt maximaal 1 nieuwsbrief per maand, vaak minder dan dat. Uitschrijven is altijd vanuit iedere nieuwsbrief mogelijk."
        >
          Ik ontvang graag updates over Buiten Buddies per e‑mail
        </CheckRow>

        {!alleenOuder && (
          <CheckRow id="beheerderInteresse" checked={v.beheerderInteresse} onChange={set("beheerderInteresse")}>
            Is er nog geen Buiten Buddies-buurtapp in mijn buurt? Dan denk ik graag mee om er samen één op te zetten.
          </CheckRow>
        )}
      </div>

      {status === "senderror" && <SendError onRetry={send} />}

      <div className="bb-form__foot">
        <Button variant="yellow" type="submit" disabled={status === "sending"}>
          {status === "sending" ? "Versturen\u2026" : (alleenOuder ? "Houd mij op de hoogte" : "Meld mijn kind aan")} <Icon name="arrowRight" />
        </Button>
        <p className="bb-form__note">Velden met een <span className="bb-field__req">*</span> zijn verplicht.</p>
      </div>
    </form>
    {wa13Open && <WhatsAppLeeftijdModal voornaam={v.kindVoornaam} onSluit={wa13Sluit} />}
    </React.Fragment>
  );
}

/* ---------------- formulier: groepsbeheerder ---------------- */
function SignupBeheerder({ go }) {
  const [v, setV] = useState({ voornaam: "", tussenvoegsel: "", achternaam: "", postcode: "", rol: "", telefoon: "", email: "", updates: false });
  const [errors, setErrors] = useState({});
  const [status, setStatus] = useState("idle");
  const [alleenNieuwsbrief, setAlleenNieuwsbrief] = useState(false);
  const summaryRef = useRef(null);
  const formRef = useRef(null);
  // Onthoudt of het venster met de werkgebiedmelding is weggeklikt, zodat het niet
  // meteen weer verschijnt zodra de bezoeker het postcodeveld verlaat.
  const pcGesloten = useRef(false);
  const buitenGebied = bbBuitenWerkgebied(v.postcode);
  // Buiten het werkgebied met een vinkje voor de nieuwsbrief: dan hoeven alleen de
  // eigen gegevens ingevuld te worden, dus vervalt ook het sterretje bij telefoon.
  const alleenOuder = buitenGebied && !!v.updates;

  const set = (k) => (val) => {
    setV((p) => Object.assign({}, p, { [k]: val }));
    if (errors[k]) setErrors((p) => { const n = Object.assign({}, p); delete n[k]; return n; });
  };
  const blur = (k) => () => {
    const e = bbValidateBeheerder(v);
    setErrors((p) => {
      const n = Object.assign({}, p);
      if (e[k] === BB_BUITEN_WERKGEBIED && pcGesloten.current) { delete n[k]; return n; }
      if (e[k]) n[k] = e[k]; else delete n[k];
      return n;
    });
  };
  const setPostcode = (val) => {
    setV((p) => Object.assign({}, p, { postcode: val }));
    pcGesloten.current = false;
    setErrors((p) => {
      const n = Object.assign({}, p);
      delete n.postcode;
      if (bbBuitenWerkgebied(val) && !v.updates) n.postcode = BB_BUITEN_WERKGEBIED;
      return n;
    });
  };
  /* Zet iemand met een postcode buiten het werkgebied het vinkje voor de nieuwsbrief
     aan, dan vervallen op slag alle verplichte velden. Alle openstaande meldingen
     verdwijnen dan ook meteen, ook de rode samenvatting bovenaan, zodat het formulier
     weer neutraal oogt. Fouten komen pas terug bij het verlaten van een veld dat dan
     nog verplicht is, of bij het versturen. */
  const setUpdates = (val) => {
    setV((p) => Object.assign({}, p, { updates: val }));
    const buiten = bbBuitenWerkgebied(v.postcode);
    if (buiten && val) {
      setErrors({});
      // Het formulier klapt nu in tot een paar velden. Zonder deze sprong staat de
      // bezoeker ineens onder het formulier te kijken, dus brengen we hem terug naar
      // de eerste velden die hij moet invullen of controleren.
      requestAnimationFrame(() => {
        if (!formRef.current) return;
        const top = formRef.current.getBoundingClientRect().top + window.pageYOffset - 96;
        window.scrollTo(0, Math.max(0, top));
      });
      return;
    }
    setErrors((p) => {
      const n = Object.assign({}, p);
      if (val) { if (n.postcode === BB_BUITEN_WERKGEBIED) delete n.postcode; }
      else if (buiten) n.postcode = BB_BUITEN_WERKGEBIED;
      return n;
    });
  };
  /* Kom je opnieuw in het postcodeveld, dan telt de controle weer: zolang het vinkje
     voor updates uit staat, verschijnt de melding elke keer opnieuw. */
  const focusPostcode = () => {
    pcGesloten.current = false;
    if (!bbBuitenWerkgebied(v.postcode) || v.updates) return;
    setErrors((p) => (p.postcode === BB_BUITEN_WERKGEBIED
      ? p
      : Object.assign({}, p, { postcode: BB_BUITEN_WERKGEBIED })));
  };
  const sluitPostcodeMelding = () => setErrors((p) => {
    pcGesloten.current = true;
    if (p.postcode !== BB_BUITEN_WERKGEBIED) return p;
    const n = Object.assign({}, p);
    delete n.postcode;
    return n;
  });
  const send = async () => {
    setStatus("sending");
    const alleen = bbBuitenWerkgebied(v.postcode);
    setAlleenNieuwsbrief(alleen);
    try {
      if (alleen) await bbSubmitAlleenNieuwsbrief("beheerder", v);
      else await bbSubmitForm("beheerder", v);
      setStatus("sent");
      if (window.bbGaEvent) window.bbGaEvent("formulier_verzonden", { formulier: alleen ? "groepsbeheerder, nieuw gebied" : "groepsbeheerder", nieuwsbrief: !!v.updates });
    }
    catch (err) { setStatus("senderror"); }
  };
  const onSubmit = (e) => {
    e.preventDefault();
    const errs = bbValidateBeheerder(v);
    // Bij versturen telt de werkgebiedmelding weer mee, ook als het venster eerder
    // was weggeklikt: anders blijft het echte struikelblok onzichtbaar.
    pcGesloten.current = false;
    setErrors(errs);
    if (Object.keys(errs).length) {
      bbFocusSamenvatting(errs, summaryRef);
      return;
    }
    send();
  };

  if (status === "sent") {
    return alleenNieuwsbrief ? (
      <ThankYou
        go={go}
        title="Bedankt, je staat op de lijst!"
        body="We laten het weten zodra Buiten Buddies in jouw buurt start."
        extra="Je interesse als groepsbeheerder kunnen we nog niet verwerken, want daar zijn we nog niet actief."
        newsletter="buurt"
      />
    ) : (
      <ThankYou
        go={go}
        title="Bedankt voor je interesse!"
        body="We nemen binnen vijf werkdagen contact met je op."
        extra="Je hoeft nog nergens ja op te zeggen: we vertellen je eerst rustig wat het inhoudt."
        newsletter={v.updates ? "buurt" : false}
      />
    );
  }

  return (
    <form className="bb-form" onSubmit={onSubmit} noValidate ref={formRef}>
      <ErrorSummary errors={errors} summaryRef={summaryRef} order={["voornaam", "achternaam", "postcode", "rol", "telefoon", "email"]} />

      <div className="bb-form__row bb-form__row--name">
        <Field id="voornaam" label="Voornaam" error={errors.voornaam} required>
          <TextInput id="voornaam" value={v.voornaam} onChange={set("voornaam")} onBlur={blur("voornaam")} error={errors.voornaam} autoComplete="given-name" />
        </Field>
        <Field id="tussenvoegsel" label="Tussenvoegsel" hideOpt>
          <TextInput id="tussenvoegsel" value={v.tussenvoegsel} onChange={set("tussenvoegsel")} autoComplete="additional-name" />
        </Field>
        <Field id="achternaam" label="Achternaam" error={errors.achternaam} required>
          <TextInput id="achternaam" value={v.achternaam} onChange={set("achternaam")} onBlur={blur("achternaam")} error={errors.achternaam} autoComplete="family-name" />
        </Field>
      </div>

      <div className="bb-form__row">
        <div className="bb-pcwrap">
          <Field id="postcode" label="Postcode" hint="De buurt waarin je groepsbeheerder wil worden. Buiten Buddies is nu alleen actief in Haarlem en directe omgeving." error={errors.postcode === BB_BUITEN_WERKGEBIED ? "" : errors.postcode} required>
            <TextInput id="postcode" value={v.postcode} onChange={setPostcode} onFocus={focusPostcode} onBlur={blur("postcode")} error={errors.postcode} hint autoComplete="postal-code" placeholder="2012 AB" />
            {errors.postcode === BB_BUITEN_WERKGEBIED && (
              <PostcodeMelding id="postcode-error" onClose={sluitPostcodeMelding} />
            )}
          </Field>
        </div>
        {!alleenOuder && (
          <Field id="telefoon" label="Telefoonnummer" error={errors.telefoon} required>
            <TextInput id="telefoon" type="tel" value={v.telefoon} onChange={set("telefoon")} onBlur={blur("telefoon")} error={errors.telefoon} autoComplete="tel" placeholder="06-12345678" />
          </Field>
        )}
      </div>

      <RadioGroup
        id="rol" label="Ouder of andere vrijwilliger?"
        options={["Ouder", "Andere vrijwilliger"]} value={v.rol} onChange={set("rol")}
        error={errors.rol} required
      />

      <Field id="email" label="E‑mailadres" error={errors.email} required>
        <TextInput id="email" type="email" value={v.email} onChange={set("email")} onBlur={blur("email")} error={errors.email} autoComplete="email" placeholder="naam@voorbeeld.nl" />
      </Field>

      <div className="bb-checks">
        <CheckRow id="updates" checked={v.updates} onChange={setUpdates} hint={BB_NIEUWSBRIEF_UITLEG}>
          Ik ontvang graag updates over Buiten Buddies per e‑mail
        </CheckRow>
      </div>
      {buitenGebied && v.updates && (
        <p className="bb-form__buiten" role="status">{BB_BUITEN_WERKGEBIED_OK_BEHEERDER}</p>
      )}

      {status === "senderror" && <SendError onRetry={send} />}

      <div className="bb-form__foot">
        <Button variant="yellow" type="submit" disabled={status === "sending"}>
          {status === "sending" ? "Versturen\u2026" : (alleenOuder ? "Houd mij op de hoogte" : "Ik heb interesse om groepsbeheerder te worden")} <Icon name="arrowRight" />
        </Button>
        <p className="bb-form__note">Velden met een <span className="bb-field__req">*</span> zijn verplicht.</p>
      </div>
    </form>
  );
}

const BB_NIEUWSBRIEF_UITLEG = "Buiten Buddies verstuurt maximaal 1 nieuwsbrief per maand, vaak minder dan dat. Uitschrijven is altijd vanuit iedere nieuwsbrief mogelijk.";
/* ---------------- nieuwsbrief in de footer ---------------- */
function FootField({ id, label, labelSmal, value, onChange, onBlur, error, autoComplete, required }) {
  return (
    <div className="bb-foot-news__f">
      <label htmlFor={id} className="bb-foot-news__label">
        {labelSmal
          ? <React.Fragment><span className="bb-lbl-breed">{label}</span><span className="bb-lbl-smal">{labelSmal}</span></React.Fragment>
          : label}
        {required ? <span className="bb-field__req" aria-hidden="true"> *</span> : null}
      </label>
      <input
        id={id} name={id} type="text" value={value}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        className={"bb-input bb-foot-news__input" + (error ? " has-error" : "")}
        autoComplete={autoComplete}
        aria-invalid={error ? "true" : undefined}
        aria-describedby={error ? id + "-error" : undefined}
      />
      {error && (
        <p className="bb-field__error bb-foot-news__error" id={id + "-error"}>
          <Icon name="close" size={14} /> {error}
        </p>
      )}
    </div>
  );
}

function FooterSignup() {
  const [v, setV] = useState({ voornaam: "", tussenvoegsel: "", achternaam: "", email: "" });
  const [errors, setErrors] = useState({});
  const [status, setStatus] = useState("idle"); // idle | sending | senderror
  const [open, setOpen] = useState(false);
  const closeRef = useRef(null);
  const cardRef = useRef(null);
  const close = () => { setOpen(false); };
  const wasOpen = useRef(false);
  useEffect(() => {
    if (!open && wasOpen.current) {
      const el = document.getElementById("footer-voornaam");
      if (el) el.focus();
    }
    wasOpen.current = open;
  }, [open]);

  const check = (val) => {
    const e = {};
    const vn = val.voornaam.trim();
    if (!vn) e.voornaam = "Vul je voornaam in.";
    else if (!bbIsVoornaam(vn)) e.voornaam = BB_VOORNAAM_FOUT;
    if (!val.achternaam.trim()) e.achternaam = "Vul je achternaam in.";
    if (!val.email.trim()) e.email = "Vul je e‑mailadres in.";
    else if (!bbIsEmail(val.email)) e.email = "Vul een geldig e‑mailadres in, bijvoorbeeld naam@voorbeeld.nl.";
    return e;
  };
  const set = (k) => (val) => {
    setV((p) => Object.assign({}, p, { [k]: val }));
    if (errors[k]) setErrors((p) => { const n = Object.assign({}, p); delete n[k]; return n; });
  };
  const blur = (k) => () => {
    const e = check(v);
    setErrors((p) => {
      const n = Object.assign({}, p);
      if (e[k]) n[k] = e[k]; else delete n[k];
      return n;
    });
  };

  const onSubmit = async (e) => {
    e.preventDefault();
    const errs = check(v);
    setErrors(errs);
    if (Object.keys(errs).length) {
      const first = ["voornaam", "achternaam", "email"].find((k) => errs[k]);
      const el = document.getElementById("footer-" + first);
      if (el) el.focus();
      return;
    }
    setStatus("sending");
    try {
      await bbSubmitForm("nieuwsbrief", {
        voornaam: v.voornaam.trim(), tussenvoegsel: v.tussenvoegsel.trim(),
        achternaam: v.achternaam.trim(), email: v.email.trim(),
        bron: "footer", optIn: true,
      });
      setStatus("idle");
      setV({ voornaam: "", tussenvoegsel: "", achternaam: "", email: "" });
      setOpen(true);
      if (window.bbGaEvent) window.bbGaEvent("formulier_verzonden", { formulier: "nieuwsbrief, footer" });
    } catch (err2) {
      setStatus("senderror");
    }
  };

  useEffect(() => {
    if (!open) return;
    if (closeRef.current) closeRef.current.focus();
    const focusables = () => Array.from(
      cardRef.current ? cardRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') : []
    ).filter((el) => !el.disabled);
    const onKey = (e) => {
      if (e.key === "Escape") { close(); return; }
      if (e.key !== "Tab") return;
      const list = focusables();
      if (!list.length) return;
      const first = list[0];
      const last = list[list.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    };
    // Focus blijft binnen de dialog, ook bij klikken of programmatische focus.
    const onFocusIn = (e) => {
      if (cardRef.current && !cardRef.current.contains(e.target) && closeRef.current) {
        closeRef.current.focus();
      }
    };
    const onHash = () => setOpen(false);
    document.addEventListener("keydown", onKey);
    document.addEventListener("focusin", onFocusIn, true);
    window.addEventListener("hashchange", onHash);
    return () => {
      document.removeEventListener("keydown", onKey);
      document.removeEventListener("focusin", onFocusIn, true);
      window.removeEventListener("hashchange", onHash);
    };
  }, [open]);

  const mail = (window.BB_FORM_CONFIG && window.BB_FORM_CONFIG.fallbackEmail) || "info@buitenbuddies.org";

  return (
    <section className="bb-foot-news" aria-labelledby="foot-news-title">
      <div className="bb-foot-news__intro">
        <div className="bb-foot-news__kop">
          <span className="bb-foot-news__badge" aria-hidden="true"><Icon name="mail" size={24} /></span>
          <h4 id="foot-news-title">Blijf op de hoogte</h4>
        </div>
        <p>Schrijf je in voor updates over Buiten Buddies.</p>
        <p className="bb-foot-news__hint" id="footer-email-hint">{BB_NIEUWSBRIEF_UITLEG}</p>
      </div>
      <form className="bb-foot-news__form" onSubmit={onSubmit} noValidate>
        <div className="bb-foot-news__names">
          <FootField id="footer-voornaam" label="Voornaam" value={v.voornaam} onChange={set("voornaam")} onBlur={blur("voornaam")} error={errors.voornaam} autoComplete="given-name" required />
          <FootField id="footer-tussenvoegsel" label="Tussenvoegsel" labelSmal="Tussenv." value={v.tussenvoegsel} onChange={set("tussenvoegsel")} autoComplete="additional-name" />
          <FootField id="footer-achternaam" label="Achternaam" value={v.achternaam} onChange={set("achternaam")} onBlur={blur("achternaam")} error={errors.achternaam} autoComplete="family-name" required />
        </div>

        <div className="bb-foot-news__row">
          <div className="bb-foot-news__f bb-foot-news__f--mail">
            <label htmlFor="footer-email" className="bb-foot-news__label">
              E‑mailadres <span className="bb-field__req" aria-hidden="true">*</span>
            </label>
            <span className="bb-foot-news__mail">
              <span className="bb-foot-news__mailicon" aria-hidden="true"><Icon name="mail" size={18} /></span>
              <input
                id="footer-email" name="footer-email" type="email" value={v.email}
                onChange={(e) => set("email")(e.target.value)}
                onBlur={blur("email")}
                className={"bb-input bb-foot-news__input bb-foot-news__input--mail" + (errors.email ? " has-error" : "")}
                placeholder="naam@voorbeeld.nl" autoComplete="email"
                aria-invalid={errors.email ? "true" : undefined}
                aria-describedby={errors.email ? "footer-email-error" : "footer-email-hint"}
              />
            </span>
            {errors.email && (
              <p className="bb-field__error bb-foot-news__error" id="footer-email-error">
                <Icon name="close" size={14} /> {errors.email}
              </p>
            )}
          </div>
          <button type="submit" className="bb-btn bb-btn--yellow bb-foot-news__btn" disabled={status === "sending"}>
            {status === "sending" ? "Versturen\u2026" : "Schrijf me in"} <Icon name="arrowRight" size={18} />
          </button>
        </div>
        {status === "senderror" && (
          <p className="bb-field__error bb-foot-news__error" role="alert">
            <Icon name="close" size={14} /> Het versturen lukte niet. Mail ons op {mail}.
          </p>
        )}
      </form>

      {open && (
        <div className="bb-modal" role="presentation" onClick={(e) => { if (e.target.classList.contains("bb-modal")) close(); }}>
          <div className="bb-modal__card" role="dialog" aria-modal="true" aria-labelledby="news-modal-title" ref={cardRef}>
            <span className="bb-thanks__mark" aria-hidden="true"><Icon name="check" size={30} stroke={2.6} /></span>
            <h2 id="news-modal-title">Gelukt, je bent ingeschreven!</h2>
            <p>
              Leuk dat je updates van Buiten Buddies wil ontvangen. We houden je graag op de hoogte
              van de aankomende activiteiten in de buurt, maximaal één keer per maand.
            </p>
            <Button variant="primary" onClick={close}>Sluiten</Button>
            <button ref={closeRef} type="button" className="bb-modal__x" onClick={close} aria-label="Sluiten">
              <Icon name="close" size={20} />
            </button>
          </div>
        </div>
      )}
    </section>
  );
}

/* ---------------- losse nieuwsbriefpagina ---------------- */
function NieuwsbriefPage({ go }) {
  const [v, setV] = useState({ voornaam: "", tussenvoegsel: "", achternaam: "", email: "", optIn: false });
  const [errors, setErrors] = useState({});
  const [status, setStatus] = useState("idle");
  const summaryRef = useRef(null);

  const validate = (val) => {
    const e = {};
    const vn = val.voornaam.trim();
    if (!vn) e.voornaam = "Vul je voornaam in.";
    else if (!bbIsVoornaam(vn)) e.voornaam = BB_VOORNAAM_FOUT;
    if (!val.achternaam.trim()) e.achternaam = "Vul je achternaam in.";
    if (!val.email.trim()) e.email = "Vul je e‑mailadres in.";
    else if (!bbIsEmail(val.email)) e.email = "Vul een geldig e‑mailadres in, bijvoorbeeld naam@voorbeeld.nl.";
    if (!val.optIn) e.optIn = "Zonder toestemming kunnen we je de nieuwsbrief niet sturen.";
    return e;
  };
  const set = (k) => (val) => {
    setV((p) => Object.assign({}, p, { [k]: val }));
    if (errors[k]) setErrors((p) => { const n = Object.assign({}, p); delete n[k]; return n; });
  };
  const blur = (k) => () => {
    const e = validate(v);
    setErrors((p) => { const n = Object.assign({}, p); if (e[k]) n[k] = e[k]; else delete n[k]; return n; });
  };
  const send = async () => {
    setStatus("sending");
    try {
      await bbSubmitForm("nieuwsbrief", Object.assign({}, v, { bron: "nieuwsbriefpagina" }));
      setStatus("sent");
      if (window.bbGaEvent) window.bbGaEvent("formulier_verzonden", { formulier: "nieuwsbrief, pagina" });
    }
    catch (err) { setStatus("senderror"); }
  };
  const onSubmit = (e) => {
    e.preventDefault();
    const errs = validate(v);
    setErrors(errs);
    if (Object.keys(errs).length) {
      bbFocusSamenvatting(errs, summaryRef);
      return;
    }
    send();
  };

  return (
    <main id="main" tabIndex="-1" data-screen-label="Nieuwsbrief">
      <section className="bb-section bb-section--cream bb-formpage__head" aria-labelledby="nieuwsbrief-heading">
        <Container>
          <FadeUp>
            <div className="bb-eyebrow">
              <span className="dot" aria-hidden="true"></span>
              Updates
            </div>
          </FadeUp>
          <FadeUp delay={80}>
            <h1 id="nieuwsbrief-heading">Schrijf je in voor updates</h1>
          </FadeUp>
          <FadeUp delay={150}>
            <p className="bb-formpage__intro">
              Buiten Buddies wil stap voor stap uitbreiden naar meer buurten en steden. We werken ook
              aan een eigen app. Wil je op de hoogte blijven? Laat je gegevens achter.
            </p>
          </FadeUp>
        </Container>
      </section>

      <section className="bb-section bb-section--alt bb-formpage__body">
        <Container>
          <div className="bb-form-card">
            {status === "sent" ? (
              <ThankYou
                go={go}
                title="Gelukt, je staat op de lijst!"
                body="Je ontvangt vanaf nu de updates van Buiten Buddies."
                extra="Bedankt voor je interesse in Buiten Buddies."
              />
            ) : (
              <form className="bb-form" onSubmit={onSubmit} noValidate>
                <ErrorSummary errors={errors} summaryRef={summaryRef} order={["voornaam", "achternaam", "email", "optIn"]} />

                <div className="bb-form__row bb-form__row--name">
                  <Field id="voornaam" label="Voornaam" error={errors.voornaam} required>
                    <TextInput id="voornaam" value={v.voornaam} onChange={set("voornaam")} onBlur={blur("voornaam")} error={errors.voornaam} autoComplete="given-name" />
                  </Field>
                  <Field id="tussenvoegsel" label="Tussenvoegsel" hideOpt>
                    <TextInput id="tussenvoegsel" value={v.tussenvoegsel} onChange={set("tussenvoegsel")} autoComplete="additional-name" />
                  </Field>
                  <Field id="achternaam" label="Achternaam" error={errors.achternaam} required>
                    <TextInput id="achternaam" value={v.achternaam} onChange={set("achternaam")} onBlur={blur("achternaam")} error={errors.achternaam} autoComplete="family-name" />
                  </Field>
                </div>

                <Field id="email" label="E‑mailadres" error={errors.email} required>
                  <TextInput id="email" type="email" value={v.email} onChange={set("email")} onBlur={blur("email")} error={errors.email} autoComplete="email" placeholder="naam@voorbeeld.nl" />
                </Field>

                <div className="bb-nieuwsbrief__opt">
                  <CheckRow id="optIn" checked={v.optIn} onChange={set("optIn")} error={errors.optIn} hint={BB_NIEUWSBRIEF_UITLEG}>
                    Ik ontvang graag updates over Buiten Buddies per e‑mail <span className="bb-field__req" aria-hidden="true">*</span>
                  </CheckRow>
                </div>

                {status === "senderror" && <SendError onRetry={send} />}

                <div className="bb-form__foot">
                  <Button variant="yellow" type="submit" disabled={status === "sending"}>
                    {status === "sending" ? "Versturen\u2026" : "Schrijf me in"} <Icon name="arrowRight" />
                  </Button>
                  <p className="bb-form__note">Velden met een <span className="bb-field__req">*</span> zijn verplicht.</p>
                </div>
              </form>
            )}
          </div>
          <p className="bb-formpage__privacy">
            We gebruiken je gegevens alleen om je de updates te sturen. Lees hoe we dat doen in ons{" "}
            <a href={window.bbHref("privacybeleid")} onClick={(e) => { e.preventDefault(); go("privacybeleid"); }}>Privacybeleid</a>.
          </p>
        </Container>
      </section>
    </main>
  );
}

/* ---------------- pagina ---------------- */
function MeedoenPage({ tab = "jongeren", go }) {
  const [active, setActive] = useState(tab);
  useEffect(() => { setActive(tab); }, [tab]);

  const pick = (t) => {
    setActive(t);
    const target = t === "beheerder" ? "meedoen-beheerder" : "meedoen";
    if (location.hash.replace("#", "") !== target) location.hash = target;
  };
  const onTabKey = (e) => {
    if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
    e.preventDefault();
    pick(active === "jongeren" ? "beheerder" : "jongeren");
  };

  const tabs = [
    ["jongeren", "Buurtapp", "Voor kinderen en ouders"],
    ["beheerder", "Groepsbeheerder", "Interesse tonen"],
  ];

  return (
    <main id="main" tabIndex="-1" data-screen-label="Meedoen">
      <section className="bb-section bb-section--cream bb-formpage__head" aria-labelledby="meedoen-heading">
        <Container>
          <FadeUp>
            <div className="bb-eyebrow">
              <span className="dot" aria-hidden="true"></span>
              Meedoen
            </div>
          </FadeUp>
          <FadeUp delay={80}>
            <h1 id="meedoen-heading">
              {active === "beheerder" ? "Ook groepsbeheerder worden?" : "Meld je kind of jezelf aan"}
            </h1>
          </FadeUp>
          <FadeUp delay={150}>
            <p className="bb-formpage__intro">
              {active === "beheerder"
                ? "Heb je interesse om groepsbeheerder te worden in jouw buurt? Laat het ons weten, dan nemen we binnen vijf werkdagen contact met je op. Je hoeft nog nergens ja op te zeggen: we vertellen je eerst rustig wat het inhoudt en helpen je van begin tot eind op weg."
                : "In een paar stappen zit je kind in de buurtapp. Aanmelden duurt minder dan 2 minuten."}
            </p>
          </FadeUp>
        </Container>
      </section>

      <section className="bb-section bb-section--alt bb-formpage__body">
        <Container>
          <div className="bb-tabs" role="tablist" aria-label="Kies een aanmeldformulier" onKeyDown={onTabKey}>
            {tabs.map(([key, label, onder]) => (
              <button
                key={key} type="button" role="tab" id={"tab-" + key}
                className={"bb-tab" + (active === key ? " is-on" : "")}
                aria-selected={active === key}
                aria-controls={"panel-" + key}
                tabIndex={active === key ? 0 : -1}
                onClick={() => pick(key)}
              >
                <span className="bb-tab__label">{label}</span>
                <span className="bb-tab__onder">{onder}</span>
              </button>
            ))}
          </div>

          <div
            className="bb-form-card"
            role="tabpanel"
            id={"panel-" + active}
            aria-labelledby={"tab-" + active}
            tabIndex={-1}
          >
            <h2 className="bb-formpage__tabtitel">
              {active === "beheerder" ? "Meld alleen jezelf aan" : "Meld jouw kind aan"}
            </h2>
            {active === "beheerder" ? <SignupBeheerder go={go} /> : <SignupJongeren go={go} />}
          </div>

          <p className="bb-formpage__privacy">
            We gebruiken je gegevens alleen om deelname aan Buiten Buddies mogelijk te maken. Lees hoe
            we dat doen in ons{" "}
            <a href={window.bbHref("privacybeleid")} onClick={(e) => { e.preventDefault(); go("privacybeleid"); }}>Privacybeleid</a>.
          </p>

          <DeelBlok
            kop="Wie neem jij mee naar buiten?"
            tekst="Hoe meer buren meedoen, hoe meer kinderen er samen buiten staan. Deel Buiten Buddies met je straat, je buurtgroep, de sportvereniging of de klas van je kind."
          />
        </Container>
      </section>
    </main>
  );
}

Object.assign(window, {
  MeedoenPage, NieuwsbriefPage, FooterSignup, FootField, SignupJongeren, SignupBeheerder, ThankYou, ConfettiRain,
  Field, TextInput, RadioGroup, CheckRow, ErrorSummary, bbSubmitForm, bbMeldPerMail, bbNieuwsbriefUitContact,
  bbIsVoornaam, bbIsPhone, BB_VOORNAAM_FOUT, BB_VOORNAAM_KIND_FOUT,
  WhatsAppLeeftijdModal, bbWa13Nodig, BB_WA13_AAN, BB_WA13_MINIMUM,
});
