/* ============================================================
   GEMS Group — Shared Components
   ============================================================ */
const { useEffect, useState, useRef } = React;

/* ----------------------------------------------------------
   Reveal hook — IntersectionObserver
   ---------------------------------------------------------- */
function useReveal() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    // Skip animation if body has no-anim class
    if (document.body.classList.contains("no-anim")) {
      el.classList.add("in");
      return;
    }

    // If already in viewport at mount, reveal immediately on next frame
    const rect = el.getBoundingClientRect();
    const vh = window.innerHeight || document.documentElement.clientHeight;
    if (rect.top < vh * 0.95 && rect.bottom > 0) {
      requestAnimationFrame(() => el.classList.add("in"));
      return;
    }

    const obs = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            el.classList.add("in");
            obs.unobserve(el);
          }
        });
      },
      { threshold: 0.05, rootMargin: "0px 0px -8% 0px" }
    );
    obs.observe(el);

    // Safety: ensure it eventually reveals
    const t = setTimeout(() => el.classList.add("in"), 1500);
    return () => { obs.disconnect(); clearTimeout(t); };
  }, []);
  return ref;
}

function Reveal({ as: As = "div", className = "", delay = 0, style, children, ...rest }) {
  const ref = useReveal();
  return (
    <As
      ref={ref}
      className={`reveal ${className}`}
      style={{ "--reveal-delay": `${delay}ms`, ...style }}
      {...rest}
    >
      {children}
    </As>
  );
}

function RevealStagger({ as: As = "div", className = "", children, ...rest }) {
  const ref = useReveal();
  return (
    <As ref={ref} className={`reveal-stagger ${className}`} {...rest}>
      {children}
    </As>
  );
}

/* ----------------------------------------------------------
   Image placeholder — supports real images (src) OR pattern fallback
   ---------------------------------------------------------- */
function ImagePlaceholder({
  label,
  tag,
  tone = "paper", // paper | dark | blue | red
  aspect,
  height,
  className = "",
  style = {},
  src,             // real image url
  overlay,         // 0..1 dark scrim opacity (when src set)
  position = "center",
  showMeta = true, // toggle the tag/label corner badges
  children,
}) {
  const toneClass =
    tone === "dark" ? "placeholder--dark" :
    tone === "blue" ? "placeholder--blue" :
    tone === "red"  ? "placeholder--red"  : "";
  const s = {
    aspectRatio: aspect,
    height: height,
    ...style,
  };
  // When src is set, render as a real image with optional dark overlay
  if (src) {
    return (
      <div className={`placeholder placeholder--photo ${toneClass} ${className}`} style={s}>
        <img
          src={src}
          alt={label || ""}
          loading="lazy"
          style={{
            position: "absolute", inset: 0,
            width: "100%", height: "100%",
            objectFit: "cover", objectPosition: position,
            zIndex: 0,
          }}
        />
        {overlay > 0 && (
          <div style={{
            position: "absolute", inset: 0, zIndex: 1,
            background: `rgba(14, 25, 31, ${overlay})`,
          }} />
        )}
        {showMeta && tag   && <span className="placeholder__tag"   style={{ zIndex: 3, color: "rgba(255,255,255,0.85)", mixBlendMode: "screen" }}>{tag}</span>}
        {showMeta && label && <span className="placeholder__label" style={{ zIndex: 3, color: "rgba(255,255,255,0.85)", mixBlendMode: "screen" }}>{label}</span>}
        {children}
      </div>
    );
  }
  return (
    <div className={`placeholder ${toneClass} ${className}`} style={s}>
      {tag && <span className="placeholder__tag">{tag}</span>}
      {label && <span className="placeholder__label">{label}</span>}
      {children}
    </div>
  );
}

/* ----------------------------------------------------------
   PHOTOS — curated brand patterns + unsplash demo refs
   ---------------------------------------------------------- */
// Local fallback paths — bundled with the site so images never blank when
// Directus is unreachable (D5). These are also the seed `source` for Phase 6.
const BG_LOCAL = {
  blueFan:      "assets/bg-1.jpg",   // deep blue · radial fan stripes (G symbol DNA)
  blueGradient: "assets/bg-2.jpg",   // soft blue/black gradient
  phoneG:       "assets/bm-0.jpg",   // red · hand holding G logo phone
  redRings:     "assets/bm-1.jpg",   // red concentric circles
  blueFolds:    "assets/bm-2.jpg",   // blue diagonal folds
  redCubes:     "assets/bm-3.jpg",   // red 3D cubes
  redRhombic:   "assets/bm-4.jpg",   // red 3D diamond blocks
  architecture: "assets/bm-5.jpg",   // warm red curved roof on blue sky
  redSpiral:    "assets/bm-6.jpg",   // red spiral staircase (compounding!)
  redCurves:    "assets/bm-7.jpg",   // red curved discs/stripes
  redSwirl:     "assets/bm-8.jpg",   // red swirling rings with orbital elements
};

// Demo photography (Unsplash, self-hosted into Directus in Phase 6). Local
// fallback keeps the original CDN URL so the site still renders pre-CMS / offline.
const UNSPLASH_LOCAL = {
  boardroom:     "https://images.unsplash.com/photo-1573164713714-d95e436ab8d6?auto=format&fit=crop&w=1600&q=80",
  meeting:       "https://images.unsplash.com/photo-1521737711867-e3b97375f902?auto=format&fit=crop&w=1600&q=80",
  founder:       "https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&w=1200&q=80",
  classroom:     "https://images.unsplash.com/photo-1577896851231-70ef18881754?auto=format&fit=crop&w=1600&q=80",
  workspace:     "https://images.unsplash.com/photo-1497215728101-856f4ea42174?auto=format&fit=crop&w=1600&q=80",
  handshake:     "https://images.unsplash.com/photo-1521791136064-7986c2920216?auto=format&fit=crop&w=1600&q=80",
  speaker:       "https://images.unsplash.com/photo-1475721027785-f74eccf877e2?auto=format&fit=crop&w=1600&q=80",
  architecture2: "https://images.unsplash.com/photo-1486325212027-8081e485255e?auto=format&fit=crop&w=1600&q=80",
};

