﻿const { useState, useMemo, useEffect, useCallback, useRef, createContext, useContext } = React;

// ─── i18n context ──────────────────────────────────────────────────
const LangContext = createContext('nl');
const CmsContext = createContext({ active: false, dirty: false, setDirty: () => {}, dataRef: null, metaVersion: 0, bumpMeta: () => {} });
const RouteContext = createContext({ path: '/', navigate: () => {} });

function useRoute() {
  const [path, setPath] = useState(window.location.pathname);
  useEffect(() => {
    const onPop = () => setPath(window.location.pathname);
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);
  const navigate = useCallback((to) => {
    window.history.pushState(null, '', to);
    setPath(to);
    window.scrollTo(0, 0);
  }, []);
  return { path, navigate };
}

function useNavigate() {
  return useContext(RouteContext).navigate;
}

const UI = {
  nl: {
    nav_about: 'Over', nav_practice: 'Praktijk', nav_agenda: 'Agenda',
    nav_partners: 'Partners', nav_contact: 'Contact',
    hero_tag: 'Ensemble · Onderzoeksplatform · Antwerpen',
    hero_next: 'Volgend concert', hero_no_events: 'Geen aankomende concerten',
    hero_subscribe: 'Schrijf je in voor de nieuwsbrief om als eerste te horen.',
    hero_agenda: 'Bekijk agenda',
    section_projects: 'Projecten', section_projects_title_1: 'Wat we ', section_projects_title_2: 'maken', section_projects_title_3: '.',
    section_about: 'Over B.O.X', section_about_title: '',
    section_practice: 'Wat we doen', section_practice_title_1: 'Vier sporen, één ',
    section_practice_title_2: 'praktijk', section_practice_title_3: '.',
    section_agenda: 'Agenda & archief',
    section_agenda_title_1: 'Wat ', section_agenda_title_2: 'komt',
    section_agenda_title_3: ', en wat ', section_agenda_title_4: 'was',
    section_agenda_title_5: '.',
    tab_upcoming: 'Aankomend', tab_history: 'Geschiedenis',
    no_upcoming: 'Nog geen aankomende concerten — schrijf je in voor de nieuwsbrief.',
    tickets: 'Tickets & info', fragments: 'Bekijk fragmenten',
    with: 'm.m.v.', projects: 'projecten', project: 'project',
    section_partners: 'Met wie',
    section_partners_title_1: 'We werken ', section_partners_title_2: 'samen',
    section_partners_title_3: '.',
    partners_intro: 'Een productie maken doe je niet alleen. Een greep uit de huizen, festivals en partners waarmee B.O.X optrekt.',
    footer_contact: 'Contact',
    footer_general: 'Algemeen', footer_follow: 'Volg ons',
    footer_press: 'Pers & boekingen', footer_presskit: 'Persmap (PDF)',
    footer_bookings: 'Boekingen', footer_photos: "Foto's",
    cms_save: 'Opslaan', cms_saving: 'Opslaan...', cms_saved: 'Opgeslagen!',
    cms_logout: 'Uitloggen', cms_editing: 'Bewerkmodus',
    cms_add_event: '+ Nieuw evenement', cms_edit: 'Bewerken', cms_delete: 'Verwijderen',
    cms_cancel: 'Annuleren', cms_event_title: 'Evenement bewerken',
    cms_new_event: 'Nieuw evenement',
    section_listen: 'Luisteren', section_listen_title_1: '', section_listen_title_2: '', section_listen_title_3: '',
    section_gallery: 'Beelden', section_gallery_title_1: 'Momenten, ', section_gallery_title_2: 'gevangen', section_gallery_title_3: '.',
    watch_video: 'Bekijk video',
    nav_listen: 'Luisteren',
    section_news: 'Nieuws', section_news_title_1: 'Wat er ', section_news_title_2: 'leeft', section_news_title_3: '.',
    section_opencalls: 'Open Calls', section_opencalls_title_1: 'Doe ', section_opencalls_title_2: 'mee', section_opencalls_title_3: '.',
    opencall_deadline: 'Deadline', opencall_status_open: 'Open', opencall_status_closed: 'Gesloten',
    opencall_apply: 'Solliciteer', opencall_more: 'Meer info', opencall_contact: 'Contact',
    news_read_more: 'Lees meer', news_open_call: 'Open Call',
    cms_add_news: '+ Nieuw bericht', cms_edit_news: 'Nieuwsbericht bewerken', cms_new_news: 'Nieuw nieuwsbericht',
    cms_add_call: '+ Nieuwe open call', cms_edit_call: 'Open call bewerken', cms_new_call: 'Nieuwe open call',
    opencall_show_more: 'Meer info ▾', opencall_show_less: 'Minder ▴',
    contact_title_1: 'Neem ', contact_title_2: 'contact', contact_title_3: ' op.',
    contact_name: 'Naam', contact_email: 'E-mail', contact_subject: 'Onderwerp',
    contact_message: 'Bericht', contact_send: 'Versturen', contact_sending: 'Versturen...',
    contact_success: 'Bericht verzonden! We nemen zo snel mogelijk contact op.',
    contact_error: 'Er ging iets mis. Probeer het opnieuw.',
    contact_subject_general: 'Algemeen', contact_subject_opencall: 'Open Call',
    contact_subject_press: 'Pers', contact_subject_collaboration: 'Samenwerking',
    cms_messages: 'Berichten', cms_no_messages: 'Geen berichten',
    cms_mark_read: 'Gelezen', cms_delete_msg: 'Verwijder',
    cms_translations: 'Vertalingen', cms_trans_source: 'bron', cms_trans_stale: 'verouderd',
    cms_no_stale: 'Geen verouderde vertalingen.', cms_all_synced: 'Alle vertalingen zijn up-to-date!',
  },
  en: {
    nav_about: 'About', nav_practice: 'Practice', nav_agenda: 'Agenda',
    nav_partners: 'Partners', nav_contact: 'Contact',
    hero_tag: 'Ensemble · Research platform · Antwerp',
    hero_next: 'Next concert', hero_no_events: 'No upcoming concerts',
    hero_subscribe: 'Subscribe to our newsletter to be the first to know.',
    hero_agenda: 'View agenda',
    section_projects: 'Projects', section_projects_title_1: 'What we ', section_projects_title_2: 'create', section_projects_title_3: '.',
    section_about: 'About B.O.X', section_about_title: '',
    section_practice: 'What we do', section_practice_title_1: 'Four tracks, one ',
    section_practice_title_2: 'practice', section_practice_title_3: '.',
    section_agenda: 'Agenda & archive',
    section_agenda_title_1: "What's ", section_agenda_title_2: 'coming',
    section_agenda_title_3: ', and what ', section_agenda_title_4: 'was',
    section_agenda_title_5: '.',
    tab_upcoming: 'Upcoming', tab_history: 'History',
    no_upcoming: 'No upcoming concerts yet — subscribe to our newsletter.',
    tickets: 'Tickets & info', fragments: 'View fragments',
    with: 'feat.', projects: 'projects', project: 'project',
    section_partners: 'With whom',
    section_partners_title_1: 'We work ', section_partners_title_2: 'together',
    section_partners_title_3: '.',
    partners_intro: "You don't make a production alone. A selection of the venues, festivals and partners B.O.X collaborates with.",
    footer_contact: 'Contact',
    footer_general: 'General', footer_follow: 'Follow us',
    footer_press: 'Press & bookings', footer_presskit: 'Press kit (PDF)',
    footer_bookings: 'Bookings', footer_photos: 'Photos',
    cms_save: 'Save', cms_saving: 'Saving...', cms_saved: 'Saved!',
    cms_logout: 'Log out', cms_editing: 'Edit mode',
    cms_add_event: '+ New event', cms_edit: 'Edit', cms_delete: 'Delete',
    cms_cancel: 'Cancel', cms_event_title: 'Edit event',
    cms_new_event: 'New event',
    section_listen: 'Listen', section_listen_title_1: '', section_listen_title_2: '', section_listen_title_3: '',
    section_gallery: 'Images', section_gallery_title_1: 'Moments, ', section_gallery_title_2: 'captured', section_gallery_title_3: '.',
    watch_video: 'Watch video',
    nav_listen: 'Listen',
    section_news: 'News', section_news_title_1: 'What’s ', section_news_title_2: 'happening', section_news_title_3: '.',
    section_opencalls: 'Open Calls', section_opencalls_title_1: 'Join ', section_opencalls_title_2: 'us', section_opencalls_title_3: '.',
    opencall_deadline: 'Deadline', opencall_status_open: 'Open', opencall_status_closed: 'Closed',
    opencall_apply: 'Apply', opencall_more: 'More info', opencall_contact: 'Contact',
    news_read_more: 'Read more', news_open_call: 'Open Call',
    cms_add_news: '+ New post', cms_edit_news: 'Edit news post', cms_new_news: 'New news post',
    cms_add_call: '+ New open call', cms_edit_call: 'Edit open call', cms_new_call: 'New open call',
    opencall_show_more: 'More info ▾', opencall_show_less: 'Less ▴',
    contact_title_1: 'Get in ', contact_title_2: 'touch', contact_title_3: '.',
    contact_name: 'Name', contact_email: 'Email', contact_subject: 'Subject',
    contact_message: 'Message', contact_send: 'Send', contact_sending: 'Sending...',
    contact_success: 'Message sent! We\'ll get back to you as soon as possible.',
    contact_error: 'Something went wrong. Please try again.',
    contact_subject_general: 'General', contact_subject_opencall: 'Open Call',
    contact_subject_press: 'Press', contact_subject_collaboration: 'Collaboration',
    cms_messages: 'Messages', cms_no_messages: 'No messages',
    cms_mark_read: 'Read', cms_delete_msg: 'Delete',
    cms_translations: 'Translations', cms_trans_source: 'source', cms_trans_stale: 'outdated',
    cms_no_stale: 'No outdated translations.', cms_all_synced: 'All translations are up to date!',
  },
  fr: {
    nav_about: 'À propos', nav_practice: 'Pratique', nav_agenda: 'Agenda',
    nav_partners: 'Partenaires', nav_contact: 'Contact',
    hero_tag: 'Ensemble · Plateforme de recherche · Anvers',
    hero_next: 'Prochain concert', hero_no_events: 'Pas de concerts à venir',
    hero_subscribe: 'Abonnez-vous à notre newsletter pour être le premier informé.',
    hero_agenda: "Voir l'agenda",
    section_projects: 'Projets', section_projects_title_1: 'Ce que nous ', section_projects_title_2: 'créons', section_projects_title_3: '.',
    section_about: 'À propos de B.O.X', section_about_title: '',
    section_practice: 'Ce que nous faisons', section_practice_title_1: 'Quatre voies, une ',
    section_practice_title_2: 'pratique', section_practice_title_3: '.',
    section_agenda: 'Agenda & archives',
    section_agenda_title_1: 'Ce qui ', section_agenda_title_2: 'vient',
    section_agenda_title_3: ', et ce qui ', section_agenda_title_4: 'était',
    section_agenda_title_5: '.',
    tab_upcoming: 'À venir', tab_history: 'Histoire',
    no_upcoming: 'Pas encore de concerts à venir — abonnez-vous à notre newsletter.',
    tickets: 'Tickets & info', fragments: 'Voir les extraits',
    with: 'avec', projects: 'projets', project: 'projet',
    section_partners: 'Avec qui',
    section_partners_title_1: 'Nous travaillons ', section_partners_title_2: 'ensemble',
    section_partners_title_3: '.',
    partners_intro: 'On ne fait pas une production seul. Une sélection des salles, festivals et partenaires avec lesquels B.O.X collabore.',
    footer_contact: 'Contact',
    footer_general: 'Général', footer_follow: 'Suivez-nous',
    footer_press: 'Presse & réservations', footer_presskit: 'Dossier de presse (PDF)',
    footer_bookings: 'Réservations', footer_photos: 'Photos',
    cms_save: 'Enregistrer', cms_saving: 'Enregistrement...', cms_saved: 'Enregistré !',
    cms_logout: 'Déconnexion', cms_editing: 'Mode édition',
    cms_add_event: '+ Nouvel événement', cms_edit: 'Modifier', cms_delete: 'Supprimer',
    cms_cancel: 'Annuler', cms_event_title: "Modifier l'événement",
    cms_new_event: 'Nouvel événement',
    section_listen: 'Écouter', section_listen_title_1: '', section_listen_title_2: '', section_listen_title_3: '',
    section_gallery: 'Images', section_gallery_title_1: 'Moments, ', section_gallery_title_2: 'capturés', section_gallery_title_3: '.',
    watch_video: 'Voir la vidéo',
    nav_listen: 'Écouter',
    section_news: 'Actualités', section_news_title_1: 'Ce qui ', section_news_title_2: 'se passe', section_news_title_3: '.',
    section_opencalls: 'Appels ouverts', section_opencalls_title_1: 'Participez ', section_opencalls_title_2: '', section_opencalls_title_3: '.',
    opencall_deadline: 'Date limite', opencall_status_open: 'Ouvert', opencall_status_closed: 'Fermé',
    opencall_apply: 'Postuler', opencall_more: 'Plus d\'info', opencall_contact: 'Contact',
    news_read_more: 'Lire la suite', news_open_call: 'Appel ouvert',
    cms_add_news: '+ Nouveau post', cms_edit_news: 'Modifier article', cms_new_news: 'Nouvel article',
    cms_add_call: '+ Nouvel appel', cms_edit_call: 'Modifier appel', cms_new_call: 'Nouvel appel',
    opencall_show_more: 'Plus d\'info ▾', opencall_show_less: 'Moins ▴',
    contact_title_1: 'Prenez ', contact_title_2: 'contact', contact_title_3: '.',
    contact_name: 'Nom', contact_email: 'E-mail', contact_subject: 'Sujet',
    contact_message: 'Message', contact_send: 'Envoyer', contact_sending: 'Envoi...',
    contact_success: 'Message envoyé ! Nous vous répondrons dès que possible.',
    contact_error: 'Une erreur est survenue. Veuillez réessayer.',
    contact_subject_general: 'Général', contact_subject_opencall: 'Open Call',
    contact_subject_press: 'Presse', contact_subject_collaboration: 'Collaboration',
    cms_messages: 'Messages', cms_no_messages: 'Aucun message',
    cms_mark_read: 'Lu', cms_delete_msg: 'Supprimer',
    cms_translations: 'Traductions', cms_trans_source: 'source', cms_trans_stale: 'obsolète',
    cms_no_stale: 'Aucune traduction obsolète.', cms_all_synced: 'Toutes les traductions sont à jour !',
  }
};

// ─── helpers ────────────────────────────────────────────────────────
const MONTHS = {
  nl: ["JAN","FEB","MRT","APR","MEI","JUN","JUL","AUG","SEP","OKT","NOV","DEC"],
  en: ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"],
  fr: ["JAN","FÉV","MAR","AVR","MAI","JUN","JUL","AOÛ","SEP","OCT","NOV","DÉC"]
};
const MONTHS_LONG = {
  nl: ["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],
  en: ["January","February","March","April","May","June","July","August","September","October","November","December"],
  fr: ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]
};

function parseISO(s) {
  if (!s) return null;
  const [y, m, d] = s.split("-").map(Number);
  return new Date(y, m - 1, d);
}

function isUpcoming(ev, today) {
  const start = parseISO(ev.date);
  const end = parseISO(ev.endDate) || start;
  const endOfDay = new Date(end.getFullYear(), end.getMonth(), end.getDate(), 23, 59, 59);
  return endOfDay >= today;
}

function useT() {
  const lang = useContext(LangContext);
  return (key) => {
    const val = UI[lang]?.[key];
    if (val !== undefined && val !== null) return val;
    const nlVal = UI.nl[key];
    if (nlVal !== undefined && nlVal !== null) return nlVal;
    return key;
  };
}

function useLang() {
  return useContext(LangContext);
}

function txt(field, lang) {
  if (!field) return '';
  if (typeof field === 'string') return field;
  return field[lang] || field.nl || '';
}

// ─── Translation drift helpers ────────────────────────────────────
const LANGS = ['nl', 'en', 'fr'];
const LANG_NAMES = { nl: 'Nederlands', en: 'English', fr: 'Français' };

function quickHash(str) {
  if (!str) return '0';
  let h = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return (h >>> 0).toString(16);
}

function updateTranslationMeta(data, fieldPath, lang, value) {
  if (!data || !fieldPath) return;
  if (!data._translationMeta) data._translationMeta = {};
  if (!data._translationMeta[fieldPath]) data._translationMeta[fieldPath] = {};
  data._translationMeta[fieldPath][lang] = {
    updatedAt: new Date().toISOString(),
    hash: quickHash(value)
  };
}

function getStaleLanguages(data, fieldPath, currentLang) {
  const result = {};
  if (!data?._translationMeta?.[fieldPath]) {
    LANGS.forEach(l => { if (l !== currentLang) result[l] = false; });
    return result;
  }
  const meta = data._translationMeta[fieldPath];
  const currentMeta = meta[currentLang];
  if (!currentMeta) {
    LANGS.forEach(l => { if (l !== currentLang) result[l] = false; });
    return result;
  }
  const currentTime = new Date(currentMeta.updatedAt).getTime();
  LANGS.forEach(l => {
    if (l === currentLang) return;
    const otherMeta = meta[l];
    if (!otherMeta) {
      result[l] = true; // no metadata = never translated
    } else {
      result[l] = new Date(otherMeta.updatedAt).getTime() < currentTime;
    }
  });
  return result;
}

function isFieldStale(data, fieldPath, lang) {
  if (!data?._translationMeta?.[fieldPath]) return false;
  const meta = data._translationMeta[fieldPath];
  const myMeta = meta[lang];
  if (!myMeta) return true; // no metadata for this lang but others exist
  const myTime = new Date(myMeta.updatedAt).getTime();
  return LANGS.some(l => {
    if (l === lang) return false;
    const other = meta[l];
    return other && new Date(other.updatedAt).getTime() > myTime;
  });
}

function getSourceLanguage(data, fieldPath) {
  if (!data?._translationMeta?.[fieldPath]) return 'nl';
  const meta = data._translationMeta[fieldPath];
  let latest = null;
  let latestLang = 'nl';
  LANGS.forEach(l => {
    if (meta[l]) {
      const t = new Date(meta[l].updatedAt).getTime();
      if (!latest || t > latest) { latest = t; latestLang = l; }
    }
  });
  return latestLang;
}

function countAllStaleFields(data) {
  if (!data?._translationMeta) return 0;
  let count = 0;
  Object.keys(data._translationMeta).forEach(path => {
    const meta = data._translationMeta[path];
    // Find the most recent language
    let latestTime = 0;
    LANGS.forEach(l => {
      if (meta[l]) {
        const t = new Date(meta[l].updatedAt).getTime();
        if (t > latestTime) latestTime = t;
      }
    });
    // Count stale languages
    LANGS.forEach(l => {
      if (meta[l]) {
        if (new Date(meta[l].updatedAt).getTime() < latestTime) count++;
      } else {
        // Language exists in another but not this one — stale
        if (latestTime > 0) count++;
      }
    });
  });
  return count;
}

function fmtDay(ev, lang) {
  const d = parseISO(ev.date);
  if (!d) return { day: "—", my: "" };
  const day = String(d.getDate()).padStart(2, "0");
  const months = MONTHS[lang] || MONTHS.nl;
  const my = `${months[d.getMonth()]} ${d.getFullYear()}`;
  return { day, my };
}

function fmtRange(ev, lang) {
  const s = parseISO(ev.date);
  const e = parseISO(ev.endDate);
  if (!e) return null;
  const months = MONTHS[lang] || MONTHS.nl;
  const monthsLong = MONTHS_LONG[lang] || MONTHS_LONG.nl;
  if (s.getMonth() === e.getMonth()) {
    return `${s.getDate()} – ${e.getDate()} ${monthsLong[s.getMonth()]}`;
  }
  return `${s.getDate()} ${months[s.getMonth()].toLowerCase()}. – ${e.getDate()} ${months[e.getMonth()].toLowerCase()}.`;
}

function fmtLong(ev, lang) {
  const d = parseISO(ev.date);
  if (!d) return "";
  const monthsLong = MONTHS_LONG[lang] || MONTHS_LONG.nl;
  return `${d.getDate()} ${monthsLong[d.getMonth()]} ${d.getFullYear()}`;
}

// ─── reusable image upload field ──────────────────────────────────
// ─── Media Library ────────────────────────────────────────────────
function MediaLibrary({ onSelect, onClose }) {
  const [images, setImages] = useState([]);
  const [loading, setLoading] = useState(true);
  const [uploading, setUploading] = useState(false);
  const [filter, setFilter] = useState('');
  const fileRef = useRef(null);

  const loadImages = () => {
    setLoading(true);
    fetch('/api/images').then(r => r.json()).then(list => {
      setImages(list.sort((a, b) => b.filename.localeCompare(a.filename)));
      setLoading(false);
    }).catch(() => setLoading(false));
  };

  useEffect(() => { loadImages(); }, []);

  const handleUpload = async (e) => {
    const files = Array.from(e.target.files);
    if (!files.length) return;
    setUploading(true);
    for (const file of files) {
      const fd = new FormData();
      fd.append('image', file);
      try {
        await fetch('/api/upload', { method: 'POST', body: fd });
      } catch (err) { /* ignore */ }
    }
    setUploading(false);
    loadImages();
  };

  const handleDelete = async (filename) => {
    if (!confirm(`"${filename}" verwijderen?`)) return;
    await fetch(`/api/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
    loadImages();
  };

  const filtered = filter
    ? images.filter(img => img.filename.toLowerCase().includes(filter.toLowerCase()))
    : images;

  const formatSize = (bytes) => {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes / 1024).toFixed(0) + ' KB';
    return (bytes / 1048576).toFixed(1) + ' MB';
  };

  return (
    <div className="media-library-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="media-library">
        <div className="media-library-header">
          <h3>Mediabibliotheek</h3>
          <div style={{display:'flex',gap:8,alignItems:'center'}}>
            <input
              className="media-search"
              type="text"
              placeholder="Zoeken..."
              value={filter}
              onChange={(e) => setFilter(e.target.value)}
            />
            <button className="cms-primary" onClick={() => fileRef.current?.click()}>
              {uploading ? 'Uploaden...' : '+ Uploaden'}
            </button>
            <input ref={fileRef} type="file" accept="image/*" multiple style={{display:'none'}} onChange={handleUpload} />
            <button className="media-close" onClick={onClose}>&times;</button>
          </div>
        </div>
        {loading ? (
          <div className="media-loading">Laden...</div>
        ) : filtered.length === 0 ? (
          <div className="media-loading">{filter ? 'Geen resultaten' : 'Nog geen afbeeldingen geüpload'}</div>
        ) : (
          <div className="media-grid">
            {filtered.map(img => (
              <div key={img.filename} className="media-item" onClick={() => { onSelect(img.url); onClose(); }}>
                <div className="media-thumb">
                  <img src={img.url} alt={img.filename} loading="lazy" />
                </div>
                <div className="media-info">
                  <span className="media-name" title={img.filename}>{img.filename}</span>
                  <span className="media-size">{formatSize(img.size)}</span>
                </div>
                <button className="media-delete" onClick={(e) => { e.stopPropagation(); handleDelete(img.filename); }} title="Verwijderen">&times;</button>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

function ImageUploadField({ value, onChange, label }) {
  const [uploading, setUploading] = useState(false);
  const [showLibrary, setShowLibrary] = useState(false);
  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    setUploading(true);
    const fd = new FormData();
    fd.append('image', file);
    try {
      const res = await fetch('/api/upload', { method: 'POST', body: fd });
      const data = await res.json();
      if (data.url) onChange(data.url);
    } catch (err) { /* ignore */ }
    setUploading(false);
  };
  return (
    <label>
      <span>{label || 'Afbeelding'}</span>
      {value && <img src={value} style={{maxWidth:120,maxHeight:80,objectFit:'cover',borderRadius:4,marginBottom:4,display:'block'}} />}
      <div style={{display:'flex',gap:8,alignItems:'center',marginBottom:4}}>
        <input type="file" accept="image/*" onChange={handleUpload} style={{flex:1}} />
        <button type="button" className="cms-btn" onClick={(e) => { e.preventDefault(); setShowLibrary(true); }} style={{fontSize:11,padding:'4px 10px',whiteSpace:'nowrap'}}>📁 Media</button>
      </div>
      {uploading && <span>Uploaden...</span>}
      <input value={value || ''} onChange={(e) => onChange(e.target.value)} placeholder="/img/... of https://..." style={{marginTop:4}} />
      {showLibrary && <MediaLibrary onSelect={(url) => onChange(url)} onClose={() => setShowLibrary(false)} />}
    </label>
  );
}

// ─── scroll animation hook ─────────────────────────────────────────
function useFadeIn() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) { el.classList.add('visible'); observer.unobserve(el); } },
      { threshold: 0.1 }
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
  return ref;
}

// ─── YouTube video embed ───────────────────────────────────────────
function getYouTubeId(url) {
  if (!url) return null;
  const m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&?#]+)/);
  return m ? m[1] : null;
}

function VideoEmbed({ url }) {
  const [playing, setPlaying] = useState(false);
  const ytId = getYouTubeId(url);
  if (!ytId) return null;

  if (playing) {
    return (
      <div className="video-embed">
        <iframe
          src={`https://www.youtube-nocookie.com/embed/${ytId}?autoplay=1&rel=0`}
          allow="autoplay; encrypted-media"
          allowFullScreen
          title="Video"
        />
      </div>
    );
  }

  return (
    <div className="video-thumb" onClick={() => setPlaying(true)}>
      <img src={`https://img.youtube.com/vi/${ytId}/hqdefault.jpg`} alt="" loading="lazy" />
      <div className="play-btn"></div>
    </div>
  );
}

// ─── Audio player context ──────────────────────────────────────────
const AudioContext2 = createContext({ currentTrack: null, isPlaying: false, play: () => {}, pause: () => {}, toggle: () => {} });

function AudioProvider({ children, tracks }) {
  const [currentTrack, setCurrentTrack] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [progress, setProgress] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const audioRef = useRef(new Audio());

  useEffect(() => {
    if (currentTrack) {
      document.body.classList.add('has-audio-bar');
    } else {
      document.body.classList.remove('has-audio-bar');
    }
  }, [currentTrack]);

  useEffect(() => {
    const audio = audioRef.current;
    const onTime = () => {
      setCurrentTime(audio.currentTime);
      if (audio.duration) setProgress((audio.currentTime / audio.duration) * 100);
    };
    const onLoaded = () => setDuration(audio.duration);
    const onEnded = () => {
      if (!currentTrack || !tracks.length) return;
      const idx = tracks.findIndex(t => t.id === currentTrack.id);
      const nextTrack = tracks[(idx + 1) % tracks.length];
      if (nextTrack && nextTrack.src) {
        setCurrentTrack(nextTrack);
        setIsPlaying(true);
      } else {
        setIsPlaying(false);
      }
    };
    audio.addEventListener('timeupdate', onTime);
    audio.addEventListener('loadedmetadata', onLoaded);
    audio.addEventListener('ended', onEnded);
    return () => {
      audio.removeEventListener('timeupdate', onTime);
      audio.removeEventListener('loadedmetadata', onLoaded);
      audio.removeEventListener('ended', onEnded);
    };
  }, [currentTrack, tracks]);

  useEffect(() => {
    const audio = audioRef.current;
    if (!currentTrack || !currentTrack.src) return;
    audio.src = currentTrack.src;
    if (isPlaying) audio.play().catch(() => {});
  }, [currentTrack]);

  const toggle = useCallback((track) => {
    const audio = audioRef.current;
    if (currentTrack && currentTrack.id === track.id) {
      if (isPlaying) { audio.pause(); setIsPlaying(false); }
      else { audio.play().catch(() => {}); setIsPlaying(true); }
    } else {
      setCurrentTrack(track);
      setIsPlaying(true);
      setProgress(0);
      setCurrentTime(0);
    }
  }, [currentTrack, isPlaying]);

  const next = useCallback(() => {
    if (!currentTrack || !tracks.length) return;
    const idx = tracks.findIndex(t => t.id === currentTrack.id);
    const nextTrack = tracks[(idx + 1) % tracks.length];
    setCurrentTrack(nextTrack);
    setIsPlaying(true);
    setProgress(0);
  }, [currentTrack, tracks]);

  const seek = useCallback((pct) => {
    const audio = audioRef.current;
    if (audio.duration) {
      audio.currentTime = (pct / 100) * audio.duration;
    }
  }, []);

  return (
    <AudioContext2.Provider value={{ currentTrack, isPlaying, toggle, progress, currentTime, duration, next, seek }}>
      {children}
      {currentTrack && (
        <StickyAudioBar
          track={currentTrack}
          isPlaying={isPlaying}
          progress={progress}
          currentTime={currentTime}
          duration={duration}
          onToggle={() => toggle(currentTrack)}
          onNext={next}
          onSeek={seek}
        />
      )}
    </AudioContext2.Provider>
  );
}

function fmtTime(secs) {
  if (!secs || !isFinite(secs)) return '0:00';
  const m = Math.floor(secs / 60);
  const s = Math.floor(secs % 60);
  return `${m}:${s.toString().padStart(2, '0')}`;
}

function StickyAudioBar({ track, isPlaying, progress, currentTime, duration, onToggle, onNext, onSeek }) {
  const barRef = useRef(null);
  const handleSeek = (e) => {
    if (!barRef.current) return;
    const rect = barRef.current.getBoundingClientRect();
    const pct = ((e.clientX - rect.left) / rect.width) * 100;
    onSeek(Math.max(0, Math.min(100, pct)));
  };

  return (
    <div className={`audio-bar ${track ? 'visible' : ''}`}>
      <button onClick={onToggle} style={{fontSize:18}}>
        {isPlaying ? '⏸' : '▶'}
      </button>
      <div className="bar-info">
        <div className="bar-title">{typeof track.title === 'object' ? (track.title.nl || Object.values(track.title)[0]) : track.title}</div>
        <div className="bar-artist">{track.artist}</div>
      </div>
      <div className="bar-time">{fmtTime(currentTime)}</div>
      <div className="bar-progress" ref={barRef} onClick={handleSeek}>
        <div className="bar-progress-fill" style={{width: `${progress}%`}}></div>
      </div>
      <div className="bar-time">{track.duration || fmtTime(duration)}</div>
      <button onClick={onNext} style={{fontSize:14}}>⏭</button>
    </div>
  );
}

// ─── HTML sanitizer for rich text ──────────────────────────────────
function sanitizeHtml(html) {
  const tmp = document.createElement('div');
  tmp.innerHTML = html;
  const allowed = new Set(['B', 'I', 'STRONG', 'EM', 'SPAN', 'BR', 'A', 'U']);
  function walk(node) {
    const children = Array.from(node.childNodes);
    for (const child of children) {
      if (child.nodeType === 1) {
        if (!allowed.has(child.tagName)) {
          while (child.firstChild) node.insertBefore(child.firstChild, child);
          node.removeChild(child);
        } else {
          const attrs = Array.from(child.attributes);
          for (const a of attrs) {
            if (a.name === 'style' || a.name === 'href' || a.name === 'class') continue;
            if (a.name.startsWith('on')) child.removeAttribute(a.name);
          }
          walk(child);
        }
      }
    }
  }
  walk(tmp);
  return tmp.innerHTML;
}

function isRichContent(str) {
  return typeof str === 'string' && /<[a-z][\s\S]*>/i.test(str);
}

// ─── Translation panel (side-by-side editing) ─────────────────────
function TranslationPanel({ fieldPath, field, data, rich, onClose, onSave }) {
  const t = useT();
  const sourceLang = getSourceLanguage(data, fieldPath);
  const [values, setValues] = useState(() => {
    const v = {};
    LANGS.forEach(l => { v[l] = (typeof field === 'object' && field !== null) ? (field[l] || '') : ''; });
    return v;
  });
  const [translating, setTranslating] = useState({});
  const [hasApiKey, setHasApiKey] = useState(true);

  // Check if API key is available
  useEffect(() => {
    fetch('/api/translate-status').then(r => r.json()).then(d => setHasApiKey(d.available)).catch(() => setHasApiKey(false));
  }, []);

  const staleLangs = getStaleLanguages(data, fieldPath, sourceLang);

  const autoTranslate = async (targetLang) => {
    setTranslating(prev => ({ ...prev, [targetLang]: true }));
    try {
      const res = await fetch('/api/translate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: values[sourceLang],
          from: sourceLang,
          to: [targetLang],
          format: rich ? 'html' : 'plain'
        })
      });
      if (res.ok) {
        const result = await res.json();
        if (result.translations?.[targetLang]) {
          setValues(prev => ({ ...prev, [targetLang]: result.translations[targetLang] }));
        }
      }
    } catch (e) {
      console.error('Translation failed:', e);
    }
    setTranslating(prev => ({ ...prev, [targetLang]: false }));
  };

  const translateAllStale = async () => {
    const targets = LANGS.filter(l => l !== sourceLang && staleLangs[l]);
    if (targets.length === 0) return;
    setTranslating(prev => {
      const next = { ...prev };
      targets.forEach(l => { next[l] = true; });
      return next;
    });
    try {
      const res = await fetch('/api/translate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: values[sourceLang],
          from: sourceLang,
          to: targets,
          format: rich ? 'html' : 'plain'
        })
      });
      if (res.ok) {
        const result = await res.json();
        if (result.translations) {
          setValues(prev => {
            const next = { ...prev };
            targets.forEach(l => { if (result.translations[l]) next[l] = result.translations[l]; });
            return next;
          });
        }
      }
    } catch (e) {
      console.error('Bulk translation failed:', e);
    }
    setTranslating({});
  };

  const handleSave = () => {
    if (typeof field === 'object' && field !== null) {
      LANGS.forEach(l => {
        if (values[l] !== (field[l] || '')) {
          field[l] = values[l];
          updateTranslationMeta(data, fieldPath, l, values[l]);
        }
      });
    }
    onSave();
  };

  const fmtDate = (iso) => {
    if (!iso) return '';
    const d = new Date(iso);
    return d.toLocaleDateString('nl-BE', { day: 'numeric', month: 'short' });
  };

  return (
    <div className="cms-modal-overlay translation-panel-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="translation-panel">
        <div className="translation-panel-header">
          <h3>🌐 {t('cms_translations') || 'Vertalingen'}</h3>
          <span className="translation-panel-path">{fieldPath}</span>
          <button className="translation-panel-close" onClick={onClose}>&times;</button>
        </div>

        <div className="translation-panel-body">
          {LANGS.map(l => {
            const isSource = l === sourceLang;
            const isStale = !isSource && staleLangs[l];
            const meta = data?._translationMeta?.[fieldPath]?.[l];
            return (
              <div key={l} className={'translation-lang-row' + (isSource ? ' source' : '') + (isStale ? ' stale' : '')}>
                <div className="translation-lang-header">
                  <span className="translation-lang-label">
                    {l.toUpperCase()} — {LANG_NAMES[l]}
                  </span>
                  {isSource && <span className="translation-lang-status source-badge">✓ {t('cms_trans_source') || 'bron'}</span>}
                  {isStale && <span className="translation-lang-status stale-badge">⚠ {t('cms_trans_stale') || 'verouderd'}</span>}
                  {meta && <span className="translation-lang-date">{fmtDate(meta.updatedAt)}</span>}
                </div>
                {rich ? (
                  <div
                    className="translation-lang-input rich"
                    contentEditable
                    suppressContentEditableWarning
                    dangerouslySetInnerHTML={{ __html: values[l] }}
                    onBlur={(e) => setValues(prev => ({ ...prev, [l]: e.target.innerHTML }))}
                  />
                ) : (
                  <textarea
                    className="translation-lang-input"
                    rows={Math.max(3, (values[l] || '').split('\n').length + 1)}
                    value={values[l]}
                    onChange={(e) => setValues(prev => ({ ...prev, [l]: e.target.value }))}
                  />
                )}
                {isStale && hasApiKey && (
                  <button
                    className="translation-auto-btn"
                    onClick={() => autoTranslate(l)}
                    disabled={translating[l]}
                  >
                    {translating[l] ? '⏳ ...' : `Auto-vertaal ${sourceLang.toUpperCase()} → ${l.toUpperCase()}`}
                  </button>
                )}
              </div>
            );
          })}
        </div>

        <div className="translation-panel-footer">
          {hasApiKey && Object.values(staleLangs).some(Boolean) && (
            <button className="translation-auto-all-btn" onClick={translateAllStale} disabled={Object.values(translating).some(Boolean)}>
              {Object.values(translating).some(Boolean) ? '⏳ Vertalen...' : '🌐 Alles vertalen'}
            </button>
          )}
          <button className="translation-save-btn" onClick={handleSave}>{t('cms_save') || 'Opslaan'}</button>
          <button className="translation-cancel-btn" onClick={onClose}>{t('cms_cancel') || 'Annuleren'}</button>
        </div>
      </div>
    </div>
  );
}

