// app.jsx — root, routing (History API), lang prefix

const { useState: useState2, useEffect: useEffect2 } = React;

// ----- URL ↔ Route helpers ---------------------------------
// URL shape:
//   /                       → pt home
//   /en                     → en home
//   /es                     → es home
//   /produtos | /en/products | /es/productos
//   /produtos/<slug> | /en/products/<slug> | /es/productos/<slug>
//   /sobre | /en/about | /es/sobre
//   /qualidade | /en/quality | /es/calidad
//   /contato | /en/contact | /es/contacto  (?product=<slug> to prefill)
//   /privacidade | /en/privacy | /es/privacidad
//   /noticias | /en/news | /es/noticias
// Unknown → { page: '404' }

const PATHS = {
  pt: { home: '', products: 'produtos',  about: 'sobre', quality: 'qualidade', contact: 'contato',  privacy: 'privacidade', news: 'noticias' },
  en: { home: '', products: 'products',  about: 'about', quality: 'quality',   contact: 'contact',  privacy: 'privacy',     news: 'news' },
  es: { home: '', products: 'productos', about: 'sobre', quality: 'calidad',   contact: 'contacto', privacy: 'privacidad',  news: 'noticias' },
};

function buildUrl(lang, page, extra = {}) {
  const prefix = lang === 'en' ? '/en' : (lang === 'es' ? '/es' : '');
  const seg = PATHS[lang][page] || '';
  let path;
  if (page === 'home') path = prefix || '/';
  else if (page === 'product') path = `${prefix}/${PATHS[lang].products}/${extra.slug || ''}`;
  else if (page === 'newsPost') path = `${prefix}/${PATHS[lang].news}/${extra.slug || ''}`;
  else path = `${prefix}/${seg}`;
  if (page === 'contact' && extra.product) path += `?product=${encodeURIComponent(extra.product)}`;
  return path;
}