// Phase 6 media wiring: BG / UNSPLASH resolve to a Directus file URL (webp,
// transformed) when CMS content is live, else the local fallback above. Built
// as getters so EVERY existing `src={BG.x}` / `src={UNSPLASH.x}` usage routes
// through the media library with no change at the call site. Getters run at
// render time, so components re-render on CMS load and swap to the managed
// image automatically. Editors can replace any of these from Directus admin.
function mediaMap(localMap, keyPrefix, width) {
  const out = {};
  Object.keys(localMap).forEach((name) => {
    Object.defineProperty(out, name, {
      enumerable: true,
      get() {
        const fallback = localMap[name];
        return window.cms
          ? window.cms.asset(keyPrefix + name, { w: width, fallback })
          : fallback;
      },
    });
  });
  return out;
}
const BG = mediaMap(BG_LOCAL, "bg_", 1920);
const UNSPLASH = mediaMap(UNSPLASH_LOCAL, "photo_", 1600);

// Resolve a single asset_library key to its managed URL (else local fallback).
// Call at render time so it picks up CMS content once loaded. Function
// declaration → shared global, usable from every page script.
function mediaSrc(key, fallback, width) {
  return window.cms ? window.cms.asset(key, { w: width || 1600, fallback }) : fallback;
}

// Reverse index: a local image path → its asset_library key. Lets module-level
// data arrays that captured a BG/UNSPLASH value at load time (frozen to the
// local fallback) re-resolve to the live CMS image at RENDER time — just wrap
// the render site with mediaFrom(item.img). If the value is already a CMS URL
// (or unknown), it is returned unchanged.
const LOCAL_TO_KEY = {};
Object.keys(BG_LOCAL).forEach((n) => { LOCAL_TO_KEY[BG_LOCAL[n]] = "bg_" + n; });
Object.keys(UNSPLASH_LOCAL).forEach((n) => { LOCAL_TO_KEY[UNSPLASH_LOCAL[n]] = "photo_" + n; });
function mediaFrom(localUrl, width) {
  const key = LOCAL_TO_KEY[localUrl];
  return key ? mediaSrc(key, localUrl, width) : localUrl;
}

/* ----------------------------------------------------------
   Brand stripe accent (small visual motif from the symbol)
   ---------------------------------------------------------- */
function BrandStripes({ count = 9, height = 24, color }) {
  const heights = [];
  for (let i = 0; i < count; i++) {
    // arc-like distribution: shorter at edges, taller in the middle
    const t = i / (count - 1);
    const h = Math.sin(t * Math.PI) * height * 0.85 + height * 0.15;
    heights.push(h);
  }
  return (
    <span className="brand-stripes" style={{ height, color }}>
      {heights.map((h, i) => (
        <span key={i} style={{ height: h }} />
      ))}
    </span>
  );
}

/* ----------------------------------------------------------
   Brand pattern primitives — derived from the GEMS Symbol
   (vertical stripes arc-distributed like the G mark)
   and the color palette (concentric rings)
   ---------------------------------------------------------- */

// Vertical stripe band — arc-shaped distribution of bar heights.
// Use as a divider or accent. `shape` is the bar-height curve:
//   "arc"     — bell curve (highest in the middle)
//   "rising"  — short to tall (left to right)
//   "falling" — tall to short
//   "wave"    — sine wave
function StripeBand({
  count = 48,
  width = "100%",
  height = 64,
  color = "currentColor",
  shape = "arc",
  gap = 3,
  thickness = 2,
  align = "bottom", // bottom | center | top
  opacity = 1,
}) {
  const heights = [];
  for (let i = 0; i < count; i++) {
    const t = i / Math.max(1, count - 1);
    let h;
    switch (shape) {
      case "rising":  h = t; break;
      case "falling": h = 1 - t; break;
      case "wave":    h = (Math.sin(t * Math.PI * 2) + 1) / 2; break;
      case "arc":
      default:        h = Math.sin(t * Math.PI);
    }
    heights.push(0.08 + 0.92 * h); // min 8% so nothing disappears
  }
  return (
    <div style={{
      width, height,
      display: "flex",
      gap,
      alignItems: align === "top" ? "flex-start" : align === "center" ? "center" : "flex-end",
      opacity,
    }}>
      {heights.map((h, i) => (
        <span key={i} style={{
          flex: 1,
          minWidth: thickness,
          height: `${h * 100}%`,
          background: color,
        }} />
      ))}
    </div>
  );
}

// Concentric arcs (half-circles) — like the GEMS Color Palette artwork
function ConcentricArcs({
  size = 320,
  color = "currentColor",
  count = 6,
  strokeWidth = 14,
  startAngle = -90, // 0 = right; -90 = up
  endAngle = 90,
  className = "",
  style = {},
}) {
  const arcs = [];
  for (let i = 0; i < count; i++) {
    const r = (i + 1) * (size / 2 / count) - strokeWidth / 2;
    const a0 = (startAngle * Math.PI) / 180;
    const a1 = (endAngle * Math.PI) / 180;
    const cx = size / 2, cy = size / 2;
    const x0 = cx + r * Math.cos(a0);
    const y0 = cy + r * Math.sin(a0);
    const x1 = cx + r * Math.cos(a1);
    const y1 = cy + r * Math.sin(a1);
    const large = Math.abs(endAngle - startAngle) > 180 ? 1 : 0;
    arcs.push(
      <path
        key={i}
        d={`M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1}`}
        stroke={color}
        strokeWidth={strokeWidth}
        fill="none"
        opacity={0.18 + (i / count) * 0.82}
      />
    );
  }
  return (
    <svg
      className={className}
      style={style}
      viewBox={`0 0 ${size} ${size}`}
      width={size}
      height={size}
      aria-hidden
    >
      {arcs}
    </svg>
  );
}

// Full concentric rings (closed circles) — for editorial section markers
function ConcentricRings({
  size = 200,
  color = "currentColor",
  count = 5,
  strokeWidth = 1,
  className = "",
  style = {},
}) {
  const rings = [];
  for (let i = 0; i < count; i++) {
    const r = ((i + 1) / count) * (size / 2 - strokeWidth);
    rings.push(
      <circle
        key={i}
        cx={size / 2}
        cy={size / 2}
        r={r}
        stroke={color}
        strokeWidth={strokeWidth}
        fill="none"
        opacity={0.2 + (i / count) * 0.8}
      />
    );
  }
  return (
    <svg className={className} style={style}
         viewBox={`0 0 ${size} ${size}`} width={size} height={size} aria-hidden>
      {rings}
    </svg>
  );
}

/* ----------------------------------------------------------
   GEMS Symbol — minified inline SVG (so we can re-color/animate)
   ---------------------------------------------------------- */
function GemsSymbol({ size = 64, color = "currentColor", className = "" }) {
  return (
    <img
      src="assets/symbol-blue.svg"
      alt="GEMS"
      className={className}
      style={{ width: size, height: size, filter: color === "currentColor" ? "none" : "none" }}
    />
  );
}