// ─── Translation Dashboard (bulk overview) ────────────────────────
function TranslationDashboard({ data, onClose }) {
  const t = useT();
  const lang = useLang();
  const [selectedField, setSelectedField] = useState(null);
  const cms = useContext(CmsContext);

  if (!data?._translationMeta) {
    return (
      <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
        <div className="translation-dashboard">
          <div className="translation-panel-header">
            <h3>🌐 {t('cms_translations') || 'Vertalingen'}</h3>
            <button className="translation-panel-close" onClick={onClose}>&times;</button>
          </div>
          <p style={{padding:24,color:'rgba(241,235,222,.6)'}}>{t('cms_no_stale') || 'Geen verouderde vertalingen gevonden.'}</p>
        </div>
      </div>
    );
  }

  // Collect stale fields
  const staleFields = [];
  Object.entries(data._translationMeta).forEach(([path, meta]) => {
    let latestTime = 0, latestLang = 'nl';
    LANGS.forEach(l => {
      if (meta[l]) {
        const t = new Date(meta[l].updatedAt).getTime();
        if (t > latestTime) { latestTime = t; latestLang = l; }
      }
    });
    const stale = [];
    LANGS.forEach(l => {
      if (l === latestLang) return;
      if (!meta[l] || new Date(meta[l].updatedAt).getTime() < latestTime) stale.push(l);
    });
    if (stale.length > 0) {
      // Resolve field value for preview
      const pathParts = path.split('.');
      let fieldObj = null;
      try {
        // Navigate the data tree to find the field
        let cur = data;
        for (let i = 0; i < pathParts.length; i++) {
          const part = pathParts[i];
          if (Array.isArray(cur)) {
            cur = cur.find(item => item.id === part);
          } else if (cur && typeof cur === 'object') {
            cur = cur[part];
          }
          if (!cur) break;
        }
        fieldObj = cur;
      } catch (e) {}

      staleFields.push({
        path,
        sourceLang: latestLang,
        staleLangs: stale,
        preview: typeof fieldObj === 'object' ? txt(fieldObj, latestLang) : (fieldObj || ''),
        field: fieldObj
      });
    }
  });

  // Group by section (first part of path)
  const grouped = {};
  staleFields.forEach(f => {
    const section = f.path.split('.')[0];
    if (!grouped[section]) grouped[section] = [];
    grouped[section].push(f);
  });

  if (selectedField) {
    return (
      <TranslationPanel
        fieldPath={selectedField.path}
        field={selectedField.field}
        data={data}
        rich={false}
        onClose={() => setSelectedField(null)}
        onSave={() => { cms.setDirty(true); setSelectedField(null); }}
      />
    );
  }

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="translation-dashboard">
        <div className="translation-panel-header">
          <h3>🌐 {t('cms_translations') || 'Vertalingen'} — {staleFields.length} {t('cms_trans_stale') || 'verouderd'}</h3>
          <button className="translation-panel-close" onClick={onClose}>&times;</button>
        </div>
        <div className="translation-dashboard-body">
          {staleFields.length === 0 ? (
            <p style={{padding:24,color:'rgba(241,235,222,.6)'}}>✓ {t('cms_all_synced') || 'Alle vertalingen zijn up-to-date!'}</p>
          ) : (
            Object.entries(grouped).map(([section, fields]) => (
              <div key={section} className="translation-dashboard-section">
                <h4>{section}</h4>
                {fields.map(f => (
                  <button key={f.path} className="translation-dashboard-row" onClick={() => setSelectedField(f)}>
                    <span className="translation-dashboard-path">{f.path.split('.').slice(1).join(' → ')}</span>
                    <span className="translation-dashboard-stale">
                      {f.staleLangs.map(l => <span key={l} className="stale-lang-chip">{l.toUpperCase()}</span>)}
                    </span>
                    <span className="translation-dashboard-preview">{(f.preview || '').substring(0, 60)}{(f.preview || '').length > 60 ? '...' : ''}</span>
                  </button>
                ))}
              </div>
            ))
          )}
        </div>
      </div>
    </div>
  );
}