function parseUrl() {
  const path = location.pathname.replace(/\/+$/, '') || '/';
  // Dev/preview fallback: if the path ends in .html or is served under a long
  // sandbox path, treat as home (with hash-route override).
  if (/\.html$/.test(path) || path.length > 80) {
    // Use hash as optional route override for dev preview: #/en/products/slug
    const hash = location.hash.replace(/^#/, '');
    if (hash && hash.startsWith('/')) {
      const save = { pathname: hash, search: '' };
      // temporarily swap location.pathname for parsing (can't actually mutate, so inline-parse)
      return _parsePath(hash.split('?')[0], new URLSearchParams(hash.split('?')[1] || ''));
    }
    return { lang: (localStorage.getItem('lesso_lang') || _detectLang()), route: { page: 'home' } };
  }
  return _parsePath(path, new URLSearchParams(location.search));
}

// Match navigator.language against the 3 supported langs. PT is the default.
function _detectLang() {
  const nav = (navigator.language || navigator.userLanguage || '').toLowerCase();
  if (nav.startsWith('es')) return 'es';
  if (nav.startsWith('en')) return 'en';
  return 'pt';
}

function _parsePath(path, search) {
  let lang = 'pt';
  let rest = path;
  if (path === '/en' || path.startsWith('/en/')) {
    lang = 'en';
    rest = path.slice(3) || '/';
  } else if (path === '/es' || path.startsWith('/es/')) {
    lang = 'es';
    rest = path.slice(3) || '/';
  }
  const segs = rest.split('/').filter(Boolean);
  const P = PATHS[lang];
  if (segs.length === 0) return { lang, route: { page: 'home' } };
  const [s0, s1] = segs;
  if (s0 === P.products) {
    if (s1) return { lang, route: { page: 'product', slug: s1 } };
    return { lang, route: { page: 'products' } };
  }
  if (s0 === P.news) {
    if (s1) return { lang, route: { page: 'newsPost', slug: s1 } };
    return { lang, route: { page: 'news' } };
  }
  const pageByPath = { [P.about]: 'about', [P.quality]: 'quality', [P.contact]: 'contact', [P.privacy]: 'privacy' };
  if (pageByPath[s0]) {
    const route = { page: pageByPath[s0] };
    if (s0 === P.contact) {
      const qp = search.get('product');
      if (qp) route.product = qp;
    }
    return { lang, route };
  }
  return { lang, route: { page: '404' } };
}

// ----- App --------------------------------------------------

function App() {
  const initial = parseUrl();
  const [lang, setLang] = useState2(initial.lang);
  const [route, setRoute] = useState2(initial.route);

  // popstate (back/forward)
  useEffect2(() => {
    const onPop = () => {
      const parsed = parseUrl();
      setLang(parsed.lang);
      setRoute(parsed.route);
    };
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);

  // SEO + scroll reset + GA page_view
  useEffect2(() => {
    window.scrollTo({ top: 0, behavior: 'auto' });
    // Detect 404 state for detail pages whose slug doesn't resolve to a real
    // product or news post — so applySEO emits noindex + the 404 title/desc,
    // matching what ProductDetail/NewsPost actually render (NotFoundPage).
    // Without this, /produtos/<bad-slug> returns the SPA shell with the
    // "Page not found" component AND robots=index,follow, which is wrong.
    let effectiveRoute = route;
    if (route.page === 'product' && route.slug) {
      const exists = ((I18N[lang] && I18N[lang].productList) || []).some(p => p.slug === route.slug);
      if (!exists) effectiveRoute = { page: '404' };
    } else if (route.page === 'newsPost' && route.slug) {
      const news = window.NEWS || [];
      const exists = Array.isArray(news) && news.some(n => n.slug === route.slug);
      if (!exists) effectiveRoute = { page: '404' };
    }
    if (window.applySEO) window.applySEO({ route: effectiveRoute, lang, t: I18N[lang] });
    // GA4 SPA page_view — fired on every route change (initial mount included
    // because the effect runs on first render). `send_page_view:false` is set
    // in index.html so the gtag('config') call doesn't double-fire here.
    if (window.gtag) {
      const path = (location.pathname || '/') + (location.search || '');
      window.gtag('event', 'page_view', {
        page_path: path,
        page_location: location.href,
        page_title: document.title,
        page_lang: lang,
      });
    }
  }, [route, lang]);

  // Global click delegation — fires `contact_click` for every wa.me /
  // mailto: / tel: link anywhere in the tree. Avoids touching every component
  // that renders such a link (Footer, ContactPage, WhatsAppFab, etc.).
  useEffect2(() => {
    const onDocClick = (e) => {
      const a = e.target.closest && e.target.closest('a[href]');
      if (!a || !window.gtag) return;
      const href = a.getAttribute('href') || '';
      let channel = null;
      if (/^https?:\/\/(api\.)?wa\.me\//i.test(href) || /whatsapp/i.test(href)) channel = 'whatsapp';
      else if (/^mailto:/i.test(href)) channel = 'email';
      else if (/^tel:/i.test(href)) channel = 'phone';
      if (!channel) return;
      window.gtag('event', 'contact_click', {
        event_category: 'engagement',
        event_label: channel,
        channel,
        link_url: href,
        link_text: (a.textContent || a.getAttribute('aria-label') || '').trim().slice(0, 80),
        page_path: location.pathname,
      });
    };
    document.addEventListener('click', onDocClick, { capture: true });
    return () => document.removeEventListener('click', onDocClick, { capture: true });
  }, []);

  // In dev/preview the real pathname is something like
  // /v1/design/projects/<id>/serve/index.html — pushing a clean "/produtos"
  // would 404 on reload. When we detect that environment, keep the pathname
  // and use hash for linkable state instead.
  const isDevPreview = /\.html$/.test(location.pathname) || location.pathname.length > 80;

  const go = (page, extra = {}) => {
    // GA: any in-app navigation to the contact page is a "contact_click"
    // (CTA / nav link / sub-product card / etc.). External wa.me / mailto /
    // tel links are caught by the global delegation above.
    if (page === 'contact' && window.gtag) {
      window.gtag('event', 'contact_click', {
        event_category: 'engagement',
        event_label: 'contact_page',
        channel: 'in_app_cta',
        product: extra && extra.product ? extra.product : '',
        from_path: location.pathname,
      });
    }
    const url = buildUrl(lang, page, extra);
    if (isDevPreview) {
      if ('#' + url !== location.hash) history.pushState({}, '', '#' + url);
    } else {
      if (url !== location.pathname + location.search) {
        history.pushState({}, '', url);
      }
    }
    setRoute({ page, ...extra });
  };

  const switchLang = (next) => {
    if (next === lang) return;
    const url = buildUrl(next, route.page, route);
    if (isDevPreview) {
      history.pushState({}, '', '#' + url);
    } else {
      history.pushState({}, '', url);
    }
    setLang(next);
  };

  const t = I18N[lang];

  let Page = null;
  switch (route.page) {
    case 'products': Page = <ProductsIndex t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case 'product': Page = <ProductDetail t={t} go={go} slug={route.slug} lang={lang} buildUrl={buildUrl} />; break;
    case 'news': Page = <NewsIndex t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case 'newsPost': Page = <NewsPost t={t} go={go} slug={route.slug} lang={lang} buildUrl={buildUrl} />; break;
    case 'about': Page = <AboutPage t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case 'quality': Page = <QualityPage t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case 'contact': Page = <ContactPage t={t} go={go} prefillProduct={route.product} lang={lang} buildUrl={buildUrl} />; break;
    case 'privacy': Page = <PrivacyPage t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case '404': Page = <NotFoundPage t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
    case 'home':
    default: Page = <HomePage t={t} go={go} lang={lang} buildUrl={buildUrl} />; break;
  }

  return (
    <ErrorBoundary>
      <Header t={t} lang={lang} setLang={switchLang} route={route} go={go} buildUrl={buildUrl} />
      <main key={route.page + (route.slug||'')}>{Page}</main>
      {/* CookieBanner before WhatsAppFab so the `.cookie-banner ~ .wa-fab`
          general-sibling selector in styles.css can actually match — it acts
          as a fallback for browsers that don't ship `:has()` (rare in 2026,
          but cheap insurance). */}
      <CookieBanner lang={lang} />
      <WhatsAppFab lang={lang} />
      <InquiryDrawer t={t} />
      <Footer t={t} go={go} buildUrl={buildUrl} lang={lang} />
    </ErrorBoundary>
  );
}

// Expose buildUrl so SEO/anchor helpers can use it
window.buildUrl = buildUrl;
window.parseUrl = parseUrl;

// ----- Content loader (CMS-editable fields) ----------------
// Loads 8 split JSON files from content/ (per spec 2026-05-04) and deep-merges
// over the defaults baked into i18n.jsx / data.jsx / extras.jsx. If a JSON
// fails to load, that slice falls back to the built-in defaults so the site
// never shows a broken state.
//
// Loading order:
//  1. content/products/_index.json → slug list (fallback to known 4)
//  2. Parallel fetch of 8 page JSONs + N product JSONs + (legacy) site.json
//  3. applyContent() merges all results into window.I18N + window.{CONTACT,DOWNLOADS,LEGAL}
async function loadContent() {
  try {
    const draft = localStorage.getItem('lesso_draft_content');
    if (draft) {
      // Preview mode: use localStorage draft (legacy single-file shape)
      const parsed = JSON.parse(draft);
      applyContent({
        site: parsed.site,
        products: parsed.products || [],
      });
      return;
    }

    // Resolve product slugs. Fallback mirrors the current 14 series from
    // content/products/_index.json so local preview (without running
    // build-products-index.js first) still renders the real catalog. Update
    // this list when product files are added/removed/renamed.
    const FALLBACK_SLUGS = [
      'agua-fornecimento','drenagem','agricola','industriais','aquecimento',
      'energia-telecom','mangueiras','incendio','gas','residenciais',
      'cabos','valvulas','aco-inox-agua','telhas-plasticas',
    ];
    const CONTENT_FETCH_TIMEOUT_MS = 12000;
    const withContentTimeout = (promise, url) => new Promise((resolve) => {
      let done = false;
      const timer = setTimeout(() => {
        if (done) return;
        done = true;
        console.warn('[content] fetch timed out:', url);
        resolve(null);
      }, CONTENT_FETCH_TIMEOUT_MS);
      promise.then(
        (value) => {
          if (done) return;
          done = true;
          clearTimeout(timer);
          resolve(value);
        },
        () => {
          if (done) return;
          done = true;
          clearTimeout(timer);
          resolve(null);
        }
      );
    });

    let slugs = FALLBACK_SLUGS;
    try {
      const idxResp = await withContentTimeout(
        fetch('content/products/_index.json', { cache: 'no-cache' }),
        'content/products/_index.json'
      );
      if (idxResp && idxResp.ok) {
        const idx = await withContentTimeout(idxResp.json(), 'content/products/_index.json parse');
        if (idx && Array.isArray(idx.slugs)) slugs = idx.slugs;  // empty array = all deleted, that's intentional
      }
    } catch (e) { /* keep fallback */ }

    // Resolve news slugs. Empty list is valid (= no news yet).
    let newsSlugs = [];
    try {
      const idxResp = await withContentTimeout(
        fetch('content/news/_index.json', { cache: 'no-cache' }),
        'content/news/_index.json'
      );
      if (idxResp && idxResp.ok) {
        const idx = await withContentTimeout(idxResp.json(), 'content/news/_index.json parse');
        if (idx && Array.isArray(idx.slugs)) newsSlugs = idx.slugs;
      }
    } catch (e) { /* no news, no problem */ }

    // Helper: fetch JSON, return null on any error or timeout (so single
    // failures are tolerated without blocking the whole page mount).
    const fetchJson = async (url) => {
      const r = await withContentTimeout(fetch(url, { cache: 'no-cache' }), url);
      if (!r || !r.ok) return null;
      return await withContentTimeout(r.json(), `${url} parse`);
    };

    // Parallel fetch: 8 split files + N product files + N news posts + legacy site.json fallback.
    const productsStart = 9;
    const newsStart = productsStart + slugs.length;
    const all = await Promise.all([
      fetchJson('content/home.json'),
      fetchJson('content/about.json'),
      fetchJson('content/quality.json'),
      fetchJson('content/contact.json'),
      fetchJson('content/nav-footer.json'),
      fetchJson('content/ui-labels.json'),
      fetchJson('content/downloads.json'),
      fetchJson('content/legal.json'),
      fetchJson('content/site.json'),  // legacy fallback — removed after G5
      ...slugs.map(slug => fetchJson(`content/products/${slug}.json`)),
      ...newsSlugs.map(slug => fetchJson(`content/news/${slug}.json`)),
    ]);
    const [home, about, quality, contact, navFooter, uiLabels, downloads, legal, siteLegacy] = all;
    const productJsons = all.slice(productsStart, newsStart);
    const newsJsons = all.slice(newsStart);

    applyContent({
      home, about, quality, contact, navFooter, uiLabels, downloads, legal,
      siteLegacy,
      products: productJsons.filter(Boolean),
      news: newsJsons.filter(Boolean),
    });
  } catch (err) {
    console.warn('[content] load failed, using built-in defaults:', err.message);
  }
}

// Decap's `list` widget wraps single-field items in an object
// (e.g. [{name:"ISO"}] instead of ["ISO"]). Unwrap them here.
function flattenList(list, key) {
  if (!Array.isArray(list)) return list;
  return list.map(item => {
    if (typeof item === 'string') return item;
    if (item && typeof item === 'object' && key in item) return item[key];
    return item;
  });
}

// ────────────────────────────────────────────────────────────────────────────
// Inquiry list — client-side accumulator, persists in localStorage.
// Spec: docs/superpowers/specs/2026-05-20-inquiry-list-design.md
// Public surface:
//   window.INQUIRY.add(item)   — idempotent; { key, label, image, url, addedAt }
//   window.INQUIRY.remove(key) — by key
//   window.INQUIRY.clear()     — empty the list
//   window.INQUIRY.has(key)    — does key exist?
//   window.INQUIRY.count()     — list.length (helper for badge)
//   window.INQUIRY.list        — Array<InquiryItem>; treat as READ-ONLY from consumers
// Every mutation also dispatches CustomEvent('lesso-inquiry-change') on window
// so React components can subscribe and re-render.
// ────────────────────────────────────────────────────────────────────────────
(function initInquiry() {
  const STORAGE_KEY = 'lesso_inquiry_list';
  const MAX_ITEMS = 20; // soft cap — see spec "Edge cases" table
  function loadFromStorage() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return [];
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed.filter(it => it && it.key && it.label) : [];
    } catch (_e) {
      // Stale/corrupt blob — wipe and start fresh.
      try { localStorage.removeItem(STORAGE_KEY); } catch (_e) {}
      return [];
    }
  }
  function persist(list) {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); return true; } catch (_e) { return false; }
  }
  function broadcast() {
    try { window.dispatchEvent(new CustomEvent('lesso-inquiry-change')); } catch (_e) {}
  }
  const INQUIRY = {
    list: loadFromStorage(),
    MAX_ITEMS,
    add(item) {
      if (!item || !item.key || !item.label) return;
      if (this.list.some(it => it.key === item.key)) return; // idempotent
      if (this.list.length >= MAX_ITEMS) return; // soft cap — toast UX in a later batch
      const next = [...this.list, item];
      if (!persist(next)) return;
      this.list = next;
      broadcast();
    },
    remove(key) {
      if (!key) return;
      const next = this.list.filter(it => it.key !== key);
      if (next.length === this.list.length) return;
      if (!persist(next)) return;
      this.list = next;
      broadcast();
    },
    clear() {
      if (!this.list.length) return;
      if (!persist([])) return;
      this.list = [];
      broadcast();
    },
    has(key) { return !!key && this.list.some(it => it.key === key); },
    count() { return this.list.length; },
  };
  window.INQUIRY = INQUIRY;
})();

