// shared.jsx — Header, Footer, Tweaks, common helpers.
// Loaded on every page before the page-specific app file.
// Components and helpers are exported to window so other Babel scripts can reach them.

// Beim Reload immer ganz oben starten (kein Scroll-Restore). So sehen User
// die Initial-Choreographie der Animationen jedes Mal von vorne, statt
// mitten in der Seite zu landen wo schon viele Reveals "is-in" sind.
if (typeof window !== "undefined") {
  if ("scrollRestoration" in history) {
    history.scrollRestoration = "manual";
  }
  // Sicherheits-Reset: Browser koennte ohne manual-Restore noch scrollen
  window.scrollTo(0, 0);
  // Auch nach DOM-Load nochmal scrollen, falls ein async resource
  // (Font, Bild) zwischendrin den Layout-Anker verschiebt
  window.addEventListener("load", () => window.scrollTo(0, 0), { once: true });
}

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

// -----------------------------
// Logo (inline SVG — Wave-Stack mit drei gestaffelten Wellen)
// Passend zur Wortmarke "Treibiq" und zum public/assets/logo-full.svg
// -----------------------------
function LogoMark({ size = 26 }) {
  // Brand-Image-1 ist das offizielle Wave-Mark als PNG (3 Wellen, voll
  // ausgearbeitet, kein SVG-Clipping-Risiko mehr).
  // Aspect-Ratio: 441 x 278 = 1.586. Width = size * 1.586.
  return (
    <img
      src="assets/brand-image-1.png"
      alt=""
      width={Math.round(size * 1.586)}
      height={size}
      decoding="async"
      style={{ display: "block" }}
      aria-hidden="true"
    />
  );
}

function Logo({ small = false }) {
  return (
    <a href="index.html" className="logo" aria-label="Treibiq Startseite">
      <span className="logo__mark">
        <LogoMark size={small ? 22 : 26} />
      </span>
      <span className="logo__word">Treibiq</span>
    </a>
  );
}

function BrandWave() {
  return (
    <svg className="page-head__wave" viewBox="0 0 930 252" fill="none" aria-hidden="true">
      <defs>
        <linearGradient id="brand-wave-grad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#AC88FF" />
          <stop offset="50%" stopColor="#FF9966" />
          <stop offset="100%" stopColor="#FFCC99" />
        </linearGradient>
      </defs>
      <path d="M 0 63 C 155 -21 310 -21 465 63 C 620 147 775 147 930 63 L 930 190 C 775 274 620 274 465 190 C 310 105 155 105 0 190 Z" fill="url(#brand-wave-grad)" />
    </svg>
  );
}

// -----------------------------
// Icons (inline SVG, lucide-style)
// -----------------------------
function Icon({ name, size = 20, stroke = 1.6 }) {
  const paths = {
    arrow: <path d="M5 12h14M13 5l7 7-7 7" />,
    check: <path d="M20 6L9 17l-5-5" />,
    plus: <><path d="M12 5v14" /><path d="M5 12h14" /></>,
    close: <><path d="M18 6L6 18" /><path d="M6 6l12 12" /></>,
    menu: <><path d="M4 7h16" /><path d="M4 12h16" /><path d="M4 17h16" /></>,
    bell: <><path d="M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" /></>,
    play: <path d="M8 5v14l11-7z" fill="currentColor" stroke="none" />,
    sparkle: <><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" /></>,
    mail: <><rect x="3" y="5" width="18" height="14" rx="2" /><path d="M3 7l9 7 9-7" /></>,
    file: <><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" /><path d="M14 3v5h5" /></>,
    chart: <><path d="M3 3v18h18" /><path d="M7 14l4-4 4 4 5-5" /></>,
    users: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></>,
    handshake: <><path d="M11 17l-5-5 5-5" /><path d="M5 12h14" /><path d="M19 17l-5-5 5-5" /></>,
    shield: <path d="M12 2l9 4v6c0 5-3.5 9-9 10-5.5-1-9-5-9-10V6z" />,
    server: <><rect x="3" y="3" width="18" height="8" rx="2" /><rect x="3" y="13" width="18" height="8" rx="2" /><circle cx="7" cy="7" r="0.5" fill="currentColor" /><circle cx="7" cy="17" r="0.5" fill="currentColor" /></>,
    zap: <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />,
    clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
    pin: <><path d="M12 22s7-7 7-12a7 7 0 1 0-14 0c0 5 7 12 7 12z" /><circle cx="12" cy="10" r="2.5" /></>,
    phone: <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7a2 2 0 0 1 1.72 2z" />,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {paths[name]}
    </svg>
  );
}

// -----------------------------
// AnimatedContent: React-Bits-aehnliche Reveal-Animation Komponente,
// vanilla JS port (statt GSAP). Wrapper-Div bekommt initial translate +
// scale + opacity, IntersectionObserver triggert die Transition zu
// "Resting State" (translate0, scale1, opacity1). Easings sind cubic-
// bezier-Approximationen der GSAP-Ease-Curves.
// API kompatibel mit reactbits.dev/animations/animated-content - subset
// (disappear-features weggelassen, brauchen wir aktuell nicht).
// -----------------------------
const ANIM_EASE_MAP = {
  "power1.out": "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
  "power2.out": "cubic-bezier(0.165, 0.84, 0.44, 1)",
  "power3.out": "cubic-bezier(0.215, 0.61, 0.355, 1)",
  "power4.out": "cubic-bezier(0.165, 0.84, 0.44, 1)",
  "expo.out":   "cubic-bezier(0.19, 1, 0.22, 1)",
  "circ.out":   "cubic-bezier(0.075, 0.82, 0.165, 1)",
  "back.out":   "cubic-bezier(0.34, 1.56, 0.64, 1)",
  "bounce.out": "cubic-bezier(0.34, 1.56, 0.64, 1)",
  "sine.out":   "cubic-bezier(0.39, 0.575, 0.565, 1)",
};