/* Horizontal logo (Tagline Layout — wide format for nav/footer) */
function GemsLogo({ variant = "black", height = 28, className = "" }) {
  const file =
    variant === "blue"  ? "assets/logo-h-blue.svg"  :
    variant === "red"   ? "assets/logo-h-red.svg"   :
    variant === "white" ? "assets/logo-h-white.svg" :
                          "assets/logo-h-black.svg";
  return (
    <img src={file} alt="GEMS" className={className} style={{ height, display: "block" }} />
  );
}

/* Stacked / primary logo (square format — for hero callouts, big moments) */
function GemsLogoStacked({ variant = "black", height = 80, className = "" }) {
  const file =
    variant === "blue"  ? "assets/logo-blue.svg"  :
    variant === "red"   ? "assets/logo-red.svg"   :
    variant === "white" ? "assets/logo-white.svg" :
                          "assets/logo-black.svg";
  return (
    <img src={file} alt="GEMS" className={className} style={{ height, display: "block" }} />
  );
}
function GemsSymbolMark({ variant = "black", size = 40, className = "" }) {
  const file =
    variant === "blue"  ? "assets/symbol-blue.svg"  :
    variant === "red"   ? "assets/symbol-red.svg"   :
    variant === "white" ? "assets/symbol-white.svg" :
                          "assets/symbol-black.svg";
  return (
    <img src={file} alt="" className={className} style={{ width: size, height: size }} />
  );
}

/* ----------------------------------------------------------
   Top navigation (shared, slight variant per option via prop)
   ---------------------------------------------------------- */
// Map a nav URL to the page key used for active-state + dropdown wiring.
const NAV_KEY_BY_URL = {
  "index.html": "home",
  "about.html": "about",
  "solutions.html": "business",
  "portfolio.html": "portfolio",
  "insights.html": "insights",
  "contact.html": "contact",
};
function navKeyForUrl(url) {
  if (!url) return "";
  const base = String(url).split("#")[0].split("?")[0];
  return NAV_KEY_BY_URL[base] || base.replace(/\.html$/, "");
}

// Resolve a nav row's href given its link_type. external passes through as-is;
// page/anchor resolve normally (home → "#" when already on home).
function navHref(row, onHome) {
  if (!row) return "#";
  const url = row.url || "#";
  if (row.link_type === "external") return url;
  if (navKeyForUrl(url) === "home" && onHome) return "#";
  return url;
}
// target/rel for new-tab links (security: noopener noreferrer).
function navTarget(row) {
  return row && row.open_new_tab ? { target: "_blank", rel: "noopener noreferrer" } : {};
}
// Build a 1-level tree (top-level + direct children) from flat nav_items rows.
function buildNavTree(rows) {
  const byId = {};
  rows.forEach((r) => { byId[r.id] = { ...r, children: [] }; });
  const top = [];
  rows.forEach((r) => {
    const node = byId[r.id];
    if (r.parent != null && byId[r.parent]) byId[r.parent].children.push(node);
    else top.push(node);
  });
  const bySort = (a, b) => (a.sort || 0) - (b.sort || 0);
  top.sort(bySort);
  top.forEach((n) => n.children.sort(bySort));
  return top;
}

