/* lawandeconomics.ch — v3
   ============================================================
   Sections:
   1. Tokens & thème
   2. Helpers visuels (PlusPattern, EngravingArt, icônes)
   3. ContentContext (charge /content.json)
   4. Header + ThemeToggler
   5. Hero (carte vedette en rotation)
   6. Categories
   7. Publications + PubCard avec hover en éventail
   8. Manifeste
   9. Footer
   10. Modales: CarouselViewer, FinderExplorer
   11. App
   ============================================================ */

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

/* ============================================================
1. TOKENS — design system
============================================================ */

const PALETTE = {
fiscalite:    "#e85d5d",  // 20 CHF
international:"#d4a23a",  // 10 CHF
economie:     "#6a8fd9",  // 100 CHF
politiques:   "#6fa97c",  // 50 CHF
droit:        "#a07a55",  // 200 CHF
arbitrage:    "#8b6fb8",  // 1000 CHF
};

const CATEGORIES = [
{ key: "fiscalite",     label: "Fiscalité",            color: PALETTE.fiscalite,     icon: "fiscalite" },
{ key: "international", label: "International",        color: PALETTE.international, icon: "international" },
{ key: "economie",      label: "Économie",             color: PALETTE.economie,      icon: "economie" },
{ key: "politiques",    label: "Politiques publiques", color: PALETTE.politiques,    icon: "politiques" },
{ key: "droit",         label: "Droit",                color: PALETTE.droit,         icon: "droit" },
{ key: "arbitrage",     label: "Arbitrage",            color: PALETTE.arbitrage,     icon: "arbitrage" },
];

const CAT_BY_KEY = Object.fromEntries(CATEGORIES.map(c => [c.key, c]));

const LIGHT = {
paper:        "#ffffff",
paper2:       "#fafaf7",
gutter:       "#f4f4f1",
ink:          "#15151a",
inkSoft:      "#56565d",
inkMute:      "#9a9aa0",
line:         "#e8e8ea",
lineSoft:     "#f0f0f1",
accent:       "#4a4fb5",
accentSoft:   "#eaebf7",
shadow:       "0 1px 2px rgba(20,20,30,.04), 0 8px 24px rgba(20,20,30,.06)",
shadowHi:     "0 2px 4px rgba(20,20,30,.06), 0 18px 50px rgba(20,20,30,.12)",
};

const DARK = {
paper:        "#0e0e12",
paper2:       "#13131a",
gutter:       "#08080b",
ink:          "#e7e7ee",
inkSoft:      "#a8a8b2",
inkMute:      "#6a6a74",
line:         "#22222b",
lineSoft:     "#1a1a22",
accent:       "#8287e2",
accentSoft:   "#1c1d2e",
shadow:       "0 1px 2px rgba(0,0,0,.5), 0 12px 32px rgba(0,0,0,.4)",
shadowHi:     "0 2px 6px rgba(0,0,0,.6), 0 24px 60px rgba(0,0,0,.55)",
};

const TYPE = {
serif: `"Fraunces", "Times New Roman", Georgia, serif`,
sans:  `"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`,
mono:  `"JetBrains Mono", ui-monospace, "SF Mono", monospace`,
};

/* ============================================================
2. HELPERS VISUELS
============================================================ */

function PlusPattern({ density = 9, opacity = 0.07, color }) {
const id = `plus-${density}-${color.replace("#","")}-${opacity*100|0}`;
return (
<svg width="100%" height="100%" style={{ display: "block", opacity }}>
  <defs>
    <pattern id={id} width={density} height={density} patternUnits="userSpaceOnUse">
      <path
        d={`M ${density/2} ${density/2 - 1.6} L ${density/2} ${density/2 + 1.6} M ${density/2 - 1.6} ${density/2} L ${density/2 + 1.6} ${density/2}`}
        stroke={color} strokeWidth="0.55" strokeLinecap="round"
      />
    </pattern>
  </defs>
  <rect width="100%" height="100%" fill={`url(#${id})`} />
</svg>
);
}

/* Petit cross icon (croix suisse stylisée) */
function CrossMark({ size = 8, color, opacity = 0.5 }) {
return (
<svg width={size} height={size} viewBox="0 0 8 8" style={{ opacity }}>
  <path d="M4 1.5 V 6.5 M 1.5 4 H 6.5" stroke={color} strokeWidth="0.9" strokeLinecap="round" />
</svg>
);
}

/* Engraving art - 11 variantes, utilisé en fallback pour les cartes sans images */
function EngravingArt({ variant = 0, color }) {
const c = color;
const uid = `${variant}-${color.replace("#","")}-${Math.random().toString(36).slice(2,7)}`;
const v = variant % 11;

if (v === 0) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <defs>
    <clipPath id={`cp${uid}a`}><rect x="15" y="10" width="110" height="105" /></clipPath>
    <clipPath id={`cp${uid}b`}><rect x="135" y="65" width="110" height="105" /></clipPath>
  </defs>
  <g clipPath={`url(#cp${uid}a)`}>
    {Array.from({length:36}, (_,i) => <line key={i} x1={-5+i*4.5} y1={10} x2={-35+i*4.5} y2={115} stroke={c} strokeWidth="0.4" opacity="0.5"/>)}
  </g>
  <g clipPath={`url(#cp${uid}b)`}>
    {Array.from({length:36}, (_,i) => <line key={i} x1={125+i*4.5} y1={65} x2={95+i*4.5} y2={170} stroke={c} strokeWidth="0.4" opacity="0.38"/>)}
  </g>
  <rect x="15" y="10" width="110" height="105" fill="none" stroke={c} strokeWidth="0.85" opacity="0.9"/>
  <rect x="135" y="65" width="110" height="105" fill="none" stroke={c} strokeWidth="0.85" opacity="0.9"/>
</svg>
);

if (v === 1) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  {Array.from({length:8}, (_,i) => (
    <circle key={i} cx="130" cy="90" r={(i+1)*10} fill="none" stroke={c} strokeWidth="0.45" opacity={0.9-i*0.09}/>
  ))}
  {Array.from({length:14}, (_,i) => {
    const a = (i/14)*Math.PI*2;
    return <line key={i} x1={130+Math.cos(a)*82} y1={90+Math.sin(a)*82} x2={130+Math.cos(a)*108} y2={90+Math.sin(a)*108} stroke={c} strokeWidth="0.55" opacity="0.6"/>;
  })}
  <circle cx="130" cy="90" r="5" fill={c} opacity="0.85"/>
</svg>
);

if (v === 2) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  {Array.from({length:16}, (_,i) => [
    <line key={`a${i}`} x1={30+i*3} y1={90} x2={100+i*3} y2={28} stroke={c} strokeWidth="0.38" opacity={0.22+i*0.025}/>,
    <line key={`b${i}`} x1={30+i*3} y1={90} x2={100+i*3} y2={152} stroke={c} strokeWidth="0.38" opacity={0.22+i*0.025}/>
  ])}
  <path d="M 30 90 L 200 28" stroke={c} strokeWidth="1.1" fill="none" opacity="0.88"/>
  <path d="M 30 90 L 200 152" stroke={c} strokeWidth="1.1" fill="none" opacity="0.88"/>
  <circle cx="30" cy="90" r="4.5" fill={c} opacity="0.8"/>
</svg>
);

if (v === 3) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <line x1="130" y1="20" x2="130" y2="160" stroke={c} strokeWidth="0.7" opacity="0.7"/>
  <line x1="55" y1="60" x2="205" y2="60" stroke={c} strokeWidth="0.8" opacity="0.8"/>
  <line x1="55" y1="60" x2="72" y2="100" stroke={c} strokeWidth="0.65" opacity="0.7"/>
  <line x1="205" y1="60" x2="188" y2="100" stroke={c} strokeWidth="0.65" opacity="0.7"/>
  <rect x="52" y="100" width="44" height="22" fill="none" stroke={c} strokeWidth="0.65" opacity="0.75"/>
  <rect x="164" y="100" width="44" height="22" fill="none" stroke={c} strokeWidth="0.65" opacity="0.75"/>
  <circle cx="130" cy="22" r="4.5" fill={c} opacity="0.75"/>
</svg>
);

if (v === 4) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  {Array.from({length:6}, (_,i) => (
    <rect key={i} x={18+i*17} y={12+i*12} width={224-i*34} height={156-i*24} fill="none" stroke={c} strokeWidth="0.5" opacity={0.85-i*0.1}/>
  ))}
  <circle cx="130" cy="90" r="18" fill="none" stroke={c} strokeWidth="0.8" opacity="0.88"/>
  <circle cx="130" cy="90" r="5" fill={c} opacity="0.82"/>
</svg>
);

if (v === 5) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <ellipse cx="130" cy="90" rx="95" ry="78" fill="none" stroke={c} strokeWidth="0.85" opacity="0.88"/>
  {[-70,-35,0,35,70].map((shift, i) => {
    const ry2 = Math.sqrt(Math.max(1, 78*78 - shift*shift));
    return <ellipse key={i} cx="130" cy={90+shift} rx={ry2*95/78} ry={Math.max(2,ry2*0.18)} fill="none" stroke={c} strokeWidth="0.42" opacity="0.48"/>;
  })}
  {[0,1,2,3,4].map(i => (
    <ellipse key={i} cx="130" cy="90" rx={Math.max(1, Math.sin((i/5)*Math.PI)*95)} ry="78" fill="none" stroke={c} strokeWidth="0.42" opacity="0.45"/>
  ))}
</svg>
);

