// Homepage sections — stats, 3-step explainer, media, testimonials, award, final CTA

/* ================= STATS ================= */
// Eigen icoonset voor deze sectie: een rijtje huizen (buurten), drie
// jongeren (jongens en meisjes) en een kalender met dagen.
const STAT_GLYPHS = {
  buurten: (
    <g>
      <path d="M2 12.6l4.1-3.4 4.1 3.4" />
      <path d="M3.4 12.6V20h5.4v-7.4" />
      <path d="M11.8 10l5.1-4 5.1 4" />
      <path d="M13.3 10v10h7.2V10" />
      <path d="M16.1 20v-3.9h1.8V20" />
      <path d="M1 20h22" />
    </g>
  ),
  jongeren: (
    <g>
      <circle cx="5.4" cy="6.6" r="2.2" />
      <path d="M2.6 20v-4.4a2.8 2.8 0 0 1 5.6 0V20" />
      <circle cx="12" cy="5.4" r="2.4" />
      <path d="M9 20l3-9.2 3 9.2" />
      <circle cx="18.6" cy="6.6" r="2.2" />
      <path d="M15.8 20v-4.4a2.8 2.8 0 0 1 5.6 0V20" />
    </g>
  ),
  maanden: (
    <g>
      <rect x="2.6" y="5.4" width="18.8" height="15.6" rx="2.4" />
      <path d="M2.6 10.4h18.8" />
      <path d="M7.6 2.8v4M16.4 2.8v4" />
      <circle cx="7.6" cy="14.2" r="1.1" fill="currentColor" stroke="none" />
      <circle cx="12" cy="14.2" r="1.1" fill="currentColor" stroke="none" />
      <circle cx="16.4" cy="14.2" r="1.1" fill="currentColor" stroke="none" />
      <circle cx="7.6" cy="17.8" r="1.1" fill="currentColor" stroke="none" />
      <circle cx="12" cy="17.8" r="1.1" fill="currentColor" stroke="none" />
    </g>
  ),
};

function StatGlyph({ name, size = 22 }) {
  return (
    <svg
      viewBox="0 0 24 24" width={size} height={size}
      fill="none" stroke="currentColor" strokeWidth="1.8"
      strokeLinecap="round" strokeLinejoin="round"
      aria-hidden="true" focusable="false"
    >
      {STAT_GLYPHS[name]}
    </svg>
  );
}

/* Voetbal in huisstijl: witte vlakken met groene vijfhoeken. */
function BrandBall({ size = 40 }) {
  const PENT = "M0 -3.4 L3.23 -1.05 L2 2.75 L-2 2.75 L-3.23 -1.05 Z";
  const patches = [
    [17.76, 4.07, 36], [21.32, 15.03, 108], [12, 21.8, 180],
    [2.68, 15.03, 252], [6.24, 4.07, 324],
  ];
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} aria-hidden="true" focusable="false">
      <defs>
        <clipPath id="bbBallClip"><circle cx="12" cy="12" r="9.5" /></clipPath>
      </defs>
      <g clipPath="url(#bbBallClip)">
        <circle cx="12" cy="12" r="9.5" fill="#FFFFFF" />
        <g fill="var(--bb-green-forest)">
          <path d="M12 8 L15.8 13.24 L14.35 15.24 L9.65 15.24 L8.2 13.24 Z" />
          {patches.map(([x, y, r]) => (
            <path key={x + "-" + y} d={PENT} transform={"translate(" + x + "," + y + ") rotate(" + r + ")"} />
          ))}
        </g>
        <g stroke="var(--bb-green-dark)" strokeWidth="0.7" strokeLinecap="round" fill="none" opacity="0.45">
          <path d="M12 8V2.5M15.8 13.24L21.04 9.06M14.35 15.24L17.58 19.69M9.65 15.24L6.42 19.69M8.2 13.24L2.96 9.06" />
        </g>
      </g>
      <circle cx="12" cy="12" r="9.5" fill="none" stroke="var(--bb-green-dark)" strokeWidth="1.1" />
    </svg>
  );
}