function AnimatedContent(props) {
  const children = props.children;
  const distance = props.distance != null ? props.distance : 100;
  const direction = props.direction || "vertical";
  const reverse = props.reverse || false;
  const duration = props.duration != null ? props.duration : 0.8;
  const ease = props.ease || "power3.out";
  const initialOpacity = props.initialOpacity != null ? props.initialOpacity : 0;
  const animateOpacity = props.animateOpacity !== false;
  const scale = props.scale != null ? props.scale : 1;
  const threshold = props.threshold != null ? props.threshold : 0.1;
  const delay = props.delay || 0;
  const onComplete = props.onComplete;
  const className = props.className || "";
  const extraStyle = props.style || {};
  const ref = useRef(null);

  // Hardening 2026-05-15: useMemo fuer das gemergde Style-Objekt.
  // Vorher entstand auf JEDEM Render ein neues Object literal, das
  // React's Style-Reconciliation als geaenderte style-Prop interpretieren
  // konnte und damit die per Imperativ gesetzten inline-Styles (opacity,
  // transform, transition) potentiell ueberschrieb. Indem wir per useMemo
  // die Referenz nur bei echtem extraStyle-Change neu erzeugen, bleibt
  // React's commit-Phase bei Re-Renders idempotent fuer die style-Prop.
  // Hinweis: wir vergleichen NICHT tief, sondern verlassen uns auf den
  // Eltern-Aufrufer ein stabiles extraStyle zu liefern - das ist auch im
  // Original-Verhalten implizit so vorausgesetzt gewesen.
  const baseStyle = useMemo(
    () => Object.assign({ visibility: "hidden" }, extraStyle),
    [extraStyle]
  );

  // Hardening 2026-05-15: useLayoutEffect statt useEffect fuer den
  // Initial-Style-Setup. useLayoutEffect laeuft synchron NACH dem DOM-
  // commit, aber VOR dem Browser-Paint. Damit ist garantiert dass der
  // User nie das Element ohne Transform / mit visibility:hidden sieht.
  // Bei useEffect (asynchron, nach Paint) konnte es einen Frame
  // Visibility-Flash geben in dem das Element ohne Initial-Transform
  // rendert - wahrnehmbar als "Sprung" wenn die Pakete-Karten remounten.
  useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (typeof window === "undefined" || typeof IntersectionObserver === "undefined") return;

    const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const cssEase = ANIM_EASE_MAP[ease] || ANIM_EASE_MAP["power3.out"];
    const axis = direction === "horizontal" ? "X" : "Y";
    // Distance kann Number (px) ODER String (CSS-Expression wie "100% + 24px")
    // sein. Bei String wird calc() drumrum gelegt damit translate eine valide
    // Single-Value-Expression bekommt. Damit lassen sich viewport-abhaengige
    // Distanzen (z.B. exakte Spaltenbreite via 100% own-width) ausdruecken.
    const distanceCss = typeof distance === "number" ? `${distance}px` : distance;
    const offsetCss = reverse
      ? `calc(-1 * (${distanceCss}))`
      : `calc(${distanceCss})`;

    if (reduceMotion) {
      el.style.opacity = "1";
      el.style.transform = "none";
      el.style.visibility = "visible";
      if (onComplete) onComplete();
      return;
    }

    // Initial-State setzen bevor Transition aktiv ist. Weil wir in
    // useLayoutEffect sind, sieht der Browser den Initial-State sicher
    // beim ersten Paint, kein Flash moeglich.
    el.style.opacity = animateOpacity ? String(initialOpacity) : "1";
    el.style.transform = `translate${axis}(${offsetCss}) scale(${scale})`;
    el.style.visibility = "visible";

    // Doppel-RAF: Browser muss den Initial-State sicher rendern, dann erst
    // wird die Transition-Property gesetzt und der IO scharfgeschaltet.
    // Vorher: IO wurde synchron erzeugt und konnte feuern BEVOR die
    // Transition gesetzt war (= Element snappt instant, keine Animation).
    //
    // Hardening 2026-05-15: RAF-IDs werden jetzt getrackt und im Cleanup
    // cancelled. Vorher liefen die geRAF-ed Callbacks bei schnellem
    // mount+unmount weiter und checkten nur das cancelled-Flag - das
    // kostete RAF-Quota und verzoegerte parallele Mounts (race conditions
    // bei mehreren AnimatedContent-Instanzen im selben Frame, z.B.
    // gestaffelte Pricing-Cards). Mit cancelAnimationFrame entstehen
    // saubere Cleanups ohne dass die Browser-Animation-Queue muellt.
    let completeTimer = null;
    let io = null;
    let cancelled = false;
    let rafId1 = 0;
    let rafId2 = 0;
    rafId1 = requestAnimationFrame(() => {
      rafId1 = 0;
      rafId2 = requestAnimationFrame(() => {
        rafId2 = 0;
        if (cancelled || !ref.current) return;
        el.style.transition = `opacity ${duration}s ${cssEase} ${delay}s, transform ${duration}s ${cssEase} ${delay}s`;
        // IO wird hier INNERHALB des zweiten RAF erzeugt - so ist
        // garantiert dass Initial-State + Transition gesetzt sind
        // bevor der erste IO-Callback feuern kann. Auch wenn das
        // Element schon im Viewport ist, kommt der Callback frueh-
        // estens im naechsten Frame nach observe().
        io = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (!entry.isIntersecting) return;
              io.unobserve(entry.target);
              el.style.opacity = "1";
              el.style.transform = "translate(0, 0) scale(1)";
              if (onComplete) {
                completeTimer = setTimeout(onComplete, (duration + delay) * 1000);
              }
            });
          },
          { threshold }
        );
        io.observe(el);
      });
    });

    return () => {
      cancelled = true;
      if (rafId1) cancelAnimationFrame(rafId1);
      if (rafId2) cancelAnimationFrame(rafId2);
      if (io) io.disconnect();
      if (completeTimer) clearTimeout(completeTimer);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div ref={ref} className={className} style={baseStyle}>
      {children}
    </div>
  );
}

