// HydroPulse — shared UI: logo, nav, footer, icons, utilities

const { useState, useEffect, useRef } = React;

// ------ Routes, per-route head metadata, path router ------
//
// HP-ROUTES-BEGIN
// The ONE place that lists the site's routes and their search metadata.
// Plain JavaScript (no JSX) on purpose: qa/generate-sitemap.mjs evaluates this
// block in node against data.js to build sitemap.xml, so the sitemap, the
// router and the <head> tags can never disagree. Keep in sync by hand:
//   - vercel.json `routes` (server side: which clean paths serve HydroPulse.html)
//   - App() in HydroPulse.html (which page component each path renders)
// Descriptions are built ONLY from copy that already exists — HP_DATA text or the
// headline the page itself shows (PageHeader titles are repeated verbatim below;
// if a page headline changes, change it here too). Never add new claims here.
(function (window) {
  var ORIGIN = "https://www.hydro-pulse.org";
  var SUFFIX = " — HydroPulse";

  function D() { return window.HP_DATA || {}; }
  function F() { return D().features || {}; }
  function P() { return D().project || {}; }

  // First sentence of a string ("…pressures. Through…" → "…pressures.").
  function firstSentence(text) {
    var s = String(text || "");
    var m = s.match(/^.*?[.!?](?=\s|$)/);
    return m ? m[0] : s;
  }
  // Join non-empty parts, then clip to ~160 chars at a word boundary (search
  // snippets are cut around there anyway; clipping here keeps the ellipsis clean).
  // Put the lead sentence first so the clipped part is the list, never the lead.
  var DESC_MAX = 160;
  function desc() {
    var s = Array.prototype.slice.call(arguments).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
    if (s.length <= DESC_MAX) return s;
    var cut = s.slice(0, DESC_MAX);
    return cut.slice(0, cut.lastIndexOf(" ")).replace(/[,;:\s]+$/, "") + "…";
  }
  function names(list, key) {
    return (list || []).map(function (x) { return x && x[key]; }).filter(Boolean);
  }
  function visibleNews() {
    var list = typeof window.HP_visibleNews === "function" ? window.HP_visibleNews() : (D().news || []);
    // NewsDetailPage also refuses visible:false items, so they can never be indexable.
    return list.filter(function (n) { return n && n.visible !== false; });
  }

  // Primary single-segment pages. `name` → "<name> — HydroPulse" title.
  // `enabled` mirrors the page's own feature-flag gate (disabled ⇒ noindex + not in sitemap).
  var PAGES = {
    about: {
      name: "About",
      description: function () { return desc(P().aboutLead, firstSentence(P().aboutBody)); },
    },
    "work-packages": {
      name: "Work packages",
      description: function () {
        return desc("Fifteen work packages, one shared vision for sustainable hydropower.",
          names(D().workPackages, "title").join(", ") + ".");
      },
    },
    "advisory-board": {
      name: "Advisory board",
      description: function () {
        return desc("Stakeholders' Advisory Board.", "Advisory board members:", names(D().advisoryBoard, "name").join(", ") + ".");
      },
    },
    sites: {
      name: "Demonstration sites",
      description: function () {
        return desc("The most critical ecosystems where hydropower plants operate in Europe.",
          names(D().sites, "name").join(", ") + ".");
      },
    },
    consortium: {
      name: "Consortium",
      description: function () {
        return desc(P().consortiumPartners + " HydroPulse consortium partners.", names(D().partners, "name").join(", ") + ".");
      },
    },
    news: {
      name: "News",
      description: function () {
        return desc("News & updates from the consortium.", names(visibleNews(), "title").join("; ") + ".");
      },
    },
    resources: {
      name: "Resources",
      description: function () {
        var items = F().showDeliverableItems
          ? (D().resources || []).filter(function (r) { return r.visible !== false; })
              .map(function (r) { return r.code + " " + r.title; })
          : [];
        return desc("Deliverables, policy briefs, datasets and publications.", items.length ? items.join(", ") + "." : firstSentence(P().summary));
      },
    },
    brand: {
      name: "Brand",
      enabled: function () { return !D().features || !!F().showBrand; },
      // The intro already restates the headline, so use it alone (headline as fallback).
      description: function () { return desc((D().brand || {}).intro || "One mark, one palette — used with care."); },
    },
    "dissemination-kit": {
      name: "Dissemination kit",
      enabled: function () { return !D().features || !!F().showDisseminationKit; },
      description: function () {
        return desc("Logos, brochure and roll-up — ready to download and share.", (D().dissemination || {}).intro);
      },
    },
    engage: {
      name: "Get involved",
      description: function () { return desc("HydroPulse is designed to be worked with — not around.", firstSentence(P().summary)); },
    },
    contact: {
      name: "Contact",
      description: function () { return desc("Reach the consortium.", firstSentence(P().summary)); },
    },
    legal: {
      name: "Legal",
      description: function () {
        // Same sentence LegalPage shows.
        return desc("Legal notice, privacy and accessibility.",
          "This site is operated by the HydroPulse consortium, coordinated by NTUA, under grant agreement No. " +
          P().grantAgreementNo + " of the European Union's Horizon Europe research and innovation programme.");
      },
    },
  };

  // Old/alternate paths that render a primary page. They canonicalise to the primary
  // path, are rewritten to it in the address bar, and are never in the sitemap.
  var ALIASES = { "/themes": "/work-packages", "/media": "/dissemination-kit" };

  // Every clean path the client router handles. vercel.json serves the same set
  // EXCEPT the ALIASES, which the server 308-redirects (/themes → /work-packages);
  // they stay in here so old "#/themes" links still migrate to the right page.
  window.HP_ROUTE_RE = /^\/(?:about|work-packages|themes|advisory-board|sites|consortium|news|resources|brand|dissemination-kit|media|engage|contact|legal|(?:sites|news)\/[^/]+)$/;
  window.HP_ORIGIN = ORIGIN;

  // "/about/" → "/about"; "/HydroPulse.html" (the file itself) → "/".
  window.HP_normalizePath = function HP_normalizePath(p) {
    p = String(p || "/");
    if (p.length > 1) p = p.replace(/\/+$/, "") || "/";
    if (p === "/HydroPulse.html" || p === "/index.html") p = "/";
    return p;
  };
  window.HP_ROUTE_ALIASES = ALIASES;

  // pathname → { path (canonical), title, description, indexable, home }.
  // title/description are null for home: the static <head> tags in HydroPulse.html own those.
  window.HP_resolveRoute = function HP_resolveRoute(pathname) {
    var p = window.HP_normalizePath(pathname);
    if (ALIASES[p]) p = ALIASES[p];
    var parts = p.split("/").filter(Boolean);
    var home = { path: "/", title: null, description: null, indexable: true, home: true };
    if (!parts.length) return home;
    var top = parts[0];
    if (parts.length === 2 && top === "sites") {
      // Site profiles can be paused (features.showSiteDetailPages); unknown ids are soft 404s.
      var site = F().showSiteDetailPages
        ? (D().sites || []).filter(function (s) { return s.id === parts[1]; })[0]
        : null;
      return {
        path: p,
        title: (site ? site.name : "Demonstration sites") + SUFFIX,
        description: site ? desc(site.summary) : PAGES.sites.description(),
        indexable: !!site,
      };
    }
    if (parts.length === 2 && top === "news") {
      var item = visibleNews().filter(function (n) { return n.id === parts[1]; })[0];
      return {
        path: p,
        title: (item ? item.title : "News") + SUFFIX,
        description: item ? desc(item.excerpt) : PAGES.news.description(),
        indexable: !!item,
      };
    }
    var page = parts.length === 1 && Object.prototype.hasOwnProperty.call(PAGES, top) ? PAGES[top] : null;
    if (!page) return { path: "/", title: null, description: null, indexable: false, home: true };
    return {
      path: p,
      title: page.name + SUFFIX,
      description: page.description(),
      indexable: page.enabled ? page.enabled() : true,
    };
  };

  // Every indexable canonical path (for sitemap.xml), with lastmod where data has a real date.
  window.HP_indexableRoutes = function HP_indexableRoutes() {
    var out = [{ path: "/" }];
    Object.keys(PAGES).forEach(function (k) {
      if (window.HP_resolveRoute("/" + k).indexable) out.push({ path: "/" + k });
    });
    (D().sites || []).forEach(function (s) {
      if (window.HP_resolveRoute("/sites/" + s.id).indexable) out.push({ path: "/sites/" + s.id });
    });
    visibleNews().forEach(function (n) {
      if (window.HP_resolveRoute("/news/" + n.id).indexable) out.push({ path: "/news/" + n.id, lastmod: n.date });
    });
    return out;
  };
})(window);
// HP-ROUTES-END