function Nav({ variant = "light", scrolled = false, sticky = true, logoColor, current = "home" }) {
  const dark = variant === "dark";
  const [menuOpen, setMenuOpen] = useState(false);
  const [lang, setLang] = useLang();
  useCms(); // re-render on CMS ready / update / language change
  // Over-hero state: nav starts on dark hero, text is white. Once scrolled, switches to paper bg + dark text.
  const overHero = !scrolled;
  // Logo color: explicit override > scrolled-aware (white over hero, black on scrolled paper)
  const navLogoColor = logoColor || (overHero ? "white" : "black");
  // Direct file path for the requested logo names (assets/GEMS-logo-{w,b}.svg)
  const logoFile = overHero ? "assets/GEMS-logo-w.svg" : "assets/GEMS-logo-b.svg";

  const textColor = overHero ? "white" : "var(--ink)";

  // Page-aware nav: hrefs adapt based on which page is active
  const onHome = current === "home";
  // Nav items from CMS (nav_items collection), falling back to hardcoded copy.
  const cmsNav = window.cms ? window.cms.get("nav_items") : null;
  // Build a tree from flat rows → top-level items, each carrying its children.
  // Tuple shape: [label, href, key, node]. node === null for hardcoded fallback.
  const items = (Array.isArray(cmsNav) && cmsNav.length)
    ? buildNavTree(cmsNav).map((node) => {
        const key = navKeyForUrl(node.url);
        return [L(node.label && node.label.vi, node.label && node.label.en), navHref(node, onHome), key, node];
      })
    : [
        [L("Trang chủ", "Home"), onHome ? "#" : "index.html", "home", null],
        [L("Về chúng tôi", "About"), "about.html", "about", null],
        [L("Giải pháp", "Solutions"), "solutions.html", "business", null],
        [L("Danh mục", "Portfolio"), "portfolio.html", "portfolio", null],
        [L("Góc nhìn", "Insights"), "insights.html", "insights", null],
        [L("Liên hệ", "Contact"), "contact.html", "contact", null],
      ];
  // Solutions dropdown: prefer CMS pillar_data, fall back to hardcoded PILLARS.
  const cmsPillarRows = window.cms ? window.cms.get("pillar_data") : null;
  const navPillars = (Array.isArray(cmsPillarRows) && cmsPillarRows.length)
    ? cmsPillarRows.map((r) => ({
        id: r.pillar_slug,
        num: r.roman_numeral,
        code: r.code,
        sol: r.solution_title && r.solution_title.vi,
        solEn: r.solution_title && r.solution_title.en,
      }))
    : PILLARS;

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

  return (
    <header
      className={`nav ${menuOpen ? "menu-open" : ""}`}
      style={{
        position: sticky ? "fixed" : "relative",
        top: 0,
        left: 0,
        right: 0,
        zIndex: 100,
        background: scrolled || menuOpen ? "rgba(251,251,249,0.92)" : "transparent",
        backdropFilter: scrolled || menuOpen ? "blur(12px)" : "none",
        borderBottom: scrolled ? "1px solid rgba(14,25,31,0.08)" : "1px solid transparent",
        color: menuOpen ? "var(--ink)" : textColor,
        transition: "all 280ms var(--ease)",
      }}
    >
      <div className="container nav-bar" style={{
        display: "flex", alignItems: "center", justifyContent: "space-between",
        height: 76,
      }}>
        <a href={onHome ? "#" : "index.html"} className="nav-logo" style={{ display: "flex", alignItems: "center" }}>
          <img src={menuOpen ? "assets/GEMS-logo-b.svg" : logoFile} alt="GEMS" style={{ height: 48, display: "block", transition: "opacity 280ms var(--ease)" }} />
        </a>
        <style>{`
          .nav-dd { position: relative; display: inline-flex; align-items: center; }
          .nav-dd-caret { font-size: 11px; margin-left: 5px; opacity: 0.6; transition: transform 200ms var(--ease); display:inline-block; }
          .nav-dd:hover .nav-dd-caret { transform: rotate(180deg); }
          .nav-dd::after { content:""; position:absolute; top:100%; left:0; right:0; height:16px; }
          .nav-dd-panel {
            position: absolute; top: calc(100% + 12px); left: 50%;
            transform: translateX(-50%) translateY(6px);
            width: 360px; background: #fff; color: var(--ink);
            border: 1px solid rgba(14,25,31,0.07); border-radius: 8px;
            box-shadow: 0 32px 70px -26px rgba(14,25,31,0.42);
            padding: 8px; opacity: 0; visibility: hidden; pointer-events: none;
            transition: opacity 220ms var(--ease), transform 220ms var(--ease); z-index: 120;
          }
          .nav-dd:hover .nav-dd-panel, .nav-dd:focus-within .nav-dd-panel {
            opacity: 1; visibility: visible; pointer-events: auto; transform: translateX(-50%) translateY(0);
          }
          .nav-dd-head {
            font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.18em;
            text-transform: uppercase; color: rgba(14,25,31,0.4);
            padding: 10px 14px 8px;
          }
          .nav-dd-item {
            display: grid; grid-template-columns: 30px 1fr auto; align-items: center;
            column-gap: 14px;
            padding: 13px 14px; border-radius: 6px; color: var(--ink);
            position: relative;
            transition: background 180ms var(--ease);
          }
          .nav-dd-item + .nav-dd-item::before {
            content: ""; position: absolute; top: 0; left: 14px; right: 14px;
            height: 1px; background: rgba(14,25,31,0.07);
          }
          .nav-dd-item:hover { background: rgba(192,5,24,0.045); }
          .nav-dd-item:hover::before, .nav-dd-item:hover + .nav-dd-item::before { opacity: 0; }
          .nav-dd-rom {
            font-family: var(--font-display, serif); font-size: 22px; font-weight: 500;
            line-height: 1; text-align: center;
            color: rgba(14,25,31,0.28);
            transition: color 180ms var(--ease);
          }
          .nav-dd-item:hover .nav-dd-rom { color: var(--gems-red, #C00518); }
          .nav-dd-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
          .nav-dd-code {
            font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.14em;
            text-transform: uppercase; color: rgba(14,25,31,0.38);
          }
          .nav-dd-label { font-size: 14.5px; font-weight: 500; line-height: 1.25; letter-spacing: -0.005em; }
          .nav-dd-arr {
            font-size: 14px; color: var(--gems-red, #C00518);
            opacity: 0; transform: translateX(-6px);
            transition: opacity 180ms var(--ease), transform 180ms var(--ease);
          }
          .nav-dd-item:hover .nav-dd-arr { opacity: 1; transform: translateX(0); }
          .nav-dd-all {
            display: flex; align-items: center; justify-content: space-between;
            margin: 6px 6px 2px; padding: 12px 8px 6px;
            border-top: 1px solid rgba(14,25,31,0.08);
            font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.14em;
            text-transform: uppercase; color: rgba(14,25,31,0.55);
            transition: color 180ms var(--ease);
          }
          .nav-dd-all span { transition: transform 180ms var(--ease); }
          .nav-dd-all:hover { color: var(--gems-red, #C00518); }
          .nav-dd-all:hover span { transform: translateX(4px); }
          /* Generic submenu panel (non-Solutions): simple single-column label list. */
          .nav-dd-panel.nav-dd-simple { width: 240px; }
          .nav-dd-item-simple {
            grid-template-columns: 1fr auto; column-gap: 10px; padding: 12px 14px;
          }
          .nav-dd-item-simple .nav-dd-label { font-size: 14.5px; font-weight: 500; }
          @media (max-width: 1024px){
            .nav-dd-panel.nav-dd-simple { width: auto; }
            .nav-dd-item-simple { grid-template-columns: 1fr; padding: 10px 0; }
            .nav-dd{ flex-direction: column; align-items: stretch; width: 100%; }
            .nav-dd-panel{ position: static; transform: none; width: auto; box-shadow: none; border: none;
              opacity: 1; visibility: visible; pointer-events: auto; background: transparent; padding: 4px 0 4px 14px; }
            .nav-dd-head{ display: none; }
            .nav-dd-item{ grid-template-columns: 28px 1fr; column-gap: 12px; padding: 10px 0; }
            .nav-dd-item + .nav-dd-item::before{ display: none; }
            .nav-dd-item:hover{ background: transparent; }
            .nav-dd-rom{ font-size: 20px; }
            .nav-dd-code{ display: none; }
            .nav-dd-label{ font-size: 18px !important; }
            .nav-dd-arr{ display: none; }
            .nav-items .nav-dd-all{
              margin: 0; padding: 12px 0 4px; border-top: none;
              font-family: var(--font-sans) !important;
              font-size: 18px !important; font-weight: 500 !important;
              letter-spacing: 0 !important; text-transform: none;
              color: rgba(14,25,31,0.6) !important;
            }
            .nav-dd-caret{ display: none; }
            .nav-dd::after{ display:none; }
          }
        `}</style>
        <nav className="nav-items" style={{ display: "flex", gap: 36 }}>
          {items.map(([label, href, key, node], i) => {
            const isActive = key === current;
            const linkStyle = {
              fontSize: 14, position: "relative", padding: "8px 0",
              opacity: isActive ? 1 : 0.72, fontWeight: isActive ? 500 : 400,
            };
            const underline = isActive && (
              <span style={{
                position: "absolute", left: 0, right: 0, bottom: 0,
                height: 1, background: "currentColor",
              }} />
            );
            // 1) Solutions dropdown = 3 pillars + any extra CMS-managed links.
            //    Pillars are nav_items children carrying a `roman` (admin-editable);
            //    if none seeded yet, fall back to navPillars (pillar_data → PILLARS)
            //    so the dropdown never empties. Non-pillar children (no roman) render
            //    as simple links below. The Solutions PAGE still reads pillar_data.
            if (key === "business") {
              const allChildren = (node && node.children) ? node.children : [];
              const pillarChildren = allChildren.filter((c) => c.roman);
              const simpleChildren = allChildren.filter((c) => !c.roman);
              const usePillarsFromNav = pillarChildren.length > 0;
              return (
                <span key={i} className="nav-dd">
                  <a href={href} onClick={() => setMenuOpen(false)} style={linkStyle}>
                    {label}<span className="nav-dd-caret" aria-hidden="true">▾</span>
                    {underline}
                  </a>
                  <div className="nav-dd-panel">
                    <div className="nav-dd-head">{L("Ba giải pháp", "Three solutions")}</div>
                    {usePillarsFromNav
                      ? pillarChildren.map((child) => (
                          <a
                            key={child.id}
                            href={navHref(child, onHome)}
                            className="nav-dd-item"
                            onClick={() => setMenuOpen(false)}
                            {...navTarget(child)}
                            data-directus={window.veAttr && window.veAttr("nav_items", child.id, ["label", "roman", "code"], "drawer")}
                          >
                            <span className="nav-dd-rom" aria-hidden="true">{child.roman}</span>
                            <span className="nav-dd-body">
                              <span className="nav-dd-code">{L("PILLAR", "PILLAR")} {child.roman} · {child.code}</span>
                              <span className="nav-dd-label">{L(child.label && child.label.vi, child.label && child.label.en)}</span>
                            </span>
                            <span className="nav-dd-arr" aria-hidden="true">→</span>
                          </a>
                        ))
                      : navPillars.map((p, pi) => (
                          <a key={p.id} href={`solutions-pillar-${pi + 1}.html`} className="nav-dd-item" onClick={() => setMenuOpen(false)}>
                            <span className="nav-dd-rom" aria-hidden="true">{p.num}</span>
                            <span className="nav-dd-body">
                              <span className="nav-dd-code">{L("PILLAR", "PILLAR")} {p.num} · {p.code}</span>
                              <span className="nav-dd-label">{L(p.sol, p.solEn)}</span>
                            </span>
                            <span className="nav-dd-arr" aria-hidden="true">→</span>
                          </a>
                        ))}
                    {simpleChildren.map((child) => (
                      <a
                        key={child.id}
                        href={navHref(child, onHome)}
                        className="nav-dd-item nav-dd-item-simple"
                        onClick={() => setMenuOpen(false)}
                        {...navTarget(child)}
                        data-directus={window.veAttr && window.veAttr("nav_items", child.id, ["label", "url"], "drawer")}
                      >
                        <span className="nav-dd-label">{L(child.label && child.label.vi, child.label && child.label.en)}</span>
                        <span className="nav-dd-arr" aria-hidden="true">→</span>
                      </a>
                    ))}
                  </div>
                </span>
              );
            }
            // 2) Any other item with children → generic submenu dropdown.
            if (node && node.children && node.children.length) {
              return (
                <span key={i} className="nav-dd">
                  <a
                    href={href}
                    onClick={() => setMenuOpen(false)}
                    style={linkStyle}
                    {...navTarget(node)}
                    data-directus={window.veAttr && window.veAttr("nav_items", node.id, ["label", "url"], "drawer")}
                  >
                    {label}<span className="nav-dd-caret" aria-hidden="true">▾</span>
                    {underline}
                  </a>
                  <div className="nav-dd-panel nav-dd-simple">
                    {node.children.map((child) => (
                      <a
                        key={child.id}
                        href={navHref(child, onHome)}
                        className="nav-dd-item nav-dd-item-simple"
                        onClick={() => setMenuOpen(false)}
                        {...navTarget(child)}
                        data-directus={window.veAttr && window.veAttr("nav_items", child.id, ["label", "url"], "drawer")}
                      >
                        <span className="nav-dd-label">{L(child.label && child.label.vi, child.label && child.label.en)}</span>
                        <span className="nav-dd-arr" aria-hidden="true">→</span>
                      </a>
                    ))}
                  </div>
                </span>
              );
            }
            // 3) Plain leaf link (resolve target/rel for external/new-tab).
            return (
              <a
                key={i}
                href={href}
                onClick={() => setMenuOpen(false)}
                style={linkStyle}
                {...navTarget(node)}
                data-directus={node && window.veAttr && window.veAttr("nav_items", node.id, ["label", "url"], "drawer")}
              >
                {label}
                {underline}
              </a>
            );
          })}
        </nav>
        <div className="nav-actions" style={{ display: "flex", alignItems: "center", gap: 20 }}>
          <div className="mono nav-lang" style={{
            fontSize: 11, letterSpacing: "0.16em", textTransform: "uppercase",
            display: "flex", gap: 6, alignItems: "center",
          }}>
            {["vi", "en"].map((lng, i) => (
              <React.Fragment key={lng}>
                {i > 0 && <span style={{ opacity: 0.4 }}>/</span>}
                <button
                  type="button"
                  onClick={() => setLang(lng)}
                  aria-pressed={lang === lng}
                  style={{
                    background: "none", border: "none", padding: 0, cursor: "pointer",
                    font: "inherit", letterSpacing: "inherit", textTransform: "inherit",
                    color: "inherit",
                    fontWeight: lang === lng ? 600 : 400,
                    opacity: lang === lng ? 1 : 0.55,
                    transition: "opacity 200ms var(--ease)",
                  }}>
                  {lng.toUpperCase()}
                </button>
              </React.Fragment>
            ))}
          </div>
          <a href="contact.html" className="btn nav-cta" style={{
            padding: "10px 18px",
            borderColor: overHero && !menuOpen ? "rgba(255,255,255,0.4)" : undefined,
            color: overHero && !menuOpen ? "white" : undefined,
          }}>
            {L("Liên hệ", "Contact")}
            <span className="btn__arrow" />
          </a>
          <button
            type="button"
            className="nav-burger"
            onClick={() => setMenuOpen(!menuOpen)}
            aria-label={menuOpen ? L("Đóng menu", "Close menu") : L("Mở menu", "Open menu")}
            aria-expanded={menuOpen}
          >
            <span /><span /><span />
          </button>
        </div>
      </div>
    </header>
  );
}