// ─── CMS editable text ─────────────────────────────────────────────
function Editable({ field, path, Tag = 'span', className, style, children, rich }) {
  const lang = useLang();
  const cms = useContext(CmsContext);
  const ref = useRef(null);
  const content = txt(field, lang);
  // useState must be called unconditionally (React rules of hooks)
  const [showTransPanel, setShowTransPanel] = useState(false);

  const handleBlur = useCallback(() => {
    if (!cms.active || !ref.current) return;
    const newVal = rich ? sanitizeHtml(ref.current.innerHTML) : ref.current.innerText.trim();
    if (newVal !== content) {
      if (typeof field === 'object' && field !== null) {
        field[lang] = newVal;
      }
      if (path && cms.dataRef?.current) {
        updateTranslationMeta(cms.dataRef.current, path, lang, newVal);
        cms.bumpMeta();
      }
      cms.setDirty(true);
    }
  }, [cms, field, lang, content, rich, path]);

  if (!cms.active) {
    if (rich && isRichContent(content)) {
      return <Tag className={className} style={style} dangerouslySetInnerHTML={{ __html: content }} />;
    }
    // Convert \n to <br> for multiline plain text
    if (content && content.includes('\n')) {
      return <Tag className={className} style={style} dangerouslySetInnerHTML={{ __html: content.replace(/\n/g, '<br>') }} />;
    }
    return <Tag className={className} style={style}>{children || content}</Tag>;
  }

  // Translation drift indicators (metaVersion triggers re-render when metadata changes)
  const _mv = cms.metaVersion; // eslint-disable-line no-unused-vars
  const data = cms.dataRef?.current;
  const stale = path && data ? isFieldStale(data, path, lang) : false;
  const othersStale = path && data ? Object.values(getStaleLanguages(data, path, lang)).some(Boolean) : false;

  const editProps = {
    ref,
    className: [className, stale ? 'cms-stale-outline' : ''].filter(Boolean).join(' '),
    style,
    'data-editable': path,
    contentEditable: true,
    suppressContentEditableWarning: true,
    onBlur: handleBlur,
  };

  const editEl = (rich && isRichContent(content))
    ? <Tag {...editProps} dangerouslySetInnerHTML={{ __html: content }} />
    : <Tag {...editProps}>{children || content}</Tag>;

  return (
    <span style={{position:'relative', display:'inline'}}>
      {editEl}
      {(stale || othersStale) && path && (
        <span className="cms-translation-icons">
          {stale && <span className="cms-stale-dot" title={lang.toUpperCase() + ' — verouderd / outdated'}>⚠</span>}
          <button className="cms-translate-btn" title="Vertalingen beheren" onClick={(e) => { e.stopPropagation(); setShowTransPanel(true); }}>🌐</button>
        </span>
      )}
      {showTransPanel && (
        <TranslationPanel
          fieldPath={path}
          field={field}
          data={data}
          rich={rich}
          onClose={() => setShowTransPanel(false)}
          onSave={() => { cms.setDirty(true); cms.bumpMeta(); setShowTransPanel(false); }}
        />
      )}
    </span>
  );
}

// ─── CMS Rich Text Toolbar ─────────────────────────────────────────
function CmsRichToolbar() {
  const cms = useContext(CmsContext);
  const [pos, setPos] = useState(null);
  const [visible, setVisible] = useState(false);
  const toolbarRef = useRef(null);

  useEffect(() => {
    if (!cms.active) return;
    const handleSelection = () => {
      const sel = window.getSelection();
      if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
        setVisible(false);
        return;
      }
      const range = sel.getRangeAt(0);
      const container = range.commonAncestorContainer;
      const editableEl = (container.nodeType === 3 ? container.parentElement : container)?.closest('[data-editable]');
      if (!editableEl) {
        setVisible(false);
        return;
      }
      const rect = range.getBoundingClientRect();
      setPos({ top: rect.top - 48 + window.scrollY, left: rect.left + rect.width / 2 });
      setVisible(true);
    };
    document.addEventListener('selectionchange', handleSelection);
    return () => document.removeEventListener('selectionchange', handleSelection);
  }, [cms.active]);

  if (!cms.active || !visible || !pos) return null;

  const exec = (cmd, val) => {
    document.execCommand(cmd, false, val);
    cms.setDirty(true);
  };

  const COLORS = [
    { label: 'Ink', value: '#1a1816' },
    { label: 'Accent', value: '#7a2e2e' },
    { label: 'Paper', value: '#f1ebde' },
    { label: 'Wit', value: '#ffffff' },
  ];

  const FONTS = [
    { label: 'Serif', value: 'Newsreader, serif' },
    { label: 'Sans', value: 'Geist, sans-serif' },
    { label: 'Mono', value: 'Geist Mono, monospace' },
  ];

  return (
    <div ref={toolbarRef} className="cms-rich-toolbar" style={{ top: pos.top, left: pos.left }}>
      <button onMouseDown={(e) => { e.preventDefault(); exec('bold'); }} title="Bold"><b>B</b></button>
      <button onMouseDown={(e) => { e.preventDefault(); exec('italic'); }} title="Italic"><i>I</i></button>
      <button onMouseDown={(e) => { e.preventDefault(); exec('underline'); }} title="Underline"><u>U</u></button>
      <span className="cms-rich-sep"></span>
      {COLORS.map(c => (
        <button key={c.value} className="cms-color-btn" title={c.label}
          onMouseDown={(e) => { e.preventDefault(); exec('foreColor', c.value); }}
          style={{ background: c.value, border: c.value === '#ffffff' ? '1px solid #ccc' : 'none' }}
        />
      ))}
      <span className="cms-rich-sep"></span>
      <select onChange={(e) => { exec('fontName', e.target.value); e.target.value = ''; }}
        onMouseDown={(e) => e.stopPropagation()} defaultValue="">
        <option value="" disabled>Font</option>
        {FONTS.map(f => <option key={f.value} value={f.value}>{f.label}</option>)}
      </select>
      <span className="cms-rich-sep"></span>
      <button onMouseDown={(e) => { e.preventDefault(); exec('removeFormat'); }} title="Opmaak wissen">✕</button>
    </div>
  );
}

// ─── section visibility toggle ────────────────────────────────────
const SECTION_LABELS = {
  hero: 'Hero',
  nieuws: 'Nieuws',
  projecten: 'Projecten',
  over: 'Over B.O.X',
  opencalls: 'Open Calls',
  agenda: 'Agenda & Archief',
  media: 'Media (Video & Audio)',
  galerij: 'Galerij',
  instagram: 'Instagram',
  partners: 'Partners',
  contact: 'Contact',
};

function SectionWrapper({ sectionId, data, children, draggable }) {
  const cms = useContext(CmsContext);
  const hidden = (data.hiddenSections || []).includes(sectionId);

  // Visitors: don't render hidden sections at all
  if (!cms.active && hidden) return null;

  const toggleVisibility = () => {
    const arr = data.hiddenSections || [];
    if (hidden) {
      data.hiddenSections = arr.filter(id => id !== sectionId);
    } else {
      data.hiddenSections = [...arr, sectionId];
    }
    cms.setDirty(true);
    cms.bumpMeta();
  };

  const isCustom = sectionId.startsWith('custom-');
  const sectionName = SECTION_LABELS[sectionId] || (data.customSections?.[sectionId] ? txt(data.customSections[sectionId].label, 'nl') : sectionId);

  const removeSection = () => {
    const msg = isCustom
      ? `Sectie "${sectionName}" definitief verwijderen? Dit verwijdert ook alle inhoud.`
      : `Sectie "${sectionName}" verwijderen van de pagina? Je kunt deze later weer toevoegen.`;
    if (!confirm(msg)) return;
    const order = data.sectionOrder || [...DEFAULT_SECTION_ORDER];
    data.sectionOrder = order.filter(id => id !== sectionId);
    // Clean up custom section data
    if (isCustom) {
      if (data.customSections) delete data.customSections[sectionId];
      if (data.sectionHeaders) delete data.sectionHeaders[sectionId];
    }
    cms.setDirty(true);
    cms.bumpMeta();
  };

  return (
    <div className={`cms-section-wrap${hidden ? ' cms-section-hidden' : ''}`} style={{ position: 'relative' }} data-section-id={sectionId}>
      {cms.active && (
        <div className="cms-section-controls">
          {draggable && <span className="cms-section-drag" title="Versleep om te herordenen">⠿</span>}
          <button
            className={`cms-visibility-toggle${hidden ? ' is-hidden' : ''}`}
            onClick={toggleVisibility}
            title={hidden ? 'Sectie tonen aan bezoekers' : 'Sectie verbergen voor bezoekers'}
          >
            {hidden ? '👁‍🗨' : '👁'}
            <span>{hidden ? 'Verborgen' : 'Zichtbaar'}</span>
          </button>
          <button
            className="cms-section-remove"
            onClick={removeSection}
            title="Sectie verwijderen"
          >
            ✕
          </button>
        </div>
      )}
      {children}
    </div>
  );
}

// ─── nav ───────────────────────────────────────────────────────────
function Nav({ lang, setLang, data }) {
  const t = useT();
  const cms = useContext(CmsContext);
  const [menuOpen, setMenuOpen] = useState(false);
  const navRef = useRef(null);
  const navItems = data.nav || [];

  // Prevent body scroll when mobile menu is open
  useEffect(() => {
    document.body.style.overflow = menuOpen ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [menuOpen]);

  // Close menu on outside click
  useEffect(() => {
    if (!menuOpen) return;
    const handleClick = (e) => {
      if (navRef.current && !navRef.current.contains(e.target)) setMenuOpen(false);
    };
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, [menuOpen]);

  const [editingHref, setEditingHref] = useState(null); // idx of nav item being href-edited

  const slugify = (str) => '#' + str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');

  const addNavItem = () => {
    const id = 'nav-' + Date.now().toString(36);
    const label = { nl: 'Nieuw', en: 'New', fr: 'Nouveau' };
    data.nav = [...navItems, { id, href: slugify(label.nl), label }];
    cms.setDirty(true);
    cms.bumpMeta();
  };

  const removeNavItem = (idx) => {
    data.nav = navItems.filter((_, i) => i !== idx);
    cms.setDirty(true);
    cms.bumpMeta();
    setEditingHref(null);
  };

  const updateNavHref = (idx, newHref) => {
    const href = newHref.startsWith('#') ? newHref : '#' + newHref;
    data.nav = navItems.map((item, i) => i === idx ? { ...item, href } : item);
    cms.setDirty(true);
    cms.bumpMeta();
  };

  return (
    <nav className="nav" ref={navRef}>
      <div className="wrap nav-row">
        <a href="#top" className="brand">
          <img src="/logo/B-O-X-logo2026-03.svg" alt="B.O.X" />
        </a>
        <button className="menu-toggle" onClick={() => setMenuOpen(!menuOpen)} aria-label="Menu">
          {menuOpen ? '✕' : '☰'}
        </button>
        <ul className={menuOpen ? 'open' : ''}>
          {navItems.map((item, idx) => (
            <li key={item.id || idx} style={cms.active ? { position: 'relative' } : undefined}>
              <a href={item.href} onClick={(e) => { if (cms.active && editingHref === idx) e.preventDefault(); setMenuOpen(false); }}>
                <Editable field={item.label} path={`nav.${item.id}.label`} Tag="span">{txt(item.label, lang)}</Editable>
              </a>
              {cms.active && (
                <span className="cms-nav-controls">
                  <button className="cms-nav-href-btn" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setEditingHref(editingHref === idx ? null : idx); }}
                    title="Link bewerken">🔗</button>
                  <button className="cms-nav-remove" onClick={(e) => { e.preventDefault(); removeNavItem(idx); }}
                    title="Verwijder">&times;</button>
                </span>
              )}
              {cms.active && editingHref === idx && (
                <div className="cms-nav-href-editor" onClick={(e) => e.stopPropagation()}>
                  <label>Link doel:</label>
                  <input type="text" value={item.href} onChange={(e) => updateNavHref(idx, e.target.value)}
                    placeholder="#sectie-id" spellCheck="false" />
                  <small>Gebruik # + sectie-ID, bijv. #over, #agenda, #media</small>
                  <button onClick={() => setEditingHref(null)}>OK</button>
                </div>
              )}
            </li>
          ))}
          {cms.active && (
            <li><button className="cms-nav-add" onClick={addNavItem}>+</button></li>
          )}
        </ul>
        <div className="nav-lang" role="group" aria-label="Taal">
          {["nl", "en", "fr"].map((l) => (
            <button
              key={l}
              aria-current={lang === l ? "true" : "false"}
              onClick={() => setLang(l)}
              title={l === "nl" ? "Nederlands" : l === "en" ? "English" : "Français"}
            >{l.toUpperCase()}</button>
          ))}
        </div>
      </div>
    </nav>
  );
}

// ─── Portal component saved to saved-components/portal-component.jsx ──────
// Removed: BOX logo with people photos (designer IP concerns). Can be restored later.

// ─── hero image with media library ─────────────────────────────────
function HeroImage({ data, heroImage, cms }) {
  const [showLib, setShowLib] = useState(false);
  return (
    <div className="hero-featured-wrap">
      {cms.active && (
        <button className="cms-image-edit-btn" onClick={() => setShowLib(true)}>Afbeelding wijzigen</button>
      )}
      <img src={heroImage} alt="" className="hero-featured" />
      {showLib && (
        <MediaLibrary
          onSelect={(url) => { data.meta.heroImage = url; cms.setDirty(true); cms.bumpMeta(); }}
          onClose={() => setShowLib(false)}
        />
      )}
    </div>
  );
}