// Head tags. The home values are whatever HydroPulse.html ships statically
// (captured once here), so the home page keeps its existing title/description.
(function () {
  function meta(attr, key) {
    var el = document.head.querySelector("meta[" + attr + '="' + key + '"]');
    if (!el) {
      el = document.createElement("meta");
      el.setAttribute(attr, key);
      document.head.appendChild(el);
    }
    return el;
  }
  var canonical = document.head.querySelector('link[rel="canonical"]');
  if (!canonical) {
    canonical = document.createElement("link");
    canonical.setAttribute("rel", "canonical");
    document.head.appendChild(canonical);
  }
  var HOME = { title: document.title, description: meta("name", "description").getAttribute("content") || "" };

  window.HP_applyRouteMeta = function HP_applyRouteMeta(pathname) {
    var r = window.HP_resolveRoute(pathname);
    var title = r.title || HOME.title;
    var description = r.description || HOME.description;
    var url = window.HP_ORIGIN + r.path;
    document.title = title;
    meta("name", "description").setAttribute("content", description);
    canonical.setAttribute("href", url);
    meta("property", "og:url").setAttribute("content", url);
    meta("property", "og:title").setAttribute("content", title);
    meta("property", "og:description").setAttribute("content", description);
    meta("name", "twitter:title").setAttribute("content", title);
    meta("name", "twitter:description").setAttribute("content", description);
    // Paused / unknown routes: keep them out of the index (the route still renders).
    var robots = document.head.querySelector('meta[name="robots"][data-hp-route]');
    if (!r.indexable) {
      if (!robots) {
        robots = document.createElement("meta");
        robots.setAttribute("name", "robots");
        robots.setAttribute("data-hp-route", "");
        document.head.appendChild(robots);
      }
      robots.setAttribute("content", "noindex");
    } else if (robots) {
      robots.parentNode.removeChild(robots);
    }
    return r;
  };
})();

// Clean path routing (History API). Names kept from the old hash router so every
// caller still works: useHashRoute() returns the current path ("/", "/about",
// "/news/<id>"), navTo(path) navigates without a reload.
function hpCurrentRoute() {
  return window.HP_normalizePath(window.location.pathname);
}
function hpScrollToHashOrTop() {
  var id = window.location.hash ? decodeURIComponent(window.location.hash.slice(1)) : "";
  if (!id) { window.scrollTo({ top: 0, behavior: "instant" }); return; }
  var tries = 0;
  (function attempt() {
    var el = document.getElementById(id);
    if (el) { el.scrollIntoView({ block: "start" }); return; }
    if (++tries < 20) requestAnimationFrame(attempt);
    else window.scrollTo({ top: 0, behavior: "instant" });
  })();
}

// Route changes always land at the top (or on the #anchor) — set explicitly so
// Back/Forward don't race the browser's own scroll restoration (which used to
// restore some history entries mid-page and others at the top).
try { if ("scrollRestoration" in window.history) window.history.scrollRestoration = "manual"; } catch (e) {}

// Backward compatibility: old hash URLs (/#/about, /HydroPulse.html#/work-packages#wp-x)
// and alias paths (/themes, /media) are rewritten in place to their clean primary
// path. Runs once before the first render, and again on every same-document
// "#/…" hash change (a hand-edited hash, or an old-style link). Returns true if
// it rewrote the URL.
function hpMigrateUrl() {
  var loc = window.location;
  var target = null, anchor = "";
  if (loc.hash.indexOf("#/") === 0) {
    var rest = loc.hash.slice(1);
    var i = rest.indexOf("#");
    target = i >= 0 ? rest.slice(0, i) : rest;
    anchor = i >= 0 ? rest.slice(i) : "";
    target = window.HP_normalizePath(target);
    if (target !== "/" && !window.HP_ROUTE_RE.test(target)) { target = "/"; anchor = ""; }
    // A bare "#/" on the file URL (plain local preview) keeps the file path.
    if (target === "/" && window.HP_normalizePath(loc.pathname) === "/") target = loc.pathname;
  } else if (window.HP_ROUTE_ALIASES[window.HP_normalizePath(loc.pathname)]) {
    target = loc.pathname;
    anchor = loc.hash;
  }
  if (target === null) return false;
  target = window.HP_ROUTE_ALIASES[window.HP_normalizePath(target)] || target;
  try { window.history.replaceState(window.history.state, "", target + loc.search + anchor); } catch (e) { return false; }
  return true;
}
hpMigrateUrl();
window.addEventListener("hashchange", function () {
  if (window.location.hash.indexOf("#/") !== 0) return;
  if (hpMigrateUrl()) window.dispatchEvent(new Event("hp-routechange"));
});