function StatsSection({ showStickerStrip = true, tone = "alt" }) {
  const stats = [
    { icon: "buurten", to: 10, suffix: "", label: "actieve buurten" },
    { icon: "jongeren", to: 100, suffix: "+", label: "aangemelde jongeren" },
    { icon: "maanden", to: 10, suffix: "", label: "maanden actief" },
  ];
  const RING = 339.3; // omtrek van r=54
  const STEP_MS = 1500;   // ring + getal per cirkel
  // Bij de eerste cirkel springt de bal al door zodra de teller op negen staat, dus
  // net voordat de tien vol is. Dat haalt de stilstand eruit die daar te lang duurde.
  const STEP1_MS = 1350;  // eerste cirkel: bal vertrekt op negen van de tien
  const FLY_IN_MS = 950;  // bal komt van links het beeld in
  const FLY_OUT_MS = 1000; // bal verlaat het beeld naar rechts
  const wrapRef = useRef(null);
  // 0 = wachten, 1 = bal vliegt binnen, 2/3/4 = cirkel 1/2/3, 5 = bal vliegt weg, 6 = klaar
  const [step, setStep] = useState(0);

  // De cijferanimatie mag pas beginnen als het introblok nagenoeg uit beeld is
  // gescrold, anders lopen de uitschrijfregel, de bol en de cirkels door elkaar.
  useEffect(() => {
    if (!wrapRef.current || step > 0) return;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
      || document.documentElement.classList.contains("bb-rm-force");
    const intro = document.querySelector(".bb-intro__p") || document.querySelector(".bb-intro");
    const klaarOmTeStarten = () => {
      const w = wrapRef.current;
      if (!w) return false;
      const vh = window.innerHeight || 0;
      const r = w.getBoundingClientRect();
      if (r.top > vh * 0.75 || r.bottom < 0) return false;
      if (intro) {
        // de introalinea moet vrijwel uit beeld zijn: zijn bovenkant zit dan
        // in de bovenste 10% van het venster
        const ir = intro.getBoundingClientRect();
        if (ir.top > vh * 0.1) return false;
      }
      return true;
    };
    const kijk = () => {
      if (!klaarOmTeStarten()) return;
      setStep(reduceMotion ? 6 : 1);
      stop();
    };
    const stop = () => {
      window.removeEventListener("scroll", kijk);
      window.removeEventListener("resize", kijk);
    };
    window.addEventListener("scroll", kijk, { passive: true });
    window.addEventListener("resize", kijk);
    kijk();
    return stop;
  }, [step]);

  // De bal landt eerst, daarna start cirkel 1. Elke volgende cirkel start als de vorige klaar is.
  useEffect(() => {
    if (step < 1 || step > 5) return;
    const duur = step === 1 ? FLY_IN_MS : step === 2 ? STEP1_MS : step === 5 ? FLY_OUT_MS : STEP_MS;
    const t = setTimeout(() => setStep(step + 1), duur);
    return () => clearTimeout(t);
  }, [step]);

  const balKlasse = ["", "ball-in", "ball-p1", "ball-p2", "ball-p3", "ball-out"][step] || "";

  return (
    <Section tone={tone} id="stats" label="stats-heading">
      <h2 id="stats-heading" className="bb-visually-hidden" style={{ position: "absolute", left: -9999 }}>
        Statistieken
      </h2>
      <div ref={wrapRef} className={"bb-statfig-wrap" + (step > 0 ? " is-in step-" + step : "")}>
        <div className="bb-statfig">
          {balKlasse && (
            <span className={"bb-statfig__ball " + balKlasse} aria-hidden="true">
              <BrandBall size={81} />
            </span>
          )}
          {stats.map((s, i) => {
            // De eerste ring loopt al vol terwijl de bal aan komt vliegen.
            const ringAan = i === 0 ? step >= 1 : step - 1 > i;
            const active = step - 1 > i;
            return (
              <div key={s.label} className={"bb-statfig__item" + (ringAan ? " is-on" : "")}>
                <div className="bb-statfig__dial">
                  <svg className="bb-statfig__gauge" viewBox="0 0 120 120" aria-hidden="true" focusable="false">
                    <circle className="bb-statfig__track" cx="60" cy="60" r="54" />
                    <circle className="bb-statfig__ring" cx="60" cy="60" r="54" strokeDasharray={RING} />
                  </svg>
                  <div className="bb-statfig__inner">
                    <span className="bb-statfig__icon">
                      <StatGlyph name={s.icon} size={38} />
                    </span>
                    <span className="bb-statfig__num">
                      {active ? <CountUp to={s.to} suffix={s.suffix} duration={2100} /> : <span>0{s.suffix}</span>}
                    </span>
                  </div>
                </div>
                <div className="bb-statfig__label">{s.label}</div>
              </div>
            );
          })}
        </div>
        {showStickerStrip && (
          <div className="bb-stats__sub">En het groeit elke week.</div>
        )}
      </div>
    </Section>
  );
}

