// Shared hooks, primitives, mock data
const { useState, useEffect, useRef, useMemo, useLayoutEffect, createContext, useContext } = React;

// ---------- Routing ----------
const RouterCtx = createContext({ path: "/", go: () => {} });
const useRoute = () => useContext(RouterCtx);

function parseHash() {
  const h = window.location.hash || "#/";
  const raw = h.replace(/^#/, "");
  const [path, query = ""] = raw.split("?");
  return { path: path || "/", query };
}

function RouterProvider({ children }) {
  const [route, setRoute] = useState(parseHash());
  useEffect(() => {
    const onChange = () => { setRoute(parseHash()); window.scrollTo({ top: 0, behavior: "instant" }); };
    window.addEventListener("hashchange", onChange);
    if (!window.location.hash) window.location.hash = "#/";
    return () => window.removeEventListener("hashchange", onChange);
  }, []);
  const go = (p) => { window.location.hash = "#" + p; };
  return <RouterCtx.Provider value={{ path: route.path, query: route.query, go }}>{children}</RouterCtx.Provider>;
}

function Link({ to, children, className = "", onClick, ...rest }) {
  const { path } = useRoute();
  const isActive = path === to || (to !== "/" && path.startsWith(to));
  return (
    <a href={"#" + to} className={className + (isActive ? " is-active" : "")} onClick={onClick} {...rest}>
      {children}
    </a>
  );
}

// ---------- Hooks ----------
function useReveal(opts = {}) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const reveal = () => {
      el.classList.add("in");
      setTimeout(() => { el.style.transform = "none"; }, 0);
    };
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) { reveal(); io.unobserve(el); }
      });
    }, { threshold: opts.threshold ?? 0.05, rootMargin: opts.rootMargin ?? "0px 0px 0px 0px" });
    io.observe(el);
    // Fallback in case IO never fires: reveal after 600ms anyway.
    const fb = setTimeout(reveal, 600);
    return () => { io.disconnect(); clearTimeout(fb); };
  }, []);
  return ref;
}

function useCountUp(target, { start = 0, duration = 1600, decimals = 0, prefix = "", suffix = "" } = {}) {
  const [val, setVal] = useState(start);
  const ref = useRef(null);
  const started = useRef(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting && !started.current) {
          started.current = true;
          const t0 = performance.now();
          const step = (t) => {
            const k = Math.min(1, (t - t0) / duration);
            // cubic-bezier-ish ease
            const eased = 1 - Math.pow(1 - k, 3);
            setVal(start + (target - start) * eased);
            if (k < 1) requestAnimationFrame(step);
          };
          requestAnimationFrame(step);
        }
      });
    }, { threshold: 0.4 });
    io.observe(el);
    return () => io.disconnect();
  }, [target]);
  const fmt = (n) => prefix + (decimals === 0 ? Math.round(n).toLocaleString() : n.toFixed(decimals)) + suffix;
  return [fmt(val), ref];
}

function useMagnetic(strength = 0.25) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const onMove = (e) => {
      const r = el.getBoundingClientRect();
      const cx = r.left + r.width / 2;
      const cy = r.top + r.height / 2;
      const dx = (e.clientX - cx) * strength;
      const dy = (e.clientY - cy) * strength;
      el.style.transform = `translate(${dx}px, ${dy}px)`;
    };
    const onLeave = () => { el.style.transform = "translate(0,0)"; };
    const parent = el.parentElement;
    parent.addEventListener("mousemove", onMove);
    parent.addEventListener("mouseleave", onLeave);
    return () => { parent.removeEventListener("mousemove", onMove); parent.removeEventListener("mouseleave", onLeave); };
  }, []);
  return ref;
}

// ---------- Primitives ----------
function Eyebrow({ num, children, color }) {
  return (
    <div className="flex items-center gap-3" style={{ flexWrap: "wrap" }}>
      {num != null && <span className="eyebrow-num">{num}</span>}
      <span className="eyebrow" style={color ? { } : null}>
        {color && <span style={{ width: 8, height: 8, background: color, borderRadius: "50%", display: "inline-block" }} />}
        <span style={{ marginLeft: color ? 0 : undefined }}>{children}</span>
      </span>
    </div>
  );
}