// Deep-merge fetched content into window.I18N + globals. Each slice is
// independently merged; missing slices fall through to baked-in defaults.
function applyContent({
  home, about, quality, contact, navFooter, uiLabels, downloads, legal,
  siteLegacy, products, news,
} = {}) {
  // Legacy fallback: if a new file is missing but site.json has the data,
  // hydrate it back into the corresponding slot so we never regress.
  if (siteLegacy) {
    if (!home && siteLegacy.home) {
      home = { pt: siteLegacy.home.pt, en: siteLegacy.home.en };
    }
    if (!about && siteLegacy.about) {
      about = { pt: siteLegacy.about.pt, en: siteLegacy.about.en, certs: siteLegacy.about.pt?.certs };
    }
    if (!quality && siteLegacy.quality) {
      quality = { pt: siteLegacy.quality.pt, en: siteLegacy.quality.en };
    }
    if (!contact && siteLegacy.contact) {
      contact = {
        info: {
          inquiryEmail: siteLegacy.contact.email,
          whatsapp: siteLegacy.contact.whatsapp,
          phone: siteLegacy.contact.phone,
          address: siteLegacy.contact.address,
        },
      };
    }
    if (!downloads && siteLegacy.downloads) downloads = siteLegacy.downloads;
  }

  for (const lang of ['en', 'pt', 'es']) {
    const L = window.I18N && window.I18N[lang];
    if (!L) continue;

    // nav-footer — nav + brand + footer
    if (navFooter && navFooter[lang]) {
      const nf = navFooter[lang];
      if (nf.nav)    Object.assign(L.nav, nf.nav);
      if (nf.brand)  Object.assign(L.brand, nf.brand);
      if (nf.footer) {
        const ftr = { ...nf.footer };
        if (ftr.prodLinks) ftr.prodLinks = flattenList(ftr.prodLinks, 'text');
        if (ftr.coLinks)   ftr.coLinks   = flattenList(ftr.coLinks, 'text');
        if (ftr.resLinks)  ftr.resLinks  = flattenList(ftr.resLinks, 'text');
        Object.assign(L.footer, ftr);
      }
    }

    // home.heroImg — shared cover image across PT/EN. CMS-editable from
    // the home collection; falls back to /assets/hero.jpg in pages.jsx if
    // empty.
    if (home && home.heroImg) {
      L.home.heroImg = home.heroImg;
    }

    // home.aboutBigImg — large photo in the "Por que LESSO" block.
    if (home && home.aboutBigImg) {
      L.home.aboutBigImg = home.aboutBigImg;
    }
    // home.aboutBigImgs — optional carousel for the same block. When present
    // with 2+ entries, pages.jsx fades between them; falls back to aboutBigImg
    // otherwise. Shared across PT/EN (top-level in home.json).
    if (home && Array.isArray(home.aboutBigImgs)) {
      L.home.aboutBigImgs = home.aboutBigImgs;
    }
    // home.gallery — optional simple product-image strip rendered BETWEEN the
    // factory carousel and the LINHAS PRINCIPAIS section. Each item: {image,
    // caption?, href?}. Shared across PT/EN. Editors can add 4-6 images via the
    // home collection in Decap. When empty/missing, the section is hidden.
    if (home && Array.isArray(home.gallery)) {
      L.home.gallery = home.gallery;
    }

    // home — hero/stats/sectors/process/etc.
    if (home && home[lang]) {
      const h = { ...home[lang] };
      // sectors and process are top-level in home.json but were top-level in
      // i18n.jsx (sectors at I18N.sectors, process at I18N.home.process).
      // Move sectors out of home back to top-level for backward compat.
      if (h.sectors) {
        L.sectors = h.sectors;
        delete h.sectors;
      }
      // ctaBand: {h, p} → ctaBandH / ctaBandP (legacy field names used by pages.jsx)
      if (h.ctaBand) {
        h.ctaBandH = h.ctaBand.h;
        h.ctaBandP = h.ctaBand.p;
        delete h.ctaBand;
      }
      Object.assign(L.home, h);
    }

    // about — story/rd/mfg + regions + heads (certs handled below as shared)
    if (about && about[lang]) {
      Object.assign(L.about, about[lang]);
    }

    // quality — pageH1, pageLead, downloadsHead, faqHead, faqs, pdfBtn, inquireBtn
    if (quality && quality[lang]) {
      Object.assign(L.quality, quality[lang]);
    }

    // contact — labels + info cards + form labels
    if (contact && contact[lang]) {
      // Form labels merge: don't blow away placeholder hints (nameH/etc.) that
      // stay hardcoded in i18n.jsx defaults.
      const c = { ...contact[lang] };
      if (c.f) {
        L.contact.f = { ...L.contact.f, ...c.f };
        delete c.f;
      }
      Object.assign(L.contact, c);
    }

    // ui-labels — pd labels + dlCats + products page heads + inquiry + news + footer.colSocial
    if (uiLabels && uiLabels[lang]) {
      const u = uiLabels[lang];
      if (u.pd)       Object.assign(L.pd, u.pd);
      // dlCats now accepts both legacy {key: label} object and the new
      // list-of-{key,label} shape (so the CMS can grow the category set).
      // Either way we hydrate L.dlCats as a plain object for the consumer
      // in pages.jsx: <span>{t.dlCats[d.cat]}</span>.
      if (u.dlCats) {
        if (Array.isArray(u.dlCats)) {
          for (const entry of u.dlCats) {
            if (entry && entry.key) L.dlCats[entry.key] = entry.label || entry.key;
          }
        } else {
          Object.assign(L.dlCats, u.dlCats);
        }
      }
      if (u.products) Object.assign(L.products, u.products);
      if (u.downloadLeadForm) {
        L.downloadLeadForm = L.downloadLeadForm || {};
        Object.assign(L.downloadLeadForm, u.downloadLeadForm);
      }
      if (u.inquiry) {
        L.inquiry = L.inquiry || {};
        Object.assign(L.inquiry, u.inquiry);
      }
      // news (added 2026-05-16) — page H1, lead, empty state, back link, "Read more" CTA
      if (u.news) {
        L.news = L.news || {};
        Object.assign(L.news, u.news);
      }
      // footer.colSocial (added 2026-05-16) — only this single field is in
      // ui-labels because the other footer text lives in nav-footer.json
      if (u.footer && u.footer.colSocial) {
        L.footer.colSocial = u.footer.colSocial;
      }
    }
  }

  // about.certsImage — single composite logo wall (replaces the per-cert grid
  // when set). Shared across PT/EN/ES; rendered on both AboutPage and HomePage.
  if (about && typeof about.certsImage === 'string') {
    for (const lng of ['pt', 'en', 'es']) {
      if (window.I18N[lng]) window.I18N[lng].about.certsImage = about.certsImage;
    }
  }

  // about.certs — shared across languages. Each entry is {label, image}
  // (image optional); legacy plain-string entries are preserved as-is and
  // rendered as text by pages.jsx.
  if (about && about.certs) {
    for (const lng of ['pt', 'en', 'es']) {
      if (window.I18N[lng]) window.I18N[lng].about.certs = about.certs;
    }
  }

  // about.aboutHqImg — shared photo of the HQ building (used on About page).
  // CMS-editable; falls back to assets/about-hq.jpg in pages.jsx if empty.
  if (about && about.aboutHqImg) {
    for (const lng of ['pt', 'en', 'es']) {
      if (window.I18N[lng]) window.I18N[lng].about.aboutHqImg = about.aboutHqImg;
    }
  }

  // about.projectsImg — major-projects mosaic shown after "Global Presence"
  // on the About page. Kept as a legacy fallback if the structured projects
  // list below is empty.
  if (about && typeof about.projectsImg === 'string') {
    for (const lng of ['pt', 'en', 'es']) {
      if (window.I18N[lng]) window.I18N[lng].about.projectsImg = about.projectsImg;
    }
  }

  // about.projects — structured, translatable major-project mosaic.
  // Shared images live once at the item root; titles use title_pt/title_en/title_es
  // so the public EN/ES pages never show Portuguese text embedded in a bitmap.
  if (about && Array.isArray(about.projects)) {
    for (const lng of ['pt', 'en', 'es']) {
      if (!window.I18N[lng]) continue;
      window.I18N[lng].about.projects = about.projects.map((item) => ({
        image: item && item.image ? item.image : '',
        title: (item && (item[`title_${lng}`] || item.title_pt || item.title_en || item.title_es)) || '',
      })).filter(item => item.image || item.title);
    }
  }

  // about.video — shared institutional video (used on About page).
  // CMS-editable {url, title, poster}; the card hides itself if url is blank.
  if (about && about.video) {
    for (const lng of ['pt', 'en', 'es']) {
      if (window.I18N[lng]) window.I18N[lng].about.video = about.video;
    }
  }

  // Contact info (shared across langs) — exposed as window.CONTACT for
  // WhatsAppFab and form submission code.
  if (contact && contact.info) {
    window.CONTACT = contact.info;
  }

  // Downloads — replace array (incl. empty array, so client-deleted entries
  // disappear from the site). Accepts either a bare array (legacy/test) or
  // {items: [...]} (Decap files-mode shape).
  const dlList = Array.isArray(downloads)
    ? downloads
    : (downloads && Array.isArray(downloads.items) ? downloads.items : null);
  if (Array.isArray(dlList)) {
    window.DOWNLOADS = dlList;
  }

  // Legal text (privacy / cookie / 404 / WhatsApp msg) — exposed as window.LEGAL.
  if (legal) {
    window.LEGAL = legal;
  }

  // Products — replace per slug. Empty array = all deleted (handled here, not
  // gated on .length, so client deletions take effect).
  if (Array.isArray(products)) {
    products.sort((a, b) => (a.order || 999) - (b.order || 999));

    const splitPipeRows = (rows) => {
      if (!Array.isArray(rows)) return rows;
      return rows.map(r => {
        const s = typeof r === 'string' ? r : (r && r.row) || '';
        return s.split('|').map(c => c.trim());
      });
    };

    // Soft cap mirroring scripts/build-products-index.js MAX_PRODUCTS. Even if
    // the CI hard-stop is bypassed (e.g. someone edits _index.json by hand),
    // the UI will not render more than this many cards.
    const MAX_PRODUCTS = 50;
    for (const lang of ['en', 'pt', 'es']) {
      const L = window.I18N && window.I18N[lang];
      if (!L) continue;
      const merged = products.slice(0, MAX_PRODUCTS).map(p => {
        const langData = p[lang] || p.en || p.pt || {};
        const gallery = Array.isArray(p.gallery)
          ? flattenList(p.gallery, 'src')
          : [];
        const subproducts = Array.isArray(p.subproducts) ? p.subproducts : [];
        return {
          slug: p.slug,
          image: p.image,
          pdf_url: p.pdf_url || '',
          gallery,
          subproducts,
          ...langData,
          specs:        flattenList(langData.specs, 'spec'),
          benefits:     flattenList(langData.benefits, 'text'),
          applications: flattenList(langData.applications, 'text'),
          specCols:     flattenList(langData.specCols, 'col'),
          specRows:     splitPipeRows(langData.specRows),
          standards:    flattenList(langData.standards, 'name'),
        };
      });
      L.productList = merged;
    }
  }

  // News posts — multilingual, exposed as window.NEWS sorted by date desc.
  // Each entry: { slug, date, title, title_en, title_es, summary, summary_en,
  // summary_es, body, body_en, body_es, image, published }.
  // Filter out drafts (published === false) defensively even though the
  // build-news-index.js script already excludes them.
  if (Array.isArray(news)) {
    const live = news.filter(n => n && n.published !== false);
    live.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
    window.NEWS = live;
  } else if (!window.NEWS) {
    window.NEWS = [];
  }
}

window.loadContent = loadContent;
window.applyContent = applyContent;

// ----- Boot -------------------------------------------------
// Load content first, then mount React. If load is slow, show a minimal
// splash; if it fails, we still mount with defaults.
(async () => {
  await loadContent();
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
})();