/* ================= 3-STEP EXPLAINER ================= */
function StepsSection({ go, withStars = true, tone = "cream" }) {
  const steps = [
    {
      n: 1,
      title: "Meld je aan",
      body: "Meld je samen met je ouder aan via het korte formulier. Het duurt minder dan twee minuten en is gratis.",
    },
    {
      n: 2,
      title: "Wij nemen contact op",
      body: "We checken even of er een groep in jouw buurt bestaat en sturen daarna de uitnodigingslink voor jouw buurtapp.",
    },
    {
      n: 3,
      title: "Afspreken maar!",
      body: "Stuur een berichtje in de buurtapp en vijf minuten later sta je samen op het veldje. Geen gedoe, gewoon doen.",
      quote: "\u201EWie gaat er mee voetballen?\u201D",
    },
  ];
  return (
    <Section tone={tone} id="hoe" label="steps-heading">
      <div className="bb-section__head">
        <FadeUp>
          <div className="bb-eyebrow">
            <span className="dot" aria-hidden="true"></span>
            In drie stappen
          </div>
        </FadeUp>
        <FadeUp delay={80}>
          <h2 id="steps-heading">Zo simpel is het</h2>
        </FadeUp>
        <FadeUp delay={160}>
          <p>Aanmelden duurt minder dan twee minuten. Daarna gaat het bijna vanzelf.</p>
        </FadeUp>
      </div>

      <div className="bb-steps3">
        {steps.map((s, i) => (
          <FadeUp key={s.n} delay={i * 110}>
            <article className="bb-step-card">
              <div className="bb-step-card__num">
                {s.n}
              </div>
              <h3>{s.title}</h3>
              {s.body && <p>{s.body}</p>}
              {s.quote && <p className="quote">{s.quote}</p>}
            </article>
          </FadeUp>
        ))}
      </div>

      <FadeUp delay={400}>
        <div style={{ textAlign: "center", marginTop: 40 }}>
          <Button variant="primary" onClick={() => go("meedoen")}>
            Doe mee! <Icon name="arrowRight" />
          </Button>
        </div>
      </FadeUp>
    </Section>
  );
}

/* ================= MEDIA CARROUSEL =================
   Nieuwe media-items komen hieronder in MEDIA_ITEMS bij; de carrousel
   rekent zelf het aantal pagina's uit (3 zichtbaar op desktop, 2 op
   tablet, 1 op mobiel) en toont pijlen en bolletjes zodra er meer dan
   één pagina is. Niets automatisch, de bezoeker bepaalt het tempo. */
const MEDIA_ITEMS = [
  {
    id: "jong023",
    source: "Jong023 Kinderkrant",
    sourceCls: "jong",
    sourceLabel: "Jong023",
    date: "sept/okt 2025",
    headline: "Minder schermpjes, meer buiten",
    summary: "Jong023 schreef over hoe jongeren via Buiten Buddies weer vaker buitenspelen, elkaar ontmoeten in de buurt en minder tijd achter schermen doorbrengen.",
    photo: "assets/photos/jong023-cover.png",
    focus: "center top",
    photoAlt: "Voorpagina van Jong023, de Haarlemse kinderkrant, met de kop Minder schermpjes, meer buiten.",
    href: "assets/media/jong023-sept25-p01.pdf",
    previewBeeld: "assets/photos/jong023-cover.png",
    cta: "Lees het artikel (pdf)",
  },
  {
    id: "initiatievencafe",
    source: "Haarlem Nieuws",
    sourceCls: "hn",
    sourceLabel: "Haarlem Nieuws",
    date: "30 oktober 2025",
    headline: "Buiten Buddies beloond tijdens Initiatievencaf\u00e9 2025",
    summary: "Tijdens het Initiatievencaf\u00e9 2025 werd Buiten Buddies beloond voor het initiatief om jongeren weer vaker buiten te laten spelen en buurtverbinding te versterken.",
    photo: "assets/photos/initiatievencafe-prijs.jpg",
    focus: "center 25%",
    photoAlt: "Sabrina Costermans met twee andere winnaars en hun cheques tijdens het Initiatievencafé 2025.",
    href: "https://haarlem.nieuws.nl/nieuws/prachtige-initiatieven-om-haarlem-nog-mooier-te-maken-beloond-met-geldprijs-tijdens-initiatievencafe-2025",
  },
  {
    id: "nh-nieuws",
    source: "NH Nieuws",
    sourceCls: "nh",
    sourceLabel: "NH Nieuws",
    date: "8 december 2025",
    headline: "\u2018Sabrina heeft een missie: de Haarlemse jeugd weer buiten laten spelen\u2019",
    quote: "Sabrina Costermans uit Haarlem maakt zich zorgen. Ze ziet, als ouder en als pedagoog, dat het niet goed gaat met de jeugd. Haar oplossing: tieners moeten weer de straat op, buitenspelen.",
    photo: "assets/photos/nh-nieuws-sabrina.jpg",
    focus: "center 30%",
    href: "https://www.nhnieuws.nl/nieuws/355015/sabrina-47-heeft-een-missie-de-haarlemse-jeugd-weer-buiten-laten-spelen",
  },
  {
    id: "haarlems-dagblad",
    source: "Haarlems Dagblad",
    sourceCls: "hd",
    sourceLabel: "Haarlems Dagblad",
    date: "13 december 2025",
    headline: "\u2018De spellen verzinnen de kinderen zelf\u2019",
    summary: "Een reportage over hoe jongeren in Haarlemse buurten elkaar weer opzoeken om buiten te spelen, en over de zoektocht naar ouders die een buurtapp willen beheren.",
    photo: "assets/photos/haarlems-dagblad-sabrina-bal.jpg",
    focus: "center 35%",
    route: "artikel-haarlems-dagblad",
    cta: "Lees het artikel uit de krant",
  },
  {
    id: "noord-aan-zet",
    source: "Noord aan Zet",
    date: "december 2025",
    headline: "Gekozen door de buurt: 1.233 stemmen",
    quote: "Gemeente Haarlem kent \u20ac 5.000 budget toe aan Buiten Buddies voor uitvoering in 2026. Dit bevestigde wat we in de praktijk al zagen: een simpel idee kan een groot verschil maken in een buurt.",
    photo: "assets/photos/wijk-aan-zet-logo.png",
    photoAlt: "Het logo van het programma Wijk aan Zet.",
    photoFit: "contain",
    href: "https://noordaanzet.nl/zuid-noord/",
    cta: "Bekijk Noord aan Zet",
  },
  {
    id: "leidsebuurtkrant",
    source: "Leidsebuurtkrant Haarlem",
    date: "lente 2026",
    headline: "Buiten Buddies in de Leidsebuurtkrant",
    summary: "Buiten Buddies verscheen in de lente-editie van de Leidsebuurtkrant Haarlem, met aandacht voor hoe buurtapps jongeren weer stimuleren om samen naar buiten te gaan.",
    photo: "assets/photos/leidsebuurtkrant-groep.jpg",
    focus: "center 30%",
    photoAlt: "Twee moeders en vijf kinderen met een basketbal op een schoolplein in de Leidsebuurt.",
    href: "assets/photos/leidsebuurtkrant-pagina.jpg",
    cta: "Bekijk de krantpagina",
  },
];