window.useHashRoute = function useHashRoute() {
  const [route, setRoute] = useState(hpCurrentRoute);
  useEffect(() => {
    const onChange = () => setRoute(hpCurrentRoute());
    window.addEventListener("popstate", onChange);
    window.addEventListener("hp-routechange", onChange);
    // Child effects run before this one, so a page that redirected on mount
    // (SiteDetailPage → navTo("/sites")) fired its event before we listened: re-sync.
    onChange();
    return () => {
      window.removeEventListener("popstate", onChange);
      window.removeEventListener("hp-routechange", onChange);
    };
  }, []);
  useEffect(() => {
    window.HP_applyRouteMeta(route);
    hpScrollToHashOrTop();
  }, [route]);
  return route;
};

// navTo("/about"), navTo("/work-packages#wp-mgmt"), navTo("/sites", { replace: true }).
// Old "#/about" style arguments are accepted too.
window.navTo = function navTo(path, opts) {
  var to = String(path || "/");
  if (to.indexOf("#/") === 0) to = to.slice(1);
  var url = new URL(to, window.location.origin);
  // "/#/consortium" (old hash form behind the home path) → "/consortium";
  // an unknown "#/x" falls back home, an alias goes to its primary path.
  if (url.hash.indexOf("#/") === 0 && window.HP_normalizePath(url.pathname) === "/") {
    url = new URL(url.hash.slice(1), window.location.origin);
    var inner = window.HP_normalizePath(url.pathname);
    if (inner !== "/" && !window.HP_ROUTE_RE.test(inner)) url = new URL("/", window.location.origin);
  }
  var alias = window.HP_ROUTE_ALIASES[window.HP_normalizePath(url.pathname)];
  if (alias) url = new URL(alias + url.search + url.hash, window.location.origin);
  var next = url.pathname + url.search + url.hash;
  var samePage = window.HP_normalizePath(url.pathname) === hpCurrentRoute();
  var here = window.location.pathname + window.location.search + window.location.hash;
  if (opts && opts.replace) window.history.replaceState(null, "", next);
  else if (next !== here) window.history.pushState(null, "", next);
  if (samePage) { hpScrollToHashOrTop(); return; }
  window.dispatchEvent(new Event("hp-routechange"));
};

// SPA navigation for plain internal links (<a href="/about">). Only known SPA
// routes are intercepted, so real files (/press, /status, /assets/*, PDFs) and
// anything with a modifier key, a target, or a download attribute load normally.
// React's own onClick handlers (e.g. closing the menu) run first: React listens
// on #root, this listener on document.
document.addEventListener("click", function (e) {
  if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
  var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
  if (!a || typeof a.href !== "string" || a.hasAttribute("download")) return;
  var target = a.getAttribute("target");
  if (target && target !== "_self") return;
  var url;
  try { url = new URL(a.href, window.location.href); } catch (err) { return; }
  if (url.origin !== window.location.origin) return;
  var p = window.HP_normalizePath(url.pathname);
  if (p !== "/" && !window.HP_ROUTE_RE.test(p)) return;
  // An old "/#/consortium" link is unwrapped by navTo(), not treated as same-page.
  e.preventDefault();
  window.navTo(url.pathname + url.search + url.hash);
});