/* ----------------------------------------------------------
   Footer (shared)
   ---------------------------------------------------------- */
const FOOTER_ROMAN = ["I", "II", "III", "IV", "V"];
function Footer() {
  useCms(); // re-render on CMS ready / update / language change
  const has = !!window.cms;
  const fcols = has ? window.cms.get("footer_columns") : null;
  const flinks = has ? window.cms.get("footer_links") : null;
  const globals = has ? window.cms.get("site_globals") : null;
  const about = has ? window.cms.get("page_sections", "footer_about") : null;

  const colBySort = {};
  if (Array.isArray(fcols)) fcols.forEach((c) => { colBySort[c.sort] = c; });
  const exploreCol = colBySort[1];
  const solutionsCol = colBySort[2];
  const hqCol = colBySort[3];
  const linksFor = (col) =>
    (col && Array.isArray(flinks)) ? flinks.filter((l) => l.footer_column === col.id) : [];
  const exploreLinks = linksFor(exploreCol);
  const solutionLinks = linksFor(solutionsCol);

  // About block (page_sections.footer_about), with hardcoded fallback.
  const aboutEyebrow = about && about.eyebrow ? L(about.eyebrow.vi, about.eyebrow.en) : L("— Về GEMS", "— About GEMS");
  const aboutFullname = about && about.heading ? L(about.heading.vi, about.heading.en) : "GEMS Education Management & Investment Jsc.";
  const aboutBlurb = about && about.body ? L(about.body.vi, about.body.en) : L(
    "Kiến trúc hệ sinh thái giáo dục tái tạo cho định chế và cá nhân — tích hợp tri thức thế giới, công nghệ và sức mạnh tài chính.",
    "Architecting a regenerative education ecosystem for institutions and individuals — integrating world knowledge, technology and financial strength."
  );

  // Globals (tagline / address / email / copyright / social), with fallbacks.
  const tagline = globals && globals.tagline
    ? L(globals.tagline.vi, globals.tagline.en)
    : "Empowering Minds — Transforming Education";
  const email = (globals && globals.footer_email) || "contact@gemsgroup.com.vn";
  const address = globals && globals.footer_address
    ? L(globals.footer_address.vi, globals.footer_address.en)
    : L("Tầng 3, Toà nhà CINC, Số 8 Trần Hưng Đạo, Hà Nội", "3rd Floor, CINC Building, 8 Tran Hung Dao Street, Hanoi");
  const copyright = globals && globals.copyright
    ? L(globals.copyright.vi, globals.copyright.en)
    : "© 2026 GEMS Education Management & Investment Jsc. · " + L("Mọi quyền được bảo lưu", "All rights reserved");
  const social = (globals && Array.isArray(globals.social_links) && globals.social_links.length)
    ? globals.social_links
    : [{ label: "LinkedIn", url: "#" }, { label: "Facebook", url: "#" }];
  const legal = (globals && Array.isArray(globals.legal_links) && globals.legal_links.length)
    ? globals.legal_links.map((x) => ({ label: L(x.label && x.label.vi, x.label && x.label.en), url: x.url || "#" }))
    : [
        { label: L("Chính sách bảo mật", "Privacy Policy"), url: "#" },
        { label: L("Điều khoản sử dụng", "Terms of Use"), url: "#" },
      ];

  const exploreTitle = exploreCol && exploreCol.column_title ? L(exploreCol.column_title.vi, exploreCol.column_title.en) : L("Khám phá", "Explore");
  const solutionsTitle = solutionsCol && solutionsCol.column_title ? L(solutionsCol.column_title.vi, solutionsCol.column_title.en) : L("Giải pháp", "Solutions");
  const hqTitle = hqCol && hqCol.column_title ? L(hqCol.column_title.vi, hqCol.column_title.en) : L("Trụ sở Hà Nội", "Hanoi Headquarters");

  return (
    <footer className="footer">
      <div className="container">
        {/* Top band — editorial masthead with red accent */}
        <div className="footer-masthead">
          <a href="#" className="footer-logo-link">
            <img src="assets/GEMS-logo-w.svg" alt="GEMS" />
          </a>
          <div className="footer-tagline">
            <span className="footer-rule" />
            <span data-directus={window.veAttr && window.veAttr("site_globals", globals && globals.id, "tagline", "popover")}>{tagline}</span>
          </div>
          <button
            type="button"
            className="footer-totop"
            onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>
            {L("Lên đầu trang", "Back to top")} <span>↑</span>
          </button>
        </div>

        <div className="grid-12">
          <div style={{ gridColumn: "span 4" }}>
            <div className="footer-eyebrow" data-directus={window.veAttr && window.veAttr("page_sections", about && about.id, "eyebrow", "popover")}>{aboutEyebrow}</div>
            <div className="footer-fullname" data-directus={window.veAttr && window.veAttr("page_sections", about && about.id, "heading", "popover")}>{aboutFullname}</div>
            <p className="footer-blurb" data-directus={window.veAttr && window.veAttr("page_sections", about && about.id, "body", "drawer")}>{aboutBlurb}</p>
          </div>
          <div style={{ gridColumn: "span 2" }}>
            <h4 data-directus={window.veAttr && window.veAttr("footer_columns", exploreCol && exploreCol.id, "column_title", "popover")}>{exploreTitle}</h4>
            <ul>
              {(exploreLinks.length ? exploreLinks : [
                { url: "about.html", label: { vi: "Về chúng tôi", en: "About us" } },
                { url: "solutions.html", label: { vi: "Giải pháp", en: "Solutions" } },
                { url: "portfolio.html", label: { vi: "Danh mục", en: "Portfolio" } },
                { url: "insights.html", label: { vi: "Góc nhìn chuyên gia", en: "Insights" } },
                { url: "contact.html", label: { vi: "Liên hệ", en: "Contact" } },
              ]).map((l, i) => (
                <li key={i}><a href={l.url}>{L(l.label && l.label.vi, l.label && l.label.en)}</a></li>
              ))}
            </ul>
          </div>
          <div style={{ gridColumn: "span 2" }}>
            <h4 data-directus={window.veAttr && window.veAttr("footer_columns", solutionsCol && solutionsCol.id, "column_title", "popover")}>{solutionsTitle}</h4>
            <ul className="footer-domains">
              {(solutionLinks.length ? solutionLinks : [
                { url: "solutions.html#pillar-1", label: { vi: "Đồng kiến tạo tài sản giáo dục", en: "Co-building education assets" } },
                { url: "solutions.html#pillar-2", label: { vi: "Chuyển đổi trường học hiện hữu", en: "Transforming existing schools" } },
                { url: "solutions.html#pillar-3", label: { vi: "Đưa tri thức thành định chế", en: "Turning knowledge into institutions" } },
              ]).map((l, i) => (
                <li key={i}><span className="dom">PILLAR {FOOTER_ROMAN[i] || i + 1}</span><a href={l.url}>{L(l.label && l.label.vi, l.label && l.label.en)}</a></li>
              ))}
            </ul>
          </div>
          <div style={{ gridColumn: "span 4" }}>
            <h4 data-directus={window.veAttr && window.veAttr("footer_columns", hqCol && hqCol.id, "column_title", "popover")}>{hqTitle}</h4>
            <p style={{ margin: 0, lineHeight: 1.7 }}>
              <span data-directus={window.veAttr && window.veAttr("site_globals", globals && globals.id, "footer_address", "popover")}>{address}</span><br />
              <a href={`mailto:${email}`} style={{ color: "white", borderBottom: "1px solid var(--gems-red)", paddingBottom: 2 }} data-directus={window.veAttr && window.veAttr("site_globals", globals && globals.id, "footer_email", "popover")}>{email}</a>
            </p>
            <div style={{ marginTop: 28, display: "flex", gap: 10 }}>
              {social.map((s, i) => (
                <a key={i} href={s.url || "#"} className="footer-social">{s.label} <span>↗</span></a>
              ))}
            </div>
          </div>
        </div>

        <div className="legal">
          <span data-directus={window.veAttr && window.veAttr("site_globals", globals && globals.id, "copyright", "popover")}>{copyright}</span>
          <div className="legal-links">
            {legal.map((x, i) => (
              <React.Fragment key={i}>
                {i > 0 && <span className="dot">·</span>}
                <a href={x.url}>{x.label}</a>
              </React.Fragment>
            ))}
            <span className="dot">·</span>
            <span>gemsgroup.com.vn</span>
          </div>
        </div>
      </div>
    </footer>
  );
}