function NewsCard({ a, tabbable = true, go }) {
  const intern = !!a.route;
  return (
    <a
      className="bb-news-card"
      href={intern ? window.bbHref(a.route) : a.href}
      target={intern ? undefined : "_blank"}
      rel={intern ? undefined : "noopener noreferrer"}
      tabIndex={tabbable ? 0 : -1}
      onClick={intern
        ? (e) => { e.preventDefault(); if (go) go(a.route); else location.hash = a.route; }
        : window.bbPreviewKlik(a.href, a.headline, a.previewBeeld)}
    >
      <div className={"bb-news-card__photo" + (a.photoFit === "contain" ? " bb-news-card__photo--contain" : "")}>
        <img src={a.photo} alt={a.photoAlt || ""} style={a.focus ? { objectPosition: a.focus } : null} />
        {a.sourceLabel && (
          <span className={"bb-news-card__source bb-news-card__source--" + a.sourceCls}>
            {a.sourceLabel}
          </span>
        )}
      </div>
      <div className="bb-news-card__body">
        <div className="bb-news-card__meta">
          <span>{a.source}</span>
          <span className="dot" aria-hidden="true"></span>
          <span>{a.date}</span>
        </div>
        <h3>{a.headline}</h3>
        {a.quote && <blockquote>{a.quote}</blockquote>}
        {a.summary && <p className="bb-news-card__sum">{a.summary}</p>}
        <span className="bb-news-card__cta">
          {a.cta || "Lees het artikel"} <Icon name="arrowRight" size={14} />
        </span>
      </div>
    </a>
  );
}