// Root-absolute asset URL. data.js stores paths like "assets/partners/x.jpg";
// relative URLs would break on two-segment routes (/news/<id>), so every asset
// path from data is passed through this. Absolute / protocol / data URLs pass through.
window.HP_asset = function HP_asset(p) {
  if (!p || typeof p !== "string") return p;
  if (/^(?:[a-z][a-z0-9+.-]*:|\/|#)/i.test(p)) return p;
  return "/" + p;
};

// ------ Reveal observer ------
window.useReveal = function useReveal() {
  useEffect(() => {
    const els = document.querySelectorAll(".reveal");
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add("in");
            io.unobserve(e.target);
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -40px 0px" }
    );
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  });
};

// ------ Icons ------
window.Icon = function Icon({ name, size = 14, stroke = 1.5 }) {
  const s = size;
  const common = { width: s, height: s, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" };
  switch (name) {
    case "arrow-right": return <svg {...common}><path d="M5 12h14M13 5l7 7-7 7"/></svg>;
    case "arrow-up-right": return <svg {...common}><path d="M7 17L17 7M8 7h9v9"/></svg>;
    case "arrow-down": return <svg {...common}><path d="M12 5v14M5 13l7 7 7-7"/></svg>;
    case "plus": return <svg {...common}><path d="M12 5v14M5 12h14"/></svg>;
    case "minus": return <svg {...common}><path d="M5 12h14"/></svg>;
    case "close": return <svg {...common}><path d="M6 6l12 12M18 6l-12 12"/></svg>;
    case "download": return <svg {...common}><path d="M12 3v14M5 11l7 7 7-7M5 21h14"/></svg>;
    case "play": return <svg {...common}><path d="M8 5l12 7-12 7z" fill="currentColor"/></svg>;
    case "menu": return <svg {...common}><path d="M4 7h16M4 12h16M4 17h16"/></svg>;
    case "search": return <svg {...common}><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.5-4.5"/></svg>;
    case "wave": return <svg {...common} viewBox="0 0 24 24"><path d="M2 12c2 0 2 -3 4 -3s2 3 4 3 2-3 4-3 2 3 4 3 2-3 4-3"/></svg>;
    case "dot": return <svg {...common} viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/></svg>;
    default: return null;
  }
};

// ------ Logo variant (root `tweaks.logoVariant`, synced in applyTweaks + data-logo-variant) ------
function readLogoVariantFromDom() {
  if (typeof document === "undefined") return "inline";
  const a = document.documentElement.getAttribute("data-logo-variant");
  if (a === "brand") return "brand";
  if (a === "inline") return "inline";
  if (typeof window !== "undefined" && window.HP_LOGO_USE_INLINE) return "inline";
  return "inline";
}

window.useLogoVariant = function useLogoVariant() {
  const [v, setV] = useState(readLogoVariantFromDom);
  useEffect(() => {
    const onTweaks = (e) => {
      if (e && e.detail && (e.detail.logoVariant === "brand" || e.detail.logoVariant === "inline")) {
        setV(e.detail.logoVariant);
        return;
      }
      setV(readLogoVariantFromDom());
    };
    window.addEventListener("hp-tweaks", onTweaks);
    return () => window.removeEventListener("hp-tweaks", onTweaks);
  }, []);
  return v;
};

// ------ Brand logos: assets/brand/HydroPulse_logo_*.svg (see EXPORT-INSTRUCTIONS.md) ------
// *Inverse* = light/white art for dark UI (full menu) — no paper chip. If missing, we fall back to the light SVG + chip.
// ?v bumped when the logo artwork is replaced on disk (URLs are otherwise cached).
const HP_LOGO = {
  noEu: "/assets/brand/HydroPulse_logo_no_eu_text.svg?v=2",
  withEu: "/assets/brand/HydroPulse_logo_eu_text.svg?v=2",
  // Same as light lockups, with light/white art on transparent — *required* for full menu to drop the white chip
  // no-eu dark export may be missing; EU dark has white art on transparent (safe on dark UI)
  noEuInverse: "/assets/brand/HydroPulse_logo_dark_mode_eu_text.svg?v=2",
  withEuInverse: "/assets/brand/HydroPulse_logo_dark_mode_eu_text.svg?v=2",
};

// Official EU "Funded by the European Union" horizontal lockup (Commission brand pack, EN).
// Per EU visual-identity rules + the brand pack: full-colour POS emblem on light
// surfaces, reversed WHITE emblem on the dark/navy footer.
const HP_EU_FUNDED = {
  onDark: "/assets/eu/funded-by-eu-white.png",
  onLight: "/assets/eu/funded-by-eu-pos.png",
};

function EuFundedBadge({ theme = "dark" }) {
  const src = theme === "light" ? HP_EU_FUNDED.onLight : HP_EU_FUNDED.onDark;
  return (
    <img
      className="hp-eu-funded"
      src={src}
      alt="Funded by the European Union"
      width="425"
      height="89"
      decoding="async"
    />
  );
}

function LogoFallback({ theme = "default" }) {
  const isInverse = theme === "inverse";
  const color = isInverse ? "var(--paper)" : "var(--ink)";
  return (
    <a href="/" className="hp-logo hp-logo--inline" style={{ display: "inline-flex", alignItems: "center", gap: 10, textDecoration: "none", color }}>
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
        <path d="M2 14c2.5 0 2.5 -3 5 -3s2.5 3 5 3 2.5 -3 5 -3 2.5 3 5 3"/>
        <path d="M2 19c2.5 0 2.5 -3 5 -3s2.5 3 5 3 2.5 -3 5 -3 2.5 3 5 3" opacity="0.55"/>
        <circle cx="12" cy="6" r="1.6" fill={color} stroke="none"/>
      </svg>
      <span style={{ fontFamily: "var(--display)", fontSize: 20, letterSpacing: "-0.01em", fontWeight: 500 }}>HydroPulse</span>
    </a>
  );
}

/**
 * @param {object} props
 * @param {boolean} [props.brandOnDarkChrome] — Look 4 (and similar): on inverse/dark header or overlay, use the white brand lockup even when the design panel is set to “Preview” (inline). The strip is built for the actual dark-mode SVG.
 * @param {boolean} [props.noInkChip] — On dark bar / inverse: if the inverse SVG fails, use inline wordmark (light on dark) instead of the light lockup on a paper chip.
 */
window.Logo = function Logo({ theme = "default", variant = "noEu", noInkChip = false, brandOnDarkChrome = false }) {
  const panelVariant = window.useLogoVariant();
  const [forceInline, setForceInline] = useState(false);
  const isInverse = theme === "inverse";
  const [revertToLightOnDark, setRevertToLightOnDark] = useState(false);
  const alt = variant === "withEu" ? "HydroPulse, co-funded by the European Union" : "HydroPulse";

  const useBrandImages = (panelVariant === "brand" || (brandOnDarkChrome && isInverse)) && !forceInline;

  useEffect(() => {
    setForceInline(false);
  }, [panelVariant, isInverse, variant, noInkChip, brandOnDarkChrome]);

  useEffect(() => {
    setRevertToLightOnDark(false);
  }, [isInverse, variant, noInkChip]);

  if (!useBrandImages) return <LogoFallback theme={theme} />;

  let src;
  let onInkChip = false;
  if (isInverse && !revertToLightOnDark) {
    src = variant === "withEu" ? HP_LOGO.withEuInverse : HP_LOGO.noEuInverse;
  } else if (isInverse && revertToLightOnDark) {
    src = variant === "withEu" ? HP_LOGO.withEu : HP_LOGO.noEu;
    onInkChip = true;
  } else {
    src = variant === "withEu" ? HP_LOGO.withEu : HP_LOGO.noEu;
  }

  return (
    <a
      href="/"
      aria-label={alt + ", home page"}
      className={"hp-logo" + (onInkChip ? " hp-logo--on-ink" : "") + (variant === "withEu" ? " hp-logo--with-eu" : "")}
      style={{ display: "inline-flex", alignItems: "center", textDecoration: "none", lineHeight: 0 }}
    >
      <img
        className="hp-logo__img"
        src={src}
        alt={alt}
        decoding="async"
        onError={() => {
          if (isInverse && !revertToLightOnDark) {
            if (noInkChip) {
              setForceInline(true);
            } else {
              setRevertToLightOnDark(true);
            }
            return;
          }
          setForceInline(true);
        }}
      />
    </a>
  );
};

// ------ data-look (L1–L4): syncs React with vanilla `applyTweaks` + design panel —------
function useDataLook() {
  const read = () => document.documentElement.getAttribute("data-look") || "1";
  const [look, setLook] = useState(read);
  useEffect(() => {
    const onTweaks = (e) => {
      const v = e && e.detail && e.detail.laf != null ? String(e.detail.laf) : read();
      setLook(v);
    };
    window.addEventListener("hp-tweaks", onTweaks);
    return () => window.removeEventListener("hp-tweaks", onTweaks);
  }, []);
  return look;
}

// ------ Nav ------
// First 6 show in the top bar; the rest live under "More". Dissemination kit is
// surfaced in the top bar (client ask, May 2026); Brand is folded into that kit
// (its featured block + /brand deep-dive), so it is intentionally not a nav item.
const NAV_ITEMS = [
  { path: "/about", label: "About" },
  { path: "/work-packages", label: "Work packages" },
  { path: "/sites", label: "Demonstration sites" },
  { path: "/consortium", label: "Consortium" },
  { path: "/news", label: "News" },
  { path: "/dissemination-kit", label: "Dissemination kit" },
  { path: "/resources", label: "Resources" },
  { path: "/advisory-board", label: "Advisory board" },
  { path: "/engage", label: "Get involved" },
];

window.Nav = function Nav({ route }) {
  const dataLook = useDataLook();
  const logoKey = window.useLogoVariant();
  const isL4 = dataLook === "4";
  const [scrolled, setScrolled] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 20);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  useEffect(() => { setMenuOpen(false); }, [route]);

  const headerClass =
    "hp-site-header" +
    (isL4 ? " hp-header--l4" : !isL4 && scrolled ? " hp-site-header--scrolled" : "");
  const linkInk = isL4 ? "var(--l4-header-fg)" : "var(--ink)";
  const linkMute = isL4 ? "var(--l4-header-muted)" : "var(--muted)";

  return (
    <>
      <header className={headerClass}>
        <div className="wrap hp-header__inner" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "18px var(--gutter)" }}>
          <Logo
            key={logoKey}
            theme={isL4 ? "inverse" : "default"}
            variant={isL4 ? "withEu" : "noEu"}
            noInkChip={isL4}
            brandOnDarkChrome={isL4}
          />
          <nav style={{ display: "flex", alignItems: "center", gap: 28 }} className="hp-nav-desktop">
            {NAV_ITEMS.slice(0, 6).map((item) => {
              const active = route === item.path;
              return (
                <a key={item.path} href={item.path}
                  aria-current={active ? "page" : undefined}
                  className={isL4 ? "hp-nav-link hp-nav-link--l4" + (active ? " hp-nav-link--active" : "") : ""}
                  style={isL4 ? {
                    textDecoration: "none",
                    position: "relative",
                    transition: "color var(--dur) var(--ease)",
                    color: active ? linkInk : linkMute,
                  } : {
                    fontSize: 13.5, textDecoration: "none",
                    color: active ? "var(--ink)" : "var(--muted)",
                    fontWeight: active ? 500 : 400,
                    position: "relative",
                    transition: "color var(--dur) var(--ease)",
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.color = linkInk; }}
                  onMouseLeave={(e) => { e.currentTarget.style.color = active ? linkInk : linkMute; }}
                >
                  {item.label}
                  {active && (
                    <span
                      className={isL4 ? "hp-nav-link__bar" : undefined}
                      style={isL4 ? { position: "absolute", left: 0, right: 0, bottom: -22, height: 2, background: "var(--logo-sky)" } : { position: "absolute", left: 0, right: 0, bottom: -22, height: 1, background: "var(--ink)" }}
                    />
                  )}
                </a>
              );
            })}
            <button type="button" onClick={() => setMenuOpen(true)}
              className={isL4 ? "hp-nav-more--l4" : ""}
              style={isL4 ? {
                background: "transparent",
                border: "1px solid var(--l4-header-rule)",
                padding: "7px 12px",
                cursor: "pointer",
                display: "inline-flex",
                alignItems: "center",
                gap: 8,
                color: "var(--l4-header-fg)",
                fontFamily: "var(--mono)",
                fontSize: 10.5,
                letterSpacing: "0.1em",
                textTransform: "uppercase",
              } : {
                background: "transparent", border: "1px solid var(--rule-strong)", padding: "7px 12px", fontFamily: "var(--sans)", fontSize: 12, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 8, color: "var(--ink)",
              }}>
              <Icon name="menu" size={12}/> More
            </button>
          </nav>
          <button type="button" onClick={() => setMenuOpen(true)} aria-label="Open menu" className={"hp-nav-mobile" + (isL4 ? " hp-nav-more--l4" : "")}
            style={isL4 ? {
              background: "transparent", border: "1px solid var(--l4-header-rule)", padding: "9px 12px", cursor: "pointer", display: "none", alignItems: "center", gap: 8, color: "var(--l4-header-fg)",
            } : {
              background: "transparent", border: "1px solid var(--rule-strong)", padding: "9px 12px", cursor: "pointer", display: "none", alignItems: "center", gap: 8, color: "var(--ink)",
            }}>
            <Icon name="menu" size={14}/>
          </button>
        </div>
      </header>

      {menuOpen && <FullMenu onClose={() => setMenuOpen(false)} route={route} dataLook={dataLook} />}

      <style>{`
        @media (max-width: 900px) {
          .hp-nav-desktop { display: none !important; }
          .hp-nav-mobile { display: inline-flex !important; }
        }
      `}</style>
    </>
  );
};