/* ----------------------------------------------------------
   Shared content data (so both options stay consistent)
   ---------------------------------------------------------- */
const VALUES = [
  { num: "01", title: "Kỷ luật",   en: "Discipline",  body: "Đầu tư có nguyên tắc. Không rượt theo chu kỳ ngắn hạn." },
  { num: "02", title: "Hiệu suất", en: "Performance", body: "Đo lường bằng dữ liệu. Báo cáo quý cho từng đối tác." },
  { num: "03", title: "Hệ thống",  en: "System",      body: "Một hệ vận hành thống nhất — đầu tư, tư vấn, đồng quản trị." },
  { num: "04", title: "Tích luỹ",  en: "Accumulation",body: "Giá trị compounding qua thập niên, không qua quý." },
];

const SOLUTIONS = [
  {
    code: "CAPITAL",
    title: "Đầu tư chiến lược",
    en: "Strategic Investment",
    body: "Đánh giá, lựa chọn và rót vốn vào các tổ chức giáo dục tư nhân có nền tảng vững vàng và tiềm năng tăng trưởng khu vực.",
  },
  {
    code: "OPERATIONS",
    title: "Tư vấn vận hành",
    en: "Operations Advisory",
    body: "Tối ưu mô hình kinh doanh, vận hành học thuật và tài chính cho các đơn vị trong danh mục đầu tư.",
  },
  {
    code: "PORTFOLIO",
    title: "Quản lý danh mục",
    en: "Portfolio Management",
    body: "Theo dõi hiệu suất, tái cấu trúc và mở rộng quy mô các tài sản giáo dục trong danh mục dài hạn.",
  },
  {
    code: "BRAND",
    title: "Phát triển thương hiệu",
    en: "Brand Development",
    body: "Định vị thương hiệu, xây dựng câu chuyện và năng lực truyền thông cho các đơn vị giáo dục đối tác.",
  },
];