function MediaSection({ tone, go }) {
  const articles = MEDIA_ITEMS;
  const [perView, setPerView] = useState(2);
  const [page, setPage] = useState(0);
  const [stap, setStap] = useState(0);
  const touchX = useRef(null);
  const trackRef = useRef(null);

  // Drie kaarten pas vanaf 1280 px: bij de grotere leestekst blijven de kaarten
  // anders te smal. Nooit meer kaarten per pagina dan er artikelen zijn.
  useEffect(() => {
    const calc = () => setPerView(Math.min(
      articles.length,
      window.innerWidth >= 1280 ? 3 : window.innerWidth >= 820 ? 2 : 1
    ));
    calc();
    window.addEventListener("resize", calc);
    return () => window.removeEventListener("resize", calc);
  }, [articles.length]);

  // Schuifafstand in pixels uit de echte kaartbreedte, in plaats van een
  // percentageberekening. Zo valt de laatste pagina altijd exact binnen de kantlijn.
  useEffect(() => {
    const meet = () => {
      const t = trackRef.current;
      const eerste = t && t.firstElementChild;
      if (!eerste) return;
      const gap = parseFloat(getComputedStyle(t).columnGap) || 24;
      setStap(perView * (eerste.getBoundingClientRect().width + gap));
    };
    meet();
    const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(meet) : null;
    if (ro && trackRef.current) ro.observe(trackRef.current);
    window.addEventListener("resize", meet);
    return () => { if (ro) ro.disconnect(); window.removeEventListener("resize", meet); };
  }, [perView, articles.length]);

  const pages = Math.max(1, Math.ceil(articles.length / perView));
  useEffect(() => { setPage((p) => Math.min(p, pages - 1)); }, [pages]);

  const goTo = (p) => setPage(Math.max(0, Math.min(pages - 1, p)));
  const onKeyDown = (e) => {
    if (e.key === "ArrowRight") { e.preventDefault(); goTo(page + 1); }
    if (e.key === "ArrowLeft")  { e.preventDefault(); goTo(page - 1); }
  };
  const onTouchStart = (e) => { touchX.current = e.touches[0].clientX; };
  const onTouchEnd = (e) => {
    if (touchX.current == null) return;
    const dx = e.changedTouches[0].clientX - touchX.current;
    if (Math.abs(dx) > 45) goTo(page + (dx < 0 ? 1 : -1));
    touchX.current = null;
  };

  return (
    <section className="bb-section bb-section--news" id="pers" aria-labelledby="media-heading" data-screen-label="In de media">
      <Container>
        <div className="bb-section__head">
          <FadeUp>
            <div className="bb-eyebrow">
              <span className="dot" aria-hidden="true"></span>
              In de media
            </div>
          </FadeUp>
          <FadeUp delay={80}>
            <h2 id="media-heading">Buiten Buddies in de media:</h2>
          </FadeUp>
          <FadeUp delay={160}>
            <p>Lokale media en de Gemeente Haarlem schreven over het idee. Lees mee waarom buurten het oppakken.</p>
          </FadeUp>
        </div>

        <FadeUp delay={200}>
          <div
            className="bb-carousel"
            role="group"
            aria-roledescription="carrousel"
            aria-label="Buiten Buddies in de media"
            onKeyDown={onKeyDown}
          >
            <div
              className="bb-carousel__viewport"
              onTouchStart={onTouchStart}
              onTouchEnd={onTouchEnd}
            >
              <div
                className="bb-carousel__track"
                ref={trackRef}
                style={{
                  "--per-view": perView,
                  transform: "translateX(" + (-page * stap) + "px)",
                }}
              >
                {articles.map((a, i) => {
                  const visible = i >= page * perView && i < page * perView + perView;
                  return (
                    <div
                      key={a.id || a.source}
                      className="bb-carousel__slide"
                      role="group"
                      aria-roledescription="slide"
                      aria-label={a.source + ": " + a.headline}
                      aria-hidden={!visible}
                    >
                      <NewsCard a={a} tabbable={visible} go={go} />
                    </div>
                  );
                })}
              </div>
            </div>

            {pages > 1 && (
              <div className="bb-carousel__controls">
                <button
                  type="button"
                  className={"bb-carousel__arrow" + (page === pages - 1 ? " is-glow" : "")}
                  onClick={() => goTo(page - 1)}
                  disabled={page === 0}
                  aria-label="Vorige artikelen"
                >
                  <Icon name="arrowLeft" size={20} />
                </button>
                <div className="bb-carousel__dots">
                  {Array.from({ length: pages }).map((_, p) => (
                    <button
                      key={p}
                      type="button"
                      className="bb-carousel__dot"
                      onClick={() => goTo(p)}
                      aria-current={p === page ? "true" : undefined}
                      aria-label={"Ga naar positie " + (p + 1) + " van " + pages}
                    >
                      <span aria-hidden="true"></span>
                    </button>
                  ))}
                </div>
                <button
                  type="button"
                  className={"bb-carousel__arrow" + (page < pages - 1 ? " is-glow" : "")}
                  onClick={() => goTo(page + 1)}
                  disabled={page === pages - 1}
                  aria-label="Volgende artikelen"
                >
                  <Icon name="arrowRight" size={20} />
                </button>
              </div>
            )}
          </div>
        </FadeUp>

        <FadeUp delay={280}>
          <div className="bb-carousel__more">
            <Button variant="secondary" onClick={() => go("in-de-media")}>
              Bekijk alle media <Icon name="arrowRight" />
            </Button>
          </div>
        </FadeUp>
      </Container>
    </section>
  );
}