// ─── hero ──────────────────────────────────────────────────────────
function Hero({ data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const tagline = txt(data.meta.tagline, lang);

  const heroImage = data.meta.heroImage || '/img/portal-1.jpg';

  return (
    <header className="hero" id="top">
      <div className="wrap hero-grid">
        <div>
          <div className="hero-tag mono">
            <span className="bar"></span>
            <Editable
              field={data.meta.heroTag || t('hero_tag')}
              path="meta.heroTag"
              Tag="span"
            />
          </div>
          <h1>
            <span className="accent italic">{tagline}</span>
          </h1>
          <Editable
            field={data.meta.intro}
            path="meta.intro"
            Tag="p"
            className="hero-sub"
            style={{marginTop:28}}
          />
        </div>

        <HeroImage data={data} heroImage={heroImage} cms={cms} />
      </div>
    </header>
  );
}

// ─── news modal (CMS) ─────────────────────────────────────────────
function NewsModal({ item, onSave, onDelete, onClose }) {
  const currentLang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const isNew = !item.id;
  const [activeLang, setActiveLang] = useState(currentLang);

  // Store all 3 languages
  const [titles, setTitles] = useState({
    nl: txt(item.title, 'nl') || '', en: txt(item.title, 'en') || '', fr: txt(item.title, 'fr') || ''
  });
  const [summaries, setSummaries] = useState({
    nl: txt(item.summary, 'nl') || '', en: txt(item.summary, 'en') || '', fr: txt(item.summary, 'fr') || ''
  });
  const [date, setDate] = useState(item.date || '');
  const [type, setType] = useState(item.type || 'general');
  const [link, setLink] = useState(item.link || '');
  const [image, setImage] = useState(item.image || '');
  const [translating, setTranslating] = useState(false);

  const handleAutoTranslate = async () => {
    const sourceLang = 'nl';
    const targetLangs = ['en', 'fr'].filter(l => l !== sourceLang);
    if (!titles.nl && !summaries.nl) return;
    setTranslating(true);
    try {
      if (titles.nl) {
        const res = await fetch('/api/translate', {
          method: 'POST', headers: {'Content-Type':'application/json'},
          body: JSON.stringify({ text: titles.nl, from: sourceLang, to: targetLangs })
        });
        const data = await res.json();
        if (data.translations) {
          setTitles(prev => ({ ...prev, ...data.translations }));
        }
      }
      if (summaries.nl) {
        const res = await fetch('/api/translate', {
          method: 'POST', headers: {'Content-Type':'application/json'},
          body: JSON.stringify({ text: summaries.nl, from: sourceLang, to: targetLangs })
        });
        const data = await res.json();
        if (data.translations) {
          setSummaries(prev => ({ ...prev, ...data.translations }));
        }
      }
    } catch (err) { /* ignore */ }
    setTranslating(false);
  };

  const handleSave = () => {
    const updated = { ...item };
    if (!updated.id) updated.id = 'news-' + Date.now().toString(36);
    const itemId = updated.id;
    updated.title = { ...titles };
    updated.summary = { ...summaries };
    // Track translation metadata for all edited languages
    if (cms.dataRef?.current) {
      ['nl','en','fr'].forEach(l => {
        if (titles[l]) updateTranslationMeta(cms.dataRef.current, `news.${itemId}.title`, l, titles[l]);
        if (summaries[l]) updateTranslationMeta(cms.dataRef.current, `news.${itemId}.summary`, l, summaries[l]);
      });
    }
    updated.date = date;
    updated.type = type;
    updated.link = link;
    updated.image = image;
    onSave(updated);
  };

  const typeLabels = { general: 'Algemeen', 'open-call': 'Open Call', event: 'Event' };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal cms-modal-wide">
        <h3>{isNew ? t('cms_new_news') : t('cms_edit_news')}</h3>
        <div className="cms-modal-split">
          {/* ─── Form ─── */}
          <div className="cms-modal-form">
            {/* Language tabs */}
            <div className="cms-lang-tabs">
              {['nl','en','fr'].map(l => (
                <button key={l} className={`cms-lang-tab ${activeLang === l ? 'active' : ''} ${!titles[l] && l !== 'nl' ? 'empty' : ''}`}
                  onClick={() => setActiveLang(l)}>{l.toUpperCase()}{!titles[l] && l !== 'nl' ? ' ○' : ''}</button>
              ))}
              <button className="cms-auto-translate" onClick={handleAutoTranslate} disabled={translating || !titles.nl}
                title="Vertaal NL → EN + FR">{translating ? '⏳' : '🌐'} Auto</button>
            </div>

            <label>
              <span>Titel ({activeLang.toUpperCase()})</span>
              <input value={titles[activeLang]} onChange={(e) => setTitles({...titles, [activeLang]: e.target.value})} />
            </label>
            <label>
              <span>Samenvatting ({activeLang.toUpperCase()})</span>
              <textarea value={summaries[activeLang]} onChange={(e) => setSummaries({...summaries, [activeLang]: e.target.value})} rows={3} />
            </label>

            {activeLang === 'nl' && <>
              <label>
                <span>Datum</span>
                <input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
              </label>
              <label>
                <span>Type</span>
                <select value={type} onChange={(e) => setType(e.target.value)}>
                  <option value="general">Algemeen</option>
                  <option value="open-call">Open Call</option>
                  <option value="event">Event</option>
                </select>
              </label>
              <label>
                <span>Link</span>
                <input value={link} onChange={(e) => setLink(e.target.value)} placeholder="https://... of #sectie" />
              </label>
              <ImageUploadField value={image} onChange={(url) => setImage(url)} label="Afbeelding" />
            </>}
          </div>

          {/* ─── Preview ─── */}
          <div className="cms-modal-preview">
            <div className="cms-preview-label">Voorbeeld</div>
            <div className="cms-preview-card">
              {image && (
                <div className="cms-preview-img">
                  <img src={image} alt="" />
                </div>
              )}
              {type !== 'general' && <span className="cms-preview-tag">{typeLabels[type]}</span>}
              <div className="cms-preview-title">{titles[activeLang] || 'Titel...'}</div>
              {date && <div className="cms-preview-date">{new Date(date + 'T12:00').toLocaleDateString('nl-BE', {day:'numeric',month:'long',year:'numeric'})}</div>}
              <div className="cms-preview-summary">{summaries[activeLang] || 'Samenvatting...'}</div>
            </div>
          </div>
        </div>

        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(item.id); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

// ─── news ─────────────────────────────────────────────────────────
function NewsSection({ data }) {
  const t = useT();
  const lang = useLang();
  const fadeRef = useFadeIn();
  const cms = useContext(CmsContext);
  const newsItems = data.news || [];
  const [editingNews, setEditingNews] = useState(null);

  const handleSaveNews = (updated) => {
    const idx = data.news.findIndex(n => n.id === updated.id);
    if (idx >= 0) data.news[idx] = updated;
    else data.news = [...(data.news || []), updated];
    cms.setDirty(true);
    setEditingNews(null);
  };

  const handleDeleteNews = (id) => {
    data.news = data.news.filter(n => n.id !== id);
    cms.setDirty(true);
  };

  if (newsItems.length === 0 && !cms.active) return null;

  return (
    <section className="section fade-in" id="nieuws" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_news')} sectionId="news" data={data} />
        <div className="news-grid">
          {newsItems.map((item, idx) => (
            <a key={item.id || idx} className="news-card" href={item.link || '#'} style={{position:'relative'}}>
              {item.image && (
                <div className="news-card-img">
                  {/\.(mp4|webm|mov)$/i.test(item.image)
                    ? <video src={item.image} autoPlay muted loop playsInline />
                    : <img src={item.image} alt={txt(item.title, lang)} loading="lazy" />
                  }
                </div>
              )}
              <div className="news-card-body">
                {item.type === 'open-call' && (
                  <span className="news-tag open-call-tag">{t('news_open_call')}</span>
                )}
                {item.type === 'event' && (
                  <span className="news-tag event-tag">Event</span>
                )}
                <h3 className="serif">{txt(item.title, lang)}</h3>
                <p className="soft">{txt(item.summary, lang)}</p>
                <span className="news-date mono">{(() => {
                  const d = parseISO(item.date);
                  if (!d) return item.date;
                  const ml = MONTHS_LONG[lang] || MONTHS_LONG.nl;
                  return `${d.getDate()} ${ml[d.getMonth()]} ${d.getFullYear()}`;
                })()}</span>
              </div>
              {cms.active && (
                <button className="cms-edit-btn" onClick={(e) => { e.preventDefault(); setEditingNews(item); }}>
                  {t('cms_edit')}
                </button>
              )}
            </a>
          ))}
        </div>
        {cms.active && (
          <button className="cms-add-btn" onClick={() => setEditingNews({ title: {}, summary: {}, date: '', type: 'general', link: '', image: '' })}>
            {t('cms_add_news')}
          </button>
        )}
      </div>
      {editingNews && (
        <NewsModal
          item={editingNews}
          onSave={handleSaveNews}
          onDelete={handleDeleteNews}
          onClose={() => setEditingNews(null)}
        />
      )}
    </section>
  );
}

// ─── open call modal (CMS) ────────────────────────────────────────
function OpenCallModal({ call, onSave, onDelete, onClose }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const isNew = !call.id;
  const [form, setForm] = useState({
    title: txt(call.title, lang) || '',
    subtitle: txt(call.subtitle, lang) || '',
    dates: txt(call.dates, lang) || '',
    deadline: call.deadline || '',
    deadlineLabel: txt(call.deadlineLabel, lang) || '',
    status: call.status || 'open',
    intro: txt(call.intro, lang) || '',
    description: txt(call.description, lang) || '',
    image: call.image || '',
    contact: call.contact || '',
  });
  const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });

  const handleSave = () => {
    const updated = { ...call };
    if (!updated.id) updated.id = 'call-' + Date.now().toString(36);
    const itemId = updated.id;
    const triFields = ['title', 'subtitle', 'dates', 'deadlineLabel', 'intro', 'description'];
    triFields.forEach(f => {
      if (typeof updated[f] === 'object' && updated[f] !== null) {
        updated[f][lang] = form[f];
      } else {
        updated[f] = { nl: '', en: '', fr: '', [lang]: form[f] };
      }
      // Track translation metadata
      if (cms.dataRef?.current) {
        updateTranslationMeta(cms.dataRef.current, `openCalls.${itemId}.${f}`, lang, form[f]);
      }
    });
    updated.deadline = form.deadline;
    updated.status = form.status;
    updated.image = form.image;
    updated.contact = form.contact;
    if (!updated.sections) updated.sections = [];
    onSave(updated);
  };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal">
        <h3>{isNew ? t('cms_new_call') : t('cms_edit_call')}</h3>
        <label>
          <span>Titel ({lang.toUpperCase()})</span>
          <input value={form.title} onChange={set('title')} />
        </label>
        <label>
          <span>Ondertitel ({lang.toUpperCase()})</span>
          <input value={form.subtitle} onChange={set('subtitle')} />
        </label>
        <label>
          <span>Data ({lang.toUpperCase()})</span>
          <input value={form.dates} onChange={set('dates')} placeholder="bijv. september 2026 – juni 2027" />
        </label>
        <label>
          <span>Deadline (YYYY-MM-DD)</span>
          <input type="date" value={form.deadline} onChange={set('deadline')} />
        </label>
        <label>
          <span>Deadline label ({lang.toUpperCase()})</span>
          <input value={form.deadlineLabel} onChange={set('deadlineLabel')} placeholder="bijv. 15 augustus 2026" />
        </label>
        <label>
          <span>Status</span>
          <select value={form.status} onChange={set('status')}>
            <option value="open">Open</option>
            <option value="closed">Gesloten</option>
          </select>
        </label>
        <label>
          <span>Intro ({lang.toUpperCase()})</span>
          <textarea value={form.intro} onChange={set('intro')} rows={2} />
        </label>
        <label>
          <span>Beschrijving ({lang.toUpperCase()})</span>
          <textarea value={form.description} onChange={set('description')} rows={4} />
        </label>
        <ImageUploadField value={form.image} onChange={(url) => setForm({ ...form, image: url })} label="Afbeelding" />
        <label>
          <span>Contact e-mail</span>
          <input value={form.contact} onChange={set('contact')} placeholder="info@boxbaroque.com" />
        </label>
        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(call.id); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

// ─── open calls ───────────────────────────────────────────────────
function OpenCallsSection({ data }) {
  const t = useT();
  const lang = useLang();
  const fadeRef = useFadeIn();
  const cms = useContext(CmsContext);
  const calls = data.openCalls || [];
  const [expandedId, setExpandedId] = useState(null);
  const [editingCall, setEditingCall] = useState(null);

  const handleSaveCall = (updated) => {
    const idx = data.openCalls.findIndex(c => c.id === updated.id);
    if (idx >= 0) data.openCalls[idx] = updated;
    else data.openCalls = [...(data.openCalls || []), updated];
    cms.setDirty(true);
    setEditingCall(null);
  };

  const handleDeleteCall = (id) => {
    data.openCalls = data.openCalls.filter(c => c.id !== id);
    cms.setDirty(true);
  };

  if (calls.length === 0 && !cms.active) return null;

  return (
    <section id="opencalls" className="section fade-in" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_opencalls')} sectionId="opencalls" data={data} />
        <div className="opencalls-list">
          {calls.map((call, idx) => {
            const isOpen = call.status === 'open';
            const deadlineDate = call.deadline ? parseISO(call.deadline) : null;
            const isExpired = deadlineDate && deadlineDate < new Date();
            const effectiveOpen = isOpen && !isExpired;
            const isExpanded = expandedId === (call.id || idx);

            return (
              <article key={call.id || idx} className={'opencall-card' + (effectiveOpen ? ' open' : ' closed')} style={{position:'relative'}}>
                {call.image && (
                  <div className="opencall-img">
                    <img src={call.image} alt={txt(call.title, lang)} loading="lazy" />
                  </div>
                )}
                <div className="opencall-content">
                  <div className="opencall-header">
                    <span className={'opencall-status ' + (effectiveOpen ? 'status-open' : 'status-closed')}>
                      {effectiveOpen ? t('opencall_status_open') : t('opencall_status_closed')}
                    </span>
                    <h3 className="serif">{txt(call.title, lang)}</h3>
                    <div className="opencall-meta mono soft">
                      {txt(call.subtitle, lang)} &middot; {txt(call.dates, lang)}
                    </div>
                    {call.deadline && (
                      <div className="opencall-deadline">
                        <strong>{txt(call.deadlineLabel, lang)}</strong>
                      </div>
                    )}
                    <p className="opencall-intro"><strong>{txt(call.intro, lang)}</strong></p>
                    <button className="opencall-toggle" onClick={() => setExpandedId(isExpanded ? null : (call.id || idx))}>
                      {isExpanded ? t('opencall_show_less') : t('opencall_show_more')}
                    </button>
                    {cms.active && (
                      <button className="cms-edit-btn" onClick={() => setEditingCall(call)}>
                        {t('cms_edit')}
                      </button>
                    )}
                  </div>
                  <div className={'opencall-details' + (isExpanded ? ' expanded' : '')}>
                    <div className="opencall-body">
                      <Editable field={call.description} path={'openCalls.' + idx + '.description'} Tag="p" rich />
                      {(call.sections || []).map((sec, si) => (
                        <div key={si} className="opencall-section">
                          <Editable field={sec.heading} path={'openCalls.' + idx + '.sections.' + si + '.heading'} Tag="h4" />
                          <Editable field={sec.body} path={'openCalls.' + idx + '.sections.' + si + '.body'} Tag="div" className="opencall-section-body" rich />
                        </div>
                      ))}
                      {call.contact && (
                        <div className="opencall-contact">
                          <strong>{t('opencall_contact')}:</strong>{' '}
                          <a href={'mailto:' + call.contact}>{call.contact}</a>
                        </div>
                      )}
                      {call.status === 'open' && (
                        <a className="opencall-apply-btn" href="https://forms.gle/KMMmhnAjdQj7RXUf7" target="_blank" rel="noopener noreferrer">
                          {t('opencall_apply')} →
                        </a>
                      )}
                    </div>
                  </div>
                </div>
              </article>
            );
          })}
        </div>
        {cms.active && (
          <button className="cms-add-btn" onClick={() => setEditingCall({ title: {}, subtitle: {}, dates: {}, deadlineLabel: {}, intro: {}, description: {}, status: 'open', deadline: '', image: '', contact: '', sections: [] })}>
            {t('cms_add_call')}
          </button>
        )}
      </div>
      {editingCall && (
        <OpenCallModal
          call={editingCall}
          onSave={handleSaveCall}
          onDelete={handleDeleteCall}
          onClose={() => setEditingCall(null)}
        />
      )}
    </section>
  );
}

// ─── about ─────────────────────────────────────────────────────────
function About({ data }) {
  const t = useT();
  const fadeRef = useFadeIn();
  return (
    <section className="section section-stage fade-in" id="over" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_about')} sectionId="about" data={data}>
          {t('section_about_title') || null}
        </SectionHead>
        <div className="about-grid">
          <div className="col-spacer"></div>
          <Editable field={data.about.lead} path="about.lead" Tag="p" className="about-lead" rich />
          <Editable field={data.about.body} path="about.body" Tag="p" className="about-body" rich />
        </div>
      </div>
    </section>
  );
}

// ─── section head ──────────────────────────────────────────────────
function SectionHead({ label, children, marginalia, sectionId, data }) {
  const lang = useLang();
  const cms = useContext(CmsContext);
  const headers = data?.sectionHeaders;
  const header = sectionId && headers ? headers[sectionId] : null;

  // Use content.json values if available, otherwise fall back to props
  const displayLabel = header ? txt(header.label, lang) || label : label;
  const displayTitle = header ? txt(header.title, lang) : null;

  const handleLabelBlur = (e) => {
    if (!cms.active || !header) return;
    const val = e.target.innerText.trim();
    if (!header.label) header.label = { nl: '', en: '', fr: '' };
    if (val !== txt(header.label, lang)) {
      header.label[lang] = val;
      if (data && cms.dataRef?.current) {
        updateTranslationMeta(cms.dataRef.current, `sectionHeaders.${sectionId}.label`, lang, val);
        cms.bumpMeta();
      }
      cms.setDirty(true);
    }
  };

  const handleTitleBlur = (e) => {
    if (!cms.active || !header) return;
    const val = e.target.innerText.trim();
    if (!header.title) header.title = { nl: '', en: '', fr: '' };
    if (val !== txt(header.title, lang)) {
      header.title[lang] = val;
      if (data && cms.dataRef?.current) {
        updateTranslationMeta(cms.dataRef.current, `sectionHeaders.${sectionId}.title`, lang, val);
        cms.bumpMeta();
      }
      cms.setDirty(true);
    }
  };

  const labelEl = cms.active && header ? (
    <span contentEditable suppressContentEditableWarning
      data-editable={`sectionHeaders.${sectionId}.label`}
      onBlur={handleLabelBlur}
    >{displayLabel}</span>
  ) : displayLabel;

  // When header exists in content.json, use ONLY content.json title (ignore children prop)
  // This way empty titles in content.json stay empty for visitors, but CMS users can type
  const effectiveTitle = header ? displayTitle : (children || null);
  const titleEl = cms.active && header ? (
    <h2>
      <span contentEditable suppressContentEditableWarning
        data-editable={`sectionHeaders.${sectionId}.title`}
        onBlur={handleTitleBlur}
        data-placeholder="Voeg titel toe..."
        style={!displayTitle ? {opacity:.4} : undefined}
      >{displayTitle || ''}</span>
    </h2>
  ) : (effectiveTitle ? <h2>{effectiveTitle}</h2> : <div></div>);

  return (
    <div className="section-head">
      <div>
        <div className="mono soft"><span className="num"></span> &nbsp;/&nbsp; {labelEl}</div>
      </div>
      {titleEl}
      {marginalia !== false && (
        <span className="section-marginalia" aria-hidden="true">
          <span className="num-marginalia"></span> &middot; {displayLabel}
        </span>
      )}
    </div>
  );
}

// ─── practice ──────────────────────────────────────────────────────
function Practice({ data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const listRef = useRef(null);
  const fadeRef = useFadeIn();

  useEffect(() => {
    if (!cms.active || !listRef.current || typeof Sortable === 'undefined') return;
    const sortable = Sortable.create(listRef.current, {
      handle: '.cms-drag-handle',
      animation: 150,
      ghostClass: 'sortable-ghost',
      onEnd: (evt) => {
        const item = data.practice.splice(evt.oldIndex, 1)[0];
        data.practice.splice(evt.newIndex, 0, item);
        data.practice.forEach((p, i) => p.n = String(i + 1).padStart(2, '0'));
        cms.setDirty(true);
      }
    });
    return () => sortable.destroy();
  }, [cms.active]);

  return (
    <section className="section fade-in" id="praktijk" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_practice')} sectionId="practice" data={data}>
          {t('section_practice_title_1')}<span className="italic">{t('section_practice_title_2')}</span>{t('section_practice_title_3')}
        </SectionHead>
        <div className="practice-list" ref={listRef}>
          {data.practice.map((p) => (
            <div className="practice-row cms-draggable" key={p.n} data-id={p.n}>
              <div className="cms-drag-handle">⋮⋮</div>
              <div className="num">— {p.n}</div>
              <Editable field={p.title} path={`practice.${p.n}.title`} Tag="h3" />
              <Editable field={p.body} path={`practice.${p.n}.body`} Tag="p" rich />
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

// ─── event row ─────────────────────────────────────────────────────
function EventRow({ ev, isPast, onEdit }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const { day, my } = fmtDay(ev, lang);
  const range = fmtRange(ev, lang);

  return (
    <article className="event">
      <div className="event-date">
        <span className="day">{day}</span>
        <span className="my">{my}</span>
        {range && <span className="range">{range}</span>}
      </div>
      <div className="event-body">
        <div className="where">
          <span className="tag">{ev.tag}</span>
          <span>{ev.venue}</span>
          <span className="pip"></span>
          <span>{ev.city}</span>
        </div>
        <h3>{txt(ev.title, lang)}</h3>
        <p>{txt(ev.description, lang)}</p>
        {ev.collaborators && ev.collaborators.length > 0 && (
          <div className="with">{t('with')} {ev.collaborators.join(" · ")}</div>
        )}
        {!isPast && ev.ticketUrl && ev.ticketUrl !== '#' && (
          <a className="ticket" href={ev.ticketUrl} target="_blank" rel="noopener">{t('tickets')}</a>
        )}
        {isPast && (
          <a className="ticket" href="#" style={{color:"var(--ink-soft)"}}>{t('fragments')}</a>
        )}
        {ev.videoUrl && <VideoEmbed url={ev.videoUrl} />}
        {cms.active && (
          <button
            className="cms-add-btn"
            style={{display:'inline-flex',marginTop:12,padding:'6px 14px',fontSize:10}}
            onClick={() => onEdit(ev)}
          >{t('cms_edit')}</button>
        )}
      </div>
    </article>
  );
}

// ─── event form modal ──────────────────────────────────────────────
function EventModal({ event, data, onSave, onDelete, onClose }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const isNew = !event.id;

  const [form, setForm] = useState({
    title: txt(event.title, lang) || '',
    date: event.date || '',
    endDate: event.endDate || '',
    city: event.city || '',
    venue: event.venue || '',
    tag: event.tag || 'Concert',
    description: txt(event.description, lang) || '',
    collaborators: (event.collaborators || []).join(', '),
    ticketUrl: event.ticketUrl || '',
    videoUrl: event.videoUrl || '',
    image: event.image || '',
    projectSlug: event.projectSlug || '',
  });

  const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });

  const handleSave = () => {
    const updated = { ...event };
    if (!updated.id) updated.id = 'evt-' + Date.now().toString(36);
    const itemId = updated.id;
    if (typeof updated.title === 'object') {
      updated.title[lang] = form.title;
    } else {
      updated.title = { nl: '', en: '', fr: '', [lang]: form.title };
    }
    if (typeof updated.description === 'object') {
      updated.description[lang] = form.description;
    } else {
      updated.description = { nl: '', en: '', fr: '', [lang]: form.description };
    }
    // Track translation metadata
    if (cms.dataRef?.current) {
      updateTranslationMeta(cms.dataRef.current, `events.${itemId}.title`, lang, form.title);
      updateTranslationMeta(cms.dataRef.current, `events.${itemId}.description`, lang, form.description);
    }
    updated.date = form.date;
    updated.endDate = form.endDate || null;
    updated.city = form.city;
    updated.venue = form.venue;
    updated.tag = form.tag;
    updated.collaborators = form.collaborators.split(',').map(s => s.trim()).filter(Boolean);
    updated.ticketUrl = form.ticketUrl || null;
    updated.videoUrl = form.videoUrl || null;
    updated.image = form.image || null;
    updated.projectSlug = form.projectSlug || null;
    if (!updated.mediaKind) updated.mediaKind = form.videoUrl ? 'video' : 'image';
    if (!updated.imageId) updated.imageId = '';
    // Sync eventIds on projects
    if (data.projects) {
      data.projects.forEach(p => {
        const ids = p.eventIds || [];
        if (p.slug === form.projectSlug) {
          if (!ids.includes(updated.id)) { p.eventIds = [...ids, updated.id]; }
        } else {
          p.eventIds = ids.filter(eid => eid !== updated.id);
        }
      });
    }
    onSave(updated);
  };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal">
        <h3>{isNew ? t('cms_new_event') : t('cms_event_title')}</h3>
        <label>
          <span>Titel ({lang.toUpperCase()})</span>
          <input value={form.title} onChange={set('title')} />
        </label>
        <label>
          <span>Datum (YYYY-MM-DD)</span>
          <input type="date" value={form.date} onChange={set('date')} />
        </label>
        <label>
          <span>Einddatum (optioneel)</span>
          <input type="date" value={form.endDate} onChange={set('endDate')} />
        </label>
        <label>
          <span>Stad</span>
          <input value={form.city} onChange={set('city')} />
        </label>
        <label>
          <span>Locatie</span>
          <input value={form.venue} onChange={set('venue')} />
        </label>
        <label>
          <span>Type</span>
          <select value={form.tag} onChange={set('tag')}>
            <option>Concert</option>
            <option>Productie</option>
            <option>Residentie</option>
            <option>Tournée</option>
            <option>Workshop</option>
          </select>
        </label>
        <label>
          <span>Beschrijving ({lang.toUpperCase()})</span>
          <textarea value={form.description} onChange={set('description')} />
        </label>
        <label>
          <span>Medewerkers (komma-gescheiden)</span>
          <input value={form.collaborators} onChange={set('collaborators')} />
        </label>
        <label>
          <span>Ticket URL</span>
          <input value={form.ticketUrl} onChange={set('ticketUrl')} placeholder="https://..." />
        </label>
        <label>
          <span>Video URL (YouTube)</span>
          <input value={form.videoUrl} onChange={set('videoUrl')} placeholder="https://youtube.com/watch?v=..." />
        </label>
        <ImageUploadField value={form.image} onChange={(url) => setForm({ ...form, image: url })} label="Afbeelding" />
        {data && data.projects && data.projects.length > 0 && (
          <label>
            <span>Project koppelen</span>
            <select value={form.projectSlug} onChange={set('projectSlug')}>
              <option value="">— Geen project —</option>
              {data.projects.map(p => (
                <option key={p.slug} value={p.slug}>{p.title?.nl || p.slug}</option>
              ))}
            </select>
          </label>
        )}
        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(event.id); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{isNew ? t('cms_add_event') : t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

// ─── agenda ────────────────────────────────────────────────────────
function Agenda({ data }) {
  const today = useMemo(() => new Date(), []);
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const [tab, setTab] = useState("upcoming");
  const [editingEvent, setEditingEvent] = useState(null);
  const [refreshKey, setRefreshKey] = useState(0);

  const { upcoming, past } = useMemo(() => {
    const up = [], pa = [];
    for (const ev of data.events) {
      if (isUpcoming(ev, today)) up.push(ev);
      else pa.push(ev);
    }
    up.sort((a, b) => parseISO(a.date) - parseISO(b.date));
    pa.sort((a, b) => parseISO(b.date) - parseISO(a.date));
    return { upcoming: up, past: pa };
  }, [data.events, today, refreshKey]);

  const pastByYear = useMemo(() => {
    const groups = [];
    let cur = null;
    for (const ev of past) {
      const y = parseISO(ev.date).getFullYear();
      if (!cur || cur.year !== y) { cur = { year: y, items: [] }; groups.push(cur); }
      cur.items.push(ev);
    }
    return groups;
  }, [past]);

  const handleSaveEvent = (updated) => {
    const idx = data.events.findIndex(e => e.id === updated.id);
    if (idx >= 0) {
      data.events[idx] = updated;
    } else {
      data.events.push(updated);
    }
    cms.setDirty(true);
    setEditingEvent(null);
    setRefreshKey(k => k + 1);
  };

  const handleDeleteEvent = (id) => {
    const idx = data.events.findIndex(e => e.id === id);
    if (idx >= 0) data.events.splice(idx, 1);
    cms.setDirty(true);
    setRefreshKey(k => k + 1);
  };

  return (
    <section className="section fade-in" id="agenda" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_agenda')} sectionId="agenda" data={data} marginalia={false}>
          {null}
        </SectionHead>

        <div className="tabs" role="tablist">
          <button role="tab" className="tab" aria-selected={tab === "upcoming"} onClick={() => setTab("upcoming")}>
            {t('tab_upcoming')} <span className="count">({upcoming.length})</span>
          </button>
          {(cms.active || !data.hideHistory) && (
            <button role="tab" className="tab" aria-selected={tab === "past"} onClick={() => setTab("past")}
              style={data.hideHistory && cms.active ? {opacity:.5, fontStyle:'italic'} : undefined}>
              {t('tab_history')} <span className="count">({past.length})</span>
              {data.hideHistory && cms.active && ' 🚫'}
            </button>
          )}
        </div>

        {tab === "upcoming" && (
          <div className="events">
            {upcoming.length === 0 && (
              <div style={{padding:"48px 0",color:"var(--ink-soft)",fontStyle:"italic",fontFamily:"var(--serif)",fontSize:24}}>
                {t('no_upcoming')}
              </div>
            )}
            {upcoming.map((ev) => <EventRow key={ev.id} ev={ev} isPast={false} onEdit={setEditingEvent} />)}
          </div>
        )}

        {tab === "past" && (cms.active || !data.hideHistory) && (
          <div className="past-rail">
            {pastByYear.map((g, i) => (
              <div key={g.year}>
                {i > 0 && <div style={{height:24}}></div>}
                <div className="year-sep">
                  <span className="y serif italic">{g.year}</span>
                  <span className="l"></span>
                  <span className="mono soft">{g.items.length} {g.items.length === 1 ? t('project') : t('projects')}</span>
                </div>
                <div className="events">
                  {g.items.map((ev) => <EventRow key={ev.id} ev={ev} isPast={true} onEdit={setEditingEvent} />)}
                </div>
              </div>
            ))}
          </div>
        )}

        {cms.active && (
          <button className="cms-add-btn" onClick={() => setEditingEvent({ title: {nl:'',en:'',fr:''}, description: {nl:'',en:'',fr:''}, date: '', collaborators: [] })}>
            {t('cms_add_event')}
          </button>
        )}

        {editingEvent && (
          <EventModal
            event={editingEvent}
            data={data}
            onSave={handleSaveEvent}
            onDelete={handleDeleteEvent}
            onClose={() => setEditingEvent(null)}
          />
        )}
      </div>
    </section>
  );
}

// ─── partners ──────────────────────────────────────────────────────
function Partners({ data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const gridRef = useRef(null);
  const fadeRef = useFadeIn();

  useEffect(() => {
    if (!cms.active || !gridRef.current || typeof Sortable === 'undefined') return;
    const sortable = Sortable.create(gridRef.current, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      onEnd: (evt) => {
        const item = data.partners.splice(evt.oldIndex, 1)[0];
        data.partners.splice(evt.newIndex, 0, item);
        cms.setDirty(true);
      }
    });
    return () => sortable.destroy();
  }, [cms.active]);

  return (
    <section className="section fade-in" id="partners" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_partners')} sectionId="partners" data={data} />
        <p className="about-body" style={{maxWidth:"60ch",marginBottom:32,color:"var(--ink-soft)"}}>
          {t('partners_intro')}
        </p>
        {!cms.active && data.partners.length > 0 && (
          <div className="partners-marquee" aria-hidden="true">
            <div className="partners-marquee-track">
              {[...data.partners, ...data.partners].map((p, i) => (
                <span className="item" key={i}>{p.name}</span>
              ))}
            </div>
          </div>
        )}
        <div className="partners-grid" ref={gridRef}>
          {data.partners.map((p, pi) => (
            <div className="partner" key={p.name} style={{position:'relative'}}>
              {p.logo && <img className="partner-logo" src={p.logo} alt={p.name} />}
              <div className="role">{txt(p.role, lang)}</div>
              <div className="name">{p.name}</div>
              {cms.active && (
                <button className="cms-partner-edit" onClick={() => {
                  const name = prompt('Naam:', p.name);
                  if (name === null) return;
                  const logo = prompt('Logo URL:', p.logo || '');
                  if (logo === null) return;
                  data.partners[pi].name = name || p.name;
                  data.partners[pi].logo = logo || '';
                  cms.setDirty(true);
                  cms.bumpMeta();
                }}>✎</button>
              )}
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

// ─── video section ────────────────────────────────────────────────
function VideoModal({ video, onSave, onDelete, onClose }) {
  const lang = useLang();
  const t = useT();
  const isNew = !video.id;
  const [form, setForm] = useState({
    title: txt(video.title, lang) || '',
    url: video.url || '',
    year: video.year || new Date().getFullYear(),
  });
  const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });

  const handleSave = () => {
    const updated = { ...video };
    if (!updated.id) updated.id = 'vid-' + Date.now().toString(36);
    if (typeof updated.title === 'object') {
      updated.title[lang] = form.title;
    } else {
      updated.title = { nl: '', en: '', fr: '', [lang]: form.title };
    }
    updated.url = form.url;
    updated.year = parseInt(form.year) || new Date().getFullYear();
    onSave(updated);
  };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal">
        <h3>{isNew ? 'Nieuwe video' : 'Video bewerken'}</h3>
        <label>
          <span>Titel ({lang.toUpperCase()})</span>
          <input value={form.title} onChange={set('title')} />
        </label>
        <label>
          <span>YouTube URL</span>
          <input value={form.url} onChange={set('url')} placeholder="https://youtube.com/watch?v=..." />
        </label>
        <label>
          <span>Jaar</span>
          <input type="number" value={form.year} onChange={set('year')} />
        </label>
        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(video.id); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

function VideoSection({ data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const videos = useMemo(() =>
    [...(data.videos || [])].sort((a, b) => (b.year || 0) - (a.year || 0)),
    [data.videos]
  );
  const [editingVideo, setEditingVideo] = useState(null);

  const handleSaveVideo = (updated) => {
    const idx = data.videos.findIndex(v => v.id === updated.id);
    if (idx >= 0) data.videos[idx] = updated;
    else data.videos = [...(data.videos || []), updated];
    cms.setDirty(true);
    setEditingVideo(null);
  };

  const handleDeleteVideo = (id) => {
    data.videos = data.videos.filter(v => v.id !== id);
    cms.setDirty(true);
  };

  if (videos.length === 0 && !cms.active) return null;

  return (
    <section className="section fade-in" id="video" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label="Video" sectionId="video" data={data} />
        <div className="video-grid">
          {videos.map((vid, idx) => (
            <div className={`video-card${idx === 0 ? ' video-card--featured' : ''}`} key={vid.id}>
              <VideoEmbed url={vid.url} />
              <div className="video-card-info">
                <h4>{txt(vid.title, lang)}</h4>
                <span className="video-year">{vid.year}</span>
              </div>
              {cms.active && (
                <button className="cms-edit-btn" onClick={() => setEditingVideo(vid)}>
                  {t('cms_edit')}
                </button>
              )}
            </div>
          ))}
        </div>
        {cms.active && (
          <button className="cms-add-btn" onClick={() => setEditingVideo({ title: {}, url: '', year: new Date().getFullYear() })}>
            + Nieuwe video
          </button>
        )}
      </div>
      {editingVideo && (
        <VideoModal
          video={editingVideo}
          onSave={handleSaveVideo}
          onDelete={handleDeleteVideo}
          onClose={() => setEditingVideo(null)}
        />
      )}
    </section>
  );
}

// ─── audio modal (CMS) ────────────────────────────────────────────
function AudioModal({ track, onSave, onDelete, onClose }) {
  const lang = useLang();
  const t = useT();
  const isNew = !track.id;
  const [form, setForm] = useState({
    title: (typeof track.title === 'object' ? txt(track.title, lang) : track.title) || '',
    artist: track.artist || '',
    album: track.album || '',
    year: track.year || '',
    duration: track.duration || '',
    src: track.src || '',
  });
  const [uploading, setUploading] = useState(false);
  const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });

  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    setUploading(true);
    const fd = new FormData();
    fd.append('audio', file);
    try {
      const res = await fetch('/api/upload-audio', { method: 'POST', body: fd });
      const data = await res.json();
      if (data.url) setForm(f => ({ ...f, src: data.url }));
    } catch (err) { /* ignore */ }
    setUploading(false);
  };

  const handleSave = () => {
    const updated = { ...track };
    if (!updated.id) updated.id = 'audio-' + Date.now().toString(36);
    if (typeof updated.title === 'object') {
      updated.title[lang] = form.title;
    } else {
      updated.title = form.title;
    }
    updated.artist = form.artist;
    updated.album = form.album;
    updated.year = form.year ? parseInt(form.year) : null;
    updated.duration = form.duration;
    updated.src = form.src;
    onSave(updated);
  };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal">
        <h3>{isNew ? 'Nieuw nummer' : 'Nummer bewerken'}</h3>
        <label>
          <span>Titel</span>
          <input value={form.title} onChange={set('title')} />
        </label>
        <label>
          <span>Artiest</span>
          <input value={form.artist} onChange={set('artist')} />
        </label>
        <label>
          <span>Album</span>
          <input value={form.album} onChange={set('album')} />
        </label>
        <label>
          <span>Jaar</span>
          <input type="number" value={form.year} onChange={set('year')} />
        </label>
        <label>
          <span>Duur (bijv. 4:32)</span>
          <input value={form.duration} onChange={set('duration')} placeholder="4:32" />
        </label>
        <label>
          <span>Audiobestand</span>
          {form.src && <div style={{fontSize:12,color:'#666',marginBottom:4}}>{form.src}</div>}
          <input type="file" accept=".mp3,.wav,.ogg,.flac,.m4a,.aac" onChange={handleUpload} />
          {uploading && <span>Uploaden...</span>}
        </label>
        <label>
          <span>Of directe URL</span>
          <input value={form.src} onChange={set('src')} placeholder="https://... of /audio/file.mp3" />
        </label>
        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(track.id); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

// ─── listen section ───────────────────────────────────────────────
function Listen({ data }) {
  const t = useT();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const audio = useContext(AudioContext2);
  const tracks = data.audio || [];
  const [editingTrack, setEditingTrack] = useState(null);

  const handleSaveTrack = (updated) => {
    const idx = data.audio.findIndex(t => t.id === updated.id);
    if (idx >= 0) data.audio[idx] = updated;
    else data.audio = [...(data.audio || []), updated];
    cms.setDirty(true);
    setEditingTrack(null);
  };

  const handleDeleteTrack = (id) => {
    data.audio = data.audio.filter(t => t.id !== id);
    cms.setDirty(true);
  };

  if (tracks.length === 0 && !cms.active) return null;

  return (
    <section className="section fade-in" id="luisteren" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_listen')} sectionId="listen" data={data}>
          {(t('section_listen_title_1') || t('section_listen_title_2')) ? <>{t('section_listen_title_1')}<span className="italic">{t('section_listen_title_2')}</span>{t('section_listen_title_3')}</> : null}
        </SectionHead>
        <div className="audio-list">
          {tracks.map((track) => {
            const isCurrent = audio.currentTrack && audio.currentTrack.id === track.id;
            const title = typeof track.title === 'object' ? txt(track.title, 'nl') : track.title;
            return (
              <div
                className={`audio-track ${isCurrent && audio.isPlaying ? 'playing' : ''}`}
                key={track.id}
              >
                <button className="audio-play-btn" onClick={() => track.src && audio.toggle(track)}>
                  {isCurrent && audio.isPlaying ? '⏸' : '▶'}
                </button>
                <div className="audio-info" onClick={() => track.src && audio.toggle(track)}>
                  <span className="track-title">{title}</span>
                  <span className="track-artist">{track.artist}</span>
                </div>
                <span className="audio-duration">{track.duration}</span>
                {cms.active && (
                  <button className="cms-edit-btn-sm" onClick={() => setEditingTrack(track)}>
                    {t('cms_edit')}
                  </button>
                )}
              </div>
            );
          })}
        </div>
        {cms.active && (
          <button className="cms-add-btn" onClick={() => setEditingTrack({ title: '', artist: '', duration: '', src: '' })}>
            + Nieuw nummer
          </button>
        )}
      </div>
      {editingTrack && (
        <AudioModal
          track={editingTrack}
          onSave={handleSaveTrack}
          onDelete={handleDeleteTrack}
          onClose={() => setEditingTrack(null)}
        />
      )}
    </section>
  );
}

// ─── instagram section ────────────────────────────────────────────
function InstagramPostModal({ post, onSave, onDelete, onClose }) {
  const lang = useLang();
  const t = useT();
  const isNew = !post.image;
  const [form, setForm] = useState({
    caption: txt(post.caption, lang) || '',
    url: post.url || '',
    image: post.image || '',
  });
  const set = (key) => (e) => setForm({ ...form, [key]: e.target.value });

  const [uploading, setUploading] = useState(false);
  const handleImageUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    setUploading(true);
    const fd = new FormData();
    fd.append('image', file);
    try {
      const res = await fetch('/api/upload', { method: 'POST', body: fd });
      const data = await res.json();
      if (data.url) setForm(f => ({ ...f, image: data.url }));
    } catch (err) { /* ignore */ }
    setUploading(false);
  };

  const handleSave = () => {
    const updated = { ...post };
    if (typeof updated.caption === 'object') {
      updated.caption[lang] = form.caption;
    } else {
      updated.caption = { nl: '', en: '', fr: '', [lang]: form.caption };
    }
    updated.url = form.url;
    updated.image = form.image;
    onSave(updated);
  };

  return (
    <div className="cms-modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="cms-modal">
        <h3>{isNew ? 'Nieuwe Instagram post' : 'Instagram post bewerken'}</h3>
        <label>
          <span>Afbeelding</span>
          {form.image && <img src={form.image} style={{width:120,height:120,objectFit:'cover',borderRadius:4,marginBottom:8}} />}
          <input type="file" accept="image/*" onChange={handleImageUpload} />
          {uploading && <span>Uploaden...</span>}
        </label>
        <label>
          <span>Bijschrift ({lang.toUpperCase()})</span>
          <input value={form.caption} onChange={set('caption')} />
        </label>
        <label>
          <span>Instagram post URL</span>
          <input value={form.url} onChange={set('url')} placeholder="https://instagram.com/p/..." />
        </label>
        <div className="cms-modal-actions">
          {!isNew && onDelete && (
            <button className="cms-danger" onClick={() => { onDelete(); onClose(); }}>
              {t('cms_delete')}
            </button>
          )}
          <button onClick={onClose}>{t('cms_cancel')}</button>
          <button className="cms-primary" onClick={handleSave}>{t('cms_save')}</button>
        </div>
      </div>
    </div>
  );
}

function InstagramEmbed({ embedCode }) {
  const containerRef = useRef(null);
  useEffect(() => {
    if (!containerRef.current || !embedCode) return;
    containerRef.current.innerHTML = embedCode;
    // Execute any <script> tags in the embed code
    const scripts = containerRef.current.querySelectorAll('script');
    scripts.forEach(oldScript => {
      const newScript = document.createElement('script');
      Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
      newScript.textContent = oldScript.textContent;
      oldScript.parentNode.replaceChild(newScript, oldScript);
    });
  }, [embedCode]);
  return <div ref={containerRef} className="insta-embed" />;
}

function InstagramSection({ data }) {
  const lang = useLang();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const ig = data.instagram || {};
  const posts = ig.posts || [];
  const embedCode = ig.embedCode || '';
  const [editingPost, setEditingPost] = useState(null);
  const [editingIdx, setEditingIdx] = useState(-1);
  const [showEmbedEditor, setShowEmbedEditor] = useState(false);
  const [embedDraft, setEmbedDraft] = useState(embedCode);

  const handleSave = (updated) => {
    if (!data.instagram) data.instagram = { handle: '@baroqueorchestrationx', profileUrl: '', posts: [] };
    if (editingIdx >= 0) {
      data.instagram.posts[editingIdx] = updated;
    } else {
      data.instagram.posts = [...data.instagram.posts, updated];
    }
    cms.setDirty(true);
    setEditingPost(null);
    setEditingIdx(-1);
  };

  const handleDelete = () => {
    if (editingIdx >= 0) {
      data.instagram.posts = data.instagram.posts.filter((_, i) => i !== editingIdx);
      cms.setDirty(true);
    }
  };

  const saveEmbedCode = () => {
    if (!data.instagram) data.instagram = { handle: '@baroqueorchestrationx', profileUrl: '', posts: [] };
    data.instagram.embedCode = embedDraft;
    cms.setDirty(true);
    setShowEmbedEditor(false);
  };

  const hasEmbed = !!embedCode.trim();
  if (!hasEmbed && posts.length === 0 && !cms.active) return null;

  return (
    <section className="section fade-in" id="instagram" ref={fadeRef}>
      <div className="wrap">
        <div className="insta-header">
          <h2 className="section-title" style={{margin:0}}>
            Instagram <a href={ig.profileUrl || '#'} target="_blank" rel="noopener" className="insta-handle">{ig.handle}</a>
          </h2>
        </div>

        {/* CMS: embed code editor */}
        {cms.active && (
          <div className="cms-embed-section">
            <button className="cms-add-btn" onClick={() => { setEmbedDraft(embedCode); setShowEmbedEditor(!showEmbedEditor); }}
              style={{marginBottom: 12}}>
              {hasEmbed ? '✎ Embed code bewerken' : '🔗 Instagram embed toevoegen (Behold.so)'}
            </button>
            {showEmbedEditor && (
              <div className="cms-embed-editor">
                <p style={{fontSize:13,color:'var(--ink-soft)',margin:'0 0 8px'}}>
                  Plak hier de embed code van <a href="https://behold.so" target="_blank" rel="noopener">behold.so</a>.
                  Maak een gratis account, koppel Instagram, en kopieer de embed code.
                </p>
                <textarea
                  value={embedDraft}
                  onChange={(e) => setEmbedDraft(e.target.value)}
                  placeholder='<div id="behold-widget-..."></div><script src="https://w.behold.so/widget.js" ...></script>'
                  style={{width:'100%',minHeight:80,fontFamily:'var(--mono)',fontSize:12,padding:8,borderRadius:4,border:'1px solid var(--rule)',resize:'vertical'}}
                />
                <div style={{display:'flex',gap:8,marginTop:8}}>
                  <button className="cms-primary" onClick={saveEmbedCode}>Opslaan</button>
                  {hasEmbed && (
                    <button className="cms-danger" onClick={() => { setEmbedDraft(''); data.instagram.embedCode = ''; cms.setDirty(true); setShowEmbedEditor(false); }}>
                      Embed verwijderen
                    </button>
                  )}
                  <button onClick={() => setShowEmbedEditor(false)}>Annuleren</button>
                </div>
              </div>
            )}
          </div>
        )}

        {/* Show embed if configured, otherwise show manual grid */}
        {hasEmbed ? (
          <InstagramEmbed embedCode={embedCode} />
        ) : (
          <>
            <div className="insta-grid">
              {posts.map((post, i) => (
                <a key={i} href={post.url || '#'} target="_blank" rel="noopener"
                  className="insta-item" onClick={cms.active ? (e) => { e.preventDefault(); setEditingIdx(i); setEditingPost(post); } : undefined}>
                  <img src={post.image} alt={txt(post.caption, lang)} loading="lazy" />
                  <div className="insta-overlay">
                    <span>{txt(post.caption, lang)}</span>
                  </div>
                </a>
              ))}
            </div>
            {cms.active && (
              <button className="cms-add-btn" onClick={() => { setEditingIdx(-1); setEditingPost({ caption: {}, url: '', image: '' }); }}>
                + Instagram post
              </button>
            )}
          </>
        )}

        {ig.profileUrl && (
          <div style={{textAlign:'center',marginTop:24}}>
            <a href={ig.profileUrl} target="_blank" rel="noopener" className="btn-outline">
              Volg {ig.handle} op Instagram
            </a>
          </div>
        )}
      </div>
      {editingPost && (
        <InstagramPostModal
          post={editingPost}
          onSave={handleSave}
          onDelete={handleDelete}
          onClose={() => { setEditingPost(null); setEditingIdx(-1); }}
        />
      )}
    </section>
  );
}

// ─── gallery section ──────────────────────────────────────────────
function Gallery({ data }) {
  const t = useT();
  const lang = useLang();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const [lightboxIdx, setLightboxIdx] = useState(null);
  const [uploading, setUploading] = useState(false);
  const images = data.gallery || [];

  const closeLightbox = useCallback(() => setLightboxIdx(null), []);
  const prevImage = useCallback(() => setLightboxIdx(i => (i - 1 + images.length) % images.length), [images.length]);
  const nextImage = useCallback(() => setLightboxIdx(i => (i + 1) % images.length), [images.length]);

  useEffect(() => {
    if (lightboxIdx === null) return;
    const handleKey = (e) => {
      if (e.key === 'Escape') closeLightbox();
      if (e.key === 'ArrowLeft') prevImage();
      if (e.key === 'ArrowRight') nextImage();
    };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [lightboxIdx, closeLightbox, prevImage, nextImage]);

  const handleAddImage = async (e) => {
    const files = Array.from(e.target.files);
    if (files.length === 0) return;
    setUploading(true);
    for (const file of files) {
      const fd = new FormData();
      fd.append('image', file);
      try {
        const res = await fetch('/api/upload', { method: 'POST', body: fd });
        const d = await res.json();
        if (d.url) {
          if (!data.gallery) data.gallery = [];
          data.gallery.push({ url: d.url, caption: { nl: '', en: '', fr: '' } });
        }
      } catch (err) { /* ignore */ }
    }
    cms.setDirty(true);
    cms.bumpMeta();
    setUploading(false);
  };

  const handleDeleteImage = (idx) => {
    if (!confirm('Deze foto verwijderen?')) return;
    data.gallery.splice(idx, 1);
    cms.setDirty(true);
    cms.bumpMeta();
    setLightboxIdx(null);
  };

  if (images.length === 0 && !cms.active) return null;

  return (
    <section className="section fade-in" id="galerij" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_gallery')} sectionId="gallery" data={data} />
        <div className="gallery-grid">
          {images.map((img, i) => (
            <div className="gallery-item" key={i} style={{position:'relative'}}>
              <img src={img.url} alt={txt(img.caption, lang) || ''} loading="lazy" onClick={() => setLightboxIdx(i)} />
              {cms.active && (
                <button className="cms-gallery-delete" onClick={() => handleDeleteImage(i)} title="Verwijderen">&times;</button>
              )}
            </div>
          ))}
          {cms.active && (
            <label className="gallery-item gallery-add-item">
              <input type="file" accept="image/*" multiple onChange={handleAddImage} style={{display:'none'}} />
              <span style={{fontSize:32,opacity:0.4}}>+</span>
              <span className="mono" style={{fontSize:11,opacity:0.5}}>{uploading ? 'Uploaden...' : "Foto's toevoegen"}</span>
            </label>
          )}
        </div>
      </div>

      {lightboxIdx !== null && (
        <div className="lightbox active" onClick={closeLightbox}>
          <img src={images[lightboxIdx].url} alt={txt(images[lightboxIdx].caption, lang) || ''} onClick={(e) => e.stopPropagation()} />
          {images[lightboxIdx].caption && <div className="lightbox-caption">{txt(images[lightboxIdx].caption, lang)}</div>}
          <button className="lightbox-close" onClick={closeLightbox}>&times;</button>
          {images.length > 1 && (
            <>
              <button className="lightbox-nav lightbox-prev" onClick={prevImage}>&lsaquo;</button>
              <button className="lightbox-nav lightbox-next" onClick={nextImage}>&rsaquo;</button>
            </>
          )}
        </div>
      )}
    </section>
  );
}

// ─── footer ───────────────────────────────────────────────────────
// ─── Contact form ────────────────────────────────────────────────
function ContactForm({ data }) {
  const lang = useLang();
  const t = useT();
  const [form, setForm] = useState({ name: '', email: '', subject: '', message: '' });
  const [sending, setSending] = useState(false);
  const [status, setStatus] = useState(null); // 'success' | 'error'

  // Allow pre-selecting subject via URL hash (e.g. #contact?subject=open-call)
  useEffect(() => {
    const hash = window.location.hash;
    if (hash.includes('subject=')) {
      const match = hash.match(/subject=([^&]+)/);
      if (match) setForm(f => ({ ...f, subject: match[1] }));
    }
  }, []);

  // Listen for custom event from open call apply button
  useEffect(() => {
    const handler = (e) => {
      if (e.detail?.subject) {
        setForm(f => ({ ...f, subject: e.detail.subject }));
        document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' });
      }
    };
    window.addEventListener('box-contact-subject', handler);
    return () => window.removeEventListener('box-contact-subject', handler);
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setSending(true);
    setStatus(null);
    try {
      const res = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form)
      });
      if (res.ok) {
        setStatus('success');
        setForm({ name: '', email: '', subject: '', message: '' });
      } else {
        setStatus('error');
      }
    } catch {
      setStatus('error');
    }
    setSending(false);
  };

  // Subject field is now free text input (no dropdown)

  const fadeRef = useRef(null);
  useEffect(() => {
    if (!fadeRef.current) return;
    const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { e.target.classList.add('visible'); obs.unobserve(e.target); } }, { threshold: 0.1 });
    obs.observe(fadeRef.current);
    return () => obs.disconnect();
  }, []);

  return (
    <section className="section contact-section fade-in" id="contact" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label="Contact" sectionId="contact" data={data} />

        {status === 'success' ? (
          <div className="contact-success">
            <p className="serif" style={{fontSize:'1.2rem'}}>{t('contact_success')}</p>
          </div>
        ) : (
          <form className="contact-form" onSubmit={handleSubmit}>
            <div className="contact-row">
              <div className="contact-field">
                <label htmlFor="cf-name">{t('contact_name')}</label>
                <input id="cf-name" type="text" required value={form.name}
                  onChange={e => setForm({...form, name: e.target.value})}
                  placeholder={t('contact_name')} />
              </div>
              <div className="contact-field">
                <label htmlFor="cf-email">{t('contact_email')}</label>
                <input id="cf-email" type="email" required value={form.email}
                  onChange={e => setForm({...form, email: e.target.value})}
                  placeholder={t('contact_email')} />
              </div>
            </div>
            <div className="contact-field">
              <label htmlFor="cf-subject">{t('contact_subject')}</label>
              <input id="cf-subject" type="text" value={form.subject}
                onChange={e => setForm({...form, subject: e.target.value})}
                placeholder={t('contact_subject')} />
            </div>
            <div className="contact-field">
              <label htmlFor="cf-message">{t('contact_message')}</label>
              <textarea id="cf-message" rows="6" required value={form.message}
                onChange={e => setForm({...form, message: e.target.value})}
                placeholder={t('contact_message')} />
            </div>
            {status === 'error' && <p className="contact-error">{t('contact_error')}</p>}
            <button type="submit" className="contact-submit" disabled={sending}>
              {sending ? t('contact_sending') : t('contact_send')}
            </button>
          </form>
        )}
      </div>
    </section>
  );
}

// ─── Messages panel (CMS only) ──────────────────────────────────
function MessagesPanel() {
  const t = useT();
  const [messages, setMessages] = useState([]);
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  const fetchMessages = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/messages');
      if (res.ok) setMessages(await res.json());
    } catch {}
    setLoading(false);
  };

  useEffect(() => { if (open) fetchMessages(); }, [open]);

  const markRead = async (id) => {
    await fetch(`/api/messages/${id}/read`, { method: 'PUT' });
    setMessages(msgs => msgs.map(m => m.id === id ? { ...m, read: true } : m));
  };

  const deleteMsg = async (id) => {
    if (!confirm('Bericht verwijderen?')) return;
    await fetch(`/api/messages/${id}`, { method: 'DELETE' });
    setMessages(msgs => msgs.filter(m => m.id !== id));
  };

  const unreadCount = messages.filter(m => !m.read).length;
  const subjectLabels = { general: 'Algemeen', 'open-call': 'Open Call', press: 'Pers', collaboration: 'Samenwerking' };

  return (
    <>
      <button className="cms-messages-btn" onClick={() => setOpen(!open)}>
        ✉ {t('cms_messages')}
        {unreadCount > 0 && <span className="cms-unread-badge">{unreadCount}</span>}
      </button>
      {open && (
        <div className="cms-messages-panel">
          <div className="cms-messages-header">
            <h3>{t('cms_messages')}</h3>
            <button onClick={() => setOpen(false)}>&times;</button>
          </div>
          {loading ? <p style={{padding:16}}>Loading...</p> :
           messages.length === 0 ? <p style={{padding:16}} className="soft">{t('cms_no_messages')}</p> :
            <div className="cms-messages-list">
              {messages.map(msg => (
                <div key={msg.id} className={`cms-message ${msg.read ? 'read' : 'unread'}`}>
                  <div className="cms-msg-header">
                    <strong>{msg.name}</strong>
                    <span className="cms-msg-subject">{subjectLabels[msg.subject] || msg.subject}</span>
                    <span className="cms-msg-date">{new Date(msg.date).toLocaleDateString('nl-BE')}</span>
                  </div>
                  <div className="cms-msg-email">{msg.email}</div>
                  <div className="cms-msg-body">{msg.message}</div>
                  <div className="cms-msg-actions">
                    {!msg.read && <button onClick={() => markRead(msg.id)}>{t('cms_mark_read')}</button>}
                    <button onClick={() => deleteMsg(msg.id)} className="cms-msg-delete">{t('cms_delete_msg')}</button>
                  </div>
                </div>
              ))}
            </div>
          }
        </div>
      )}
    </>
  );
}

// ─── projects section (homepage) ──────────────────────────────────
function ProjectsSection({ data }) {
  const t = useT();
  const lang = useLang();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  const [editProject, setEditProject] = useState(null);
  const projects = data.projects || [];

  if (projects.length === 0 && !cms.active) return null;

  return (
    <section id="projecten" className="section fade-in" ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={t('section_projects')} sectionId="projects" data={data} />
        <div className="projects-grid">
          {projects.map((proj, pi) => (
            <div key={proj.id} style={{position:'relative'}}>
              <a href={'/projects/' + proj.slug} className="project-card">
                {proj.heroVideo ? (
                  <video src={proj.heroVideo} className="project-card-img" autoPlay muted loop playsInline />
                ) : proj.heroImage ? (
                  <img src={proj.heroImage} alt="" className="project-card-img" />
                ) : null}
                <div className="project-card-info">
                  <h3 className="serif italic">{txt(proj.title, lang)}</h3>
                  <p>{txt(proj.subtitle, lang)}</p>
                </div>
              </a>
              {cms.active && (
                <button className="cms-btn project-card-edit" onClick={() => setEditProject(pi)}>✎</button>
              )}
            </div>
          ))}
          {cms.active && (
            <button className="project-card project-card-add" onClick={() => setEditProject(-1)}>
              <span style={{fontSize:32,opacity:0.5}}>+</span>
              <span className="mono" style={{marginTop:8}}>Nieuw project</span>
            </button>
          )}
        </div>
        {editProject !== null && (
          <ProjectModal
            data={data}
            projectIndex={editProject}
            onClose={() => setEditProject(null)}
          />
        )}
      </div>
    </section>
  );
}

function ProjectModal({ data, projectIndex, onClose }) {
  const cms = useContext(CmsContext);
  const isNew = projectIndex === -1;
  const existing = isNew ? null : (data.projects || [])[projectIndex];
  const [activeLang, setActiveLang] = useState('nl');

  const [titles, setTitles] = useState({
    nl: existing?.title?.nl || '', en: existing?.title?.en || '', fr: existing?.title?.fr || ''
  });
  const [subtitles, setSubtitles] = useState({
    nl: existing?.subtitle?.nl || '', en: existing?.subtitle?.en || '', fr: existing?.subtitle?.fr || ''
  });
  const [descs, setDescs] = useState({
    nl: existing?.description?.nl || '', en: existing?.description?.en || '', fr: existing?.description?.fr || ''
  });
  const [heroImage, setHeroImage] = useState(existing?.heroImage || '');
  const [heroVideo, setHeroVideo] = useState(existing?.heroVideo || '');
  const [artists, setArtists] = useState(existing ? (existing.artists || []).map(a => ({...a})) : []);
  const [translating, setTranslating] = useState(false);

  function addArtist() {
    setArtists([...artists, { name: '', role: { nl: '', en: '', fr: '' }, image: '', bio: { nl: '', en: '', fr: '' } }]);
  }
  function updateArtist(idx, field, value) {
    const copy = artists.map((a, i) => i === idx ? { ...a, [field]: value } : a);
    setArtists(copy);
  }
  function updateArtistLang(idx, field, lang, value) {
    const copy = artists.map((a, i) => {
      if (i !== idx) return a;
      return { ...a, [field]: { ...(a[field] || {}), [lang]: value } };
    });
    setArtists(copy);
  }
  function removeArtist(idx) {
    setArtists(artists.filter((_, i) => i !== idx));
  }

  const handleAutoTranslate = async () => {
    if (!titles.nl && !subtitles.nl && !descs.nl) return;
    setTranslating(true);
    const targetLangs = ['en', 'fr'];
    try {
      for (const [src, setter] of [[titles, setTitles], [subtitles, setSubtitles], [descs, setDescs]]) {
        if (!src.nl) continue;
        const res = await fetch('/api/translate', {
          method: 'POST', headers: {'Content-Type':'application/json'},
          body: JSON.stringify({ text: src.nl, from: 'nl', to: targetLangs })
        });
        const d = await res.json();
        if (d.translations) setter(prev => ({ ...prev, ...d.translations }));
      }
    } catch (err) { /* ignore */ }
    setTranslating(false);
  };

  function handleSave() {
    const slug = titles.nl.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
    const proj = {
      id: existing ? existing.id : slug,
      slug: existing ? existing.slug : slug,
      title: { nl: titles.nl, en: titles.en || titles.nl, fr: titles.fr || titles.nl },
      subtitle: { nl: subtitles.nl, en: subtitles.en || subtitles.nl, fr: subtitles.fr || subtitles.nl },
      heroImage: heroImage || '',
      heroVideo: heroVideo || '',
      description: { nl: descs.nl, en: descs.en || descs.nl, fr: descs.fr || descs.nl },
      artists: artists.filter(a => a.name.trim()),
      eventIds: existing ? existing.eventIds : [],
      gallery: existing ? existing.gallery : [],
      credits: existing ? existing.credits : { nl: '', en: '', fr: '' },
    };
    if (!data.projects) data.projects = [];
    if (isNew) {
      data.projects.push(proj);
    } else {
      data.projects[projectIndex] = proj;
    }
    cms.setDirty(true);
    cms.bumpMeta();
    onClose();
  }

  function handleDelete() {
    if (!confirm('Dit project verwijderen?')) return;
    data.projects.splice(projectIndex, 1);
    cms.setDirty(true);
    cms.bumpMeta();
    onClose();
  }

  return (
    <div className="cms-modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="cms-modal cms-modal-wide">
        <h3>{isNew ? 'Nieuw project' : 'Project bewerken'}</h3>
        <div className="cms-modal-split">
          {/* ─── Form ─── */}
          <div className="cms-modal-form">
            <div className="cms-lang-tabs">
              {['nl','en','fr'].map(l => (
                <button key={l} className={`cms-lang-tab ${activeLang === l ? 'active' : ''} ${!titles[l] && l !== 'nl' ? 'empty' : ''}`}
                  onClick={() => setActiveLang(l)}>{l.toUpperCase()}{!titles[l] && l !== 'nl' ? ' ○' : ''}</button>
              ))}
              <button className="cms-auto-translate" onClick={handleAutoTranslate} disabled={translating || !titles.nl}
                title="Vertaal NL → EN + FR">{translating ? '⏳' : '🌐'} Auto</button>
            </div>

            <label><span>Titel ({activeLang.toUpperCase()}) {activeLang === 'nl' ? '*' : ''}</span>
              <input value={titles[activeLang]} onChange={e => setTitles({...titles, [activeLang]: e.target.value})} />
            </label>
            <label><span>Subtitel ({activeLang.toUpperCase()})</span>
              <input value={subtitles[activeLang]} onChange={e => setSubtitles({...subtitles, [activeLang]: e.target.value})}
                placeholder={activeLang !== 'nl' ? subtitles.nl : ''} />
            </label>
            <label><span>Beschrijving ({activeLang.toUpperCase()})</span>
              <textarea value={descs[activeLang]} onChange={e => setDescs({...descs, [activeLang]: e.target.value})} rows={4}
                placeholder={activeLang !== 'nl' ? descs.nl : ''} />
            </label>

            {activeLang === 'nl' && <>
              <ImageUploadField value={heroImage} onChange={(url) => setHeroImage(url)} label="Hero afbeelding" />
              <label><span>Hero video URL</span>
                <input value={heroVideo} onChange={e => setHeroVideo(e.target.value)} placeholder="/vid/..." />
              </label>

              <div style={{borderTop:'1px solid rgba(241,235,222,.15)',margin:'16px 0 8px',padding:'12px 0 0'}}>
                <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
                  <strong>Artiesten</strong>
                  <button type="button" className="cms-btn" onClick={addArtist} style={{fontSize:12,padding:'4px 10px'}}>+ Artiest</button>
                </div>
                {artists.map((artist, ai) => (
                  <div key={ai} style={{background:'rgba(241,235,222,.08)',padding:'10px 12px',borderRadius:8,marginBottom:8}}>
                    <div style={{display:'flex',gap:8,marginBottom:6}}>
                      <label style={{flex:1}}><span style={{fontSize:11}}>Naam</span>
                        <input value={artist.name} onChange={e => updateArtist(ai, 'name', e.target.value)} />
                      </label>
                      <label style={{flex:1}}><span style={{fontSize:11}}>Rol (NL)</span>
                        <input value={artist.role?.nl || ''} onChange={e => updateArtistLang(ai, 'role', 'nl', e.target.value)} />
                      </label>
                    </div>
                    <div style={{marginBottom:6}}>
                      <ImageUploadField value={artist.image || ''} onChange={(url) => updateArtist(ai, 'image', url)} label="Foto" />
                    </div>
                    <button type="button" className="cms-danger" onClick={() => removeArtist(ai)} style={{fontSize:11,padding:'3px 8px',marginTop:4}}>Verwijder artiest</button>
                  </div>
                ))}
              </div>
            </>}
          </div>

          {/* ─── Preview ─── */}
          <div className="cms-modal-preview">
            <div className="cms-preview-label">Voorbeeld</div>
            <div className="cms-preview-card" style={{aspectRatio:'3/4',overflow:'hidden'}}>
              {heroImage && (
                <div className="cms-preview-img" style={{height:'60%'}}>
                  <img src={heroImage} alt="" />
                </div>
              )}
              <div style={{padding:'12px 16px'}}>
                <div className="cms-preview-title" style={{fontStyle:'italic'}}>{titles[activeLang] || 'Titel...'}</div>
                <div className="cms-preview-date">{subtitles[activeLang] || 'Subtitel...'}</div>
                {descs[activeLang] && <div className="cms-preview-summary" style={{marginTop:8,fontSize:11}}>{descs[activeLang].substring(0, 120)}...</div>}
              </div>
            </div>
          </div>
        </div>

        <div className="cms-modal-actions">
          {!isNew && <button className="cms-danger" onClick={handleDelete} style={{marginRight:'auto'}}>Verwijderen</button>}
          <button onClick={onClose}>Annuleren</button>
          <button className="cms-primary" onClick={handleSave} disabled={!titles.nl.trim()}>Opslaan</button>
        </div>
      </div>
    </div>
  );
}

// ─── project page ─────────────────────────────────────────────────
function ProjectPage({ slug, data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const navigate = useNavigate();
  const fadeRef = useFadeIn();

  const project = useMemo(() => {
    return (data.projects || []).find(p => p.slug === slug);
  }, [data.projects, slug]);

  const projectEvents = useMemo(() => {
    if (!project) return [];
    return (data.events || []).filter(ev => (project.eventIds || []).includes(ev.id));
  }, [project, data.events]);

  useEffect(() => {
    if (project) {
      document.title = txt(project.title, lang) + ' — B.O.X';
    }
    return () => { document.title = 'B.O.X — Baroque Orchestration X'; };
  }, [project, lang]);

  if (!project) {
    return (
      <div className="wrap" style={{padding:'120px 0 80px',textAlign:'center'}}>
        <h2>Project niet gevonden</h2>
        <p style={{marginTop:16}}><a href="/">← Terug naar B.O.X</a></p>
      </div>
    );
  }

  const projectIdx = (data.projects || []).findIndex(p => p.slug === slug);
  const pathPrefix = 'projects.' + projectIdx;

  return (
    <div className="project-page" ref={fadeRef}>
      <div className="project-breadcrumb">
        <div className="wrap">
          <a href="/" className="project-breadcrumb-home">B.O.X</a>
          <span className="project-breadcrumb-sep">/</span>
          <span className="project-breadcrumb-current">{txt(project.title, lang)}</span>
        </div>
      </div>
      <div className="project-hero">
        {project.heroVideo ? (
          <video className="project-hero-video" src={project.heroVideo} autoPlay muted loop playsInline />
        ) : (
          <div className="project-hero-bg" style={{backgroundImage: `url(${project.heroImage || '/img/portal-1.jpg'})`}} />
        )}
        <div className="project-hero-overlay">
          <div className="wrap">
            <a href="/" className="project-back mono">← {lang === 'fr' ? 'Retour' : lang === 'en' ? 'Back' : 'Terug'}</a>
            <h1 className="project-title">
              <Editable field={project.title} path={pathPrefix + '.title'} Tag="span" />
            </h1>
            <Editable field={project.subtitle} path={pathPrefix + '.subtitle'} Tag="p" className="project-subtitle" />
          </div>
        </div>
      </div>

      <section className="project-description">
        <div className="wrap">
          <Editable field={project.description} path={pathPrefix + '.description'} Tag="div" className="project-text" rich />
        </div>
      </section>

      {project.artists && project.artists.length > 0 && (
        <section className="project-artists-section">
          <div className="wrap">
            <h2 className="project-section-title mono">{lang === 'fr' ? 'Artistes' : lang === 'en' ? 'Artists' : 'Artiesten'}</h2>
            <div className="project-artists">
              {project.artists.map((artist, ai) => (
                <div key={ai} className="project-artist-card" style={{position:'relative'}}>
                  {artist.image && <img src={artist.image} alt={artist.name} className="project-artist-img" />}
                  {cms.active && (
                    <button className="cms-btn" style={{position:'absolute',top:8,right:8,fontSize:11,padding:'4px 8px'}} onClick={() => {
                      const url = prompt('Artiest foto URL:', artist.image || '');
                      if (url !== null) { project.artists[ai].image = url; cms.setDirty(true); cms.bumpMeta(); }
                    }}>📷</button>
                  )}
                  <div className="project-artist-info">
                    <h3>{artist.name}</h3>
                    <Editable field={artist.role} path={pathPrefix + '.artists.' + ai + '.role'} Tag="p" className="project-artist-role mono" />
                    <Editable field={artist.bio} path={pathPrefix + '.artists.' + ai + '.bio'} Tag="p" className="project-artist-bio" rich />
                  </div>
                </div>
              ))}
            </div>
          </div>
        </section>
      )}

      {projectEvents.length > 0 && (
        <section className="project-dates-section">
          <div className="wrap">
            <h2 className="project-section-title mono">{lang === 'fr' ? 'Dates' : lang === 'en' ? 'Dates' : 'Data'}</h2>
            <div className="project-dates">
              {projectEvents.map(ev => {
                const d = parseISO(ev.date);
                return (
                  <div key={ev.id} className="project-date-row">
                    <div className="project-date-info">
                      <span className="project-date-day">{fmtLong(ev, lang)}</span>
                      <span className="project-date-venue">{ev.venue}, {ev.city}</span>
                    </div>
                    {ev.ticketUrl && ev.ticketUrl !== '#' && (
                      <a href={ev.ticketUrl} target="_blank" rel="noopener" className="project-date-tickets mono">Tickets →</a>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        </section>
      )}

      {project.credits && (
        <section className="project-credits-section">
          <div className="wrap">
            <Editable field={project.credits} path={pathPrefix + '.credits'} Tag="p" className="project-credits mono" />
          </div>
        </section>
      )}
    </div>
  );
}

function Footer({ data }) {
  const lang = useLang();
  const t = useT();
  const cms = useContext(CmsContext);
  const footer = data.meta.footer || {};
  const logos = footer.logos || [];
  const [showMediaLib, setShowMediaLib] = useState(false);

  const handleAddLogo = (src) => {
    if (!src) return;
    if (!data.meta.footer) data.meta.footer = { copyright: 'box office vzw', subtitle: 'Baroque Orchestration X', logos: [] };
    if (!data.meta.footer.logos) data.meta.footer.logos = [];
    data.meta.footer.logos.push({ src, alt: '' });
    cms.setDirty(true);
    cms.bumpMeta();
  };

  const handleRemoveLogo = (idx) => {
    if (!confirm('Dit logo verwijderen?')) return;
    data.meta.footer.logos.splice(idx, 1);
    cms.setDirty(true);
    cms.bumpMeta();
  };

  return (
    <footer>
      <div className="wrap">
        <div className="col">
          <h4>{t('footer_general')}</h4>
          <ul>
            <li><a href={`mailto:${data.meta.contact.email}`}>{data.meta.contact.email}</a></li>
            <li>{data.meta.contact.address}</li>
          </ul>
        </div>
        <div className="col">
          <h4>{t('footer_follow')}</h4>
          <ul>
            {data.meta.contact.socials.map((s) => (
              <li key={s.label}><a href={s.url} target="_blank" rel="noopener">{s.label} →</a></li>
            ))}
          </ul>
        </div>

        {logos.length > 0 && (
          <div className="footer-logos">
            {logos.map((logo, i) => (
              <span key={i} className="footer-logo-wrap">
                <img src={logo.src} alt={logo.alt} className="footer-partner-logo" />
                {cms.active && (
                  <button className="cms-remove-logo" onClick={() => handleRemoveLogo(i)} title="Verwijder logo">&times;</button>
                )}
              </span>
            ))}
            {cms.active && (
              <button className="cms-btn" onClick={() => setShowMediaLib(true)} style={{fontSize:11,padding:'4px 10px'}}>+ Logo</button>
            )}
          </div>
        )}
        {logos.length === 0 && cms.active && (
          <div className="footer-logos">
            <button className="cms-btn" onClick={() => setShowMediaLib(true)} style={{fontSize:11,padding:'4px 10px'}}>+ Logo toevoegen</button>
          </div>
        )}
        {showMediaLib && (
          <MediaLibrary onSelect={(url) => handleAddLogo(url)} onClose={() => setShowMediaLib(false)} />
        )}

        <div className="footer-meta">
          <span>&copy;{' '}
            {cms.active ? (
              <span
                contentEditable suppressContentEditableWarning
                data-editable="meta.footer.copyright"
                onBlur={(e) => {
                  const val = e.target.innerText.trim();
                  if (!data.meta.footer) data.meta.footer = {};
                  if (val !== (data.meta.footer.copyright || '')) {
                    data.meta.footer.copyright = val;
                    cms.setDirty(true);
                  }
                }}
              >{footer.copyright || 'box office vzw'}</span>
            ) : (footer.copyright || 'box office vzw')}
            {' '}&mdash; 2010&ndash;{new Date().getFullYear()}
          </span>
          {cms.active ? (
            <span
              contentEditable suppressContentEditableWarning
              data-editable="meta.footer.subtitle"
              onBlur={(e) => {
                const val = e.target.innerText.trim();
                if (!data.meta.footer) data.meta.footer = {};
                if (val !== (data.meta.footer.subtitle || '')) {
                  data.meta.footer.subtitle = val;
                  cms.setDirty(true);
                }
              }}
            >{footer.subtitle || 'Baroque Orchestration X'}</span>
          ) : (
            <span>{footer.subtitle || 'Baroque Orchestration X'}</span>
          )}
        </div>
      </div>
    </footer>
  );
}

// ─── CMS toolbar ──────────────────────────────────────────────────
function CmsToolbar({ dirty, onSave, saving }) {
  const t = useT();
  const cms = useContext(CmsContext);
  const [showDashboard, setShowDashboard] = useState(false);
  const staleCount = cms.dataRef?.current ? countAllStaleFields(cms.dataRef.current) : 0;

  const handleLogout = () => {
    fetch('/api/logout', { method: 'POST' })
      .finally(() => { window.location.href = '/'; });
  };

  return (
    <div className="cms-toolbar">
      <div className="cms-left">
        <span className="cms-label">{t('cms_editing')}</span>
        {dirty && <span style={{width:8,height:8,borderRadius:'50%',background:'#c87b7b',display:'inline-block'}}></span>}
      </div>
      <div className="cms-right">
        <button className="cms-translation-toolbar-btn" onClick={() => setShowDashboard(true)}>
          🌐 {t('cms_translations') || 'Vertalingen'}
          {staleCount > 0 && <span className="cms-stale-count">{staleCount}</span>}
        </button>
        {showDashboard && cms.dataRef?.current && (
          <TranslationDashboard data={cms.dataRef.current} onClose={() => setShowDashboard(false)} />
        )}
        <MessagesPanel />
        <button className={`cms-save ${dirty ? 'has-changes' : ''}`} onClick={onSave} disabled={saving}>
          {saving ? t('cms_saving') : dirty ? t('cms_save') : t('cms_saved')}
        </button>
        <button className="cms-logout" onClick={handleLogout}>{t('cms_logout')}</button>
      </div>
    </div>
  );
}

// ─── theme picker ────────────────────────────────────────────────
function ThemePicker() {
  const normalizeTheme = (value) => {
    if (value === 'porselein') return 'chroom';
    if (value === 'partituur') return 'citroen';
    return value || 'woud';
  };

  const [theme, setTheme] = useState(() => normalizeTheme(localStorage.getItem('box-theme')));
  const [fonts, setFonts] = useState(() => localStorage.getItem('box-fonts') || 'classic');
  const [highlight, setHighlight] = useState(() => localStorage.getItem('box-highlight') || 'off');
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (theme) document.body.dataset.theme = theme;
    else delete document.body.dataset.theme;
    if (fonts && fonts !== 'classic') document.body.dataset.fonts = fonts;
    else delete document.body.dataset.fonts;
    document.body.dataset.highlight = highlight;
    localStorage.setItem('box-theme', theme);
    localStorage.setItem('box-fonts', fonts);
    localStorage.setItem('box-highlight', highlight);
  }, [theme, fonts, highlight]);

  const themes = [
    { id: 'nacht', label: 'Nacht', desc: 'Exact als "Over B.O.X"', colors: ['#1a1816','#f1ebde','#c87b7b','#defe00'] },
    { id: 'oceaan', label: 'Oceaan', desc: 'Diep nachtblauw', colors: ['#0b1520','#d4dee8','#4db8a4','#4db8a4'] },
    { id: 'contrast', label: 'Contrast', desc: 'Strak wit & rood', colors: ['#ffffff','#111111','#b11515','#defe00'] },
    { id: 'affiche', label: 'Affiche', desc: 'Vintage poster, groen', colors: ['#f4f0e4','#1c2e1c','#2d5a3d','#c4873b'] },
    { id: 'fluweel', label: 'Fluweel', desc: 'Barok theater, goud', colors: ['#140e1e','#e8dfd0','#c9a84c','#d4a0b0'] },
    { id: 'koper', label: 'Koper', desc: 'Warm koper en espresso', colors: ['#2b1d14','#fceee3','#c46a45','#facc73'] },
    { id: 'woud', label: 'Woud', desc: 'Diep donkergroen', colors: ['#0d1a13','#e0ebd5','#a8e635','#defe00'] },
    { id: 'neon', label: 'Neon', desc: 'Paars met roze & cyaan', colors: ['#120a14','#ffffff','#f000ff','#00e5ff'] },
    { id: 'chroom', label: 'Chroom', desc: 'Koel staal, kobalt & signaaloranje', colors: ['#eef1f4','#14181d','#2667ff','#ff6a3d'] },
    { id: 'citroen', label: 'Citroen', desc: 'Zonnegeel, ultramarijn & koraal', colors: ['#fff6bf','#1b2340','#2457ff','#ff5c39'] },
    { id: 'kathedraal', label: 'Kathedraal', desc: 'Lapisblauw met wierookgoud', colors: ['#101426','#efe6d4','#d9ab52','#7b88ff'] }
  ];

  const fontOptions = [
    { id: 'classic', label: 'Klassiek', preview: 'Cormorant', family: '"Cormorant Garamond"' },
    { id: 'editorial', label: 'Editoriaal', preview: 'Playfair', family: '"Playfair Display"' },
    { id: 'modern', label: 'Modern', preview: 'DM Sans', family: '"DM Sans"' },
  ];

  return (
    <div className="theme-picker">
      <button className="theme-picker-toggle" onClick={() => setOpen(!open)} title="Thema wijzigen">
        🎨
      </button>
      {open && (
        <div className="theme-picker-panel">
          <h4>Kleurenpalet</h4>
          {themes.map(t => (
            <button key={t.id} className={`theme-option${theme === t.id ? ' active' : ''}`}
              onClick={() => setTheme(t.id)}>
              <span className="theme-swatches">
                {t.colors.map((c, i) => <span key={i} className="theme-swatch" style={{background:c}} />)}
              </span>
              <span>
                <strong>{t.label}</strong>
                <br />
                <span style={{fontSize:11,opacity:.6}}>{t.desc}</span>
              </span>
            </button>
          ))}
          <h4>Lettertype</h4>
          {fontOptions.map(f => (
            <button key={f.id} className={`font-option${fonts === f.id ? ' active' : ''}`}
              onClick={() => setFonts(f.id)}>
              <span className="font-preview" style={{fontFamily:f.family}}>{f.preview}</span>
              <span className="font-name">{f.label}</span>
            </button>
          ))}
          <h4>Opties</h4>
          <label className="theme-toggle">
            <span>Titel highlight</span>
            <span className={`toggle-switch${highlight === 'on' ? ' on' : ''}`}
              onClick={() => setHighlight(highlight === 'on' ? 'off' : 'on')}>
              <span className="toggle-knob" />
            </span>
          </label>
        </div>
      )}
    </div>
  );
}

// ─── custom sections ──────────────────────────────────────────────
function CustomSection({ data, sectionId }) {
  const lang = useLang();
  const cms = useContext(CmsContext);
  const fadeRef = useFadeIn();
  if (!data.customSections) data.customSections = {};
  if (!data.customSections[sectionId]) {
    data.customSections[sectionId] = {
      label: { nl: 'Nieuwe sectie', en: 'New section', fr: 'Nouvelle section' },
      title: { nl: '', en: '', fr: '' },
      body: { nl: '<p>Klik hier om tekst te bewerken...</p>', en: '', fr: '' },
      image: ''
    };
  }
  const sec = data.customSections[sectionId];

  // Ensure sectionHeaders entry exists for SectionHead
  if (!data.sectionHeaders) data.sectionHeaders = {};
  if (!data.sectionHeaders[sectionId]) {
    data.sectionHeaders[sectionId] = { label: sec.label, title: sec.title };
  }

  const handleImageSelect = (url) => {
    sec.image = url;
    cms.setDirty(true);
    cms.bumpMeta();
  };

  const [showMediaLib, setShowMediaLib] = useState(false);

  return (
    <section className="section section-stage fade-in" id={sectionId} ref={fadeRef}>
      <div className="wrap">
        <SectionHead label={txt(sec.label, lang)} sectionId={sectionId} data={data} />
        <div className="custom-section-content">
          {sec.image && (
            <div className="custom-section-image">
              <img src={sec.image} alt="" />
              {cms.active && (
                <div className="custom-section-image-controls">
                  <button className="cms-btn" onClick={() => setShowMediaLib(true)} style={{fontSize:11}}>Wijzig afbeelding</button>
                  <button className="cms-btn" onClick={() => { sec.image = ''; cms.setDirty(true); cms.bumpMeta(); }} style={{fontSize:11}}>Verwijder</button>
                </div>
              )}
            </div>
          )}
          {!sec.image && cms.active && (
            <button className="cms-btn" onClick={() => setShowMediaLib(true)} style={{marginBottom:16}}>+ Afbeelding toevoegen</button>
          )}
          <Editable field={sec.body} path={`customSections.${sectionId}.body`} Tag="div" className="custom-section-body" rich />
        </div>
      </div>
      {showMediaLib && (
        <MediaLibrary onSelect={handleImageSelect} onClose={() => setShowMediaLib(false)} />
      )}
    </section>
  );
}

// ─── sortable sections ────────────────────────────────────────────
const DEFAULT_SECTION_ORDER = ['hero','nieuws','projecten','over','opencalls','agenda','media','galerij','instagram','partners','contact'];

const SECTION_COMPONENTS = {
  hero: (data) => <Hero data={data} />,
  nieuws: (data) => <NewsSection data={data} />,
  projecten: (data) => <ProjectsSection data={data} />,
  over: (data) => <About data={data} />,
  opencalls: (data) => <OpenCallsSection data={data} />,
  agenda: (data) => <Agenda data={data} />,
  media: (data) => <div id="media"><VideoSection data={data} /><Listen data={data} /></div>,
  galerij: (data) => <Gallery data={data} />,
  instagram: (data) => <InstagramSection data={data} />,
  partners: (data) => <Partners data={data} />,
  contact: (data) => <ContactForm data={data} />,
};

function SortableSections({ data, cmsActive }) {
  const cms = useContext(CmsContext);
  const containerRef = useRef(null);
  // Initialize sectionOrder if not set (first load)
  if (!data.sectionOrder) data.sectionOrder = [...DEFAULT_SECTION_ORDER];
  const order = data.sectionOrder;

  // Sections available but not in the current order (removed ones)
  const removedSections = useMemo(() => {
    return DEFAULT_SECTION_ORDER.filter(id => !order.includes(id));
  }, [order]);

  const addSection = (sectionId) => {
    data.sectionOrder = [...order, sectionId];
    cms.setDirty(true);
    cms.bumpMeta();
  };

  const addCustomSection = () => {
    const id = 'custom-' + Date.now().toString(36);
    if (!data.customSections) data.customSections = {};
    data.customSections[id] = {
      label: { nl: 'Nieuwe sectie', en: 'New section', fr: 'Nouvelle section' },
      title: { nl: '', en: '', fr: '' },
      body: { nl: '<p>Klik hier om tekst te bewerken...</p>', en: '', fr: '' },
      image: ''
    };
    // Add to section headers so SectionHead can edit it
    if (!data.sectionHeaders) data.sectionHeaders = {};
    data.sectionHeaders[id] = {
      label: data.customSections[id].label,
      title: data.customSections[id].title
    };
    data.sectionOrder = [...order, id];
    cms.setDirty(true);
    cms.bumpMeta();
  };

  // Get render function: built-in or custom section
  const getRender = (sectionId) => {
    if (SECTION_COMPONENTS[sectionId]) return SECTION_COMPONENTS[sectionId];
    if (sectionId.startsWith('custom-')) return (d) => <CustomSection data={d} sectionId={sectionId} />;
    return null;
  };

  // Get label for a section (built-in or custom)
  const getSectionLabel = (sectionId) => {
    if (SECTION_LABELS[sectionId]) return SECTION_LABELS[sectionId];
    if (data.customSections?.[sectionId]) {
      return txt(data.customSections[sectionId].label, 'nl') || sectionId;
    }
    return sectionId;
  };

  useEffect(() => {
    if (!cmsActive || !containerRef.current || typeof Sortable === 'undefined') return;
    const sortable = Sortable.create(containerRef.current, {
      animation: 200,
      handle: '.cms-section-drag',
      ghostClass: 'sortable-ghost',
      onEnd: (evt) => {
        const newOrder = [...order];
        const [moved] = newOrder.splice(evt.oldIndex, 1);
        newOrder.splice(evt.newIndex, 0, moved);
        data.sectionOrder = newOrder;
        cms.setDirty(true);
      }
    });
    return () => sortable.destroy();
  }, [cmsActive, order]);

  return (
    <React.Fragment>
      <div className="sections-counter" ref={containerRef}>
        {order.map(sectionId => {
          const render = getRender(sectionId);
          if (!render) return null;
          return (
            <SectionWrapper key={sectionId} sectionId={sectionId} data={data} draggable={cmsActive}>
              {render(data)}
            </SectionWrapper>
          );
        })}
      </div>
      {cmsActive && (
        <div className="cms-add-section-panel">
          <h4>Secties</h4>
          <div className="cms-add-section-grid">
            {removedSections.map(id => (
              <button key={id} className="cms-add-section-btn" onClick={() => addSection(id)}>
                + {SECTION_LABELS[id] || id}
              </button>
            ))}
            <button className="cms-add-section-btn cms-add-custom" onClick={addCustomSection}>
              + Nieuwe sectie aanmaken
            </button>
          </div>
        </div>
      )}
    </React.Fragment>
  );
}

// ─── app root ──────────────────────────────────────────────────────
function App() {
  const [data, setData] = useState(null);
  const [lang, setLang] = useState(() => localStorage.getItem('box-lang') || 'nl');
  const [cmsActive, setCmsActive] = useState(false);
  const [dirty, setDirty] = useState(false);
  const [saving, setSaving] = useState(false);
  const [metaVersion, setMetaVersion] = useState(0);
  const bumpMeta = useCallback(() => setMetaVersion(v => v + 1), []);
  const dataRef = useRef(null);

  useEffect(() => {
    localStorage.setItem('box-lang', lang);
    document.documentElement.lang = lang;
  }, [lang]);

  // Scroll-aware nav darkening
  useEffect(() => {
    const onScroll = () => {
      const past = window.scrollY > Math.max(220, window.innerHeight * 0.4);
      document.body.classList.toggle('scrolled', past);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);

  useEffect(() => {
    fetch('/api/content')
      .then(r => r.json())
      .then(d => { dataRef.current = d; setData(d); })
      .catch(() => {});

    fetch('/api/session')
      .then(r => r.json())
      .then(s => {
        if (s.authenticated) {
          setCmsActive(true);
          document.body.classList.add('cms-active');
        }
      })
      .catch(() => {});
  }, []);

  const handleSave = useCallback(async () => {
    if (!dataRef.current) return;
    setSaving(true);
    try {
      const res = await fetch('/api/content', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(dataRef.current)
      });
      if (res.ok) {
        setDirty(false);
      }
    } catch (e) {
      alert('Opslaan mislukt: ' + e.message);
    }
    setSaving(false);
  }, []);

  const route = useRoute();

  // Intercept internal links for SPA navigation
  useEffect(() => {
    const handleClick = (e) => {
      if (e.defaultPrevented) return;
      const a = e.target.closest('a[href]');
      if (!a) return;
      const href = a.getAttribute('href');
      if (href && href.startsWith('/projects/')) {
        e.preventDefault();
        route.navigate(href);
      }
      if (href === '/' || href === '#top') {
        if (route.path !== '/') {
          e.preventDefault();
          route.navigate('/');
        }
      }
      // Hash links on project pages → navigate home first, then scroll
      if (href && href.startsWith('#') && href !== '#' && route.path !== '/') {
        e.preventDefault();
        route.navigate('/');
        // Wait for React to render the homepage sections before scrolling
        const id = href.replace('#', '').split('?')[0];
        const tryScroll = (attempts) => {
          const el = document.getElementById(id);
          if (el) { el.scrollIntoView({ behavior: 'smooth' }); }
          else if (attempts > 0) { requestAnimationFrame(() => tryScroll(attempts - 1)); }
        };
        requestAnimationFrame(() => tryScroll(10));
      }
    };
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, [route.path, route.navigate]);

  if (!data) return <div className="loading">Laden...</div>;

  const isProjectPage = route.path.startsWith('/projects/');
  const projectSlug = isProjectPage ? route.path.replace('/projects/', '').replace(/\/$/, '') : null;

  return (
    <LangContext.Provider value={lang}>
      <CmsContext.Provider value={{ active: cmsActive, dirty, setDirty, dataRef, metaVersion, bumpMeta }}>
        <RouteContext.Provider value={route}>
          <AudioProvider tracks={data.audio || []}>
            {cmsActive && <CmsToolbar dirty={dirty} onSave={handleSave} saving={saving} />}
            {cmsActive && <CmsRichToolbar />}
            <Nav lang={lang} setLang={setLang} data={data} />
            {isProjectPage ? (
              <ProjectPage slug={projectSlug} data={data} />
            ) : (
              <SortableSections data={data} cmsActive={cmsActive} />
            )}
            <Footer data={data} />
            {cmsActive && <ThemePicker />}
          </AudioProvider>
        </RouteContext.Provider>
      </CmsContext.Provider>
    </LangContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