if (v === 6) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <defs>
    <clipPath id={`cp${uid}a`}><circle cx="95" cy="90" r="68"/></clipPath>
    <clipPath id={`cp${uid}b`}><circle cx="165" cy="90" r="68"/></clipPath>
  </defs>
  <g clipPath={`url(#cp${uid}a)`}>
    {Array.from({length:32}, (_,i) => <line key={i} x1={15+i*4.5} y1={20} x2={-5+i*4.5} y2={160} stroke={c} strokeWidth="0.38" opacity="0.45"/>)}
  </g>
  <g clipPath={`url(#cp${uid}b)`}>
    {Array.from({length:32}, (_,i) => <line key={i} x1={95+i*4.5} y1={20} x2={75+i*4.5} y2={160} stroke={c} strokeWidth="0.38" opacity="0.35"/>)}
  </g>
  <circle cx="95" cy="90" r="68" fill="none" stroke={c} strokeWidth="0.88" opacity="0.88"/>
  <circle cx="165" cy="90" r="68" fill="none" stroke={c} strokeWidth="0.88" opacity="0.88"/>
</svg>
);

if (v === 7) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  {Array.from({length:20}, (_,i) => (
    <line key={i} x1={130-(18+i*11)/2} y1={18+i*8} x2={130+(18+i*11)/2} y2={18+i*8} stroke={c} strokeWidth="0.5" opacity={0.28+(i/19)*0.55}/>
  ))}
  <path d="M 130 12 L 248 175 L 12 175 Z" fill="none" stroke={c} strokeWidth="1" opacity="0.88"/>
  <circle cx="130" cy="16" r="4.5" fill={c} opacity="0.78"/>
</svg>
);

if (v === 8) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <path d="M 20 165 Q 130 10 240 165" fill="none" stroke={c} strokeWidth="1.1" opacity="0.9"/>
  {Array.from({length:5}, (_,i) => (
    <path key={i} d={`M ${20+i*14} ${165-i*5} Q 130 ${10+i*18} ${240-i*14} ${165-i*5}`} fill="none" stroke={c} strokeWidth="0.38" opacity={0.48-i*0.07}/>
  ))}
  <line x1="20" y1="165" x2="240" y2="165" stroke={c} strokeWidth="0.6" opacity="0.5"/>
  <circle cx="130" cy="68" r="4" fill={c} opacity="0.72"/>
</svg>
);

if (v === 9) return (
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <rect x="38" y="28" width="184" height="130" fill="none" stroke={c} strokeWidth="0.88" opacity="0.88"/>
  {Array.from({length:15}, (_,i) => {
    const col = i % 5, row = Math.floor(i / 5);
    return <rect key={i} x={50+col*32} y={45+row*32} width="16" height="20" fill="none" stroke={c} strokeWidth="0.4" opacity="0.55"/>;
  })}
  <rect x="108" y="118" width="44" height="40" fill="none" stroke={c} strokeWidth="0.6" opacity="0.65"/>
</svg>
);

return ( /* v === 10 */
<svg viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet" width="100%" height="100%">
  <path d="M 35 62 Q 130 24 225 62" fill="none" stroke={c} strokeWidth="1.1" opacity="0.88"/>
  <polyline points="213,52 225,62 213,70" fill="none" stroke={c} strokeWidth="1.1" opacity="0.88"/>
  <path d="M 225 118 Q 130 156 35 118" fill="none" stroke={c} strokeWidth="1.1" opacity="0.88"/>
  <polyline points="47,110 35,118 47,126" fill="none" stroke={c} strokeWidth="1.1" opacity="0.88"/>
</svg>
);
}

/* Icônes de catégorie en SVG fait main */
function CategoryIcon({ kind, color, animated }) {
const sw = 1.2;
const baseStyle = {
transition: "transform 420ms cubic-bezier(.34,1.4,.64,1)",
transformOrigin: "center",
transform: animated ? "scale(1.06)" : "scale(1)",
};
if (kind === "fiscalite") {
const dots = [];
for (let y = 0; y < 4; y++)
  for (let x = 0; x < 6; x++) dots.push(
    <circle key={`${x}-${y}`} cx={6+x*6} cy={6+y*6} r={animated ? 1.2 : 1} fill={color}
      style={{transition:`r 300ms ${(x+y)*15}ms cubic-bezier(.34,1.4,.64,1)`}}/>
  );
return <svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>{dots}</svg>;
}
if (kind === "economie") return (
<svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>
  <rect x="4"  y={animated?16:18} width="6" height={animated?12:10} fill="none" stroke={color} strokeWidth={sw} style={{transition:"all 280ms cubic-bezier(.34,1.4,.64,1)"}}/>
  <rect x="14" y={animated?10:12} width="6" height={animated?18:16} fill="none" stroke={color} strokeWidth={sw} style={{transition:"all 280ms 60ms cubic-bezier(.34,1.4,.64,1)"}}/>
  <rect x="24" y={animated?4:6}   width="6" height={animated?24:22} fill="none" stroke={color} strokeWidth={sw} style={{transition:"all 280ms 120ms cubic-bezier(.34,1.4,.64,1)"}}/>
</svg>
);
if (kind === "droit") return (
<svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>
  <line x1="6"  y1="8"  x2={animated?32:34} y2="8"  stroke={color} strokeWidth={sw} style={{transition:"all 280ms cubic-bezier(.34,1.4,.64,1)"}}/>
  <line x1="6"  y1="15" x2={animated?28:34} y2="15" stroke={color} strokeWidth={sw} style={{transition:"all 280ms 80ms cubic-bezier(.34,1.4,.64,1)"}}/>
  <line x1="6"  y1="22" x2={animated?30:34} y2="22" stroke={color} strokeWidth={sw} style={{transition:"all 280ms 140ms cubic-bezier(.34,1.4,.64,1)"}}/>
</svg>
);
if (kind === "international") return (
<svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>
  <circle cx="20" cy="15" r="11" fill="none" stroke={color} strokeWidth={sw}/>
  <ellipse cx="20" cy="15" rx="5" ry="11" fill="none" stroke={color} strokeWidth={sw}
    style={{transformOrigin:"20px 15px", transform: animated ? "rotateY(40deg)" : "rotateY(0)", transition:"transform 500ms cubic-bezier(.4,0,.2,1)"}}/>
  <line x1="9" y1="15" x2="31" y2="15" stroke={color} strokeWidth={sw}/>
</svg>
);
if (kind === "politiques") {
const dots = [];
for (let y = 0; y < 4; y++)
  for (let x = 0; x < 4; x++) dots.push(
    <circle key={`${x}-${y}`} cx={10+x*6} cy={6+y*6} r={animated ? 1.2 : 1} fill={color}
      style={{transition:`r 300ms ${(3-x+y)*15}ms cubic-bezier(.34,1.4,.64,1)`}}/>
  );
return <svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>{dots}</svg>;
}
if (kind === "arbitrage") return (
<svg viewBox="0 0 40 30" width="40" height="30" style={baseStyle}>
  <line x1="20" y1="4" x2="20" y2="26" stroke={color} strokeWidth={sw}/>
  <line x1="6" y1="12" x2="34" y2="12" stroke={color} strokeWidth={sw}/>
  <polygon points="6,12 2,20 10,20" fill={animated ? color : "none"} stroke={color} strokeWidth={sw}
    style={{transition:"fill 240ms"}}/>
  <polygon points="34,12 30,20 38,20" fill="none" stroke={color} strokeWidth={sw}/>
</svg>
);
return null;
}

/* ============================================================
3. CONTEXTE CONTENU — charge content.json
============================================================ */

const ContentCtx = createContext({ items: [], byCat: {}, tree: {}, ready: false });

function useContent() { return useContext(ContentCtx); }

function ContentProvider({ children }) {
const [state, setState] = useState({ items: [], byCat: {}, tree: {}, ready: false });

useEffect(() => {
let alive = true;
fetch("/content.json", { cache: "no-cache" })
  .then(r => r.ok ? r.json() : { items: [] })
  .catch(() => ({ items: [] }))
  .then(json => {
    if (!alive) return;
    const items = (json.items || []).map(it => ({
      ...it,
      color: CAT_BY_KEY[it.category]?.color || "#999",
      categoryLabel: CAT_BY_KEY[it.category]?.label || it.category,
    }));
    const byCat = {};
    for (const it of items) (byCat[it.category] = byCat[it.category] || []).push(it);

    // build folder tree for finder
    const tree = {};
    for (const it of items) {
      let node = tree;
      const parts = [it.category, ...(it.topicPath || []), it.slug];
      parts.forEach((p, idx) => {
        node[p] = node[p] || { _children: {}, _items: [], _key: p };
        if (idx === parts.length - 1) {
          node[p]._concept = it;
        }
        node = node[p]._children;
      });
    }
    setState({ items, byCat, tree, ready: true });
  });
return () => { alive = false; };
}, []);

return <ContentCtx.Provider value={state}>{children}</ContentCtx.Provider>;
}

/* ============================================================
4. HEADER + ThemeToggler
============================================================ */