// -----------------------------
// Header (with sticky-on-scroll)
// -----------------------------
function Header({ active }) {
  const [scrolled, setScrolled] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);

  useEffect(() => {
    // Hysterese verhindert Rattern am Threshold: einmal scrolled bleibt
    // scrolled bis man wirklich wieder am Top ist (< 2px). Einmal oben
    // bleibt oben bis man deutlich gescrollt hat (> 40px). Dazwischen
    // kein toggle - keine Glitch-Loop durch Header-Reflow.
    // rAF dedup verhindert dass der Handler mehrmals pro Frame feuert.
    let ticking = false;
    const onScroll = () => {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(() => {
        const y = window.scrollY;
        setScrolled((prev) => {
          if (!prev && y > 40) return true;
          if (prev && y < 2) return false;
          return prev;
        });
        ticking = false;
      });
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Body-Scroll-Lock wenn Mobile-Menu offen + Escape-Taste schliesst
  useEffect(() => {
    if (menuOpen) {
      document.body.classList.add("menu-open");
      const onEsc = (e) => { if (e.key === "Escape") setMenuOpen(false); };
      window.addEventListener("keydown", onEsc);
      return () => {
        document.body.classList.remove("menu-open");
        window.removeEventListener("keydown", onEsc);
      };
    }
  }, [menuOpen]);

  const links = [
    { href: "use-cases.html", label: "Use Cases", key: "use-cases" },
    { href: "leistungen.html", label: "Leistungen", key: "leistungen" },
    { href: "pakete.html", label: "Preise", key: "pakete" },
    { href: "ueber-uns.html", label: "Über uns", key: "ueber-uns" },
    { href: "kontakt.html", label: "Kontakt", key: "kontakt" },
  ];

  return (
    <>
      <header className={`header ${scrolled ? "is-scrolled" : ""}`}>
        <div className="header__inner">
          <Logo />
          <nav className="nav">
            {links.map((l) => (
              <a key={l.key} href={l.href} className={active === l.key ? "is-active" : ""}>
                {l.label}
              </a>
            ))}
          </nav>
          <div className="header__right">
            <a href="kontakt.html" className="btn btn--primary btn--md">
              <span>Erstgespräch buchen</span>
              <Icon name="arrow" size={16} />
            </a>
            <button className="menu-toggle" onClick={() => setMenuOpen(true)} aria-label="Menü öffnen">
              <Icon name="menu" />
            </button>
          </div>
        </div>
      </header>

      {/* Mobile-Menu: immer im DOM, Klasse is-open toggelt fade+slide-Animation */}
      <div className={`mobile-menu ${menuOpen ? "is-open" : ""}`} aria-hidden={!menuOpen}>
        <div className="mobile-menu__top">
          <Logo />
          <button className="btn btn--icon-sm btn--icon--paper" onClick={() => setMenuOpen(false)} aria-label="Schließen">
            <Icon name="close" />
          </button>
        </div>
        <nav>
          <a href="index.html">Home</a>
          {links.map((l) => <a key={l.key} href={l.href}>{l.label}</a>)}
        </nav>
        <a href="kontakt.html" className="btn btn--primary btn--lg" style={{ marginTop: 24 }}>
          Erstgespräch buchen <Icon name="arrow" size={16} />
        </a>
      </div>
    </>
  );
}

// -----------------------------
// Footer
// -----------------------------
function Footer() {
  return (
    <footer className="footer">
      <div className="footer__inner">
        <div className="footer__top">
          <div className="footer__brand">
            <a href="index.html" className="logo" style={{ color: "var(--paper)" }}>
              <span className="logo__mark"><LogoMark size={26} /></span>
              <span className="logo__word" style={{ color: "var(--paper)" }}>Treibiq</span>
            </a>
            <p className="footer__tagline">
              Wir automatisieren Ihre Prozesse mit KI – damit Sie sich auf das Wesentliche konzentrieren. DSGVO-konform, in wenigen Tagen, ohne IT-Abteilung.
            </p>
            <a href="kontakt.html" className="footer__cta">
              Kostenloses Erstgespräch <Icon name="arrow" size={16} />
            </a>
          </div>
          <div className="footer__cols">
            <div>
              <h4>Produkt</h4>
              <a href="use-cases.html">Use Cases</a>
              <a href="leistungen.html">Leistungen</a>
              <a href="pakete.html">Preise</a>
              <a href="leistungen.html#prozess">So funktioniert's</a>
            </div>
            <div>
              <h4>Unternehmen</h4>
              <a href="ueber-uns.html">Über uns</a>
              <a href="kontakt.html">Kontakt</a>
              <a href="ueber-uns.html#team">Team</a>
              <a href="kontakt.html">Erstgespräch</a>
            </div>
            <div>
              <h4>Rechtliches</h4>
              <a href="impressum.html">Impressum</a>
              <a href="impressum.html#datenschutz">Datenschutz</a>
              <a href="impressum.html#agb">AGB</a>
            </div>
          </div>
        </div>
        <div className="footer__bottom">
          <span>© 2026 Treibiq. Wir treiben euch voran.</span>
          <div className="footer__legal">
            <a href="impressum.html">Impressum</a>
            <a href="impressum.html#datenschutz">Datenschutz</a>
          </div>
        </div>
      </div>
    </footer>
  );
}

// -----------------------------
// Tools logos (simpleicons CDN)
// -----------------------------
const TOOLS = {
  gmail:    { name: "Gmail",     slug: "gmail",                 color: "EA4335" },
  outlook:  { name: "Outlook",   slug: "microsoftoutlook",      color: "0078D4" },
  openai:   { name: "GPT-4",     slug: "openai",                color: "412991" },
  n8n:      { name: "n8n",       slug: "n8n",                   color: "EA4B71" },
  hubspot:  { name: "HubSpot",   slug: "hubspot",               color: "FF7A59" },
  pipedrive:{ name: "Pipedrive", slug: "pipedrive",             color: "1A1A1A" },
  notion:   { name: "Notion",    slug: "notion",                color: "000000" },
  slack:    { name: "Slack",     slug: "slack",                 color: "4A154B" },
  sheets:   { name: "Sheets",    slug: "googlesheets",          color: "34A853" },
  powerbi:  { name: "Power BI",  slug: "powerbi",               color: "F2C811" },
  workspace:{ name: "Workspace", slug: "googleworkspace",       color: "4285F4" },
  ms365:    { name: "MS 365",    slug: "microsoft365",          color: "D83B01" },
  datev:    { name: "DATEV",     slug: null,                    color: "00945E" },
  sap:      { name: "SAP",       slug: "sap",                   color: "0FAAFF" },
  zapier:   { name: "Zapier",    slug: "zapier",                color: "FF4F00" },
  airtable: { name: "Airtable",  slug: "airtable",              color: "18BFFF" },
  stripe:   { name: "Stripe",    slug: "stripe",                color: "635BFF" },
};

// Lokale Tool-Logos (assets/tools/{slug}) nutzen statt simpleicons CDN.
// Die Assets sind in _legacy/assets/tools/ und enthalten transparente Hintergruende
// plus angepasste Logos fuer DATEV, Clay, Mantle, LGM (Wikimedia + custom-tiles).
const LOCAL_TOOL_FILES = {
  gmail: "assets/tools/gmail.png",
  outlook: "assets/tools/outlook.webp",
  n8n: "assets/tools/n8n.webp",
  hubspot: "assets/tools/hubspot.webp",
  pipedrive: "assets/tools/pipedrive.webp",
  notion: "assets/tools/notion.webp",
  slack: "assets/tools/slack.webp",
  sheets: "assets/tools/sheets.webp",
  powerbi: "assets/tools/powerbi.webp",
  workspace: "assets/tools/workspace.webp",
  ms365: "assets/tools/ms365.webp",
  datev: "assets/tools/datev.webp",
  sap: "assets/tools/sap.png",
  whatsapp: "assets/tools/whatsapp.webp",
  telegram: "assets/tools/telegram.webp",
  mail: "assets/tools/mail.webp",
  phone: "assets/tools/phone.webp",
  clay: "assets/tools/clay.webp",
  mantle: "assets/tools/mantle.webp",
  lgm: "assets/tools/lgm.webp",
};

function ToolLogo({ tool, size = 18, loading = "lazy" }) {
  const t = TOOLS[tool];
  if (!t) return null;
  const localSrc = LOCAL_TOOL_FILES[tool];
  if (localSrc) {
    return (
      <img
        src={localSrc}
        alt={t.name}
        width={size}
        height={size}
        style={{ width: size, height: size, objectFit: "contain", flexShrink: 0 }}
        loading={loading}
        decoding="async"
      />
    );
  }
  // Fallback: First-Letter-Tile (fuer tools ohne lokales PNG)
  return (
    <span style={{
      width: size, height: size, borderRadius: 5,
      background: `#${t.color || "888"}`, color: "white",
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      fontSize: size * 0.55, fontWeight: 700, flexShrink: 0,
    }}>{t.name.charAt(0)}</span>
  );
}

function ToolChip({ tool, size = "md", logoOnly = false }) {
  const t = TOOLS[tool];
  if (!t) return null;
  if (logoOnly) {
    // Nur Logo, groesser, in einem runden weissen Tile mit Soft-Shadow
    const logoSize = size === "lg" ? 32 : size === "sm" ? 22 : 28;
    return (
      <span
        className="tool-chip tool-chip--logo-only"
        title={t.name}
        aria-label={t.name}
      >
        <ToolLogo tool={tool} size={logoSize} />
      </span>
    );
  }
  const dims = size === "sm" ? { fs: 11.5, pad: "5px 10px 5px 8px", logo: 13 } : { fs: 13, pad: "8px 14px 8px 10px", logo: 18 };
  return (
    <span className="tool-chip" style={{ fontSize: dims.fs, padding: dims.pad }}>
      <ToolLogo tool={tool} size={dims.logo} />
      {t.name}
    </span>
  );
}

// -----------------------------
// Tweaks panel (keys persisted in localStorage; not file-based)
// -----------------------------
const TweakCtx = createContext(null);

function TweaksProvider({ children }) {
  const defaults = { accent: "violet", density: "default", heroVariant: "video", darkBand: true };
  const [tweaks, setTweaks] = useState(() => {
    try {
      const stored = JSON.parse(localStorage.getItem("treibiq-tweaks") || "{}");
      return { ...defaults, ...stored };
    } catch { return defaults; }
  });
  const [open, setOpen] = useState(false);

  useEffect(() => {
    localStorage.setItem("treibiq-tweaks", JSON.stringify(tweaks));
    document.documentElement.setAttribute("data-accent", tweaks.accent === "violet" ? "" : tweaks.accent);
    document.documentElement.setAttribute("data-density", tweaks.density === "default" ? "" : tweaks.density);
  }, [tweaks]);

  // Edit-mode protocol
  useEffect(() => {
    const handler = (e) => {
      if (!e.data || typeof e.data !== "object") return;
      if (e.data.type === "__activate_edit_mode") setOpen(true);
      if (e.data.type === "__deactivate_edit_mode") setOpen(false);
    };
    window.addEventListener("message", handler);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", handler);
  }, []);

  const setTweak = (k, v) => setTweaks((t) => ({ ...t, [k]: v }));

  return (
    <TweakCtx.Provider value={{ tweaks, setTweak, open, setOpen }}>
      {children}
      <TweaksPanel />
    </TweakCtx.Provider>
  );
}
const useTweaks = () => useContext(TweakCtx);

function TweaksPanel() {
  const { tweaks, setTweak, open, setOpen } = useTweaks();
  if (!open) return null;
  const close = () => {
    setOpen(false);
    window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
  };
  const Chip = ({ k, value, label }) => (
    <button className={`tweaks__chip ${tweaks[k] === value ? "is-active" : ""}`} onClick={() => setTweak(k, value)}>
      {label}
    </button>
  );
  return (
    <div className="tweaks is-open">
      <div className="tweaks__head">
        <h4>Tweaks</h4>
        <button className="btn btn--icon-sm btn--icon--paper" onClick={close} aria-label="Schließen" style={{ width: 28, height: 28 }}>
          <Icon name="close" size={14} />
        </button>
      </div>
      <div className="tweaks__row">
        <label>Akzentfarbe</label>
        <div className="tweaks__chips">
          <Chip k="accent" value="violet" label="Violett" />
          <Chip k="accent" value="peach" label="Pfirsich" />
          <Chip k="accent" value="ink" label="Ink" />
        </div>
      </div>
      <div className="tweaks__row">
        <label>Dichte</label>
        <div className="tweaks__chips">
          <Chip k="density" value="compact" label="Kompakt" />
          <Chip k="density" value="default" label="Standard" />
          <Chip k="density" value="airy" label="Luftig" />
        </div>
      </div>
      <div className="tweaks__row">
        <label>Hero-Variante</label>
        <div className="tweaks__chips">
          <Chip k="heroVariant" value="video" label="Video-Slot" />
          <Chip k="heroVariant" value="flow" label="Workflow" />
          <Chip k="heroVariant" value="stat" label="Stat-Card" />
        </div>
      </div>
    </div>
  );
}

// -----------------------------
// Use case modal (shared across pages)
// -----------------------------
function UseCaseModal({ wf, onClose }) {
  if (!wf) return null;
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <button className="modal__close" onClick={onClose} aria-label="Schließen"><Icon name="close" size={16} /></button>
        <div className="modal__head">
          <span className="wf-card__icon"><Icon name={wf.icon} size={24} /></span>
          <div>
            <span className="eyebrow">Use Case · {wf.num}</span>
            <h3 className="modal__title">{wf.title}</h3>
          </div>
        </div>
        <p className="modal__lede">{wf.problem || wf.body}</p>
        <div className="modal__pipe">
          {wf.tools.map((t, i) => (
            <React.Fragment key={t}>
              <ToolChip tool={t} size="sm" />
              {i < wf.tools.length - 1 && <span className="arrow"><Icon name="arrow" size={14} /></span>}
            </React.Fragment>
          ))}
        </div>
        <dl style={{ margin: 0 }}>
          <div className="modal__row"><dt>Zeitersparnis</dt><dd>{wf.hours} Std / Woche</dd></div>
          <div className="modal__row"><dt>Aufbauzeit</dt><dd>1–2 Wochen</dd></div>
          <div className="modal__row"><dt>Status</dt><dd style={{ color: "var(--success)" }}>● Live bei Kunden</dd></div>
        </dl>
        <div className="modal__actions">
          <a href="kontakt.html" className="btn btn--primary btn--md" style={{ flex: 1 }}>
            Diesen Workflow besprechen <Icon name="arrow" size={14} />
          </a>
          <button className="btn btn--ghost btn--md" onClick={onClose}>Schließen</button>
        </div>
      </div>
    </div>
  );
}

// -----------------------------
// Animation-Hook: Initial-Load-Choreographie + Scroll-Reveals + Word-Stagger + Counter
// IntersectionObserver-basiert, ohne externe Library.
// -----------------------------
function useAnimations() {
  useEffect(() => {
    if (typeof window === "undefined") return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const setTemporaryWillChange = (el) => {
      if (!el || el.dataset.willChangeManaged === "1") return;
      el.dataset.willChangeManaged = "1";
      el.style.willChange = "opacity, transform";
      let done = false;
      const clear = () => {
        if (done) return;
        done = true;
        el.style.willChange = "";
        delete el.dataset.willChangeManaged;
        el.removeEventListener("transitionend", clear);
      };
      el.addEventListener("transitionend", clear, { once: true });
      setTimeout(clear, 1800);
    };

    const prepareReveal = (el) => {
      if (!el) return;
      if (el.classList.contains("reveal-stagger")) {
        Array.from(el.children).forEach(setTemporaryWillChange);
      }
      if (
        el.classList.contains("reveal") ||
        el.classList.contains("reveal-left") ||
        el.classList.contains("reveal-right") ||
        el.classList.contains("hero-word")
      ) {
        setTemporaryWillChange(el);
      }
    };

    const markIn = (el) => {
      if (!el || el.classList.contains("is-in")) return;
      prepareReveal(el);
      el.classList.add("is-in");
    };

    // Counter-Animation: zaehlt von 0 hoch zum Target.
    // Audit Jannik 2026-05-12: vorher Overshoot-runter, jetzt klassisch
    // unten-nach-oben. Dauer skaliert mit Target-Groesse.
    const activeCounters = new Map();
    let counterRaf = null;
    const counterEase = (t) => 1 - Math.pow(1 - t, 3);
    const tickCounters = (now) => {
      activeCounters.forEach((state, el) => {
        const t = Math.min((now - state.start) / state.dur, 1);
        const val = state.target * counterEase(t);
        el.textContent = state.isInt
          ? Math.round(val).toLocaleString("de-DE")
          : val.toFixed(1).replace(".", ",");
        if (t >= 1) activeCounters.delete(el);
      });
      counterRaf = activeCounters.size > 0 ? requestAnimationFrame(tickCounters) : null;
    };
    const animateCounter = (el) => {
      if (el.dataset.counted === "1") return;
      el.dataset.counted = "1";
      const target = parseFloat(el.dataset.counter || "0");
      if (isNaN(target)) return;
      if (target === 0) { el.textContent = "0"; return; }
      // Kleine Zahlen ~700ms, grosse bis 1100ms. Etwas laenger als vorher
      // damit der Hochzaehl-Effekt sichtbar ist.
      const dur = Math.min(1100, 700 + Math.log10(Math.max(target, 2)) * 130);
      const isInt = Number.isInteger(target);
      activeCounters.set(el, { target, dur, isInt, start: performance.now() });
      if (counterRaf === null) counterRaf = requestAnimationFrame(tickCounters);
    };

    // Schritt 1: Klassen auf semantische Elemente hinzufuegen
    const apply = () => {
      // 1a) Standard-Reveal: einzelne Elemente fade-up
      const revealSelectors = [
        ".section-head",
        ".page-head__inner",
      ];
      revealSelectors.forEach((sel) => {
        document.querySelectorAll(sel).forEach((el) => {
          if (!el.classList.contains("reveal")) el.classList.add("reveal");
        });
      });

      // 1b) Hero-Bereich + Welcome-Section: linke Spalte von links, rechte Spalte von rechts
      document.querySelectorAll(".hero__copy, .welcome__copy").forEach((el) => {
        if (!el.classList.contains("reveal-left")) el.classList.add("reveal-left");
      });
      document.querySelectorAll(".hero__visual, .hero__stage, .welcome__visual").forEach((el) => {
        if (!el.classList.contains("reveal-right")) el.classList.add("reveal-right");
      });

      // 1c) Stagger-Container with from-sides (Cards springen alternierend)
      const fromSidesSelectors = [
        ".wf-grid",
        ".features",
        ".pakete-grid",
        ".team-grid",
        ".values-grid",
        ".bio-grid",
        ".contact-split",
        ".split",
      ];
      fromSidesSelectors.forEach((sel) => {
        document.querySelectorAll(sel).forEach((el) => {
          if (!el.classList.contains("reveal-stagger")) {
            el.classList.add("reveal-stagger");
            el.classList.add("from-sides");
          }
        });
      });

      // 1d) Standard-Stagger (ohne sides) fuer Steps/Stats/Process
      // Plus alle Zenith-Pattern-Grids: stats-row (4 Ghost-Cards), services-grid
      // (6 Feature-Cards), service-grid (Use-Case-Cards mit Bild), testimonials-grid
      // (Quote-Cards), process-split__steps (numerated Cards).
      const plainStaggerSelectors = [
        ".steps",
        ".stat-band",
        ".process",
        ".stats-row",
        ".services-grid",
        ".service-grid",
        ".testimonials-grid",
        ".process-split__steps",
        ".why-grid",
        ".workflow-grid",
        ".comparison-strip",
        ".stack-grid",
        ".features",
        ".pkg-grid",
      ];
      plainStaggerSelectors.forEach((sel) => {
        document.querySelectorAll(sel).forEach((el) => {
          if (!el.classList.contains("reveal-stagger")) el.classList.add("reveal-stagger");
        });
      });

      // 1e) Hero-Title Wort-Stagger
      // ACHTUNG: Inline-Elemente wie <em> die background-clip:text nutzen
      // (Gradient-Text) duerfen NICHT recursive zerlegt werden, sonst
      // bricht das Rendering. Stattdessen werden sie als ganzes als
      // ein .hero-word eingewickelt.
      const heroTitle = document.querySelector(".hero__title");
      if (heroTitle && !heroTitle.dataset.staggered) {
        heroTitle.dataset.staggered = "1";
        const PRESERVE_TAGS = ["EM", "STRONG", "B", "I", "MARK"];
        const wrapTextNodes = (node) => {
          if (node.nodeType === Node.TEXT_NODE) {
            const txt = node.textContent;
            if (!txt.trim()) return null;
            const frag = document.createDocumentFragment();
            txt.split(/(\s+)/).forEach((part) => {
              if (/^\s+$/.test(part)) {
                frag.appendChild(document.createTextNode(part));
              } else if (part) {
                const span = document.createElement("span");
                span.className = "hero-word";
                span.textContent = part;
                frag.appendChild(span);
              }
            });
            return frag;
          }
          if (node.nodeType === Node.ELEMENT_NODE) {
            // Inline-Style-Elemente komplett als ein hero-word wrappen
            if (PRESERVE_TAGS.includes(node.tagName)) {
              const wrapper = document.createElement("span");
              wrapper.className = "hero-word hero-word--preserve";
              wrapper.appendChild(node.cloneNode(true));
              return wrapper;
            }
            const clone = node.cloneNode(false);
            node.childNodes.forEach((child) => {
              const wrapped = wrapTextNodes(child);
              if (wrapped) clone.appendChild(wrapped);
              else clone.appendChild(child.cloneNode(true));
            });
            return clone;
          }
          return node.cloneNode(true);
        };
        const tmp = document.createElement("div");
        tmp.innerHTML = heroTitle.innerHTML;
        const newFrag = document.createDocumentFragment();
        tmp.childNodes.forEach((c) => {
          const r = wrapTextNodes(c);
          if (r) newFrag.appendChild(r);
        });
        heroTitle.innerHTML = "";
        heroTitle.appendChild(newFrag);

        // Delay pro Wort, schnell aber wahrnehmbar
        heroTitle.querySelectorAll(".hero-word").forEach((w, i) => {
          w.style.transitionDelay = `${80 + i * 50}ms`;
        });
      }
    };

    apply();

    // Schritt 2: IntersectionObserver fuer normale Scroll-Reveals
    const obs = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            markIn(entry.target);
            obs.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.1, rootMargin: "0px 0px -5% 0px" }
    );
    // Dedicated Observer fuer "late triggers" - elements die explizit erst
    // animieren sollen wenn man richtig drauf ist (nicht schon beim Anpeeken).
    // Hoehere threshold + groesserer negative root-margin am unteren Edge.
    const lateObs = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            markIn(entry.target);
            lateObs.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.25, rootMargin: "0px 0px -120px 0px" }
    );
    const isLateTarget = (el) =>
      el.classList.contains("faq__list--delayed") ||
      el.classList.contains("process-split__steps--delayed");
    document
      .querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-stagger")
      .forEach((el) => {
        if (isLateTarget(el)) lateObs.observe(el);
        else obs.observe(el);
      });

    // Schritt 3: Initial-Load-Choreographie
    // Alle Elemente die im ersten Viewport sind, kriegen .is-in mit kleinen Delays.
    // Stellt sicher dass auch wenn IntersectionObserver lazy ist, der Hero animiert.
    const initialAnimate = () => {
      const vh = window.innerHeight;
      const initialEls = document.querySelectorAll(
        ".reveal, .reveal-left, .reveal-right, .reveal-stagger, .hero-word"
      );
      let idx = 0;
      initialEls.forEach((el) => {
        const rect = el.getBoundingClientRect();
        if (rect.top < vh * 0.95) {
          // Im Viewport sichtbar
          const delay = el.classList.contains("hero-word")
            ? 0
            : Math.min(idx * 90, 600);
          setTimeout(() => markIn(el), delay);
          idx += 1;
          // Counter im Viewport sofort starten - sowohl auf el selbst als auch
          // auf Kindern. AnimatedStatNum rendert die <span data-counter> als
          // Child der reveal-stagger Karte, also muss man auch children scannen.
          if (el.dataset && el.dataset.counter) {
            setTimeout(() => animateCounter(el), 300 + delay);
          }
          const childCounters = el.querySelectorAll("[data-counter]");
          if (childCounters.length > 0) {
            childCounters.forEach((cc) => {
              if (cc.dataset.counted === "1") return;
              // Delay setzen sodass Counter startet wenn die Karte sichtbar wird.
              // markIn passiert bei `delay`ms, plus CSS transition-delay des
              // staggered childs - der getStaggerDelayMs Helper rechnet das aus.
              const totalDelay = delay + 100 + getStaggerDelayMs(cc);
              setTimeout(() => animateCounter(cc), totalDelay);
            });
          }
        }
      });
    };
    // Trigger nach 80ms, damit React-Render durch ist und CSS settled
    setTimeout(initialAnimate, 80);

    // Safety-Net: nach 2.5 Sekunden ALLE noch nicht animierten Reveals zwangsaktivieren
    // (verhindert dass Elemente unsichtbar bleiben falls Observer / MutationObserver lagged)
    // EXKLUSIVE der `--delayed`-Varianten (faq__list, process-split__steps) - die
    // sollen wirklich erst triggern wenn man drauf scrollt, nicht durch das
    // safety-timeout zwangsweise eingeblendet werden.
    setTimeout(() => {
      document
        .querySelectorAll(
          ".reveal:not(.is-in):not(.faq__list--delayed):not(.process-split__steps--delayed), .reveal-left:not(.is-in), .reveal-right:not(.is-in), .reveal-stagger:not(.is-in):not(.faq__list--delayed):not(.process-split__steps--delayed)"
        )
        .forEach(markIn);
      document.querySelectorAll(".hero-word:not(.is-in)").forEach(markIn);
    }, 2500);

    // Schritt 4: Number-Counter
    // threshold 0 (any visibility) statt 0.5 - bei kleinen inline-spans
    // (z.B. <span class="counter"> innerhalb von .pkg__price-num) feuert
    // 50%-threshold oft nie, wenn die span nur ein paar px hoch ist.
    // rootMargin: vorher -60px am unteren Edge - das hat bei kleinen Counter-
    // spans (~24px hoch) dazu gefuehrt, dass die Intersection auf der
    // Home-Page nicht zuverlaessig getriggert hat. Jetzt 0px, dafuer
    // verlassen wir uns auf die initial-Pass + Scroll-In-Pass Kombination.
    const counterObs = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          // Falls counter-span in einem reveal-stagger Container sitzt,
          // warten bis die Karte sichtbar ist (sonst zaehlt der Counter
          // auf einem opacity:0 Element runter und ist beim Reveal schon fertig).
          scheduleCounterWithStaggerSync(entry.target);
          counterObs.unobserve(entry.target);
        });
      },
      { threshold: 0, rootMargin: "0px 0px 0px 0px" }
    );

    // Helper: ermittelt die total transition-delay einer Counter-Span basierend
    // auf der Position innerhalb des reveal-stagger Parents. So koennen wir
    // den Counter genau dann starten wenn die Karte sichtbar wird.
    const getStaggerDelayMs = (el) => {
      // Walk up the tree um die direkte Card unterhalb eines .reveal-stagger
      // Containers zu finden.
      let card = el;
      while (card && card.parentElement) {
        const parent = card.parentElement;
        if (parent.classList && parent.classList.contains("reveal-stagger")) {
          const siblings = Array.from(parent.children);
          const idx = siblings.indexOf(card);
          if (idx < 0) return 0;
          // CSS-Werte aus site.css: 0/70/140/210/280/350/420/490ms (>=8 capped at 490)
          const steps = [0, 70, 140, 210, 280, 350, 420];
          return idx < steps.length ? steps[idx] : 490;
        }
        card = parent;
      }
      return 0;
    };

    const scheduleCounterWithStaggerSync = (el) => {
      if (el.dataset.counted === "1") return;
      const stagger = el.closest(".reveal-stagger");
      if (stagger && !stagger.classList.contains("is-in")) {
        // Parent noch nicht enthuellt - poll bis is-in gesetzt wird,
        // dann starte Counter mit dem passenden Stagger-Delay.
        const pollStart = performance.now();
        const startWhenIn = () => {
          if (el.dataset.counted === "1") return;
          if (stagger.classList.contains("is-in")) {
            const delayMs = getStaggerDelayMs(el);
            setTimeout(() => animateCounter(el), delayMs + 80);
            return;
          }
          // Safety-Net: nach 3s zwangsstarten - sonst koennte ein nie
          // sichtbarer reveal-stagger den Counter ewig blockieren.
          if (performance.now() - pollStart > 3000) {
            animateCounter(el);
            return;
          }
          requestAnimationFrame(startWhenIn);
        };
        requestAnimationFrame(startWhenIn);
      } else {
        // Parent ist bereits is-in (oder es gibt keinen stagger).
        // Kurze Verzoegerung damit der erste Render durch ist.
        setTimeout(() => animateCounter(el), 120);
      }
    };

    // Counter-Pass: alle [data-counter] elements direkt durchgehen.
    // - Im viewport: sofort animieren (sync mit stagger-reveal)
    // - Ausserhalb: observen, animiert beim scroll-in
    document.querySelectorAll("[data-counter]").forEach((el) => {
      const rect = el.getBoundingClientRect();
      const inViewport = rect.top < window.innerHeight && rect.bottom > 0;
      if (inViewport) {
        scheduleCounterWithStaggerSync(el);
      } else {
        counterObs.observe(el);
      }
    });

    // Schritt 5: MutationObserver fuer dynamisch dazu kommende Elemente
    // (z.B. Pakete-Toggle wechselt Einmalig <-> Retainer und rendered die
    // counter-spans neu - die brauchen eine neue Animation)
    const mutationRoot = document.querySelector("main") || document.getElementById("app") || document.body;
    const mutationObs = new MutationObserver(() => {
      apply();
      mutationRoot
        .querySelectorAll(".reveal:not(.is-in), .reveal-left:not(.is-in), .reveal-right:not(.is-in), .reveal-stagger:not(.is-in)")
        .forEach((el) => {
          if (!el.dataset.observed) {
            el.dataset.observed = "1";
            if (isLateTarget(el)) lateObs.observe(el);
            else obs.observe(el);
          }
        });
      // Frische counter-spans (z.B. nach Toggle-Switch)
      mutationRoot.querySelectorAll("[data-counter]").forEach((el) => {
        if (el.dataset.counted === "1" || el.dataset.counterObserved === "1") return;
        el.dataset.counterObserved = "1";
        const rect = el.getBoundingClientRect();
        if (rect.top < window.innerHeight && rect.bottom > 0) {
          scheduleCounterWithStaggerSync(el);
        } else {
          counterObs.observe(el);
        }
      });
    });
    mutationObs.observe(mutationRoot, { childList: true, subtree: true });

    return () => {
      obs.disconnect();
      lateObs.disconnect();
      counterObs.disconnect();
      mutationObs.disconnect();
      activeCounters.clear();
      if (counterRaf) cancelAnimationFrame(counterRaf);
    };
  }, []);
}