/* ================= TESTIMONIALS ================= */
function TestimonialsSection({ tone = "cream" }) {
  const items = [
    {
      quote: "Mijn kinderen spelen nu bijna elke dag buiten, het is geweldig dat ze dat helemaal zelf kunnen regelen en minder tijd achter hun schermen doorbrengen.",
      who: "Lindsay",
      role: "moeder uit Houtvaartkwartier, Haarlem",
      photo: "assets/photos/lindsay-citaat-1.jpg",
      imgStyle: { objectPosition: "center 12%" },
      alt: "Lindsay, moeder uit Houtvaartkwartier in Haarlem",
    },
    {
      quote: "Buiten Buddies maakt de buurt meteen een stuk gezelliger. Ik spreek nu veel makkelijker af met anderen en maak snel nieuwe vrienden.",
      who: "Charlie, 13 jaar",
      role: "Haarlem Zuid-West",
      photo: "assets/photos/charlie-citaat-2.jpg",
      imgStyle: { objectPosition: "center 20%", transform: "scale(1.3)" },
      alt: "Charlie, 13 jaar, deelnemer uit Haarlem Zuid-West",
    },
    {
      quote: "Door een appgroep aan te maken en af en toe een activiteit voor te stellen, help je de kinderen op weg, zodat ze daarna zelf blijven afspreken.",
      who: "Mirjam",
      role: "groepsbeheerder Haarlem-Zuid",
      photo: "assets/photos/mirjam-citaat-3.jpg",
      imgStyle: { objectPosition: "52% 15%", transform: "scale(1.08)" },
      alt: "Mirjam, groepsbeheerder in Haarlem-Zuid",
    },
  ];
  return (
    <Section tone={tone} id="ervaringen" label="testimonials-heading">
      <div className="bb-section__head">
        <FadeUp>
          <div className="bb-eyebrow">
            <span className="dot" aria-hidden="true"></span>
            Succesverhalen
          </div>
        </FadeUp>
        <FadeUp delay={80}>
          <h2 id="testimonials-heading">Wat ouders en jongeren ervaren</h2>
        </FadeUp>
      </div>

      <div className="bb-testimonials">
        {items.map((it, i) => (
          <FadeUp key={it.who} delay={i * 110}>
            <article className="bb-quote-card">
              <span className="mark" aria-hidden="true">&ldquo;</span>
              <div className="bb-quote-card__head">
                {it.photo && (
                  <div className="bb-quote-card__avatar">
                    <img src={it.photo} alt={it.alt} loading="lazy" style={it.imgStyle} />
                  </div>
                )}
              </div>
              <blockquote>{it.quote}</blockquote>
              <div className="who">
                <div>
                  {it.who}
                  <small>{it.role}</small>
                </div>
              </div>
            </article>
          </FadeUp>
        ))}
      </div>
    </Section>
  );
}

/* ================= AWARD BANNER ================= */
function Trophy() {
  return (
    <svg className="bb-trophy" viewBox="0 0 140 175" aria-hidden="true">
      <defs>
        <linearGradient id="bbGold" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%"  stopColor="#FCE372" />
          <stop offset="55%" stopColor="#FAD728" />
          <stop offset="100%" stopColor="#E8B800" />
        </linearGradient>
      </defs>
      {/* handles */}
      <path d="M28 36 Q10 36 10 58 Q10 78 28 78" fill="none" stroke="url(#bbGold)" strokeWidth="7" strokeLinecap="round" />
      <path d="M112 36 Q130 36 130 58 Q130 78 112 78" fill="none" stroke="url(#bbGold)" strokeWidth="7" strokeLinecap="round" />
      {/* cup body */}
      <path d="M20 30 L120 30 L114 95 Q114 108 102 108 L38 108 Q26 108 26 95 Z"
            fill="url(#bbGold)" stroke="#064530" strokeWidth="3" />
      {/* engraved band met €5.000 */}
      <rect x="30" y="62" width="80" height="24" rx="3" fill="#064530" />
      <text x="70" y="79" textAnchor="middle"
            fontFamily="Fredoka One, sans-serif" fontSize="15" fontWeight="700"
            fill="#FAD728">€ 5.000</text>
      {/* small star top */}
      <path d="M70 42 L72.5 48 L79 48.6 L74 53 L75.5 59.5 L70 56 L64.5 59.5 L66 53 L61 48.6 L67.5 48 Z" fill="#FFF" opacity="0.9" />
      {/* shine */}
      <path d="M30 38 L36 38 L34 70 L28 70 Z" fill="#FFF" opacity="0.45" />
      {/* stem */}
      <path d="M55 108 L85 108 L82 124 L58 124 Z" fill="url(#bbGold)" stroke="#064530" strokeWidth="3" />
      {/* base */}
      <rect x="32" y="124" width="76" height="11" rx="2" fill="#064530" />
      <rect x="26" y="135" width="88" height="7"  rx="2" fill="#064530" />
      {/* shadow */}
      <ellipse cx="70" cy="155" rx="48" ry="6" fill="#000" opacity="0.18" />
    </svg>
  );
}

function bbSmalScherm() {
  return typeof window !== "undefined" && window.matchMedia("(max-width: 719px)").matches;
}