function ThemeToggler({ isDark, onToggle, t }) {
return (
<button
  onClick={onToggle}
  className="hdr-icon"
  style={{
    background: "transparent", border: 0,
    borderLeft: `1px solid ${t.line}`,
    cursor: "pointer", padding: 0,
    display: "flex", alignItems: "center", justifyContent: "center",
    width: 56, color: t.ink,
  }}
  aria-label={isDark ? "Mode jour" : "Mode nuit"}
>
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="1.6" strokeLinecap="round"
       style={{
         transform: isDark ? "rotate(210deg)" : "rotate(0deg)",
         transition: "transform 600ms cubic-bezier(.34,1.3,.64,1)",
       }}>
    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
          style={{opacity: isDark?1:0, transition:"opacity 350ms"}}/>
    <circle cx="12" cy="12" r="4.8" fill="currentColor" stroke="none"
            style={{opacity: isDark?0:1, transition:"opacity 350ms"}}/>
    {[0,45,90,135,180,225,270,315].map((deg, i) => {
      const r = Math.PI*deg/180;
      return <line key={i}
        x1={12+Math.cos(r)*7} y1={12+Math.sin(r)*7}
        x2={12+Math.cos(r)*9.2} y2={12+Math.sin(r)*9.2}
        style={{opacity: isDark?0:1, transition:`opacity 300ms ${i*20}ms`}}/>;
    })}
  </svg>
</button>
);
}

function FolderIcon({ size = 18 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
  <path d="M3 6.5 a 1.5 1.5 0 0 1 1.5 -1.5 H 9 l 2 2 H 19.5 a 1.5 1.5 0 0 1 1.5 1.5 V 18 a 1.5 1.5 0 0 1 -1.5 1.5 H 4.5 a 1.5 1.5 0 0 1 -1.5 -1.5 Z" strokeLinejoin="round"/>
</svg>
);
}

function Header({ t, isDark, onToggleTheme, onOpenFinder }) {
return (
<header style={{
  borderBottom: `1px solid ${t.line}`,
  display: "grid",
  gridTemplateColumns: "1fr auto 56px 56px 56px",
  alignItems: "stretch",
  background: t.paper,
  position: "sticky", top: 0, zIndex: 50,
  backdropFilter: "saturate(160%) blur(8px)",
  WebkitBackdropFilter: "saturate(160%) blur(8px)",
  transition: "background 400ms, border-color 400ms",
}}>
  <div style={{ padding: "22px 28px", display: "flex", alignItems: "center" }}>
    <a href="#" style={{
      textDecoration: "none", color: t.ink, fontSize: 16,
      letterSpacing: "-0.01em", fontWeight: 500,
      display: "inline-flex", alignItems: "baseline", gap: 0,
    }}>
      <span style={{ position: "relative" }}>
        lawandeconomics
        <span style={{
          position: "absolute", left: 0, bottom: -2, height: 1, width: "100%",
          background: t.accent, transform: "scaleX(0)", transformOrigin: "left",
          transition: "transform 320ms cubic-bezier(.4,0,.2,1)",
        }} className="logo-line"/>
      </span>
      <span style={{ opacity: 0.4 }}>.ch</span>
    </a>
  </div>
  <nav style={{ display: "flex", alignItems: "center", gap: 32, padding: "0 28px" }}>
    {["Concepts", "Thématiques", "À propos", "Ressources"].map(n => (
      <a key={n} href="#" className="nav-link" style={{
        textDecoration: "none", color: t.ink, fontSize: 10.5,
        letterSpacing: "0.18em", textTransform: "uppercase", fontWeight: 500,
        position: "relative", padding: "4px 0",
      }}>{n}</a>
    ))}
  </nav>
  <button className="hdr-icon" style={iconBtn(t)} aria-label="Recherche">
    <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
      <circle cx="10.5" cy="10.5" r="6.5"/>
      <path d="M15 15 L20 20" strokeLinecap="round"/>
    </svg>
  </button>
  <ThemeToggler isDark={isDark} onToggle={onToggleTheme} t={t}/>
  <button className="hdr-icon" style={iconBtn(t, true)} aria-label="Explorer les dossiers" onClick={onOpenFinder}>
    <FolderIcon/>
  </button>
</header>
);
}
const iconBtn = (t, leftBorder=false) => ({
background: "transparent", border: 0,
borderLeft: `1px solid ${t.line}`,
cursor: "pointer", color: t.ink,
display: "flex", alignItems: "center", justifyContent: "center",
});

/* ============================================================
5. HERO — carte vedette en rotation + cartes en peeking
============================================================ */

function HeroFeaturedCard({ item, t, slideIdx, total }) {
return (
<div className="hero-card" key={item.id} style={{
  background: t.paper,
  border: `1px solid ${t.line}`,
  borderTop: `2px solid ${item.color}`,
  padding: "22px 26px 18px",
  display: "flex", flexDirection: "column",
  height: "100%", position: "relative",
  boxShadow: t.shadowHi,
  animation: "heroIn 540ms cubic-bezier(.4,0,.2,1) both",
}}>
  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 18 }}>
    <span style={{ color: item.color, fontSize: 10.5, letterSpacing: "0.22em", textTransform: "uppercase", fontWeight: 600 }}>
      {item.categoryLabel}
    </span>
    <span style={{ color: t.inkMute, fontSize: 11, fontFamily: TYPE.mono }}>
      {String(slideIdx + 1).padStart(2, "0")}
    </span>
  </div>
  <h2 style={{
    fontFamily: TYPE.serif, fontWeight: 400,
    fontSize: 28, lineHeight: 1.13, letterSpacing: "-0.02em",
    margin: 0, color: t.ink, whiteSpace: "pre-line",
  }}>{item.title}</h2>
  <p style={{ marginTop: 12, color: t.inkSoft, fontSize: 13, lineHeight: 1.6, maxWidth: 320 }}>
    {item.summary}
  </p>
  <div style={{ flex: 1, minHeight: 140, marginTop: 16, display: "flex", alignItems: "center", justifyContent: "center" }}>
    {item.images && item.images.length > 0 ? (
      <img src={item.images[0]} alt="" style={{
        maxWidth: "100%", maxHeight: 200, objectFit: "contain",
        opacity: 0.95,
      }}/>
    ) : (
      <EngravingArt variant={item.artVariant ?? 0} color={item.color}/>
    )}
  </div>
  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
                paddingTop: 14, borderTop: `1px solid ${t.line}` }}>
    <span style={{ fontSize: 11, fontFamily: TYPE.mono, color: t.inkMute }}>
      {String(slideIdx + 1).padStart(2,"0")} / {String(total).padStart(2,"0")}
    </span>
    <span style={{ fontSize: 10, color: t.inkMute, letterSpacing: "0.14em", textTransform: "uppercase" }}>
      Carrousel · {item.imageCount || 0} slides
    </span>
  </div>
</div>
);
}

function HeroPeekStack({ items, activeIdx, t }) {
return (
<div style={{
  position: "absolute", left: "100%", top: 0, height: "100%",
  display: "flex", gap: 0, paddingLeft: 8, alignItems: "stretch",
}}>
  {items.slice(0, 4).map((it, i) => {
    if (i === 0) return null; // 1ère est la grosse carte
    const offset = i * 14;
    const opacity = 1 - i * 0.18;
    return (
      <div key={it.id} style={{
        width: 60, marginLeft: i === 1 ? 0 : -42,
        position: "relative", overflow: "hidden",
        background: t.paper,
        border: `1px solid ${t.line}`, borderTop: `2px solid ${it.color}`,
        opacity,
        transform: `translateY(${i*4}px)`,
        transition: "all 480ms cubic-bezier(.4,0,.2,1)",
        boxShadow: t.shadow,
      }}>
        <div style={{ padding: "16px 8px", height: "100%" }}>
          <div style={{ color: it.color, fontSize: 9, letterSpacing: "0.14em", fontWeight: 600,
                        transform: "rotate(-90deg)", transformOrigin: "top left",
                        position: "absolute", left: 14, top: "50%", whiteSpace: "nowrap" }}>
            {String(i+1).padStart(2,"0")}
          </div>
        </div>
        <div style={{ position: "absolute", inset: 0, opacity: 0.45 }}>
          <EngravingArt variant={it.artVariant ?? i*2} color={it.color}/>
        </div>
      </div>
    );
  })}
</div>
);
}

const FALLBACK_HERO = [
{ id:"f1", category:"fiscalite", title:"Convention de\ndouble imposition", summary:"Accord bilatéral pour éviter qu'un même revenu soit imposé deux fois.", artVariant:5, imageCount:6 },
{ id:"f2", category:"economie", title:"Coût\nd'opportunité", summary:"La valeur de la meilleure alternative à laquelle on renonce.", artVariant:1, imageCount:5 },
{ id:"f3", category:"droit", title:"Force majeure", summary:"Événement imprévisible qui exonère une partie de ses obligations.", artVariant:0, imageCount:4 },
{ id:"f4", category:"international", title:"BEPS — Action 14", summary:"Mécanismes de résolution des différends entre administrations fiscales.", artVariant:2, imageCount:7 },
{ id:"f5", category:"arbitrage", title:"Lex mercatoria", summary:"Corpus de règles d'usage commercial international entre opérateurs privés.", artVariant:7, imageCount:5 },
{ id:"f6", category:"politiques", title:"Principe de\nsubsidiarité", summary:"Ne déléguer à un niveau supérieur que ce qu'il fait mieux.", artVariant:3, imageCount:4 },
];