function useAnimationVisibilityPauser() {
  useEffect(() => {
    if (typeof window === "undefined" || typeof IntersectionObserver === "undefined") return;

    const hosts = Array.from(new Set(
      document.querySelectorAll([
        ".testimonials-carousel",
        ".logos-carousel",
        ".cta-band",
        ".hero-mobile-flow",
        ".hero__stage",
        ".hero__wave",
        ".hero-floating",
        ".wf-canvas-block",
      ].join(", "))
    ));
    if (hosts.length === 0) return;

    const setOffscreen = (el, isOffscreen) => {
      el.classList.toggle("is-offscreen", isOffscreen);
    };

    const initialMargin = 160;
    hosts.forEach((el) => {
      const rect = el.getBoundingClientRect();
      setOffscreen(el, rect.bottom < -initialMargin || rect.top > window.innerHeight + initialMargin);
    });

    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => setOffscreen(entry.target, !entry.isIntersecting));
      },
      { threshold: 0, rootMargin: "160px 0px" }
    );

    hosts.forEach((el) => io.observe(el));

    return () => {
      io.disconnect();
      hosts.forEach((el) => el.classList.remove("is-offscreen"));
    };
  }, []);
}

// -----------------------------
// Page wrapper — easy boilerplate per page
// -----------------------------
function Page({ active, children }) {
  useAnimations();
  useAnimationVisibilityPauser();
  return (
    <TweaksProvider>
      <Header active={active} />
      <main>{children}</main>
      <Footer />
    </TweaksProvider>
  );
}