function ConfettiBurst() {
  // Canvas-based confetti: echte fysica (zwaartekracht, drag, tilt-flutter).
  // Kanon-burst links onderaan en rechts (180ms later). 110 deeltjes totaal.
  // Levensduur 7s — laatste 3s vervaagt het naar niets.
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches ||
        document.documentElement.classList.contains("bb-rm-force")) {
      return;
    }

    const w = canvas.offsetWidth;
    const h = canvas.offsetHeight;
    if (w === 0 || h === 0) return;
    // Smalle schermen: minder pixels en minder deeltjes, zodat de burst vloeiend blijft.
    const smal = w < 520;
    const dpr = Math.min(window.devicePixelRatio || 1, smal ? 1.5 : 2);
    canvas.width  = w * dpr;
    canvas.height = h * dpr;
    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);

    // Brand-paletten: groen + cream + wit als hoofdkleuren, geel als spaarzaam accent.
    const palette = ["#94B143", "#6FB561", "#8DBB4E", "#AFC600", "#FDFDF5", "#FFFFFF"];
    const accent  = "#FAD728";
    const particles = [];
    const rand = (a, b) => a + Math.random() * (b - a);

    function kleur() {
      return Math.random() < 0.12 ? accent : palette[Math.floor(Math.random() * palette.length)];
    }
    function vorm() {
      return Math.random() < 0.55 ? "rect" : Math.random() < 0.5 ? "circle" : "ribbon";
    }

    function spawn(originX, originY, baseAngle, count) {
      for (let i = 0; i < count; i++) {
        const angle = baseAngle + rand(-0.5, 0.5);
        const speed = rand(6.5, 12);
        particles.push({
          x: originX, y: originY,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          gravity: rand(0.1, 0.16),
          drag: 0.993,
          size: rand(4, 8.5),
          color: kleur(),
          shape: vorm(),
          rotation:    rand(0, Math.PI * 2),
          spinSpeed:   rand(-0.16, 0.16),
          tilt:        rand(0, Math.PI * 2),
          tiltSpeed:   rand(0.04, 0.11),
          zwaai:       0.18,
        });
      }
    }

    // Mobiel: geen kanonnen, maar rustig dwarrelende confetti die van bovenaf valt.
    function regen(count) {
      for (let i = 0; i < count; i++) {
        particles.push({
          x: rand(-10, w + 10),
          y: rand(-110, -6),
          vx: rand(-0.35, 0.35),
          vy: rand(0.45, 1.05),
          gravity: rand(0.006, 0.016),
          drag: 0.9995,
          size: rand(3.5, 8),
          color: kleur(),
          shape: vorm(),
          rotation:    rand(0, Math.PI * 2),
          spinSpeed:   rand(-0.06, 0.06),
          tilt:        rand(0, Math.PI * 2),
          tiltSpeed:   rand(0.02, 0.055),
          zwaai:       rand(0.2, 0.6),   // horizontale dwarreling
        });
      }
    }

    const timers = [];
    if (smal) {
      // Dichte opening die golf na golf uitdunt: een explosie die rustig wegsterft.
      const golven = [[0, 44], [450, 30], [1000, 24], [1700, 19], [2600, 15], [3600, 11], [4700, 8], [5900, 6]];
      golven.forEach(([na, aantal]) => {
        if (na === 0) regen(aantal);
        else timers.push(setTimeout(() => regen(aantal), na));
      });
    } else {
      // Links: 60°-up-right ; Rechts (later): 60°-up-left. Rustige salvo's in plaats van
      // één harde knal, met daarna zachte naschokken zodat het niet abrupt stopt.
      spawn(30, h - 30, -Math.PI / 3, 34);
      timers.push(setTimeout(() => spawn(w - 30, h - 30, -2 * Math.PI / 3, 34), 260));
      [[1200, 15], [2100, 12], [3000, 9]].forEach(([na, aantal]) => {
        timers.push(setTimeout(() => spawn(30, h - 30, -Math.PI / 3, aantal), na));
        timers.push(setTimeout(() => spawn(w - 30, h - 30, -2 * Math.PI / 3, aantal), na + 240));
      });
      // De kanonnen zijn binnen twee seconden uitgevallen; deze traag zakkende laag
      // vult de rest van de tijd, zodat er tot het uitdoven iets in beeld blijft.
      [[700, 14], [1500, 13], [2300, 12], [3100, 11], [3900, 9], [4700, 8], [5500, 6], [6300, 5], [7100, 4]]
        .forEach(([na, aantal]) => timers.push(setTimeout(() => regen(aantal), na)));
    }

    const startTime  = performance.now();
    const TOTAL      = smal ? 13000 : 12000;
    const FADE_START = smal ? 8500 : 7500;

    let raf;
    function frame() {
      const now     = performance.now();
      const elapsed = now - startTime;

      ctx.clearRect(0, 0, w, h);

      // Globale alpha — start op 1, fadet ease-out naar 0 in laatste 3s.
      let alpha = 1;
      if (elapsed > FADE_START) {
        const t = (elapsed - FADE_START) / (TOTAL - FADE_START);
        // Mobiel dooft gelijkmatig uit (4,5s), desktop houdt zijn snellere staart.
        alpha = Math.max(0, Math.pow(1 - t, 1.4));
      }

      for (const p of particles) {
        p.vy += p.gravity;
        p.vx *= p.drag;
        p.vy *= p.drag;
        p.x  += p.vx + (p.zwaai ? Math.sin(p.tilt) * p.zwaai : 0);
        p.y  += p.vy;
        p.rotation += p.spinSpeed;
        p.tilt     += p.tiltSpeed;

        if (p.y > h + 80 || p.x < -80 || p.x > w + 80) continue;

        // Tilt geeft een 3D-flutter effect via horizontale schaal.
        const scaleX = Math.cos(p.tilt);

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

      if (elapsed < TOTAL) {
        raf = requestAnimationFrame(frame);
      } else {
        ctx.clearRect(0, 0, w, h);
      }
    }

    raf = requestAnimationFrame(frame);
    return () => {
      cancelAnimationFrame(raf);
      timers.forEach(clearTimeout);
    };
  }, []);

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