function Hero({ t, onOpenCarousel }) {
const { items, ready } = useContent();
const featured = useMemo(() => {
const list = (items.length ? items.slice(0, 6) : FALLBACK_HERO).map((it, i) => ({
  ...it,
  color: CAT_BY_KEY[it.category]?.color || "#999",
  categoryLabel: CAT_BY_KEY[it.category]?.label || it.category,
  summary: it.summary || it.subtitle || "",
  imageCount: it.images?.length || it.imageCount || 0,
}));
return list.length ? list : FALLBACK_HERO.map(f => ({...f, color: PALETTE[f.category], categoryLabel: CAT_BY_KEY[f.category].label}));
}, [items]);

const [idx, setIdx] = useState(0);
const [paused, setPaused] = useState(false);

useEffect(() => {
if (paused || featured.length < 2) return;
const id = setInterval(() => setIdx(i => (i+1) % featured.length), 6500);
return () => clearInterval(id);
}, [paused, featured.length]);

const cur = featured[idx];
const next = (i) => setIdx((idx + i + featured.length) % featured.length);
const reordered = useMemo(() => {
const r = []; for (let k = 0; k < featured.length; k++) r.push(featured[(idx+k) % featured.length]);
return r;
}, [featured, idx]);

return (
<section style={{
  display: "grid", gridTemplateColumns: "1fr 1fr",
  borderBottom: `1px solid ${t.line}`,
  background: t.paper,
  minHeight: 580, overflow: "hidden",
}}>
  <div style={{ padding: "60px 56px 40px", display: "flex", flexDirection: "column",
                borderRight: `1px solid ${t.line}`, position: "relative" }}>
    <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center" }}>
      <div style={{ display: "inline-flex", alignItems: "center", gap: 12, marginBottom: 24 }}>
        <CrossMark size={9} color={t.accent} opacity={1}/>
        <span style={{ color: t.accent, fontSize: 11, letterSpacing: "0.22em",
                       textTransform: "uppercase", fontWeight: 600 }}>
          Law and Economics
        </span>
      </div>
      <h1 style={{
        fontFamily: TYPE.serif, fontWeight: 400,
        fontSize: "clamp(40px, 4.6vw, 60px)",
        lineHeight: 1.02, letterSpacing: "-0.025em",
        margin: 0, color: t.ink,
      }}>
        <div style={{ animation: "lineUp 700ms cubic-bezier(.4,0,.2,1) both" }}>Idée complexe,</div>
        <div style={{ animation: "lineUp 700ms 100ms cubic-bezier(.4,0,.2,1) both" }}>
          expliquer{" "}
          <span style={{ color: t.accent, fontStyle: "italic", fontWeight: 400 }}>facilement.</span>
        </div>
      </h1>
      <div style={{
        width: 64, height: 2, background: t.accent, marginTop: 20, marginBottom: 16,
        animation: "barIn 600ms 400ms cubic-bezier(.4,0,.2,1) both", transformOrigin: "left",
      }}/>
      <p style={{ maxWidth: 380, color: t.inkSoft, fontSize: 14.5, lineHeight: 1.7, margin: 0,
                  animation: "fadeUp 700ms 250ms cubic-bezier(.4,0,.2,1) both" }}>
        Des notions clés de droit, d'économie et de fiscalité expliquées en carrousels visuels et interactifs.
        Six familles, des centaines de concepts.
      </p>
      <div style={{ display: "flex", gap: 14, marginTop: 32,
                    animation: "fadeUp 700ms 380ms cubic-bezier(.4,0,.2,1) both" }}>
        <a href="#concepts" className="cta-primary" style={{
          display: "inline-flex", alignItems: "center", gap: 14,
          background: t.ink, color: t.paper, textDecoration: "none",
          padding: "13px 22px", fontSize: 10.5, letterSpacing: "0.22em",
          textTransform: "uppercase", fontWeight: 600,
          border: `1px solid ${t.ink}`,
        }}>
          <span>Explorer les concepts</span>
          <svg className="cta-arrow" width="22" height="10" viewBox="0 0 22 10" fill="none" stroke="currentColor" strokeWidth="1.3">
            <line x1="0" y1="5" x2="20" y2="5"/><polyline points="16,1 20,5 16,9" fill="none"/>
          </svg>
        </a>
        <a href="#manifeste" className="cta-secondary" style={{
          display: "inline-flex", alignItems: "center", gap: 10,
          color: t.ink, textDecoration: "none",
          padding: "13px 18px", fontSize: 10.5, letterSpacing: "0.22em",
          textTransform: "uppercase", fontWeight: 600,
          border: `1px solid ${t.line}`,
        }}>
          <span>Manifeste</span>
        </a>
      </div>
    </div>
    {/* compteur en bas */}
    <div style={{
      display: "flex", justifyContent: "space-between", alignItems: "center",
      marginTop: 40, paddingTop: 20, borderTop: `1px solid ${t.line}`,
      color: t.inkMute, fontSize: 11, fontFamily: TYPE.mono, letterSpacing: "0.04em",
    }}>
      <span>{ready ? `${items.length} CONCEPTS` : "CHARGEMENT…"}</span>
      <span>6 THÉMATIQUES</span>
      <span>FR · 2026</span>
    </div>
  </div>

  {/* RIGHT — carte vedette + peeking */}
  <div
    onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
    style={{ padding: "60px 56px 40px", position: "relative", display: "flex", flexDirection: "column" }}>
    <div style={{ flex: 1, position: "relative", paddingRight: "20%" }}>
      <div style={{ height: "100%", minHeight: 380 }}>
        <HeroFeaturedCard item={cur} t={t} slideIdx={idx} total={featured.length}/>
      </div>
      <HeroPeekStack items={reordered} activeIdx={0} t={t}/>
    </div>

    {/* Controls */}
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 24 }}>
      <div style={{ display: "flex", gap: 4 }}>
        {featured.map((_, i) => (
          <button key={i} onClick={() => setIdx(i)} aria-label={`Slide ${i+1}`} style={{
            width: i === idx ? 28 : 12, height: 2,
            background: i === idx ? t.ink : t.line, border: 0, cursor: "pointer", padding: 0,
            transition: "width 240ms cubic-bezier(.4,0,.2,1), background 200ms",
          }}/>
        ))}
      </div>
      <div style={{ display: "flex", border: `1px solid ${t.line}` }}>
        <button onClick={() => next(-1)} className="nav-btn" style={navBtn(t)} aria-label="Précédent">
          <svg width="14" height="10" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.3">
            <polyline points="6,1 1,6 6,11"/><line x1="1" y1="6" x2="14" y2="6"/>
          </svg>
        </button>
        <button onClick={() => next(1)} className="nav-btn" style={{...navBtn(t), borderLeft:`1px solid ${t.line}`}} aria-label="Suivant">
          <svg width="14" height="10" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.3">
            <line x1="2" y1="6" x2="15" y2="6"/><polyline points="10,1 15,6 10,11"/>
          </svg>
        </button>
      </div>
    </div>
  </div>
</section>
);
}

const navBtn = (t) => ({
width: 40, height: 32, background: "transparent", border: 0, cursor: "pointer",
display: "flex", alignItems: "center", justifyContent: "center", color: t.ink,
transition: "background 160ms",
});

/* ============================================================
6. CATEGORIES
============================================================ */

function Categories({ t }) {
const { byCat, ready } = useContent();
const [hoverKey, setHoverKey] = useState(null);

return (
<section id="thematiques" style={{
  background: t.paper2, borderBottom: `1px solid ${t.line}`,
  display: "grid", gridTemplateColumns: `repeat(${CATEGORIES.length}, 1fr)`,
}}>
  {CATEGORIES.map((c, i) => {
    const count = byCat[c.key]?.length || 0;
    const hovered = hoverKey === c.key;
    return (
      <a key={c.key} href={`#cat-${c.key}`}
        onMouseEnter={() => setHoverKey(c.key)} onMouseLeave={() => setHoverKey(null)}
        className="cat-tile"
        style={{
          padding: "44px 26px 36px", textDecoration: "none", color: t.ink,
          borderRight: i < CATEGORIES.length - 1 ? `1px solid ${t.line}` : 0,
          display: "flex", flexDirection: "column", gap: 24,
          position: "relative", overflow: "hidden",
          background: hovered ? t.paper : "transparent",
          transition: "background 280ms ease",
        }}>
        <CategoryIcon kind={c.icon} color={hovered ? c.color : t.ink} animated={hovered}/>
        <div>
          <div style={{ fontSize: 14.5, fontWeight: 500, letterSpacing: "-0.005em",
                        color: hovered ? c.color : t.ink, transition: "color 240ms" }}>{c.label}</div>
          <div style={{ fontSize: 11, color: t.inkMute, marginTop: 6, fontFamily: TYPE.mono, letterSpacing: "0.04em" }}>
            {ready ? `${count} concept${count > 1 ? "s" : ""}` : "—"}
          </div>
        </div>
        {/* barre colorée en bas qui grandit */}
        <div style={{
          position: "absolute", left: 0, bottom: 0, height: 2,
          width: hovered ? "100%" : 0, background: c.color,
          transition: "width 380ms cubic-bezier(.4,0,.2,1)",
        }}/>
        {/* motif croix subtil au survol */}
        <div style={{
          position: "absolute", inset: 0, opacity: hovered ? 0.04 : 0,
          transition: "opacity 380ms",
          pointerEvents: "none",
        }}>
          <PlusPattern density={12} opacity={1} color={c.color}/>
        </div>
      </a>
    );
  })}
</section>
);
}

/* ============================================================
7. PUBLICATIONS — carte avec hover en éventail
============================================================ */