function FullMenu({ onClose, route, dataLook = "1" }) {
  const logoKey = window.useLogoVariant();
  const l4 = dataLook === "4";
  useEffect(() => {
    const onKey = (e) => e.key === "Escape" && onClose();
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose]);

  const line = l4 ? "var(--l4-overlay-line)" : "color-mix(in srgb, var(--paper) 16%, transparent)";
  const sub = l4 ? "var(--l4-header-muted)" : "color-mix(in srgb, var(--paper) 55%, transparent)";

  if (l4) {
    return (
      <div className="hp-fullmenu hp-fullmenu--l4" role="dialog" aria-modal="true" aria-label="Main menu">
        <div className="wrap hp-fullmenu__closebar" style={{ padding: "22px var(--gutter)", display: "flex", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid var(--l4-overlay-line)" }}>
          <Logo key={logoKey} theme="inverse" variant="withEu" noInkChip brandOnDarkChrome />
          <button type="button" className="hp-fullmenu__close" onClick={onClose} style={{ background: "transparent", border: "1px solid var(--l4-overlay-line)", color: "var(--l4-header-fg)", padding: "10px 14px", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 10, fontFamily: "var(--sans)", fontSize: 12 }}>
            Close <Icon name="close" size={12} />
          </button>
        </div>
        <div className="wrap" style={{ padding: "clamp(28px, 5vw, 64px) var(--gutter) 80px", maxWidth: 720 }}>
          <div className="eyebrow hp-fullmenu__note" style={{ marginBottom: 20 }}>Menu</div>
          <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
            {[...NAV_ITEMS, { path: "/contact", label: "Contact" }].map((item, i) => (
              <li key={item.path} className="hp-fullmenu__row" style={{ borderTop: i === 0 ? "1px solid " + "var(--l4-overlay-line)" : "none", borderBottom: "1px solid var(--l4-overlay-line)" }}>
                <a className="hp-fullmenu__link" href={item.path} aria-current={route === item.path ? "page" : undefined}
                  onClick={onClose}
                  style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "18px 2px", textDecoration: "none", fontFamily: "var(--display)", fontWeight: 400, opacity: route === item.path ? 1 : 0.75 }}>
                  <span><span style={{ fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.12em", marginRight: 20, color: "var(--l4-header-muted)" }}>{String(i + 1).padStart(2, "0")}</span>{item.label}</span>
                  <Icon name="arrow-up-right" size={16} stroke={1.2} />
                </a>
              </li>
            ))}
          </ul>
          <p className="hp-fullmenu__lede" style={{ fontFamily: "var(--display)", fontSize: 20, lineHeight: 1.5, fontWeight: 300, maxWidth: "52ch", margin: "48px 0 0" }}>
            A four-year European research programme redesigning hydropower so that clean energy and living rivers are no longer a trade-off, but a joint design problem.
          </p>
        </div>
      </div>
    );
  }

  return (
    <div className="hp-fullmenu" role="dialog" aria-modal="true" aria-label="Main menu">
      <div className="wrap" style={{ padding: "22px var(--gutter)", display: "flex", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid " + line }}>
        <Logo key={logoKey} theme="inverse" variant="withEu" />
        <button type="button" onClick={onClose} style={{ background: "transparent", border: "1px solid " + "color-mix(in srgb, var(--paper) 24%, transparent)", color: "var(--paper)", padding: "10px 14px", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 10, fontFamily: "var(--sans)", fontSize: 12 }}>
          Close <Icon name="close" size={12} />
        </button>
      </div>
      <div className="wrap hp-fullmenu__grid" style={{ padding: "clamp(32px, 7vw, 96px) var(--gutter)", display: "grid", gridTemplateColumns: "1fr 1fr", gap: "clamp(32px, 6vw, 80px)" }}>
        <div>
          <div className="eyebrow" style={{ color: sub, marginBottom: 28 }}>Navigate</div>
          <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
            {[...NAV_ITEMS, { path: "/contact", label: "Contact" }].map((item, i) => (
              <li key={item.path} style={{ borderTop: i === 0 ? "1px solid " + line : "none", borderBottom: "1px solid " + line }}>
                <a href={item.path} aria-current={route === item.path ? "page" : undefined}
                  onClick={onClose}
                  style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "22px 4px", color: "var(--paper)", textDecoration: "none", fontFamily: "var(--display)", fontSize: "clamp(28px, 3.4vw, 44px)", letterSpacing: "-0.02em", fontWeight: 300, opacity: route === item.path ? 1 : 0.8 }}>
                  <span><span style={{ fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.12em", marginRight: 22, opacity: 0.5 }}>{String(i + 1).padStart(2, "0")}</span>{item.label}</span>
                  <Icon name="arrow-up-right" size={18} stroke={1.2} />
                </a>
              </li>
            ))}
          </ul>
        </div>
        <div style={{ alignSelf: "end" }}>
          <div className="eyebrow" style={{ color: sub, marginBottom: 20 }}>HydroPulse — a note</div>
          <p style={{ fontFamily: "var(--display)", fontSize: 22, lineHeight: 1.45, fontWeight: 300, color: "var(--paper)", maxWidth: 460, margin: 0, opacity: 0.88 }}>
            A four-year European research programme redesigning hydropower so that clean energy and living rivers are no longer a trade-off, but a joint design problem.
          </p>
          <div style={{ marginTop: 40, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24, fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.06em", color: "color-mix(in srgb, var(--paper) 70%, transparent)" }}>
            <div><div style={{ opacity: 0.55, marginBottom: 4 }}>DURATION</div>2026 — 2030</div>
            <div><div style={{ opacity: 0.55, marginBottom: 4 }}>FUNDING</div>Horizon Europe</div>
            <div><div style={{ opacity: 0.55, marginBottom: 4 }}>PARTNERS</div>{HP_DATA.project.partnerCount} across {HP_DATA.project.countries} countries</div>
            <div><div style={{ opacity: 0.55, marginBottom: 4 }}>DEMO SITES</div>{HP_DATA.project.demoSites} across Europe</div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ------ Footer ------
const FOOTER_COLS = [
  { h: "Project", links: [["About", "/about"], ["Work packages", "/work-packages"], ["Demonstration sites", "/sites"], ["Consortium", "/consortium"], ["Advisory board", "/advisory-board"]] },
  { h: "Public outputs", links: [["News", "/news"], ["Resources", "/resources"], ["Dissemination kit", "/dissemination-kit"], ["Get involved", "/engage"]] },
  { h: "Contact", links: [[HP_DATA.project.emails.info, "/contact"], ["Press & media", "/contact"], ["Partner login", "/contact"], ["Newsletter", "/contact"]] },
];

// ───────── Footer social links — one reusable component, both footer tones ─────────
// Reads HP_DATA.project.social and renders official 24×24 brand glyphs as
// <path fill="currentColor">. `tone` ("dark"|"light") sets colors from the footer's
// own tokens; glyphs use currentColor so one component inverts on both footers.
// Renders nothing if social is missing; silently skips any channel without a URL.
const SOCIAL_GLYPHS = {
  linkedin: "M20.45 20.45h-3.56v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.8 0 0 .77 0 1.73v20.54C0 23.22.8 24 1.77 24h20.45C23.2 24 24 23.22 24 22.27V1.73C24 .77 23.2 0 22.22 0z",
  instagram: "M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.72 3.72 0 0 1-1.38-.9 3.72 3.72 0 0 1-.9-1.38c-.16-.42-.36-1.06-.41-2.23-.06-1.27-.07-1.65-.07-4.85s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63c-.79.3-1.46.72-2.12 1.38C1.35 2.68.94 3.35.63 4.14.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91.3.79.72 1.46 1.38 2.12.66.66 1.33 1.08 2.12 1.38.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.88 5.88 0 0 0 2.12-1.38c.66-.66 1.08-1.33 1.38-2.12.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.88 5.88 0 0 0-1.38-2.12A5.88 5.88 0 0 0 19.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0zm0 5.84A6.16 6.16 0 0 0 5.84 12 6.16 6.16 0 0 0 12 18.16 6.16 6.16 0 0 0 18.16 12 6.16 6.16 0 0 0 12 5.84zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.41-10.85a1.44 1.44 0 1 0 0 2.88 1.44 1.44 0 0 0 0-2.88z",
  facebook: "M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07c0 6.03 4.39 11.03 10.13 11.93v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.69.24 2.69.24v2.97h-1.52c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8v8.44C19.61 23.1 24 18.1 24 12.07z",
  x: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
  tiktok: "M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64c.3 0 .59.04.86.13V9.4a6.33 6.33 0 0 0-5.94 10.36 6.33 6.33 0 0 0 10.86-4.43V8.69a8.27 8.27 0 0 0 4.83 1.54V6.69h-.5z",
  youtube: "M23.5 6.2a3.02 3.02 0 0 0-2.12-2.14C19.5 3.55 12 3.55 12 3.55s-7.5 0-9.38.51A3.02 3.02 0 0 0 .5 6.2C0 8.08 0 12 0 12s0 3.92.5 5.8a3.02 3.02 0 0 0 2.12 2.14c1.88.51 9.38.51 9.38.51s7.5 0 9.38-.51a3.02 3.02 0 0 0 2.12-2.14C24 15.92 24 12 24 12s0-3.92-.5-5.8zM9.6 15.6V8.4l6.27 3.6L9.6 15.6z",
};
const SOCIAL_ORDER = [
  ["linkedin", "LinkedIn"],
  ["instagram", "Instagram"],
  ["facebook", "Facebook"],
  ["x", "X"],
  ["tiktok", "TikTok"],
  ["youtube", "YouTube"],
];
function SocialRow({ tone = "dark", style = {} }) {
  const social = (window.HP_DATA && HP_DATA.project && HP_DATA.project.social) || {};
  const items = SOCIAL_ORDER.filter(([key]) => !!social[key]);
  if (!items.length) return null;
  const dark = tone !== "light";
  const labelColor = dark ? "color-mix(in srgb, var(--paper) 56%, transparent)" : "var(--l4-footer-muted)";
  const restColor = dark ? "color-mix(in srgb, var(--paper) 72%, transparent)" : "color-mix(in srgb, var(--l4-footer-ink) 70%, var(--l4-footer-surface))";
  const ringColor = dark ? "color-mix(in srgb, var(--paper) 22%, transparent)" : "var(--l4-footer-rule)";
  return (
    <div className="hp-social" style={{ "--hp-social-rest": restColor, "--hp-social-ring": ringColor, ...style }}>
      <div className="eyebrow hp-social__label" style={{ color: labelColor, marginBottom: 14 }}>Follow</div>
      <ul className="hp-social__list" role="list">
        {items.map(([key, label]) => (
          <li key={key}>
            <a className="hp-social__link" href={social[key]} target="_blank" rel="noopener noreferrer"
              aria-label={"HydroPulse on " + label + " (opens in a new tab)"} title={label}>
              <svg className="hp-social__glyph" width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
                <path d={SOCIAL_GLYPHS[key]} />
              </svg>
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}
window.SocialRow = SocialRow;

function Footer() {
  const dataLook = useDataLook();
  const logoKey = window.useLogoVariant();
  const grantNo = HP_DATA.project.grantAgreementNo;
  const l4 = dataLook === "4";
  const onInk = "color-mix(in srgb, var(--paper) 88%, transparent)";
  const onInk2 = "color-mix(in srgb, var(--paper) 56%, transparent)";
  const onInk3 = "color-mix(in srgb, var(--paper) 50%, transparent)";
  const line = "color-mix(in srgb, var(--paper) 16%, transparent)";

  if (l4) {
    return (
      <footer className="hp-footer--light" style={{ marginTop: 120 }}>
        <div className="wrap" style={{ padding: "clamp(56px, 7vw, 100px) var(--gutter) 48px" }}>
          <div className="hp-footer__head hp-l4-foot-grid">
            <div>
              <Logo key={logoKey} theme="default" variant="withEu" />
              <p className="body" style={{ color: "var(--l4-footer-muted)", maxWidth: 360, marginTop: 20, fontSize: 14, lineHeight: 1.55 }}>
                A European research consortium advancing ecosystem-based hydropower. Coordinated by NTUA under Horizon Europe, 2026 — 2030.
              </p>
              <SocialRow tone="light" style={{ marginTop: 22 }} />
            </div>
            {FOOTER_COLS.map((col) => (
              <div key={col.h}>
                <div className="eyebrow hp-footer__eyebrow" style={{ marginBottom: 16 }}>{col.h}</div>
                <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
                  {col.links.map(([l, p]) => (
                    <li key={l}><a href={p} style={{ color: "var(--l4-footer-ink)", textDecoration: "none", fontSize: 13.5, fontWeight: 400 }}>{l}</a></li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
          <div className="hp-footer__funding" style={{ marginTop: 40 }}>
            <EuFundedBadge theme="light" />
          </div>
          <div className="hp-footer__rule" style={{ marginTop: 48, paddingTop: 24, borderTop: "1px solid var(--l4-footer-rule)", display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 16 }}>
            <div className="caption" style={{ color: "var(--l4-footer-muted)" }}>
              © 2026 HydroPulse Consortium · Grant Agreement No. {grantNo} · Site content reflects authors' views only
            </div>
            <div style={{ display: "flex", gap: 20, fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--l4-footer-muted)" }}>
              <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Legal</a>
              <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Privacy</a>
              <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Accessibility</a>
            </div>
          </div>
        </div>
      </footer>
    );
  }

  return (
    <footer style={{ background: "var(--ink)", color: "var(--paper)", marginTop: 120 }}>
      <div className="wrap" style={{ padding: "clamp(60px, 8vw, 120px) var(--gutter) 40px" }}>
        <div className="hp-footer__grid" style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr 1fr 1fr", gap: "clamp(24px, 4vw, 60px)", alignItems: "start" }}>
          <div>
            <Logo key={logoKey} theme="inverse" variant="withEu" />
            <p className="body" style={{ color: onInk, maxWidth: 360, marginTop: 24, fontSize: 14 }}>
              A European research consortium advancing ecosystem-based hydropower. Coordinated by NTUA under Horizon Europe, 2026 — 2030.
            </p>
            <SocialRow tone="dark" style={{ marginTop: 28 }} />
            <div className="hp-footer__funding" style={{ marginTop: 32 }}>
              <EuFundedBadge theme="dark" />
            </div>
          </div>
          {FOOTER_COLS.map((col) => (
            <div key={col.h}>
              <div className="eyebrow" style={{ color: onInk2, marginBottom: 18 }}>{col.h}</div>
              <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 11 }}>
                {col.links.map(([l, p]) => (
                  <li key={l}><a href={p} style={{ color: onInk, textDecoration: "none", fontSize: 14 }}>{l}</a></li>
                ))}
              </ul>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 80, paddingTop: 28, borderTop: "1px solid " + line, display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 18 }}>
          <div className="caption" style={{ color: onInk3 }}>
            © 2026 HydroPulse Consortium · Grant Agreement No. {grantNo} · Site content reflects authors' views only
          </div>
          <div style={{ display: "flex", gap: 22, fontFamily: "var(--mono)", fontSize: 10.5, letterSpacing: "0.08em", textTransform: "uppercase", color: onInk3 }}>
            <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Legal notice</a>
            <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Privacy</a>
            <a href="/legal" style={{ color: "inherit", textDecoration: "none" }}>Accessibility</a>
          </div>
        </div>
      </div>
    </footer>
  );
}

window.Footer = Footer;

// ------ Image with placeholder ------
window.PImage = function PImage({ src, alt = "", caption, ratio = "16 / 9", label = "PLACEHOLDER IMAGE", duotone = true, style = {}, children }) {
  const [loaded, setLoaded] = useState(false);
  return (
    <figure style={{ margin: 0, position: "relative", ...style }}>
      <div className={duotone ? "img-duotone" : ""} style={{ aspectRatio: ratio, background: "var(--ink)", position: "relative" }}>
        <img src={HP_asset(src)} alt={alt} onLoad={() => setLoaded(true)}
          style={{ opacity: loaded ? (duotone ? 0.86 : 1) : 0, transition: "opacity 700ms var(--ease)" }}/>
        {label && <div className="img-placeholder-label">{label}</div>}
        {children}
      </div>
      {caption && <figcaption className="caption" style={{ marginTop: 10, color: "var(--muted)" }}>{caption}</figcaption>}
    </figure>
  );
};

// ------ Brand motifs & patterns ------
// Ported verbatim from the brand pack (hydropulse-brand-new/lib/hp-brand.jsx) so the
// native /brand page renders the same five reusable motifs + background patterns the
// designer system defines. Colours are passed as explicit hexes (the site does not
// define the brand pack's --hp-* custom properties), and the hero motif is fixed to the
// rotor direction — no motif switcher is needed here.

// Real vector paths lifted from the logo artwork (HydroPulse-02-01.svg, 1200×1200 space).
const HP_DOLPHIN_D = "M457.77,586.52s-21.62,22.34-10.09,51.17c0,0-13.69,10.81-5.77,32.43,0,0,5.77-5.04,15.13-7.21,0,0,24.5,60.54,85.76,57.65,0,0,12.25,17.3,36.75,12.97,0,0-4.32-10.81-15.85-17.3,0,0,10.09-4.32,18.74-28.11,0,0-30.27,4.32-39.64,22.34,0,0-45.4-5.77-54.77-60.54,0,0,12.25,1.44,17.3,12.25,0,0,5.04-26.66-20.9-30.27,0,0,0-28.83-19.46-44.68v13.69s-7.21-7.93-7.21-14.41Z";
const HP_LEAF_D = "M679.01,590.13s39.64,62.7-9.37,100.89l-8.65,9.37,18.74-3.6s19.46-27.39,44.68-7.21c0,0-18.74,33.87-46.12,10.81l-33.15,15.85h9.37s23.78-12.25,33.15,12.25c0,0-21.62,15.13-34.59-7.93l-12.25-1.44s-32.21,19.99-64.75,8.91l-1.55-2.43s31.71,4.32,46.12-11.53c0,0-7.21-20.9,18.74-32.43,0,0,5.77,20.18-15.85,36.75,0,0,24.5-5.04,42.52-30.99,0,0-11.53-28.11-2.16-51.89l8.65-21.62s7.21-18.74,6.49-23.78Z";
// Swept three-blade hydro-turbine impeller — odd blade count reads as rotation, not a flower.
const HP_ROTOR_BLADE = "M -3.5 -15 C 3 -27, 14 -36, 26 -37 C 24 -27, 17 -19, 6.5 -15 C 3 -13.5, -0.5 -13.5, -3.5 -15 Z";

window.HPRotor = function HPRotor({ size = 120, color = "#000547", accent = "#F1F6FB", stroke = 0, style = {} }) {
  return (
    <svg width={size} height={size} viewBox="-50 -50 100 100" style={{ display: "block", ...style }} aria-hidden>
      {stroke ? <circle r="47" fill="none" stroke={color} strokeWidth={stroke} opacity="0.26"/> : null}
      {[0, 120, 240].map((r, i) => <path key={i} transform={`rotate(${r})`} d={HP_ROTOR_BLADE} fill={color}/>)}
      <circle r="11" fill={color}/>
      <circle r="6" fill={accent}/>
      <circle r="2.4" fill={color}/>
    </svg>
  );
};

// Three community figures arching above the wheel — partner / participation badge.
window.HPCommunityTrio = function HPCommunityTrio({ size = 120, color = "#25B8F0", style = {} }) {
  return (
    <svg width={size} height={size * 0.5} viewBox="0 0 100 50" style={{ display: "block", ...style }} aria-hidden>
      <g fill={color}>
        <circle cx="20" cy="14" r="9"/>
        <path d="M 5 44 C 8 32, 14 28, 20 28 C 26 28, 32 32, 35 44 Z"/>
        <circle cx="50" cy="10" r="9"/>
        <path d="M 35 40 C 38 28, 44 24, 50 24 C 56 24, 62 28, 65 40 Z"/>
        <circle cx="80" cy="14" r="9"/>
        <path d="M 65 44 C 68 32, 74 28, 80 28 C 86 28, 92 32, 95 44 Z"/>
      </g>
    </svg>
  );
};

// Fish + leaf wreath — the ecology band, real dolphin + leaf paths from the mark.
window.HPFishLeaf = function HPFishLeaf({ size = 180, fishColor = "#25B8F0", leafColor = "#3DA17A", style = {} }) {
  return (
    <svg width={size} height={size * (192 / 312)} viewBox="425 562 312 192" style={{ display: "block", overflow: "visible", ...style }} aria-hidden>
      <path d={HP_DOLPHIN_D} fill={fishColor}/>
      <ellipse cx="458.65" cy="613.13" rx="3.05" ry="4.32" transform="rotate(-26.64 458.65 613.13)" fill="#fff"/>
      <path d={HP_LEAF_D} fill={leafColor}/>
    </svg>
  );
};

// Pulse waveform — a confident horizontal line that doubles as a section accent.
window.HPPulseLine = function HPPulseLine({ width = 200, height = 36, color = "#0078B6", stroke = 3, style = {} }) {
  return (
    <svg width={width} height={height} viewBox="0 0 240 28" style={{ display: "block", ...style }} aria-hidden>
      <path d="M 0 14 H 60 L 75 4 L 90 24 L 105 14 H 240"
            stroke={color} strokeWidth={stroke} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
};

// EU 12-star constellation — the canonical 12 dots on a circle (also: partners orbiting the mission).
window.HPConstellation = function HPConstellation({ size = 120, color = "#000547", dotR = 28, count = 12, style = {} }) {
  const dots = Array.from({ length: count }, (_, i) => {
    const a = (i / count) * Math.PI * 2 - Math.PI / 2;
    return [Math.cos(a) * 40, Math.sin(a) * 40];
  });
  return (
    <svg width={size} height={size} viewBox="-50 -50 100 100" style={{ display: "block", ...style }} aria-hidden>
      <circle cx="0" cy="0" r="40" fill="none" stroke={color} strokeWidth="0.6" opacity="0.3"/>
      {dots.map(([x, y], i) => <circle key={i} cx={x} cy={y} r={dotR / 10} fill={color}/>)}
    </svg>
  );
};

// Background-pattern data: URIs (survive copy/paste). Hero motif = rotor.
window.HP_turbinePattern = function HP_turbinePattern(color = "#000547", opacity = 0.06, size = 160) {
  const body = `<g fill='${color}' opacity='${opacity}' transform='translate(${size / 2} ${size / 2}) scale(${size / 110})'>` +
    [0, 120, 240].map((r) => `<path transform='rotate(${r})' d='${HP_ROTOR_BLADE}'/>`).join("") +
    `<circle r='9'/></g>`;
  const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}' viewBox='0 0 ${size} ${size}'>${body}</svg>`;
  return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
};

window.HP_dotPattern = function HP_dotPattern(color = "#000547", opacity = 0.22, spacing = 18, dotR = 1.2) {
  const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${spacing}' height='${spacing}'>` +
    `<circle cx='${spacing / 2}' cy='${spacing / 2}' r='${dotR}' fill='${color}' fill-opacity='${opacity}'/></svg>`;
  return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
};

// Export to window for cross-file access
Object.assign(window, { NAV_ITEMS });