const INSIGHTS = [
  {
    cat: "Đầu tư",
    dom: "CAPITAL",
    date: "12.05.2026",
    read: "8 phút",
    readEn: "8 min",
    title: "Đầu tư đơn lẻ không đủ — bài học vận hành từ Châu Á",
    titleEn: "Capital alone is not enough — operating lessons from Asia",
    excerpt: "Khi vòng đời sản phẩm giáo dục dài, đầu tư đơn thuần là không đủ. Bài viết phân tích vai trò của hệ vận hành tích hợp.",
    excerptEn: "When the education product lifecycle is long, capital alone falls short. On the role of an integrated operating architecture.",
    img: "investor meeting / architecture",
  },
  {
    cat: "Vận hành",
    dom: "OPERATIONS",
    date: "05.05.2026",
    read: "6 phút",
    readEn: "6 min",
    title: "Bốn chỉ số quyết định một học viện tư nhân có thể nhân bản",
    titleEn: "Four metrics that decide whether a private academy can scale",
    excerpt: "Tỷ lệ giữ chân học viên, hiệu suất giảng viên, biên đóng góp đơn vị và chu kỳ hoàn vốn — bốn KPI cốt lõi.",
    excerptEn: "Retention, faculty productivity, unit contribution margin and payback period — the four core KPIs.",
    img: "classroom interior",
  },
  {
    cat: "Chiến lược",
    dom: "STRATEGY",
    date: "28.04.2026",
    read: "11 phút",
    readEn: "11 min",
    title: "Vì sao GEMS chọn đứng cùng người sáng lập thay vì thay thế họ",
    titleEn: "Why GEMS stands beside founders rather than replacing them",
    excerpt: "Triết lý đầu tư của chúng tôi: vốn là phương tiện, người sáng lập mới là động lực dài hạn của một thương hiệu giáo dục.",
    excerptEn: "Our investment philosophy: capital is the means, but the founder is the long-term engine of an education brand.",
    img: "founders portrait",
  },
];

/* ----------------------------------------------------------
   PILLARS — Ba trụ cột kinh doanh (từ GEMS Introduction 2026)
   ---------------------------------------------------------- */
const PILLARS = [
  {
    id: "pillar-1",
    num: "I",
    code: "FOUNDATION",
    en: "GEMS Foundation Partners",
    vi: "Đối tác Kiến tạo Nền móng",
    sol: "Đồng kiến tạo tài sản giáo dục",
    solEn: "Co-building education assets",
    audienceVi: "Dành cho nhà đầu tư & văn phòng gia đình sẵn sàng đồng kiến tạo những tài sản giáo dục trường tồn.",
    audienceEn: "For investors & family offices ready to co-build long-duration education assets.",
    leadVi: "Những công trình vĩ đại nhất sống lâu hơn người dựng nên chúng. Chúng có thể là của bạn.",
    leadEn: "The greatest structures outlive their builders. They can be yours.",
  },
  {
    id: "pillar-2",
    num: "II",
    code: "TRANSFORMATION",
    en: "GEMS Transformation Partners",
    vi: "Đối tác Chuyển đổi",
    sol: "Chuyển đổi trường học hiện hữu",
    solEn: "Transforming existing schools",
    audienceVi: "Dành cho chủ trường & nhà đầu tư sẵn sàng chuyển đổi những tài sản giáo dục hiện hữu.",
    audienceEn: "For school owners & investors ready to transform existing educational assets.",
    leadVi: "Bạn đã xây nên một điều có thật. Hãy để nó trở thành tất cả những gì nó vốn có thể trở thành.",
    leadEn: "You built something real. Now let it become everything it was always capable of being.",
  },
  {
    id: "pillar-3",
    num: "III",
    code: "ACADEMY",
    en: "GEMS Academy Partners",
    vi: "Đối tác Học thuật",
    sol: "Đưa tri thức thành định chế",
    solEn: "Turning expertise into institutions",
    audienceVi: "Dành cho nhà thực hành & học giả sẵn sàng biến tri thức thành những định chế trường tồn.",
    audienceEn: "For practitioners & scholars ready to turn expertise into lasting institutions.",
    leadVi: "Câu hỏi không còn là bạn biết gì — mà là ai sẽ mang nó đi tiếp khi bạn lùi lại phía sau.",
    leadEn: "The question is no longer what you know. It is who will carry it forward when you step back.",
  },
];