// -----------------------------
// CTA band (used on most pages)
// -----------------------------
function CtaBand({ title = "Bereit, Zeit zurückzugewinnen?", sub = "Bucht euer kostenloses 30-Minuten-Gespräch. Wir schauen gemeinsam, welcher Workflow bei euch sofort Wirkung hat.", buttonLabel = "Jetzt Termin buchen", buttonHref = "kontakt.html" }) {
  return (
    <section className="section">
      <div className="container">
        <div className="cta-band reveal">
          <span className="eyebrow"><span className="dot" />Erstgespräch · 30 Min · kostenlos</span>
          <h2 className="cta-band__title" style={{ marginTop: 18 }}>{title}</h2>
          <p className="cta-band__sub">{sub}</p>
          <a href={buttonHref} className="btn btn--primary btn--lg">
            {buttonLabel} <Icon name="arrow" size={16} />
          </a>
        </div>
      </div>
    </section>
  );
}

// -----------------------------
// Hero Floating Tool-Icons
// - Initial-Animation: Cards fallen von oben (hinter Header) auf ihre Position
// - Konstante subtile Eigenbewegung (sin/cos je Card mit eigener Phase)
// - Maus-Parallax overlayed auf die Eigenbewegung
// -----------------------------
function HeroFloating() {
  const containerRef = useRef(null);
  const canvasRef = useRef(null);

  // Layout: 13 Tools in 4 Reihen, KEIN Logo darf hinter der Title-Zone
  // liegen (x ~18-82%, y ~14-61% bei zentriertem Title max-width 900px).
  // Reihe 1 (y < 14%) und Reihe 4 (y > 61%) duerfen quer ueber den ganzen
  // Hero. Reihe 2 und 3 sind in der Title-Zone, also nur an den Raendern.
  // Order in Array == Index fuer CONNECTIONS-Array.
  const tools = [
    // Reihe 1 (top, y 0-1%, 5 Logos quer): OBEN ueber dem Title (Title
    // startet bei y ~80px, also Icons mit max 62-74px Hoehe muessen
    // moeglichst oben sitzen damit sie nicht in den Title hineinragen).
    { src: "assets/tools/gmail.png",      top: "1%",  left: "6%",  depth: 0.9, rot: -4,  size: "md", freqX: 0.40, freqY: 0.30, phase: 0.0 },
    { src: "assets/tools/hubspot.webp",   top: "1%",  left: "22%", depth: 1.0, rot: 6,   size: "md", freqX: 0.50, freqY: 0.40, phase: 2.4 },
    { src: "assets/tools/clay.webp",      top: "1%",  left: "50%", depth: 1.0, rot: 8,   size: "",   freqX: 0.30, freqY: 0.55, phase: 1.8 },
    { src: "assets/tools/workspace.webp", top: "1%",  left: "78%", depth: 0.9, rot: -6,  size: "",   freqX: 0.35, freqY: 0.50, phase: 3.4 },
    { src: "assets/tools/slack.webp",     top: "0%",  left: "91%", depth: 1.1, rot: 6,   size: "lg", freqX: 0.40, freqY: 0.45, phase: 5.0 },

    // Reihe 2 (y 24-28%, 2 Logos NUR an den Raendern wegen Title-Zone)
    { src: "assets/tools/n8n.webp",      top: "24%", left: "3%",  depth: 1.4, rot: -10, size: "lg", freqX: 0.35, freqY: 0.50, phase: 1.2 },
    { src: "assets/tools/sheets.webp",   top: "24%", left: "92%", depth: 0.9, rot: -4,  size: "md", freqX: 0.50, freqY: 0.35, phase: 4.1 },

    // Reihe 3 (y 48-52%, 2 Logos NUR an den Raendern wegen Title-Zone)
    { src: "assets/tools/whatsapp.webp", top: "50%", left: "4%",  depth: 0.7, rot: 4,   size: "",   freqX: 0.55, freqY: 0.30, phase: 4.2 },
    { src: "assets/tools/telegram.webp", top: "48%", left: "92%", depth: 1.0, rot: 3,   size: "md", freqX: 0.45, freqY: 0.50, phase: 3.6 },

    // Reihe 4 (y 85-88%, 4 Logos quer): UNTEN unter Title + CTAs
    { src: "assets/tools/ms365.webp",    top: "88%", left: "12%", depth: 0.8, rot: -7,  size: "",   freqX: 0.45, freqY: 0.35, phase: 3.0 },
    { src: "assets/tools/notion.webp",   top: "85%", left: "22%", depth: 1.2, rot: -3,  size: "md", freqX: 0.40, freqY: 0.45, phase: 0.6 },
    { src: "assets/tools/datev.webp",    top: "88%", left: "72%", depth: 0.9, rot: -5,  size: "",   freqX: 0.40, freqY: 0.40, phase: 4.8 },
    { src: "assets/tools/outlook.webp",  top: "88%", left: "90%", depth: 0.7, rot: 2,   size: "",   freqX: 0.55, freqY: 0.30, phase: 5.7 },
  ];

  // Workflow-Verbindungen: Bezier-Kurven zwischen Tools.
  // Indices: 0-4 Reihe 1, 5-6 Reihe 2, 7-8 Reihe 3, 9-12 Reihe 4.
  // Mix aus horizontalen Linien + kurzen Saeulen + Diagonalen (X-Cross
  // in der Mitte und Back-Loops wie auf Mobile mit [3,1]), damit es nicht
  // wie ein steifes Gitter aussieht. KEINE langen Cross-Linien quer
  // durchs Bild von Reihe 1 nach Reihe 4.
  const CONNECTIONS = [
    // Horizontal innerhalb der Reihen
    [0, 1], [1, 2], [2, 3], [3, 4],   // Reihe 1
    [9, 10], [10, 11], [11, 12],      // Reihe 4
    // Kurze vertikale Verbindungen zwischen Nachbar-Reihen (Saeulen)
    [0, 5],  [4, 6],                  // Reihe 1 -> Reihe 2 (Ecken)
    [5, 7],  [6, 8],                  // Reihe 2 -> Reihe 3 (Saeulen)
    [7, 9],  [8, 12],                 // Reihe 3 -> Reihe 4 (Ecken)
    // Diagonal-Cross im Mittelbereich (zwischen Title-Zonen-Raendern):
    // n8n -> telegram (/) und sheets -> whatsapp (\) kreuzen sich
    // in der Mitte. Ersetzen die alten parallelen Mittel-Horizontalen.
    [5, 8],                           // n8n  -> telegram (Diagonale /)
    [6, 7],                           // sheets -> whatsapp (Diagonale \)
    // Back-Loops (Pfad geht zurueck zu frueheren Knoten, wie [3,1] Mobile):
    [2, 5],                           // clay (top mitte) -> n8n (row2 links)
    [10, 7],                          // notion (row4 links) -> whatsapp (row3 links)
  ];

  useEffect(() => {
    const container = containerRef.current;
    const canvas = canvasRef.current;
    if (!container || !canvas) return;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const cards = Array.from(container.querySelectorAll(".tool-float"));
    if (cards.length === 0) return;

    const ctx = canvas.getContext("2d");
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    const resizeCanvas = () => {
      const rect = container.getBoundingClientRect();
      canvas.width = rect.width * dpr;
      canvas.height = rect.height * dpr;
      canvas.style.width = rect.width + "px";
      canvas.style.height = rect.height + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resizeCanvas();

    // Bezier-Helper: berechnet Punkt auf einer kubischen Bezier-Kurve bei Parameter u in [0,1]
    const bezierPoint = (u, p0, p1, p2, p3) => {
      const mt = 1 - u;
      const mt2 = mt * mt;
      const u2 = u * u;
      return {
        x: mt2 * mt * p0.x + 3 * mt2 * u * p1.x + 3 * mt * u2 * p2.x + u2 * u * p3.x,
        y: mt2 * mt * p0.y + 3 * mt2 * u * p1.y + 3 * mt * u2 * p2.y + u2 * u * p3.y,
      };
    };

    // Linien werden sequentiell aufgebaut: jede CONNECTION startet GROWTH_STAGGER
    // Sekunden nach der vorherigen und waechst in GROWTH_DURATION Sekunden von
    // Logo zu Logo. Waehrend des Wachstums sitzt der Pulse-Punkt an der Spitze
    // und "zieht" die Linie. Wenn alle Linien fertig sind, wandert der Pulse
    // zyklisch.
    const GROWTH_DURATION = 1.4;
    const GROWTH_STAGGER = 0.55;

    const drawCanvas = (t) => {
      const rect = container.getBoundingClientRect();
      ctx.clearRect(0, 0, rect.width, rect.height);
      // Aktuelle Card-Center berechnen
      const centers = cards.map((card) => {
        const cr = card.getBoundingClientRect();
        return {
          x: cr.left - rect.left + cr.width / 2,
          y: cr.top - rect.top + cr.height / 2,
        };
      });
      CONNECTIONS.forEach(([a, b], idx) => {
        const p0 = centers[a]; const p3 = centers[b];
        if (!p0 || !p3) return;

        // Wachstums-Progress: 0 = noch nicht gestartet, 1 = voll gezeichnet
        const startT = idx * GROWTH_STAGGER;
        const rawProgress = (t - startT) / GROWTH_DURATION;
        if (rawProgress <= 0) return;
        // Easing fuer organisches Wachstum (cubic-out)
        const linear = Math.min(1, rawProgress);
        const progress = 1 - Math.pow(1 - linear, 3);

        // Control-Points: leicht versetzt vertikal damit Linie geschwungen wirkt
        const dx = p3.x - p0.x;
        const sway = 18 + Math.sin(t * 0.4 + idx * 0.7) * 14;
        const dir = idx % 2 === 0 ? 1 : -1;
        const cp1 = { x: p0.x + dx * 0.33, y: p0.y + sway * dir };
        const cp2 = { x: p0.x + dx * 0.66, y: p3.y - sway * dir };

        // Linie progressiv zeichnen: nur bis u = progress
        const steps = 48;
        const maxStep = Math.max(1, Math.floor(steps * progress));
        ctx.beginPath();
        for (let i = 0; i <= maxStep; i++) {
          const u = (i / steps) * progress;
          const pt = bezierPoint(u, p0, cp1, cp2, p3);
          if (i === 0) ctx.moveTo(pt.x, pt.y);
          else ctx.lineTo(pt.x, pt.y);
        }
        ctx.strokeStyle = "rgba(83, 74, 183, 0.22)";
        ctx.lineWidth = 1.4;
        ctx.lineCap = "round";
        ctx.stroke();

        // Pulse-Punkt: waehrend Wachstum an der Spitze, danach zyklisch.
        // Nach 2 vollen Pulse-Zyklen pro Linie sanft auf 30% opacity dimmen,
        // damit die Animation nicht permanent vom Lesen ablenkt. Fade-Out
        // ueber einen weiteren halben Zyklus damit es nicht abrupt cutet.
        let pp;
        let pulseAlpha = 1; // Multiplikator auf die Pulse-Farben
        if (rawProgress < 1) {
          // An der Wachstums-Spitze - voll hell
          pp = bezierPoint(progress, p0, cp1, cp2, p3);
        } else {
          // Zyklisch entlang der fertigen Linie
          const speed = 0.12;
          const offset = idx * 0.13;
          const u = ((t * speed + offset) % 1);
          pp = bezierPoint(u, p0, cp1, cp2, p3);

          // Zyklen seit Wachstum fertig (kontinuierlich, inkl. Bruchteilen)
          const cyclesDone = (t - (startT + GROWTH_DURATION)) * speed;
          const DIM_AFTER_CYCLES = 2;
          const FADE_DURATION_CYCLES = 0.5;
          const DIM_TARGET = 0.3;
          if (cyclesDone > DIM_AFTER_CYCLES) {
            const fadeT = Math.min(1, (cyclesDone - DIM_AFTER_CYCLES) / FADE_DURATION_CYCLES);
            // ease-out cubic - sanfter Abklang
            const eased = 1 - Math.pow(1 - fadeT, 3);
            pulseAlpha = 1 - (1 - DIM_TARGET) * eased;
          }
        }
        ctx.beginPath();
        ctx.arc(pp.x, pp.y, 3.6, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(127, 112, 222, ${(0.78 * pulseAlpha).toFixed(3)})`;
        ctx.fill();
        // Sanfter Glow
        ctx.beginPath();
        ctx.arc(pp.x, pp.y, 8, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(127, 112, 222, ${(0.20 * pulseAlpha).toFixed(3)})`;
        ctx.fill();
      });
    };

    // Star-Burst Initial-Animation: Cards starten visuell im Center des Heros
    // und expandieren auf ihre finale top/left Position.
    // Dafuer setzen wir --bx und --by auf die negative Differenz zur Center.
    const setupBurstOrigin = () => {
      const containerRect = container.getBoundingClientRect();
      const cx = containerRect.width / 2;
      const cy = containerRect.height / 2;
      cards.forEach((card) => {
        const cardRect = card.getBoundingClientRect();
        // Card-Center in Container-Koordinaten:
        const cardCx = cardRect.left - containerRect.left + cardRect.width / 2;
        const cardCy = cardRect.top - containerRect.top + cardRect.height / 2;
        // Initial-Offset = vom Container-Center bis zur final-Position invertiert
        // damit die Card optisch im Center startet:
        const dx = cx - cardCx;
        const dy = cy - cardCy;
        card.style.setProperty("--bx", `${dx}px`);
        card.style.setProperty("--by", `${dy}px`);
      });
    };

    // Cards positionieren bevor die Transition triggert
    setupBurstOrigin();
    // Nochmal nach kurzer Zeit falls Layout sich aendert
    const onResize = () => { setupBurstOrigin(); resizeCanvas(); };
    window.addEventListener("resize", onResize, { passive: true });

    // Phase 1: Initial-Reveal via CSS-Transition (Cards fliegen aus Center raus)
    let lastDelayMs = 0;
    cards.forEach((card) => {
      const initialDelay = parseFloat(card.dataset.initialDelay) || 0;
      const ms = initialDelay * 1000;
      lastDelayMs = Math.max(lastDelayMs, ms);
      setTimeout(() => card.classList.add("is-in"), ms);
    });

    if (reduceMotion) return;

    // Phase 2: Live-Loop (Eigenbewegung + Maus-Parallax)
    const transitionMs = 900;
    const liveStartAt = lastDelayMs + transitionMs;

    const state = new Map();
    cards.forEach((card) => {
      state.set(card, {
        bx: 0, by: 0,
        mxOff: 0, myOff: 0,
        depth: parseFloat(card.dataset.depth) || 1,
        rot: parseFloat(card.dataset.rot) || 0,
        freqX: parseFloat(card.dataset.freqx) || 0.4,
        freqY: parseFloat(card.dataset.freqy) || 0.4,
        phase: parseFloat(card.dataset.phase) || 0,
      });
    });

    let mx = 0, my = 0;
    const isMobile = window.innerWidth < 720;
    const updateMouse = (e) => {
      const rect = container.getBoundingClientRect();
      mx = e.clientX - rect.left - rect.width / 2;
      my = e.clientY - rect.top - rect.height / 2;
    };
    if (!isMobile) {
      window.addEventListener("mousemove", updateMouse, { passive: true });
    }

    let raf = null;
    let frameCount = 0;
    let liveStarted = false;
    let liveStartTime = 0;
    // Performance-Fix 2026-05-13: Loop pausiert wenn Hero ausserhalb
    // Viewport. Vorher lief das Canvas-Drawing + transform-Updates der
    // 14 Tool-Cards permanent, auch wenn der User schon weit unten war.
    let isVisible = true;
    // Maus-Parallax dezenter (war -0.35)
    const sensitivity = -0.10;
    const easing = 0.06;
    // Eigenbewegung-Amplitude reduziert damit Cards in der enger gepackten
    // 2-Saeulen-Anordnung sich auch in Spitzen-Positionen nicht ueberlappen.
    const ampX = 6;
    const ampY = 5;

    setTimeout(() => {
      liveStarted = true;
      liveStartTime = performance.now();
    }, liveStartAt);

    const tick = (now) => {
      if (!isVisible) { raf = null; return; }
      frameCount += 1;
      if (frameCount % 2 !== 0) {
        raf = requestAnimationFrame(tick);
        return;
      }
      if (liveStarted) {
        const t = (now - liveStartTime) / 1000;
        cards.forEach((card) => {
          const s = state.get(card);

          const targetBx = Math.sin(t * s.freqX + s.phase) * ampX;
          const targetBy = Math.cos(t * s.freqY + s.phase * 1.3) * ampY;
          s.bx += (targetBx - s.bx) * 0.08;
          s.by += (targetBy - s.by) * 0.08;

          const strength = (s.depth * sensitivity) / 25;
          const targetMx = mx * strength;
          const targetMy = my * strength;
          s.mxOff += (targetMx - s.mxOff) * easing;
          s.myOff += (targetMy - s.myOff) * easing;

          const totalX = s.bx + s.mxOff;
          const totalY = s.by + s.myOff;
          const rotSway = Math.sin(t * s.freqX * 0.5 + s.phase) * 1.5;

          card.style.transform =
            `translate3d(${totalX.toFixed(2)}px, ${totalY.toFixed(2)}px, 0) rotate(${(s.rot + rotSway).toFixed(2)}deg) scale(1)`;
        });
        // Canvas-Layer mit Bezier-Verbindungen + Pulse-Punkten
        drawCanvas(t);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    // IntersectionObserver: Loop pausieren wenn Hero ausserhalb Viewport.
    const io = new IntersectionObserver((entries) => {
      isVisible = entries[0].isIntersecting;
      if (isVisible && raf === null) {
        raf = requestAnimationFrame(tick);
      }
    }, { threshold: 0, rootMargin: "100px" });
    io.observe(container);

    return () => {
      if (!isMobile) window.removeEventListener("mousemove", updateMouse);
      window.removeEventListener("resize", onResize);
      if (raf) cancelAnimationFrame(raf);
      io.disconnect();
    };
  }, []);

  return (
    <div ref={containerRef} className="hero-floating" aria-hidden="true">
      <canvas ref={canvasRef} className="hero-floating__canvas" />
      {tools.map((t, i) => {
        const sizeClass = t.size === "md" ? "tool-float--md" : t.size === "lg" ? "tool-float--lg" : "";
        // Alle Cards starten gleichzeitig (Burst) - kein Stagger
        const initialDelay = 0.15;
        return (
          <div
            key={i}
            className={`tool-float ${sizeClass}`.trim()}
            data-depth={t.depth}
            data-rot={t.rot}
            data-freqx={t.freqX}
            data-freqy={t.freqY}
            data-phase={t.phase}
            data-initial-delay={initialDelay}
            style={{
              top: t.top,
              left: t.left,
              "--rot": `${t.rot}deg`,
            }}
          >
            <img src={t.src} alt="" loading="eager" decoding="async" />
          </div>
        );
      })}
    </div>
  );
}

// -----------------------------
// AnimatedStatNum: parsed Werte wie "20+", "7 Std", "100%", "1-2"
// und wickelt den Zahl-Anteil in einen <span data-counter> so dass
// useAnimations() den Counter beim Eintritt in den Viewport hochzaehlt.
// Prefix und Suffix bleiben statisch, nur die Zahl animiert.
// Sonderfall "1-2" -> prefix "1-", num "2" so dass "1-0" -> "1-2" zaehlt.
// -----------------------------
function AnimatedStatNum({ value }) {
  const str = String(value);
  // Priorisiert: Range > Tausender-Format > einfache Zahl
  // Range "1-2" / "5-10" / "3–6": erste Zahl bleibt, zweite zaehlt hoch
  const rangeMatch = str.match(/^(.*?)(\d+(?:[.,]\d+)?)([–—-])(\d+(?:[.,]\d+)?)(.*)$/);
  if (rangeMatch) {
    const [, pre, a, dash, b, rest] = rangeMatch;
    return React.createElement(React.Fragment, null,
      (pre || "") + a + dash,
      React.createElement("span", {
        className: "counter", "data-counter": b.replace(",", "."),
      }, "0"),
      rest || ""
    );
  }
  // Deutsches Tausender-Format: "1.900", "5.900", "10.000" - Punkt als Tausender,
  // bei mindestens 4 Stellen (also 1.000+). Pattern: \d{1,3}(\.\d{3})+
  const thousandsMatch = str.match(/^(.*?)(\d{1,3}(?:\.\d{3})+)(.*)$/);
  if (thousandsMatch) {
    const [, pre, numStr, rest] = thousandsMatch;
    const targetNum = parseInt(numStr.replace(/\./g, ""), 10);
    return React.createElement(React.Fragment, null,
      pre || "",
      React.createElement("span", {
        className: "counter", "data-counter": String(targetNum),
      }, "0"),
      rest || ""
    );
  }
  // Einfache Zahl mit optionalem Dezimal-Komma/Punkt
  const simpleMatch = str.match(/^([^\d]*)(\d+(?:[.,]\d+)?)(.*)$/);
  if (!simpleMatch) return str;
  const [, prefix, num, suffix] = simpleMatch;
  return React.createElement(React.Fragment, null,
    prefix || "",
    React.createElement("span", {
      className: "counter", "data-counter": num.replace(",", "."),
    }, "0"),
    suffix || ""
  );
}

// Export to window so other Babel files can use them
Object.assign(window, {
  Logo, LogoMark, BrandWave, Icon, Header, Footer, Page, CtaBand, UseCaseModal,
  TOOLS, ToolLogo, ToolChip, HeroFloating, AnimatedStatNum, AnimatedContent,
  TweaksProvider, TweaksPanel, useTweaks,
});