function PubCard({ item, t, onOpen }) {
const [hover, setHover] = useState(false);
const hasImages = item.images && item.images.length > 0;
const previews = hasImages ? item.images.slice(0, 3) : [];

return (
<button
  onClick={() => onOpen(item)}
  onMouseEnter={() => setHover(true)}
  onMouseLeave={() => setHover(false)}
  className="pub-card"
  style={{
    textAlign: "left", cursor: "pointer", color: t.ink,
    background: t.paper,
    border: `1px solid ${hover ? t.ink : t.line}`,
    borderTop: `2px solid ${item.color}`,
    padding: "24px 26px 56px",
    display: "flex", flexDirection: "column", gap: 12,
    minHeight: 380, position: "relative",
    boxShadow: hover ? t.shadowHi : t.shadow,
    transform: hover ? "translateY(-2px)" : "translateY(0)",
    transition: "transform 320ms cubic-bezier(.4,0,.2,1), border-color 200ms, box-shadow 320ms",
    fontFamily: TYPE.sans, font: "inherit",
    overflow: "hidden",
  }}>
  <div style={{ color: item.color, fontSize: 10.5, letterSpacing: "0.22em",
                textTransform: "uppercase", fontWeight: 600 }}>
    {item.categoryLabel}
  </div>
  <h3 style={{
    fontFamily: TYPE.serif, fontWeight: 400, fontSize: 22,
    lineHeight: 1.16, letterSpacing: "-0.012em", margin: 0,
    whiteSpace: "pre-line", color: t.ink,
  }}>{item.title}</h3>
  <p style={{ margin: 0, color: t.inkSoft, fontSize: 12.5, lineHeight: 1.55 }}>
    {item.summary}
  </p>

  {/* zone visuelle : engraving fallback OU stack d'images au hover */}
  <div style={{ flex: 1, marginTop: 14, minHeight: 130, position: "relative",
                display: "flex", alignItems: "flex-end", justifyContent: "center" }}>
    {/* l'art par défaut, fade out au hover si on a des images */}
    <div style={{
      position: "absolute", inset: 0,
      opacity: hasImages && hover ? 0 : 1,
      transition: "opacity 280ms",
      display: "flex", alignItems: "flex-end",
    }}>
      <div style={{ width: "100%", aspectRatio: "1.5/1" }}>
        <EngravingArt variant={item.artVariant ?? 0} color={item.color}/>
      </div>
    </div>

    {/* fan-out des images au hover */}
    {hasImages && (
      <div style={{
        position: "relative", width: "100%", aspectRatio: "1.5/1",
        opacity: hover ? 1 : 0, transition: "opacity 280ms",
        pointerEvents: hover ? "auto" : "none",
      }}>
        {previews.map((src, i) => {
          const center = (previews.length - 1) / 2;
          const offset = i - center;
          const rot = hover ? offset * 5 : 0;
          const tx = hover ? offset * 18 : 0;
          const ty = hover ? -Math.abs(offset) * 4 - 6 : 0;
          const sc = hover ? 1 - Math.abs(offset) * 0.025 : 1;
          return (
            <img key={src} src={src} alt=""
              style={{
                position: "absolute", left: "12%", right: "12%", top: "8%", bottom: "8%",
                width: "76%", height: "84%", objectFit: "cover",
                border: `1px solid ${t.line}`,
                background: t.paper2,
                boxShadow: t.shadow,
                transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(${sc})`,
                transition: `transform 420ms cubic-bezier(.34,1.3,.64,1) ${i*40}ms`,
                zIndex: 10 - Math.abs(offset),
              }}/>
          );
        })}
      </div>
    )}
  </div>

  {/* Bottom row */}
  <div style={{
    position: "absolute", left: 26, right: 26, bottom: 18,
    display: "flex", justifyContent: "space-between", alignItems: "center",
  }}>
    <span style={{ fontSize: 10, fontFamily: TYPE.mono, color: t.inkMute, letterSpacing: "0.06em" }}>
      {item.imageCount ? `${item.imageCount} slides` : "Concept"}
    </span>
    <div style={{
      transform: hover ? "translateX(5px)" : "translateX(0)",
      transition: "transform 240ms cubic-bezier(.4,0,.2,1)",
      color: hover ? item.color : t.ink,
    }}>
      <svg width="22" height="10" viewBox="0 0 22 10" fill="none" stroke="currentColor" strokeWidth="1.3">
        <line x1="0" y1="5" x2="20" y2="5"/><polyline points="16,1 20,5 16,9"/>
      </svg>
    </div>
  </div>
</button>
);
}

/* contenu de fallback si content.json absent / vide */
const FALLBACK_PUBS = [
{ id:"droit/force-majeure", category:"droit", slug:"force-majeure", title:"Force majeure", summary:"Un événement imprévisible qui exonère une partie de ses obligations.", artVariant:0 },
{ id:"economie/cout-opportunite", category:"economie", slug:"cout-opportunite", title:"Coût d'opportunité", summary:"La valeur de la meilleure alternative à laquelle on renonce.", artVariant:1 },
{ id:"fiscalite/prix-transfert", category:"fiscalite", slug:"prix-transfert", title:"Prix de transfert", summary:"Fixation des prix dans les transactions entre sociétés d'un même groupe.", artVariant:2 },
{ id:"arbitrage/nation-favorisee", category:"arbitrage", slug:"nation-favorisee", title:"Clause de la nation\nla plus favorisée", summary:"Accorder à un partenaire le même avantage qu'à tout autre.", artVariant:3 },
{ id:"droit/personnalite-juridique", category:"droit", slug:"personnalite-juridique", title:"Personnalité juridique", summary:"Aptitude à être titulaire de droits et d'obligations.", artVariant:4 },
{ id:"international/beps-14", category:"international", slug:"beps-14", title:"BEPS — Action 14", summary:"Améliorer le règlement des différends entre États.", artVariant:5 },
{ id:"fiscalite/double-imposition", category:"fiscalite", slug:"double-imposition", title:"Convention de\ndouble imposition", summary:"Accord bilatéral pour éviter qu'un revenu soit imposé deux fois.", artVariant:6 },
{ id:"politiques/subsidiarite", category:"politiques", slug:"subsidiarite", title:"Principe de subsidiarité", summary:"Ne déléguer à un niveau supérieur que ce qu'il fait mieux.", artVariant:7 },
];

function Publications({ t, onOpenItem }) {
const { items, ready } = useContent();
const [filter, setFilter] = useState("all");

const list = useMemo(() => {
const src = items.length ? items : FALLBACK_PUBS.map(p => ({
  ...p, color: PALETTE[p.category], categoryLabel: CAT_BY_KEY[p.category].label,
}));
return filter === "all" ? src : src.filter(it => it.category === filter);
}, [items, filter]);

return (
<section id="concepts" style={{ background: t.paper, borderBottom: `1px solid ${t.line}` }}>
  <div style={{
    padding: "56px 32px 28px",
    display: "flex", justifyContent: "space-between", alignItems: "flex-end",
    flexWrap: "wrap", gap: 24,
  }}>
    <div>
      <div style={{ fontSize: 10.5, color: t.accent, letterSpacing: "0.22em",
                    textTransform: "uppercase", fontWeight: 600, marginBottom: 12 }}>
        Bibliothèque
      </div>
      <h2 style={{
        fontFamily: TYPE.serif, fontWeight: 400,
        fontSize: 38, lineHeight: 1.05, letterSpacing: "-0.022em",
        margin: 0, color: t.ink,
      }}>
        Dernières publications.
      </h2>
    </div>
    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
      <FilterChip active={filter === "all"} onClick={() => setFilter("all")} t={t} label="Toutes" color={t.ink} count={items.length || FALLBACK_PUBS.length}/>
      {CATEGORIES.map(c => (
        <FilterChip key={c.key} active={filter === c.key} onClick={() => setFilter(c.key)} t={t}
          label={c.label} color={c.color}/>
      ))}
    </div>
  </div>

  <div style={{
    display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 18,
    padding: "0 32px 32px",
  }}>
    {list.map((p, i) => (
      <div key={p.id || i} style={{
        animation: `cardIn 540ms ${i*50}ms cubic-bezier(.4,0,.2,1) both`,
      }}>
        <PubCard item={p} t={t} onOpen={onOpenItem}/>
      </div>
    ))}
  </div>
</section>
);
}

function FilterChip({ active, onClick, t, label, color, count }) {
return (
<button onClick={onClick} className="filter-chip" style={{
  padding: "6px 12px", fontSize: 10.5,
  letterSpacing: "0.16em", textTransform: "uppercase", fontWeight: 600,
  background: active ? t.ink : "transparent",
  color: active ? t.paper : t.ink,
  border: `1px solid ${active ? t.ink : t.line}`,
  cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 8,
  transition: "all 200ms",
}}>
  <span style={{ width: 6, height: 6, background: color, borderRadius: 0,
                 opacity: active ? 1 : 0.85 }}/>
  <span>{label}</span>
  {count != null && <span style={{ opacity: 0.5, fontFamily: TYPE.mono, fontWeight: 400 }}>{count}</span>}
</button>
);
}

/* ============================================================
8. MANIFESTE
============================================================ */

const MANIFESTE_POINTS = [
{ num: "01", title: "Densité visuelle",   body: "Une diapositive condense ce qu'un long article dilue. La cognition spatiale fait le reste — une image vaut mille mots de doctrine." },
{ num: "02", title: "Mémorisation accrue", body: "Le format séquentiel mobilise la mémoire de travail bien mieux qu'un texte continu. Une notion par slide, ancrée par le visuel." },
{ num: "03", title: "Référence rapide",    body: "Chaque fiche est une unité atomique citable, partageable, archivable. Pas de scroll fatigue, pas de contenu dilué." },
{ num: "04", title: "Rigueur académique",  body: "Sources primaires citées, doctrine référencée, jurisprudence indexée. Le fond sans le format blog." },
];

function Manifeste({ t }) {
return (
<section id="manifeste" style={{ background: t.paper2, borderBottom: `1px solid ${t.line}` }}>
  <div style={{
    display: "grid", gridTemplateColumns: "1fr 1.875fr",
    borderBottom: `1px solid ${t.line}`,
  }}>
    <div style={{
      padding: "72px 56px", borderRight: `1px solid ${t.line}`,
      display: "flex", flexDirection: "column", justifyContent: "center", gap: 26,
    }}>
      <div style={{ fontSize: 10.5, color: t.accent, letterSpacing: "0.22em",
                    textTransform: "uppercase", fontWeight: 600 }}>
        Manifeste
      </div>
      <h2 style={{
        fontFamily: TYPE.serif, fontWeight: 400,
        fontSize: 44, lineHeight: 1.04, letterSpacing: "-0.025em",
        margin: 0, color: t.ink,
      }}>
        Pourquoi le format<br/>
        <span style={{ color: t.accent, fontStyle: "italic" }}>carrousel</span> ?
      </h2>
      <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.72, color: t.inkSoft, maxWidth: 380 }}>
        Les longs articles ne sont plus lus. Les fiches visuelles, oui. Le format carrousel impose la rigueur :
        une idée par slide, une source par citation, une notion par fiche.
      </p>
      <blockquote style={{
        margin: "8px 0 0", padding: "16px 0 0",
        borderTop: `1px solid ${t.line}`,
        fontSize: 12.5, fontFamily: TYPE.mono,
        color: t.inkMute, letterSpacing: "0.02em",
      }}>
        « La densité visuelle est l'arme moderne de la doctrine. »
      </blockquote>
    </div>

    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
      {MANIFESTE_POINTS.map((pt, i) => (
        <div key={pt.num} style={{
          padding: "44px 38px",
          borderRight: i % 2 === 0 ? `1px solid ${t.line}` : 0,
          borderBottom: i < 2 ? `1px solid ${t.line}` : 0,
          background: t.paper, position: "relative",
        }}>
          <div style={{ fontSize: 10, fontFamily: TYPE.mono, color: t.accent,
                        letterSpacing: "0.1em", marginBottom: 18 }}>
            {pt.num} —
          </div>
          <h3 style={{
            fontFamily: TYPE.serif, fontWeight: 400,
            fontSize: 21, lineHeight: 1.18, letterSpacing: "-0.012em",
            margin: "0 0 12px", color: t.ink,
          }}>{pt.title}</h3>
          <p style={{ margin: 0, fontSize: 13, lineHeight: 1.65, color: t.inkSoft }}>
            {pt.body}
          </p>
        </div>
      ))}
    </div>
  </div>
</section>
);
}

/* ============================================================
9. FOOTER
============================================================ */

const FOOTER_COLS = [
{ label: "Thématiques", links: ["Fiscalité internationale", "Arbitrage & MAP", "Analyse économique du droit", "Droit international", "Politiques publiques", "Droit suisse"] },
{ label: "Ressources",  links: ["Index des concepts", "Bibliographie", "Citations & DOI", "Publications LinkedIn", "Glossaire"] },
{ label: "À propos",    links: ["Le projet", "Recherche doctorale", "Méthodologie", "Contact", "Mentions légales", "Licence CC BY-NC 4.0"] },
];

function Footer({ t }) {
return (
<footer style={{ background: t.paper, borderTop: `1px solid ${t.line}` }}>
  <div style={{
    display: "grid", gridTemplateColumns: "1.6fr 1fr 1fr 1fr",
    borderBottom: `1px solid ${t.line}`,
  }}>
    <div style={{ padding: "56px 48px 56px 36px", borderRight: `1px solid ${t.line}`,
                  display: "flex", flexDirection: "column" }}>
      <div style={{ fontSize: 15, fontWeight: 500, letterSpacing: "-0.01em", color: t.ink, marginBottom: 6 }}>
        lawandeconomics<span style={{ opacity: 0.4 }}>.ch</span>
      </div>
      <p style={{ margin: "16px 0 0", fontSize: 13.5, lineHeight: 1.7, color: t.inkSoft, maxWidth: 320 }}>
        Plateforme de vulgarisation juridique et économique. Des notions clés du droit et de l'économie
        expliquées en carrousels visuels, en appui à la recherche doctorale.
      </p>
      <div style={{ display: "flex", gap: 16, marginTop: 28 }}>
        <SocialIcon kind="linkedin" ink={t.ink}/>
        <SocialIcon kind="instagram" ink={t.ink}/>
        <SocialIcon kind="x" ink={t.ink}/>
        <SocialIcon kind="mail" ink={t.ink}/>
      </div>
      <div style={{ marginTop: "auto", paddingTop: 36 }}>
        <a href="#concepts" className="cta-primary" style={{
          background: t.ink, color: t.paper, textDecoration: "none",
          padding: "12px 20px", fontSize: 10, letterSpacing: "0.2em", textTransform: "uppercase",
          fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 12,
          border: `1px solid ${t.ink}`,
        }}>
          <span>Explorer les concepts</span>
          <svg className="cta-arrow" width="18" height="8" viewBox="0 0 18 8" fill="none" stroke="currentColor" strokeWidth="1.2">
            <line x1="0" y1="4" x2="16" y2="4"/><polyline points="12,1 16,4 12,7"/>
          </svg>
        </a>
      </div>
    </div>

    {FOOTER_COLS.map((col, ci) => (
      <div key={ci} style={{
        padding: "56px 32px",
        borderRight: ci < FOOTER_COLS.length - 1 ? `1px solid ${t.line}` : 0,
      }}>
        <div style={{
          fontSize: 9.5, letterSpacing: "0.18em", textTransform: "uppercase",
          fontWeight: 600, color: t.inkMute, fontFamily: TYPE.mono, marginBottom: 22,
        }}>{col.label}</div>
        <nav>
          {col.links.map((lnk, li) => (
            <a key={li} href="#" className="footer-link" style={{
              display: "block", textDecoration: "none", color: t.ink,
              fontSize: 13, lineHeight: 1, paddingBottom: 12, opacity: 0.62,
            }}>{lnk}</a>
          ))}
        </nav>
      </div>
    ))}
  </div>
  <div style={{
    padding: "16px 36px",
    display: "flex", justifyContent: "space-between", alignItems: "center",
    fontSize: 10.5, color: t.inkMute, fontFamily: TYPE.mono, letterSpacing: "0.04em",
  }}>
    <span>© 2026 LAWANDECONOMICS.CH — GENÈVE, SUISSE</span>
    <span>LICENCE CC BY-NC 4.0</span>
  </div>
</footer>
);
}

function SocialIcon({ kind, ink }) {
const wrap = (svg) => <a href="#" className="soc" aria-label={kind} style={{
display: "inline-flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, color: ink, opacity: 0.7,
transition: "opacity 180ms",
}}>{svg}</a>;
if (kind === "linkedin") return wrap(
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
  <rect x="3" y="3" width="18" height="18" rx="1"/>
  <path d="M7 10v7 M7 7v.01" strokeLinecap="round"/>
  <path d="M11 17v-4 a2 2 0 0 1 4 0 v4 M11 10v7"/>
</svg>
);
if (kind === "instagram") return wrap(
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
  <rect x="3" y="3" width="18" height="18" rx="4"/><circle cx="12" cy="12" r="4"/>
  <circle cx="17.5" cy="6.5" r="0.8" fill="currentColor"/>
</svg>
);
if (kind === "x") return wrap(
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
  <line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/>
</svg>
);
return wrap(
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
  <rect x="3" y="5" width="18" height="14" rx="0.5"/><polyline points="3,7 12,14 21,7"/>
</svg>
);
}

/* ============================================================
10. MODALES — CarouselViewer + FinderExplorer
============================================================ */

function Modal({ open, onClose, t, children, maxWidth = 1200 }) {
useEffect(() => {
if (!open) return;
const onKey = (e) => { if (e.key === "Escape") onClose(); };
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
}, [open, onClose]);
if (!open) return null;
return (
<div onClick={onClose} style={{
  position: "fixed", inset: 0, zIndex: 100,
  background: "rgba(8,8,12,0.62)",
  backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)",
  display: "flex", alignItems: "center", justifyContent: "center",
  padding: 32, animation: "fadeIn 240ms ease both",
}}>
  <div onClick={e => e.stopPropagation()} style={{
    width: "100%", maxWidth, maxHeight: "92vh",
    background: t.paper, color: t.ink,
    border: `1px solid ${t.line}`,
    boxShadow: "0 24px 80px rgba(0,0,0,.4)",
    display: "flex", flexDirection: "column",
    animation: "modalIn 360ms cubic-bezier(.34,1.2,.64,1) both",
    overflow: "hidden",
  }}>
    {children}
  </div>
</div>
);
}

function CarouselViewer({ open, item, onClose, t }) {
const [idx, setIdx] = useState(0);
useEffect(() => { setIdx(0); }, [item?.id]);
useEffect(() => {
if (!open || !item) return;
const onKey = (e) => {
  if (e.key === "ArrowLeft") setIdx(i => Math.max(0, i-1));
  if (e.key === "ArrowRight") setIdx(i => Math.min((item.images?.length||1)-1, i+1));
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, item]);

if (!item) return null;
const imgs = item.images || [];
const total = imgs.length;
const hasImages = total > 0;

return (
<Modal open={open} onClose={onClose} t={t} maxWidth={1100}>
  <div style={{
    padding: "20px 28px", borderBottom: `1px solid ${t.line}`,
    display: "flex", justifyContent: "space-between", alignItems: "center",
  }}>
    <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
      <span style={{ width: 8, height: 8, background: item.color }}/>
      <span style={{ color: item.color, fontSize: 10.5, letterSpacing: "0.22em",
                     textTransform: "uppercase", fontWeight: 600 }}>
        {item.categoryLabel}
      </span>
      <span style={{ color: t.inkMute, fontFamily: TYPE.mono, fontSize: 11 }}>·</span>
      <span style={{ fontFamily: TYPE.serif, fontSize: 18, color: t.ink, whiteSpace: "pre-line" }}>
        {item.title.replace(/\n/g, " ")}
      </span>
    </div>
    <button onClick={onClose} className="x-btn" style={{
      width: 36, height: 36, background: "transparent", border: `1px solid ${t.line}`,
      color: t.ink, cursor: "pointer",
      display: "flex", alignItems: "center", justifyContent: "center",
    }} aria-label="Fermer">
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">
        <line x1="2" y1="2" x2="12" y2="12"/><line x1="12" y1="2" x2="2" y2="12"/>
      </svg>
    </button>
  </div>

  <div style={{
    flex: 1, padding: 32, background: t.paper2,
    display: "flex", alignItems: "center", justifyContent: "center",
    position: "relative", minHeight: 480,
  }}>
    {hasImages ? (
      <img key={imgs[idx]} src={imgs[idx]} alt=""
        style={{
          maxWidth: "100%", maxHeight: "70vh", objectFit: "contain",
          boxShadow: t.shadow, border: `1px solid ${t.line}`,
          animation: "fadeIn 280ms ease both",
        }}/>
    ) : (
      <div style={{ width: 420, height: 280 }}>
        <EngravingArt variant={item.artVariant ?? 0} color={item.color}/>
        <div style={{ marginTop: 20, color: t.inkMute, fontSize: 13, textAlign: "center" }}>
          Carrousel à venir — ajoutez des images dans <code style={{ fontFamily: TYPE.mono, fontSize: 12 }}>
            public/content/{item.category}/{item.slug}/
          </code>
        </div>
      </div>
    )}

    {hasImages && total > 1 && (
      <>
        <button onClick={() => setIdx(i => Math.max(0, i-1))} disabled={idx === 0}
          className="carou-arrow" style={carouArrow(t, "left")} aria-label="Précédent">
          <svg width="18" height="14" viewBox="0 0 18 14" fill="none" stroke="currentColor" strokeWidth="1.5">
            <polyline points="8,1 1,7 8,13"/><line x1="1" y1="7" x2="17" y2="7"/>
          </svg>
        </button>
        <button onClick={() => setIdx(i => Math.min(total-1, i+1))} disabled={idx === total-1}
          className="carou-arrow" style={carouArrow(t, "right")} aria-label="Suivant">
          <svg width="18" height="14" viewBox="0 0 18 14" fill="none" stroke="currentColor" strokeWidth="1.5">
            <line x1="1" y1="7" x2="17" y2="7"/><polyline points="10,1 17,7 10,13"/>
          </svg>
        </button>
      </>
    )}
  </div>

  {hasImages && (
    <div style={{
      padding: "14px 28px", borderTop: `1px solid ${t.line}`,
      display: "flex", justifyContent: "space-between", alignItems: "center",
      fontFamily: TYPE.mono, fontSize: 11, color: t.inkMute,
    }}>
      <span>{String(idx+1).padStart(2,"0")} / {String(total).padStart(2,"0")}</span>
      <div style={{ display: "flex", gap: 4 }}>
        {imgs.map((_, i) => (
          <button key={i} onClick={() => setIdx(i)} aria-label={`Slide ${i+1}`} style={{
            width: i === idx ? 24 : 10, height: 2,
            background: i === idx ? t.ink : t.line, border: 0, cursor: "pointer", padding: 0,
            transition: "width 220ms",
          }}/>
        ))}
      </div>
      <span style={{ letterSpacing: "0.06em", textTransform: "uppercase" }}>
        ← → Naviguer · ESC Fermer
      </span>
    </div>
  )}
</Modal>
);
}
const carouArrow = (t, side) => ({
position: "absolute", [side]: 24, top: "50%", transform: "translateY(-50%)",
width: 44, height: 44, background: t.paper, border: `1px solid ${t.line}`,
color: t.ink, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
transition: "all 200ms",
});

/* --- Finder explorer (3 colonnes Apple-style) --- */
function FinderExplorer({ open, onClose, onOpenItem, t }) {
const { tree, items } = useContent();
// path = chemin courant dans l'arbre, ex ["fiscalite", "conventions"]
const [path, setPath] = useState([]);

useEffect(() => { if (open) setPath([]); }, [open]);

// Build columns from path
const columns = useMemo(() => {
const cols = [];
// Root column = catégories
const rootEntries = CATEGORIES.map(c => ({
  key: c.key, label: c.label, color: c.color, type: "category",
  count: (items.filter(it => it.category === c.key).length),
}));
cols.push({ title: "Catégories", entries: rootEntries });

// Walk path
let node = tree;
for (let depth = 0; depth < path.length; depth++) {
  const seg = path[depth];
  if (!node[seg]) break;
  const children = node[seg]._children || {};
  const entries = Object.values(children).map(child => {
    const isConcept = !!child._concept;
    return {
      key: child._key,
      label: isConcept ? child._concept.title.replace(/\n/g," ") : child._key.charAt(0).toUpperCase()+child._key.slice(1).replace(/-/g," "),
      color: CAT_BY_KEY[path[0]]?.color || t.ink,
      type: isConcept ? "concept" : "topic",
      concept: child._concept,
      count: isConcept ? 0 : Object.keys(child._children || {}).length,
    };
  }).sort((a,b) => (a.type === "topic" ? -1 : 1) - (b.type === "topic" ? -1 : 1) || a.label.localeCompare(b.label));
  cols.push({ title: depth === 0 ? "Sujets" : "Sous-thèmes", entries });
  node = children;
}

return cols;
}, [tree, path, items, t.ink]);

const selectedConcept = useMemo(() => {
let node = tree;
for (const seg of path) {
  if (!node[seg]) return null;
  if (node[seg]._concept && path.indexOf(seg) === path.length-1) {
    return node[seg]._concept;
  }
  node = node[seg]._children;
}
return null;
}, [tree, path]);

const onClickEntry = (depth, entry) => {
const newPath = [...path.slice(0, depth), entry.key];
if (entry.type === "concept") {
  onOpenItem(entry.concept);
  onClose();
  return;
}
setPath(newPath);
};

return (
<Modal open={open} onClose={onClose} t={t} maxWidth={1200}>
  {/* Bar du haut style Finder */}
  <div style={{
    padding: "14px 20px", borderBottom: `1px solid ${t.line}`,
    display: "flex", justifyContent: "space-between", alignItems: "center",
    background: t.paper2,
  }}>
    <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
      <div style={{ display: "flex", gap: 7 }}>
        <button onClick={onClose} aria-label="Fermer" style={trafficDot("#ff5f57")}/>
        <span style={trafficDot("#febc2e", true)}/>
        <span style={trafficDot("#28c840", true)}/>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 8, color: t.inkSoft, fontSize: 12 }}>
        <FolderIcon size={14}/>
        <span style={{ fontFamily: TYPE.mono, fontSize: 11.5 }}>
          lawandeconomics{path.length ? " / " + path.join(" / ") : ""}
        </span>
      </div>
    </div>
    <div style={{ display: "flex", gap: 4 }}>
      <button onClick={() => setPath(p => p.slice(0, -1))} disabled={path.length === 0}
        style={navBtn(t)} aria-label="Retour">
        <svg width="13" height="10" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.3">
          <polyline points="6,1 1,6 6,11"/><line x1="1" y1="6" x2="14" y2="6"/>
        </svg>
      </button>
    </div>
  </div>

  {/* Columns */}
  <div style={{
    display: "grid", gridTemplateColumns: `repeat(${Math.max(columns.length, 2)}, 1fr) 1.4fr`,
    height: 540, overflow: "hidden",
  }}>
    {columns.map((col, depth) => (
      <FinderColumn key={depth} col={col} t={t} depth={depth}
        selectedKey={path[depth] || null}
        onClick={(entry) => onClickEntry(depth, entry)}/>
    ))}
    {/* Aperçu détail */}
    <FinderPreview t={t} concept={selectedConcept} onOpen={(c) => { onOpenItem(c); onClose(); }}/>
  </div>
</Modal>
);
}
const trafficDot = (color, passive=false) => ({
width: 12, height: 12, borderRadius: 6, background: color,
border: 0, padding: 0, cursor: passive ? "default" : "pointer",
});

function FinderColumn({ col, t, depth, selectedKey, onClick }) {
return (
<div style={{
  borderRight: `1px solid ${t.line}`,
  background: depth === 0 ? t.paper2 : t.paper,
  display: "flex", flexDirection: "column",
  overflow: "hidden",
}}>
  <div style={{
    padding: "10px 14px", fontSize: 9.5, letterSpacing: "0.18em",
    textTransform: "uppercase", fontWeight: 600, color: t.inkMute,
    fontFamily: TYPE.mono, borderBottom: `1px solid ${t.lineSoft}`,
  }}>{col.title}</div>
  <div style={{ flex: 1, overflow: "auto" }}>
    {col.entries.length === 0 ? (
      <div style={{ padding: 20, fontSize: 12, color: t.inkMute }}>Vide.</div>
    ) : col.entries.map(e => {
      const active = e.key === selectedKey;
      return (
        <button key={e.key} onClick={() => onClick(e)}
          className="finder-row"
          style={{
            width: "100%", textAlign: "left", border: 0, cursor: "pointer",
            padding: "10px 14px", display: "flex", alignItems: "center", gap: 10,
            background: active ? t.accent : "transparent",
            color: active ? "#fff" : t.ink, fontSize: 13, fontFamily: TYPE.sans,
            borderBottom: `1px solid ${t.lineSoft}`,
            transition: "background 140ms",
          }}>
          {e.type === "category" || e.type === "topic" ? (
            <FolderIcon size={14}/>
          ) : (
            <FileIconColored color={e.color || t.inkMute} size={14}/>
          )}
          <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {e.label}
          </span>
          {e.count > 0 && (
            <span style={{
              fontSize: 10, color: active ? "rgba(255,255,255,.7)" : t.inkMute,
              fontFamily: TYPE.mono,
            }}>{e.count}</span>
          )}
          {(e.type === "category" || e.type === "topic") && (
            <svg width="6" height="9" viewBox="0 0 6 9" fill="none" stroke="currentColor" strokeWidth="1.3">
              <polyline points="1,1 5,4.5 1,8"/>
            </svg>
          )}
        </button>
      );
    })}
  </div>
</div>
);
}

function FileIconColored({ color, size = 14 }) {
return (
<svg width={size} height={size+2} viewBox="0 0 14 16" fill="none">
  <path d="M1.5 0 H 9 L 13 4 V 14.5 a 1.5 1.5 0 0 1 -1.5 1.5 H 1.5 a 1.5 1.5 0 0 1 -1.5 -1.5 V 1.5 a 1.5 1.5 0 0 1 1.5 -1.5 Z"
        fill={color} fillOpacity="0.18" stroke={color} strokeWidth="0.9"/>
  <path d="M9 0 V 4 H 13" stroke={color} strokeWidth="0.9"/>
</svg>
);
}

function FinderPreview({ t, concept, onOpen }) {
if (!concept) return (
<div style={{
  padding: 36, background: t.paper2, color: t.inkMute,
  display: "flex", alignItems: "center", justifyContent: "center",
  fontSize: 13, textAlign: "center",
}}>
  Sélectionnez un concept pour afficher son aperçu.
</div>
);
const c = concept;
const color = CAT_BY_KEY[c.category]?.color || t.ink;
const imgs = c.images || [];

return (
<div style={{
  padding: 28, background: t.paper2, overflow: "auto",
  display: "flex", flexDirection: "column", gap: 16,
}}>
  <div style={{ color, fontSize: 10.5, letterSpacing: "0.22em",
                textTransform: "uppercase", fontWeight: 600 }}>
    {CAT_BY_KEY[c.category]?.label}
  </div>
  <h3 style={{
    fontFamily: TYPE.serif, fontWeight: 400, fontSize: 24,
    lineHeight: 1.16, letterSpacing: "-0.012em", margin: 0,
    whiteSpace: "pre-line", color: t.ink,
  }}>{c.title}</h3>
  <p style={{ margin: 0, color: t.inkSoft, fontSize: 13, lineHeight: 1.6 }}>
    {c.summary}
  </p>

  <div style={{ aspectRatio: "1.5/1", background: t.paper, border: `1px solid ${t.line}`,
                display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden" }}>
    {imgs.length > 0 ? (
      <img src={imgs[0]} alt="" style={{ width: "100%", height: "100%", objectFit: "contain" }}/>
    ) : (
      <EngravingArt variant={c.artVariant ?? 0} color={color}/>
    )}
  </div>

  {imgs.length > 1 && (
    <div>
      <div style={{ fontSize: 10, color: t.inkMute, letterSpacing: "0.16em",
                    textTransform: "uppercase", fontWeight: 600, marginBottom: 10 }}>
        {imgs.length} slides
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 }}>
        {imgs.slice(0, 8).map((src, i) => (
          <div key={i} style={{ aspectRatio: "1/1", background: t.paper, border: `1px solid ${t.line}`,
                                overflow: "hidden" }}>
            <img src={src} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
          </div>
        ))}
      </div>
    </div>
  )}

  <div style={{ marginTop: "auto", paddingTop: 18, borderTop: `1px solid ${t.line}` }}>
    <button onClick={() => onOpen(c)} style={{
      background: t.ink, color: t.paper, border: 0, padding: "12px 20px",
      fontSize: 10.5, letterSpacing: "0.22em", textTransform: "uppercase", fontWeight: 600,
      cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 12,
    }}>
      <span>Ouvrir le carrousel</span>
      <svg width="16" height="8" viewBox="0 0 18 8" fill="none" stroke="currentColor" strokeWidth="1.3">
        <line x1="0" y1="4" x2="14" y2="4"/><polyline points="10,1 14,4 10,7"/>
      </svg>
    </button>
  </div>
</div>
);
}

/* ============================================================
11. APP
============================================================ */

function App() {
const [isDark, setIsDark] = useState(() =>
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
);
const t = isDark ? DARK : LIGHT;

const [openItem, setOpenItem] = useState(null);
const [finderOpen, setFinderOpen] = useState(false);

const css = `
@keyframes fadeIn { from {opacity:0} to {opacity:1} }
@keyframes fadeUp { from {opacity:0; transform: translateY(8px)} to {opacity:1; transform:translateY(0)} }
@keyframes lineUp { from {opacity:0; transform: translateY(14px)} to {opacity:1; transform:translateY(0)} }
@keyframes barIn  { from {transform:scaleX(0)} to {transform:scaleX(1)} }
@keyframes cardIn { from {opacity:0; transform: translateY(16px)} to {opacity:1; transform:translateY(0)} }
@keyframes heroIn { from {opacity:0; transform: translateY(8px) scale(.98)} to {opacity:1; transform: translateY(0) scale(1)} }
@keyframes modalIn { from {opacity:0; transform: translateY(20px) scale(.98)} to {opacity:1; transform: translateY(0) scale(1)} }

body, section, header, footer, nav, div, a, p, h1, h2, h3, span, button {
  transition: background-color 380ms, border-color 380ms, color 380ms;
}

.nav-link::after {
  content:""; position:absolute; left:0; bottom:-2px; height:1px; width:0;
  background:${t.accent}; transition: width 280ms cubic-bezier(.4,0,.2,1);
}
.nav-link:hover { color:${t.accent}; }
.nav-link:hover::after { width:100%; }

a:hover .logo-line { transform: scaleX(1) !important; }

.hdr-icon { transition: background 200ms, color 200ms; }
.hdr-icon:hover { background:${t.lineSoft}; color:${t.accent}; }

.nav-btn:hover { background:${t.lineSoft}; }
.nav-btn[disabled] { opacity:.3; cursor:not-allowed; }

.cta-primary .cta-arrow { transition: transform 240ms cubic-bezier(.4,0,.2,1); }
.cta-primary:hover .cta-arrow { transform: translateX(6px); }

.cta-secondary { transition: background 200ms; }
.cta-secondary:hover { background:${t.lineSoft}; }

.filter-chip:hover { border-color:${t.ink} !important; }

.carou-arrow:hover:not([disabled]) { background:${t.ink}; color:${t.paper}; border-color:${t.ink}; }
.carou-arrow[disabled] { opacity:.25; cursor:not-allowed; }

.x-btn:hover { background:${t.ink}; color:${t.paper}; border-color:${t.ink}; }

.soc:hover { opacity:1 !important; color:${t.accent}; }
.footer-link:hover { opacity:1 !important; color:${t.accent}; }

.finder-row:hover:not(:disabled) {
  background: ${t.lineSoft};
}
`;

return (
<ContentProvider>
  <div style={{
    display: "grid",
    gridTemplateColumns: "44px 1fr 44px",
    minHeight: "100vh",
    background: t.gutter,
    color: t.ink,
  }}>
    <style>{css}</style>

    <div style={{ position: "sticky", top: 0, height: "100vh", overflow: "hidden" }}>
      <PlusPattern density={9} opacity={isDark ? 0.06 : 0.08} color={t.ink}/>
    </div>

    <main style={{
      background: t.paper,
      borderLeft: `1px solid ${t.line}`,
      borderRight: `1px solid ${t.line}`,
    }}>
      <Header t={t} isDark={isDark}
        onToggleTheme={() => setIsDark(d => !d)}
        onOpenFinder={() => setFinderOpen(true)}/>
      <Hero t={t}/>
      <Categories t={t}/>
      <Publications t={t} onOpenItem={setOpenItem}/>
      <Manifeste t={t}/>
      <Footer t={t}/>
    </main>

    <div style={{ position: "sticky", top: 0, height: "100vh", overflow: "hidden" }}>
      <PlusPattern density={9} opacity={isDark ? 0.06 : 0.08} color={t.ink}/>
    </div>

    <CarouselViewer open={!!openItem} item={openItem} onClose={() => setOpenItem(null)} t={t}/>
    <FinderExplorer open={finderOpen} onClose={() => setFinderOpen(false)}
      onOpenItem={setOpenItem} t={t}/>
  </div>
</ContentProvider>
);
}

ReactDOM.createRoot(document.getElementById("root")).render(<App/>);