/* ----------------------------------------------------------
   Verify — đánh dấu nội dung placeholder cần client xác nhận
   (ẩn/hiện qua Tweaks → body.hide-verify)
   ---------------------------------------------------------- */
function Verify({ children, label = "Cần xác nhận", className = "", style }) {
  return (
    <span
      className={`verify ${className}`}
      style={style}
      title="Nội dung placeholder — chưa có trong tài liệu GEMS Introduction 2026, cần xác nhận trước khi xuất bản."
    >
      {children}
      <span className="verify-badge">{label}</span>
    </span>
  );
}

/* expose globals so other babel files can use them */
/* ----------------------------------------------------------
   Video background (HLS / .m3u8 via hls.js)
   ---------------------------------------------------------- */
function VideoBg({ src, poster, className = "", style = {} }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const v = ref.current;
    if (!v || !src) return;
    let hls;
    const isHls = /\.m3u8(\?|$)/.test(src);
    if (isHls && v.canPlayType("application/vnd.apple.mpegurl")) {
      // Safari native
      v.src = src;
    } else if (isHls && window.Hls && window.Hls.isSupported()) {
      hls = new window.Hls({ maxBufferLength: 12 });
      hls.loadSource(src);
      hls.attachMedia(v);
    } else {
      v.src = src;
    }
    return () => { if (hls) hls.destroy(); };
  }, [src]);
  return (
    <video
      ref={ref}
      className={className}
      style={{
        width: "100%", height: "100%", objectFit: "cover",
        display: "block", ...style,
      }}
      muted
      autoPlay
      loop
      playsInline
      poster={poster}
    />
  );
}

Object.assign(window, {
  useReveal, Reveal, RevealStagger,
  ImagePlaceholder, BrandStripes, VideoBg,
  StripeBand, ConcentricArcs, ConcentricRings,
  GemsSymbol, GemsLogo, GemsLogoStacked, GemsSymbolMark,
  Nav, Footer, Verify,
  VALUES, SOLUTIONS, INSIGHTS, PILLARS,
  BG, UNSPLASH,
  ScrollProgress, BackToTop, useParallax, useCountUp,
});

/* ----------------------------------------------------------
   ScrollProgress — thin top progress line (GEMS Red → Blue)
   ---------------------------------------------------------- */
function ScrollProgress() {
  const ref = useRef(null);
  useEffect(() => {
    if (document.body.classList.contains("no-anim")) return;
    let raf = 0;
    const update = () => {
      const el = ref.current;
      if (!el) return;
      const max = (document.documentElement.scrollHeight - window.innerHeight) || 1;
      const p = Math.min(1, Math.max(0, window.scrollY / max));
      el.style.transform = `scaleX(${p})`;
      raf = 0;
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => { window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); };
  }, []);
  return <div ref={ref} className="scroll-progress" aria-hidden="true" />;
}

/* ----------------------------------------------------------
   useParallax — translateY based on viewport position
   speed: 0 = static, 0.2 = subtle, 0.5 = strong; negative = reverse
   ---------------------------------------------------------- */
function useParallax(speed = 0.18) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (document.body.classList.contains("no-anim")) return;
    let raf = 0;
    const update = () => {
      const rect = el.getBoundingClientRect();
      const vh = window.innerHeight || 1;
      // Distance of section center from viewport center, normalized
      const center = rect.top + rect.height / 2;
      const offset = (center - vh / 2) * speed * -1;
      el.style.transform = `translate3d(0, ${offset.toFixed(1)}px, 0)`;
      raf = 0;
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [speed]);
  return ref;
}

/* ----------------------------------------------------------
   useCountUp — animate number from 0 → target when in view
   ---------------------------------------------------------- */
function useCountUp(target, { duration = 1400, suffix = "", prefix = "", decimals = 0 } = {}) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (document.body.classList.contains("no-anim")) {
      el.textContent = `${prefix}${target.toFixed(decimals)}${suffix}`;
      return;
    }
    let started = false;
    const ease = (t) => 1 - Math.pow(1 - t, 3);
    const run = () => {
      const start = performance.now();
      const step = (now) => {
        const t = Math.min(1, (now - start) / duration);
        const v = target * ease(t);
        el.textContent = `${prefix}${v.toFixed(decimals)}${suffix}`;
        if (t < 1) requestAnimationFrame(step);
      };
      requestAnimationFrame(step);
    };
    const obs = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting && !started) { started = true; run(); obs.unobserve(el); }
      });
    }, { threshold: 0.4 });
    obs.observe(el);
    return () => obs.disconnect();
  }, [target, duration, suffix, prefix, decimals]);
  return ref;
}

/* ----------------------------------------------------------
   BackToTop — appears after first viewport
   ---------------------------------------------------------- */
function BackToTop() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    const onScroll = () => setShow(window.scrollY > window.innerHeight * 0.8);
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return (
    <button
      type="button"
      className={`back-to-top ${show ? "in" : ""}`}
      onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
      aria-label="Cuộn lên đầu trang"
    >
      <span className="bt-arrow">↑</span>
      <span className="bt-label">Lên đầu</span>
    </button>
  );
}

/* ============================================================
   RichText (Phase 5.5) — renders CMS rich-text HTML safely.
   `html` is an HTML string from a CMS body field; it is sanitized
   through window.gemsSanitize (DOMPurify allowlist) before injection.
   Returns null on empty so callers can fall back to hardcoded JSX.
   Defaults to a <div> wrapper; pages may style it transparent
   (e.g. `display: contents`) to preserve layout parity.
   ============================================================ */
function RichText({ html, className = "", tag = "div", ...rest }) {
  if (!html) return null;
  const clean = typeof window.gemsSanitize === "function" ? window.gemsSanitize(html) : "";
  if (!clean) return null;
  const Tag = tag;
  return <Tag className={className} dangerouslySetInnerHTML={{ __html: clean }} {...rest} />;
}
window.RichText = RichText;