function Btn({ to, href, children, variant = "primary", magnetic = false, ...rest }) {
  const cls = "btn " + (variant === "ghost" ? "btn-ghost" : variant === "acid" ? "btn-acid" : "");
  const inner = <>{children} <span className="arrow">→</span></>;
  const node = href
    ? <a href={href} className={cls} {...rest}>{inner}</a>
    : <Link to={to || "/"} className={cls} {...rest}>{inner}</Link>;
  if (magnetic) {
    return <MagneticWrap>{node}</MagneticWrap>;
  }
  return node;
}

function MagneticWrap({ children }) {
  const ref = useMagnetic(0.25);
  return (
    <span style={{ display: "inline-block" }}>
      <span ref={ref} className="magnetic">{children}</span>
    </span>
  );
}

function Marquee({ items, italicIdxs = [] }) {
  const span = (
    <span>
      {items.map((it, i) => (
        <React.Fragment key={i}>
          <span className={italicIdxs.includes(i) ? "acid" : ""} style={italicIdxs.includes(i) ? { fontStyle: "italic" } : null}>{it}</span>
          <span className="dot" />
        </React.Fragment>
      ))}
    </span>
  );
  return (
    <div className="marquee">
      <div className="marquee-track">
        {span}{span}
      </div>
    </div>
  );
}