function AwardSection({ tone = "soft" }) {
  const ref = useRef(null);
  const [played, setPlayed] = useState(false);
  const [morphed, setMorphed] = useState(false);

  useEffect(() => {
    if (!ref.current || played) return;
    const obs = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            setPlayed(true);
            obs.disconnect();
          }
        });
      },
      { threshold: 0.35 }
    );
    obs.observe(ref.current);
    return () => obs.disconnect();
  }, [played]);

  // Na count-up (3s) start de morph naar het Buiten Buddies-logo.
  // De morph zelf duurt iets langer en is rustiger dan voorheen.
  useEffect(() => {
    if (!played || morphed) return;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
      || document.documentElement.classList.contains("bb-rm-force");
    const mobiel = window.matchMedia("(max-width: 719px)").matches;
    // Mobiel: eerst de confettiregen laten uitdunnen, daarna pas de morph.
    const delay = reduceMotion ? 800 : (mobiel ? 7200 : 3200);
    const t = setTimeout(() => setMorphed(true), delay);
    return () => clearTimeout(t);
  }, [played, morphed]);

  return (
    <Section tone={tone} id="award" label="award-heading">
      <div
        ref={ref}
        className={"bb-award-banner" + (played ? " bb-award-banner--in" : "")}
      >
        {played && <ConfettiBurst />}

        <div className="bb-award-banner__grid">
          {/* TROPHY — morpht na 3s naar Buiten Buddies-logo in groene cirkel */}
          <div className={"bb-award-banner__trophy" + (morphed ? " is-morphed" : "")}>
            <div className="bb-trophy-stack">
              <div className="bb-trophy-stack__trophy"><Trophy /></div>
              <div className="bb-trophy-stack__logo-frame">
                <img
                  src="assets/logo/logo.png"
                  alt="Buiten Buddies, winnaar van Noord aan Zet"
                  aria-hidden={!morphed}
                />
              </div>
              <span className="bb-trophy-stack__flash" aria-hidden="true"></span>
            </div>
          </div>

          {/* BODY */}
          <div className="bb-award-banner__body">
            <div className="bb-eyebrow">
              <span className="dot" aria-hidden="true"></span>
              Top-idee Noord aan Zet 2025
            </div>
            <h3 id="award-heading">
              Gekozen door{" "}
              <span className="bb-award-banner__count">
                {played ? <CountUp to={1233} duration={bbSmalScherm() ? 3800 : 3000} /> : "0"}
              </span>{" "}
              buurtbewoners
            </h3>
            <p>
              Gemeente Haarlem kent <strong>€ 5.000</strong> toe aan Buiten Buddies
              voor uitvoering in 2026.
            </p>
            <div className="bb-award-banner__partner">
              <span>Mogelijk gemaakt door</span>
              <a className="bb-logolink" href="https://haarlem.nl/" target="_blank" rel="noopener noreferrer" aria-label="Naar de website van de gemeente Haarlem, opent in een nieuw tabblad">
                <img src="assets/logo/gemeente-haarlem.png" alt="Gemeente Haarlem" />
              </a>
            </div>
          </div>

          {/* CERTIFICATE THUMBNAIL */}
          <div className="bb-award-banner__cert">
            <img src="assets/award/geldprijs-noord-aan-zet.jpg" alt="Certificaat Noord aan Zet voor Buiten Buddies" />
          </div>
        </div>
      </div>
    </Section>
  );
}

/* ================= FINAL CTA ================= */
function FinalCtaSection({ go }) {
  return (
    <Section tone="dark" id="cta" label="final-heading">
      <FadeUp>
        <div className="bb-final">
          <h2 id="final-heading">Klaar om mee te doen?</h2>
          <p>
            Buiten Buddies is gratis en voor jongeren met of zonder WhatsApp. Meld je kind
            aan of word zelf groepsbeheerder in jouw buurt.
          </p>
          <div className="bb-final__cta-row">
            <Button variant="yellow" onClick={() => go("meedoen")}>
              Meld mijn kind aan <Icon name="arrowRight" />
            </Button>
            <Button variant="ghost" onClick={() => go("meedoen-beheerder")}>
              Word groepsbeheerder
            </Button>
          </div>
        </div>
      </FadeUp>
    </Section>
  );
}

Object.assign(window, {
  StatsSection, StepsSection, MediaSection, NewsCard, MEDIA_ITEMS, TestimonialsSection,
  AwardSection, FinalCtaSection,
});