// Editorial "image" block — typographic composition, not a striped placeholder.
function MediaPh({ label, tr, ratio = "16/10", style, accent = "ink", children }) {
  const palette = {
    ink:    { bg: "#0F0F0E", fg: "#FFFFFF", mark: "oklch(88% 0.21 128)" },
    acid:   { bg: "oklch(88% 0.21 128)", fg: "#0F0F0E", mark: "#5A1A1B" },
    ox:     { bg: "#5A1A1B", fg: "#FFFFFF", mark: "oklch(88% 0.21 128)" },
    cobalt: { bg: "#1E3FFF", fg: "#FFFFFF", mark: "oklch(88% 0.21 128)" },
    cream:  { bg: "#F6F4EF", fg: "#0F0F0E", mark: "#5A1A1B" },
  };
  const p = palette[accent] || palette.ink;
  const isLight = accent === "acid" || accent === "cream";
  // Pull initial from the label so each block reads differently.
  const initial = label ? (label.match(/[A-Za-z0-9]/)?.[0] || "•") : "•";
  return (
    <div style={{
      aspectRatio: ratio, background: p.bg, color: p.fg,
      position: "relative", overflow: "hidden",
      border: accent === "cream" ? "1px solid var(--rule)" : "none",
      ...style,
    }}>
      {/* Faint geometric backdrop (lines, not stripes) */}
      <svg viewBox="0 0 100 100" preserveAspectRatio="none"
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", opacity: isLight ? 0.08 : 0.10, pointerEvents: "none" }}>
        <line x1="0" y1="50" x2="100" y2="50" stroke={p.fg} strokeWidth="0.15" />
        <line x1="50" y1="0" x2="50" y2="100" stroke={p.fg} strokeWidth="0.15" />
        <circle cx="50" cy="50" r="42" stroke={p.fg} strokeWidth="0.15" fill="none" />
        <circle cx="50" cy="50" r="22" stroke={p.fg} strokeWidth="0.15" fill="none" />
      </svg>
      {/* Default typographic composition — only shown when no children passed */}
      {!children && (
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center" }}>
          <span className="serif" style={{
            fontSize: "min(38cqw, 220px)", lineHeight: 0.85, letterSpacing: "-0.04em",
            color: p.fg,
          }}>
            {initial}<span style={{ color: p.mark, fontStyle: "italic" }}>.</span>
          </span>
        </div>
      )}
      {/* Corner labels */}
      {tr && <div style={{ position: "absolute", top: 18, right: 18, color: p.fg, opacity: 0.7, fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase" }}>{tr}</div>}
      {label && <div style={{ position: "absolute", bottom: 18, left: 18, color: p.fg, opacity: 0.7, fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase" }}>{label}</div>}
      {children}
    </div>
  );
}

// ---------- Mock Data ----------
const CASES = [
  {
    slug: "olvera",
    client: "Marque A — Café",
    title: "Faire d'une marque de café de spécialité une activité DTC à 7 chiffres.",
    industry: "DTC / Consommation",
    services: ["Marketing à la performance", "Cycle de vie & CRM", "Production créative"],
    year: "2025",
    location: "Côte Ouest, USA",
    duration: "14 mois",
    accent: "ox",
    metrics: [
      { label: "CAC payant", before: 64, after: 22, suffix: "$" },
      { label: "Taux de rachat", before: 12, after: 41, suffix: "%" },
      { label: "Revenu trimestriel", before: 480, after: 2400, prefix: "$", suffix: "k" },
    ],
    quote: "Ils n'ont pas seulement géré nos campagnes — ils ont reformulé la façon dont nous pensions à notre client. Le brief nous est revenu plus net que le nôtre.",
    quoteAttr: "Co-fondatrice, client confidentiel (DTC Café)",
  },
  {
    slug: "halden",
    client: "Studio B — Cycles",
    title: "Repositionner un fabricant de vélos artisanaux pour le marché européen du fitness.",
    industry: "DTC / Sport",
    services: ["Stratégie de marque", "Contenu & SEO"],
    year: "2024",
    accent: "cobalt",
    location: "Europe du Nord",
    duration: "9 mois",
    metrics: [
      { label: "Trafic organique", before: 11, after: 84, suffix: "k/mois" },
      { label: "Visites en showroom", before: 60, after: 320, suffix: "/mois" },
      { label: "Panier moyen", before: 1840, after: 2410, prefix: "$" },
    ],
    quote: "Nous attendions une campagne. Nous avons obtenu un positionnement que nous utilisons désormais en conseil d'administration.",
    quoteAttr: "Directeur général, client confidentiel (vélos)",
  },
  {
    slug: "pierpoint",
    client: "Client confidentiel — Santé",
    title: "Construire un programme de cycle de vie qui finance l'ensemble de l'équipe croissance.",
    industry: "B2B SaaS / Santé",
    services: ["Cycle de vie & CRM", "Analytique & Attribution"],
    year: "2025",
    accent: "acid",
    location: "Côte Est, USA",
    duration: "En cours, 18 mois",
    metrics: [
      { label: "ARR d'expansion", before: 0, after: 4.1, prefix: "$", suffix: "M" },
      { label: "Taux d'activation", before: 38, after: 71, suffix: "%" },
      { label: "Essai → Payant", before: 14, after: 33, suffix: "%" },
    ],
    quote: "Les tableaux de bord seuls ont remplacé un plan d'embauche de 180 000 $.",
    quoteAttr: "VP Croissance, client confidentiel (SaaS Santé)",
  },
  {
    slug: "fernweh",
    client: "Marque C — Voyage",
    title: "Une marque de voyage pour les gens qui détestent le contenu voyage.",
    industry: "Consommation / Voyage",
    services: ["Stratégie de marque", "Production créative"],
    year: "2024",
    accent: "ink",
    location: "Europe du Sud",
    duration: "6 mois",
    metrics: [
      { label: "Inscriptions email", before: 8400, after: 61200 },
      { label: "Retombées presse", before: 2, after: 24 },
      { label: "Valeur des réservations", before: 1.1, after: 3.8, prefix: "$", suffix: "M" },
    ],
    quote: "Je n'ai jamais été aussi peu gênée par notre propre page d'accueil.",
    quoteAttr: "Fondatrice, client confidentiel (Voyage)",
  },
  {
    slug: "northwind",
    client: "Studio D — Outdoor",
    title: "Un système de canaux propriétaires qui résiste au chaos des plateformes publicitaires.",
    industry: "DTC / Outdoor",
    services: ["Cycle de vie & CRM", "Contenu & SEO", "Analytique & Attribution"],
    year: "2025",
    accent: "ox",
    location: "Rocheuses, USA",
    duration: "11 mois",
    metrics: [
      { label: "Part du revenu email", before: 9, after: 38, suffix: "%" },
      { label: "Clics organiques", before: 22, after: 188, suffix: "k/mois" },
      { label: "Part du budget Meta", before: 71, after: 28, suffix: "%" },
    ],
    quote: "Nous avons enfin cessé de paniquer chaque fois que l'algorithme Meta éternuait.",
    quoteAttr: "Responsable Croissance, client confidentiel (Outdoor)",
  },
  {
    slug: "marlowe",
    client: "Cabinet E — Patrimoine",
    title: "Nommer et lancer une marque de gestion de patrimoine en 11 semaines.",
    industry: "Finance",
    services: ["Stratégie de marque", "Production créative"],
    year: "2025",
    accent: "cobalt",
    location: "Europe de l'Ouest",
    duration: "11 semaines",
    metrics: [
      { label: "Encours à 6 mois", before: 0, after: 240, prefix: "$", suffix: "M" },
      { label: "Demandes entrantes", before: 0, after: 1100 },
      { label: "Cycle de vente", before: 90, after: 38, suffix: " jours" },
    ],
    quote: "Ils nous ont nommés dès la deuxième semaine. Nous utilisons toujours cette signature sur notre porte d'entrée.",
    quoteAttr: "Associée fondatrice, client confidentiel (Patrimoine)",
  },
];

const SERVICES = [
  { slug: "brand-strategy", num: "01", title: "Stratégie de marque", lead: "Positionnement, nommage, messages, voix. Nous rédigeons le document opérationnel que votre équipe ouvre vraiment.", deliverables: ["Architecture de positionnement", "Nommage + identité verbale", "Cadre de messages", "Guide de marque"], starts: "à partir de 60 k$" },
  { slug: "performance", num: "02", title: "Marketing à la performance", lead: "Acquisition payante qui respecte votre marge. Meta, Google, TikTok, programmatique, et même les canaux moins glamour.", deliverables: ["Architecture des canaux", "Système de tests créatifs", "Opérations enchères + budgets", "P&L hebdomadaire"], starts: "à partir de 28 k$/mois" },
  { slug: "content-seo", num: "03", title: "Contenu & SEO", lead: "Une ligne éditoriale qui se classe parce qu'elle mérite d'être lue. Nous traitons votre blog comme une publication, pas comme une ferme à contenu.", deliverables: ["Stratégie éditoriale", "Cartes d'autorité thématique", "Studio de production", "SEO technique"], starts: "à partir de 22 k$/mois" },
  { slug: "lifecycle-crm", num: "04", title: "Cycle de vie & CRM", lead: "Email, SMS, in-product. Le chiffre d'affaires que vous avez déjà payé, qui se met enfin à capitaliser.", deliverables: ["Architecture de cycle de vie", "Moteur de segmentation", "Système créatif", "Reporting"], starts: "à partir de 18 k$/mois" },
  { slug: "creative-production", num: "05", title: "Production créative", lead: "Photo, motion, design, créations publicitaires — produits en interne, rapidement, fidèlement à votre marque. Pas de photos de stock.", deliverables: ["Photo + film", "Motion + montage", "Unités créatives", "Système de design"], starts: "à partir de 30 k$/mois" },
  { slug: "analytics", num: "06", title: "Analytique & Attribution", lead: "Mesure mixte : MMM, incrémentalité, geo-lift, MTA lorsque c'est encore utile.", deliverables: ["Plan de mesure", "Modèle MMM", "Tests d'incrémentalité", "Infrastructure de reporting"], starts: "à partir de 40 k$" },
];

const TEAM = [
  { name: "Inès Marchetti", role: "Fondatrice, Directrice de la création", city: "Cedar Rapids", tool: "Crayon + un Moleskine papier, toujours.", opinion: "Un brief qui ne tient pas sur une page n'est pas un brief.", years: 14, accent: "acid" },
  { name: "Daiyu Okafor", role: "Responsable stratégie", city: "Chicago", tool: "Figma + un mur de Post-it", opinion: "Si vous ne pouvez pas le dire sans « leverage », ne le dites pas.", years: 11, accent: "ox" },
  { name: "Rune Halvorsen", role: "Responsable performance", city: "Distance", tool: "Looker + café glacé", opinion: "Le ROAS sans marge sur coûts directs, c'est du théâtre.", years: 9, accent: "cobalt" },
  { name: "Pilar Quesada", role: "Directrice création, Production", city: "Distance", tool: "DaVinci Resolve à 2 h du matin", opinion: "La photo lifestyle générique est une scène de crime.", years: 12, accent: "ink" },
  { name: "Théo Krammer", role: "Responsable ingénierie, Plateforme", city: "Distance", tool: "Neovim, avec un peu de honte", opinion: "Les dashboards sont un produit. Traitez-les comme tel.", years: 13, accent: "acid" },
  { name: "Sade Owolabi", role: "Responsable cycle de vie", city: "Distance", tool: "Notion + un vrai carnet", opinion: "La plupart des newsletters seraient meilleures en un bon texto.", years: 8, accent: "ox" },
  { name: "Lior Ben-Ari", role: "Responsable analytique", city: "Distance", tool: "DuckDB, parcimonieusement Excel", opinion: "Le MTA est surtout du storytelling. Racontez de meilleures histoires.", years: 10, accent: "cobalt" },
  { name: "June Watanabe", role: "Directrice éditoriale", city: "Distance", tool: "Scrivener, stylo plume", opinion: "La plupart du « thought leadership » n'est pas lu par les thought leaders.", years: 9, accent: "ink" },
];

const ARTICLES = [
  { slug: "stop-optimizing", title: "Arrêtez d'optimiser le funnel. Construisez une meilleure porte d'entrée.", category: "Stratégie", author: "Inès Marchetti", date: "02 mai 2026", read: 7 },
  { slug: "post-mta", title: "Post-MTA : comment nous mesurons réellement les médias payants en 2026.", category: "Performance", author: "Rune Halvorsen", date: "18 avril 2026", read: 11 },
  { slug: "newsletter-or-not", title: "Le contre-plaidoyer du lancement d'une énième newsletter.", category: "Cycle de vie", author: "Sade Owolabi", date: "06 avril 2026", read: 6 },
  { slug: "brief-on-one-page", title: "Le brief d'une page, défendu.", category: "Stratégie", author: "Daiyu Okafor", date: "22 mars 2026", read: 5 },
  { slug: "ads-photoshoot", title: "Pourquoi nos shootings publicitaires prennent 11 jours, pas trois.", category: "Artisanat", author: "Pilar Quesada", date: "10 mars 2026", read: 8 },
  { slug: "reporting-as-product", title: "Le reporting est un produit. Cessez de le traiter comme une corvée.", category: "Plateforme", author: "Théo Krammer", date: "27 février 2026", read: 9 },
  { slug: "geo-lift-cheap", title: "Un geo-lift accessible, presque rigoureux, en cinq jours.", category: "Performance", author: "Lior Ben-Ari", date: "14 février 2026", read: 10 },
  { slug: "voice-cardinal-sin", title: "Le péché capital de la voix de marque : sonner comme tout le monde.", category: "Stratégie", author: "June Watanabe", date: "30 janvier 2026", read: 6 },
];

const ROLES = [
  { title: "Stratège performance senior", team: "Performance", location: "Cedar Rapids ou distance (US)", type: "Temps plein" },
  { title: "Producteur cycle de vie", team: "Cycle de vie & CRM", location: "Distance (EU)", type: "Temps plein" },
  { title: "Stratège de marque", team: "Stratégie", location: "Distance (EU/US)", type: "Temps plein" },
  { title: "Motion Designer (intermédiaire)", team: "Production créative", location: "Distance", type: "Temps plein" },
  { title: "Analytics Engineer", team: "Plateforme", location: "Distance (EU)", type: "Temps plein" },
  { title: "Coordinateur de production", team: "Production créative", location: "Cedar Rapids", type: "Temps plein" },
  { title: "Bourse éditoriale (Fellowship)", team: "Éditorial", location: "Distance (monde entier)", type: "Bourse rémunérée 6 mois" },
];

const OFFICES = [
  { city: "Cedar Rapids", line: "5249 North Park Place NE, place 5945, Cedar Rapids, Iowa 52402 — USA", hours: "Lun–Ven · 9h–18h CT", role: "Siège social + studio" },
  { city: "À distance · EU", line: "Équipe distribuée en Europe", hours: "Lun–Ven · 9h–18h CET", role: "Stratégie + production" },
  { city: "À distance · Amériques", line: "Équipe distribuée en Amérique du Nord", hours: "Lun–Ven · 9h–18h CT", role: "Studio de production" },
  { city: "À distance · Ingénierie", line: "Pôle plateforme distribué", hours: "Lun–Ven · 9h–18h CET", role: "Ingénierie plateforme" },
];

Object.assign(window, {
  React, useState, useEffect, useRef, useMemo, useLayoutEffect,
  RouterProvider, useRoute, Link, Eyebrow, Btn, MagneticWrap, Marquee, MediaPh,
  useReveal, useCountUp, useMagnetic,
  CASES, SERVICES, TEAM, ARTICLES, ROLES, OFFICES,
});
