/* lawandeconomics.ch — v4
   ============================================================
   Architecture:
   1. Tokens & catalogue catégories
   2. Theme (jour/nuit) + ThemeToggle (soleil/lune conservé)
   3. ContentProvider — charge /content.json
   4. WebGLShader — fond animé du hero (Three.js)
   5. AuroraField — fond mode nuit alternatif
   6. LiquidGlass primitives (Dock, Button)
   7. Navbar minimaliste
   8. Hero — typographie + shader + dock
   9. Categories rail (billets suisses)
   10. FinderExplorer — navigation principale 3 colonnes (page, pas modale)
   11. InfoCard fan-out — pour la grille de publications
   12. Publications grid + filtres
   13. Manifeste
   14. Footer
   15. CarouselViewer — modale plein écran
   16. App
   ============================================================ */

   const { useState, useEffect, useRef, useMemo, useCallback,
    createContext, useContext, Fragment } = React;
const { motion, AnimatePresence, useMotionValue, useSpring, useTransform } = window.Motion || window.FramerMotion || window.framerMotion || {};

/* Fallback si l'UMD framer-motion expose autrement */
const FM = window.Motion || window.FramerMotion || window.framerMotion;
const M = FM?.motion || {};
const Anim = FM?.AnimatePresence || (({children}) => <>{children}</>);

/* ============================================================
1. TOKENS
============================================================ */

const CATS = [
{ key: 'fiscalite',     label: 'Fiscalité',            denom: '20 CHF',   tw: 'ch20',   hex: '#e85d5d', desc: 'Impôt, prélèvements, conventions' },
{ key: 'international', label: 'International',        denom: '10 CHF',   tw: 'ch10',   hex: '#d4a23a', desc: 'Traités, OCDE, ordre mondial' },
{ key: 'economie',      label: 'Économie',             denom: '100 CHF',  tw: 'ch100',  hex: '#5e84d4', desc: 'Microéconomie, macro, choix' },
{ key: 'politiques',    label: 'Politiques publiques', denom: '50 CHF',   tw: 'ch50',   hex: '#6fa97c', desc: 'Régulation, État, intérêt général' },
{ key: 'droit',         label: 'Droit',                denom: '200 CHF',  tw: 'ch200',  hex: '#a07a55', desc: 'Doctrine, jurisprudence, code' },
{ key: 'arbitrage',     label: 'Arbitrage',            denom: '1000 CHF', tw: 'ch1000', hex: '#8b6fb8', desc: 'Différends, sentence, médiation' },
];
const CAT_BY_KEY = Object.fromEntries(CATS.map(c => [c.key, c]));

/* ============================================================
2. THÈME — jour / nuit
============================================================ */

const ThemeCtx = createContext({ dark: false, toggle: () => {} });

function ThemeProvider({ children }) {
const [dark, setDark] = useState(() => {
if (typeof window === 'undefined') return false;
const saved = localStorage.getItem('lae-theme');
if (saved) return saved === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});

useEffect(() => {
document.documentElement.classList.toggle('dark', dark);
localStorage.setItem('lae-theme', dark ? 'dark' : 'light');
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', dark ? '#0b0b10' : '#fafaf7');
}, [dark]);

return <ThemeCtx.Provider value={{ dark, toggle: () => setDark(d => !d) }}>{children}</ThemeCtx.Provider>;
}
const useTheme = () => useContext(ThemeCtx);

/* Toggle soleil/lune — conservé tel quel, retravaillé */
function ThemeToggle() {
const { dark, toggle } = useTheme();
return (
<button
  onClick={toggle}
  aria-label={dark ? 'Passer en mode jour' : 'Passer en mode nuit'}
  className="relative w-10 h-10 rounded-full flex items-center justify-center
             text-ink dark:text-white/85 hover:bg-black/5 dark:hover:bg-white/10
             transition-colors"
>
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="1.6" strokeLinecap="round"
       style={{
         transform: dark ? 'rotate(220deg)' : 'rotate(0deg)',
         transition: 'transform 700ms 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: dark ? 1 : 0, transition: 'opacity 360ms' }}/>
    <circle cx="12" cy="12" r="4.6" fill="currentColor" stroke="none"
            style={{ opacity: dark ? 0 : 1, transition: 'opacity 360ms' }}/>
    {[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.4} y2={12+Math.sin(r)*9.4}
        style={{ opacity: dark ? 0 : 1, transition: `opacity 320ms ${i*22}ms` }}/>;
    })}
  </svg>
</button>
);
}

/* ============================================================
3. CONTENT PROVIDER
============================================================ */

const ContentCtx = createContext({ items: [], byCat: {}, tree: {}, ready: false });
const useContent = () => 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]?.hex || '#999',
      tw: CAT_BY_KEY[it.category]?.tw || 'ink',
      categoryLabel: CAT_BY_KEY[it.category]?.label || it.category,
    }));
    const byCat = {};
    for (const it of items) (byCat[it.category] = byCat[it.category] || []).push(it);

    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: {}, _key: p, _depth: idx };
        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. WebGL Shader Hero (Three.js — palette adaptée thème)
============================================================ */

function HeroShader({ dark }) {
const canvasRef = useRef(null);
const stateRef = useRef({ animationId: null });

useEffect(() => {
if (!canvasRef.current || !window.THREE) return;
const THREE = window.THREE;
const canvas = canvasRef.current;
const refs = stateRef.current;

const vertexShader = `
  attribute vec3 position;
  void main() { gl_Position = vec4(position, 1.0); }
`;
const fragmentShader = `
  precision highp float;
  uniform vec2 resolution;
  uniform float time;
  uniform float xScale;
  uniform float yScale;
  uniform float distortion;
  uniform vec3 tintA;
  uniform vec3 tintB;
  uniform float bgLight;
  void main() {
    vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
    float d = length(p) * distortion;
    float rx = p.x * (1.0 + d);
    float gx = p.x;
    float bx = p.x * (1.0 - d);
    float r = 0.045 / abs(p.y + sin((rx + time) * xScale) * yScale);
    float g = 0.045 / abs(p.y + sin((gx + time) * xScale) * yScale);
    float b = 0.045 / abs(p.y + sin((bx + time) * xScale) * yScale);
    vec3 col = vec3(r,g,b);
    vec3 tinted = mix(tintA, tintB, clamp(length(col), 0.0, 1.0));
    col = col * tinted;
    // En mode jour, on inverse vers un fond clair
    col = mix(col, vec3(bgLight) - col * 0.7, bgLight);
    gl_FragColor = vec4(col, 1.0);
  }
`;

refs.scene = new THREE.Scene();
refs.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: false });
refs.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
refs.renderer.setClearColor(0x000000, 0);
refs.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, -1);

refs.uniforms = {
  resolution: { value: new THREE.Vector2(canvas.clientWidth, canvas.clientHeight) },
  time: { value: 0 },
  xScale: { value: 1.0 },
  yScale: { value: 0.55 },
  distortion: { value: 0.06 },
  tintA: { value: new THREE.Vector3(...(dark ? [0.29, 0.31, 0.71] : [0.92, 0.93, 0.99])) },
  tintB: { value: new THREE.Vector3(...(dark ? [0.55, 0.45, 0.85] : [0.78, 0.81, 0.96])) },
  bgLight: { value: dark ? 0.0 : 0.92 },
};

const positions = new Float32Array([
  -1,-1,0, 1,-1,0, -1,1,0,
   1,-1,0, -1,1,0,  1,1,0,
]);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.RawShaderMaterial({
  vertexShader, fragmentShader, uniforms: refs.uniforms, side: THREE.DoubleSide,
});
refs.mesh = new THREE.Mesh(geometry, material);
refs.scene.add(refs.mesh);

const resize = () => {
  const w = canvas.clientWidth, h = canvas.clientHeight;
  refs.renderer.setSize(w, h, false);
  refs.uniforms.resolution.value.set(w, h);
};
resize();
window.addEventListener('resize', resize);

const tick = () => {
  refs.uniforms.time.value += 0.008;
  refs.renderer.render(refs.scene, refs.camera);
  refs.animationId = requestAnimationFrame(tick);
};
tick();

return () => {
  window.removeEventListener('resize', resize);
  if (refs.animationId) cancelAnimationFrame(refs.animationId);
  geometry.dispose(); material.dispose();
  refs.renderer?.dispose();
};
}, [dark]);

return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full block opacity-[0.55] dark:opacity-80"/>;
}

/* ============================================================
5. AURORA — fond doux pour les sections secondaires
============================================================ */

function AuroraBackdrop() {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none -z-10">
  <div
    aria-hidden
    style={{
      backgroundImage: `
        repeating-linear-gradient(100deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 7%, transparent 10%, transparent 12%, rgba(255,255,255,1) 16%),
        repeating-linear-gradient(100deg, #5e84d4 10%, #a5b4fc 15%, #93c5fd 20%, #ddd6fe 25%, #60a5fa 30%)
      `,
      backgroundSize: '300% 200%',
      backgroundPosition: '50% 50%, 50% 50%',
      maskImage: 'radial-gradient(ellipse at 100% 0%, black 10%, transparent 70%)',
      WebkitMaskImage: 'radial-gradient(ellipse at 100% 0%, black 10%, transparent 70%)',
    }}
    className="absolute -inset-[10px] opacity-[0.18] dark:opacity-25 blur-[10px] dark:invert-0 invert animate-aurora pointer-events-none will-change-transform mix-blend-multiply dark:mix-blend-screen"
  />
</div>
);
}

/* ============================================================
6. LIQUID GLASS primitives (Dock + Button)
============================================================ */

function LiquidGlass({ children, className = '', as: Tag = 'div', ...rest }) {
return (
<Tag
  {...rest}
  className={`relative overflow-hidden text-ink dark:text-white/90 transition-all duration-700 lg-glass ${className}`}
  style={{
    boxShadow: '0 6px 18px rgba(0,0,0,.18), 0 0 24px rgba(0,0,0,.06)',
    transitionTimingFunction: 'cubic-bezier(0.175,0.885,0.32,2.2)',
  }}
>
  <div
    className="absolute inset-0 z-0 rounded-[inherit]"
    style={{ backdropFilter: 'blur(14px) saturate(180%)', WebkitBackdropFilter: 'blur(14px) saturate(180%)', filter: 'url(#lg-distortion)', isolation: 'isolate' }}
  />
  <div className="absolute inset-0 z-10 rounded-[inherit] bg-white/35 dark:bg-white/[0.06]" />
  <div
    className="absolute inset-0 z-20 rounded-[inherit]"
    style={{ boxShadow: 'inset 2px 2px 1px 0 rgba(255,255,255,.5), inset -1px -1px 1px 1px rgba(255,255,255,.35)' }}
  />
  <div className="relative z-30">{children}</div>
</Tag>
);
}

/* ============================================================
7. NAVBAR — pill flottant
============================================================ */

function Navbar({ onJump }) {
const links = [
{ id: 'concepts',   label: 'Concepts' },
{ id: 'finder',     label: 'Explorer' },
{ id: 'manifeste',  label: 'Manifeste' },
{ id: 'about',      label: 'À propos' },
];
return (
<header className="fixed top-5 left-1/2 -translate-x-1/2 z-40 w-[min(92vw,920px)]">
  <LiquidGlass className="rounded-full px-4 py-2.5 flex items-center gap-2">
    <a href="#top" className="flex items-center gap-2 px-2 py-1 group">
      <span className="relative w-5 h-5 grid place-items-center">
        <span className="absolute w-1.5 h-1.5 rounded-full bg-ch20 top-0 left-1/2 -translate-x-1/2"/>
        <span className="absolute w-1.5 h-1.5 rounded-full bg-ch100 left-0 top-1/2 -translate-y-1/2"/>
        <span className="absolute w-1.5 h-1.5 rounded-full bg-ch1000 right-0 top-1/2 -translate-y-1/2"/>
        <span className="absolute w-1.5 h-1.5 rounded-full bg-ch50 bottom-0 left-1/2 -translate-x-1/2"/>
      </span>
      <span className="font-medium text-[13px] tracking-tight">
        lawandeconomics<span className="opacity-40">.ch</span>
      </span>
    </a>

    <nav className="hidden md:flex items-center gap-1 mx-auto">
      {links.map(l => (
        <button key={l.id} onClick={() => onJump(l.id)}
          className="relative px-3 py-1.5 text-[12px] tracking-wide rounded-full
                     hover:bg-black/5 dark:hover:bg-white/10 transition-colors">
          {l.label}
        </button>
      ))}
    </nav>

    <div className="flex items-center gap-1 ml-auto">
      <button aria-label="Recherche"
        className="w-10 h-10 rounded-full flex items-center justify-center
                   hover:bg-black/5 dark:hover:bg-white/10 transition-colors">
        <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>
      <ThemeToggle/>
    </div>
  </LiquidGlass>
</header>
);
}

/* ============================================================
8. HERO — typographie + shader + dock catégories
============================================================ */

function Hero({ onJump }) {
const { dark } = useTheme();
const { items, ready } = useContent();
const title = 'Idée complexe, expliquer facilement.';
const words = title.split(' ');

return (
<section id="top" className="relative isolate min-h-[100svh] flex flex-col items-center justify-center pt-28 pb-20 overflow-hidden">
  {/* Shader animé */}
  <div className="absolute inset-0 -z-10">
    <HeroShader dark={dark}/>
    <div className="absolute inset-0 bg-gradient-to-b from-paper/0 via-paper/0 to-paper dark:from-night/0 dark:via-night/0 dark:to-night"/>
    <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_transparent_30%,_rgba(250,250,247,.85)_85%)] dark:bg-[radial-gradient(ellipse_at_center,_transparent_30%,_rgba(11,11,16,.85)_85%)]"/>
  </div>

  <div className="relative z-10 px-6 text-center max-w-5xl mx-auto">
    {/* Eyebrow */}
    <div className="inline-flex items-center gap-3 mb-8 px-4 py-1.5 rounded-full
                    border border-black/10 dark:border-white/15
                    bg-white/40 dark:bg-white/5 backdrop-blur-md">
      <span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"/>
      <span className="text-[11px] tracking-[0.22em] uppercase font-semibold text-ink/80 dark:text-white/80">
        Law &amp; Economics — Genève
      </span>
    </div>

    {/* Heading avec stagger letter par letter */}
    <h1 className="font-serif font-light tracking-[-0.03em] leading-[0.98]
                   text-[clamp(2.5rem,7vw,5.5rem)] text-ink dark:text-white">
      {words.map((word, wi) => (
        <span key={wi} className="inline-block mr-[0.25em] last:mr-0">
          {[...word].map((ch, ci) => (
            <span
              key={ci}
              className="inline-block opacity-0"
              style={{
                animation: `letterIn 700ms ${(wi*120 + ci*22)}ms cubic-bezier(.34,1.2,.64,1) forwards`,
              }}
            >
              {ch === ',' ? <span className="text-accent">,</span>
               : ci === word.length-1 && wi === words.length-1 ? <em className="not-italic text-accent font-normal">{ch}</em>
               : ch}
            </span>
          ))}
        </span>
      ))}
    </h1>

    <p className="mt-8 max-w-xl mx-auto text-[15px] leading-relaxed text-ink/70 dark:text-white/70"
       style={{ animation: 'fadeUp 600ms 1100ms cubic-bezier(.4,0,.2,1) both' }}>
      Une bibliothèque visuelle de notions de droit, d'économie et de fiscalité.
      Des concepts denses, expliqués en carrousels — six familles, des centaines de fiches.
    </p>

    <div className="mt-10 flex items-center justify-center gap-4 flex-wrap"
         style={{ animation: 'fadeUp 600ms 1300ms cubic-bezier(.4,0,.2,1) both' }}>
      <button onClick={() => onJump('finder')}
        className="group inline-flex items-center gap-3 px-6 py-3 rounded-full
                   bg-ink dark:bg-white text-paper dark:text-ink font-medium text-[12px]
                   tracking-[0.18em] uppercase
                   hover:translate-y-[-1px] transition-transform shadow-lg">
        Explorer la bibliothèque
        <svg width="16" height="10" viewBox="0 0 22 10" fill="none" stroke="currentColor" strokeWidth="1.4"
             className="group-hover:translate-x-1 transition-transform">
          <line x1="0" y1="5" x2="20" y2="5"/><polyline points="16,1 20,5 16,9"/>
        </svg>
      </button>
      <button onClick={() => onJump('manifeste')}
        className="inline-flex items-center gap-2 px-6 py-3 rounded-full
                   border border-black/15 dark:border-white/20
                   text-ink dark:text-white text-[12px] tracking-[0.18em] uppercase font-medium
                   hover:bg-black/5 dark:hover:bg-white/10 transition-colors backdrop-blur-md">
        Lire le manifeste
      </button>
    </div>

    {/* Métadonnées */}
    <div className="mt-16 flex items-center justify-center gap-10 text-[10.5px] font-mono tracking-[0.14em] text-ink/55 dark:text-white/50 uppercase"
         style={{ animation: 'fadeUp 600ms 1500ms cubic-bezier(.4,0,.2,1) both' }}>
      <span>{ready ? `${items.length} concepts` : 'Chargement…'}</span>
      <span className="w-px h-3 bg-current opacity-30"/>
      <span>6 thématiques</span>
      <span className="w-px h-3 bg-current opacity-30"/>
      <span>FR · 2026</span>
    </div>
  </div>

  {/* Keyframes inline pour stagger */}
  <style>{`
    @keyframes letterIn { from { opacity: 0; transform: translateY(28px) } to { opacity: 1; transform: translateY(0) } }
    @keyframes fadeUp { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: translateY(0) } }
  `}</style>
</section>
);
}

/* ============================================================
9. CATEGORIES rail — billets suisses
============================================================ */

function CategoriesRail({ onJump }) {
const { byCat, ready } = useContent();
const [hover, setHover] = useState(null);

return (
<section className="relative px-6 py-20 border-y border-black/[0.06] dark:border-white/[0.08] bg-paper-sunk dark:bg-night-sunk">
  <div className="max-w-7xl mx-auto">
    <div className="flex items-end justify-between mb-10 flex-wrap gap-4">
      <div>
        <div className="text-[11px] tracking-[0.22em] uppercase font-semibold text-accent mb-3">
          Six familles
        </div>
        <h2 className="font-serif font-light text-4xl md:text-5xl tracking-[-0.025em] text-ink dark:text-white">
          Le code couleur des billets suisses.
        </h2>
      </div>
      <p className="text-[13px] text-ink/60 dark:text-white/60 max-w-sm leading-relaxed">
        Chaque famille reprend une dénomination — du rouge du 20 francs (Fiscalité)
        au violet du 1000 (Arbitrage).
      </p>
    </div>

    <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
      {CATS.map((c, i) => {
        const count = byCat[c.key]?.length || 0;
        const active = hover === c.key;
        return (
          <button key={c.key}
            onClick={() => onJump('finder', c.key)}
            onMouseEnter={() => setHover(c.key)} onMouseLeave={() => setHover(null)}
            className="group relative aspect-[3/4] rounded-2xl overflow-hidden text-left
                       border border-black/[0.08] dark:border-white/[0.1]
                       bg-paper-raised dark:bg-night-raised
                       transition-all duration-500 ease-out
                       hover:-translate-y-1"
            style={{
              boxShadow: active
                ? `0 18px 40px ${c.hex}30, 0 2px 8px ${c.hex}25`
                : '0 1px 2px rgba(0,0,0,.04)',
            }}>
            {/* Gradient banknote */}
            <div className="absolute inset-0 transition-opacity duration-500"
                 style={{
                   background: `linear-gradient(160deg, ${c.hex}00 0%, ${c.hex}18 60%, ${c.hex}45 100%)`,
                   opacity: active ? 1 : 0.4,
                 }}/>
            {/* Trame guillochée subtile (SVG inline) */}
            <svg className="absolute inset-0 w-full h-full opacity-[0.07] dark:opacity-[0.12]" preserveAspectRatio="none">
              <defs>
                <pattern id={`grain-${c.key}`} x="0" y="0" width="14" height="14" patternUnits="userSpaceOnUse">
                  <path d="M 0 7 Q 3.5 0 7 7 T 14 7" stroke={c.hex} strokeWidth="0.6" fill="none"/>
                </pattern>
              </defs>
              <rect width="100%" height="100%" fill={`url(#grain-${c.key})`}/>
            </svg>

            {/* Contenu */}
            <div className="relative h-full p-4 flex flex-col">
              <div className="flex items-start justify-between">
                <span className="font-mono text-[10px] tracking-[0.14em] uppercase font-semibold"
                      style={{ color: c.hex }}>
                  {c.denom}
                </span>
                <span className="font-mono text-[10px] text-ink/40 dark:text-white/40">
                  {String(i+1).padStart(2,'0')}
                </span>
              </div>
              <div className="mt-auto">
                <h3 className="font-serif text-xl leading-[1.1] tracking-[-0.01em] text-ink dark:text-white">
                  {c.label}
                </h3>
                <p className="mt-1 text-[11px] text-ink/55 dark:text-white/55 leading-tight">
                  {c.desc}
                </p>
                <div className="mt-4 flex items-center justify-between text-[10px] font-mono">
                  <span className="text-ink/45 dark:text-white/45 tracking-wide">
                    {ready ? `${count} concept${count > 1 ? 's' : ''}` : '—'}
                  </span>
                  <svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="currentColor" strokeWidth="1.3"
                       className="opacity-40 group-hover:opacity-100 group-hover:translate-x-1 transition-all">
                    <line x1="0" y1="4" x2="11" y2="4"/><polyline points="9,1 12,4 9,7"/>
                  </svg>
                </div>
              </div>
            </div>

            {/* Bottom accent bar */}
            <div className="absolute bottom-0 left-0 h-[3px] transition-all duration-500"
                 style={{ background: c.hex, width: active ? '100%' : '24%' }}/>
          </button>
        );
      })}
    </div>
  </div>
</section>
);
}

/* ============================================================
10. FINDER EXPLORER — page principale (3 colonnes Apple-style)
============================================================ */

function FinderExplorer({ presetCategory, onOpenItem }) {
const { tree, items, ready } = useContent();
const [path, setPath] = useState([]);
const [view, setView] = useState('columns'); // 'columns' | 'tree'
const lastApplied = useRef(null);

useEffect(() => {
if (presetCategory && presetCategory !== lastApplied.current) {
  setPath([presetCategory]);
  lastApplied.current = presetCategory;
}
}, [presetCategory]);

// Build columns
const columns = useMemo(() => {
const cols = [];
cols.push({
  title: 'Catégories',
  entries: CATS.map(c => ({
    key: c.key, label: c.label, color: c.hex, type: 'category',
    count: items.filter(it => it.category === c.key).length,
    denom: c.denom,
  })),
});

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 cat = path[0];
  const color = CAT_BY_KEY[cat]?.hex || '#999';
  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,
      type: isConcept ? 'concept' : 'topic',
      concept: child._concept,
      count: isConcept ? (child._concept.imageCount || 0) : Object.keys(child._children || {}).length,
    };
  }).sort((a,b) => {
    if (a.type !== b.type) return a.type === 'topic' ? -1 : 1;
    return a.label.localeCompare(b.label);
  });
  cols.push({ title: depth === 0 ? 'Sujets' : `Sous-thèmes`, entries });
  node = children;
}
return cols;
}, [tree, path, items]);

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

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

return (
<section id="finder" className="relative px-6 py-20">
  <div className="max-w-7xl mx-auto">
    <div className="flex items-end justify-between mb-8 flex-wrap gap-4">
      <div>
        <div className="text-[11px] tracking-[0.22em] uppercase font-semibold text-accent mb-3">
          Explorer
        </div>
        <h2 className="font-serif font-light text-4xl md:text-5xl tracking-[-0.025em] text-ink dark:text-white">
          Naviguer comme dans un dossier.
        </h2>
        <p className="mt-3 text-[13px] text-ink/60 dark:text-white/60 max-w-lg leading-relaxed">
          Chaque catégorie ouvre ses sujets, qui ouvrent leurs concepts. Sélectionnez un fichier pour en voir l'aperçu.
        </p>
      </div>
      <div className="inline-flex rounded-full border border-black/10 dark:border-white/15 p-1 bg-paper-raised dark:bg-night-raised">
        <button onClick={() => setView('columns')}
          className={`px-4 py-1.5 rounded-full text-[11px] tracking-[0.16em] uppercase font-semibold transition-colors
            ${view==='columns'
              ? 'bg-ink text-paper dark:bg-white dark:text-night'
              : 'text-ink/60 dark:text-white/60 hover:text-ink dark:hover:text-white'}`}>
          Colonnes
        </button>
        <button onClick={() => setView('tree')}
          className={`px-4 py-1.5 rounded-full text-[11px] tracking-[0.16em] uppercase font-semibold transition-colors
            ${view==='tree'
              ? 'bg-ink text-paper dark:bg-white dark:text-night'
              : 'text-ink/60 dark:text-white/60 hover:text-ink dark:hover:text-white'}`}>
          Arbre
        </button>
      </div>
    </div>

    {/* Window chrome */}
    <div className="rounded-2xl overflow-hidden border border-black/10 dark:border-white/10
                    bg-paper-raised dark:bg-night-raised shadow-2xl shadow-black/5">
      {/* Title bar */}
      <div className="flex items-center gap-3 px-4 py-3 border-b border-black/[0.07] dark:border-white/[0.1]
                      bg-paper-sunk dark:bg-night-sunk">
        <div className="flex gap-1.5">
          <span className="w-3 h-3 rounded-full bg-[#ff5f57]"/>
          <span className="w-3 h-3 rounded-full bg-[#febc2e]"/>
          <span className="w-3 h-3 rounded-full bg-[#28c840]"/>
        </div>
        <div className="flex items-center gap-1 ml-2">
          <button disabled={path.length === 0}
            onClick={() => setPath(p => p.slice(0,-1))}
            className="w-7 h-7 rounded-md flex items-center justify-center text-ink/60 dark:text-white/60
                       hover:bg-black/5 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed">
            <svg width="11" height="11" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.4">
              <polyline points="6,1 1,6 6,11"/><line x1="1" y1="6" x2="14" y2="6"/>
            </svg>
          </button>
          <button disabled
            className="w-7 h-7 rounded-md flex items-center justify-center text-ink/30 dark:text-white/30">
            <svg width="11" height="11" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.4">
              <line x1="2" y1="6" x2="15" y2="6"/><polyline points="10,1 15,6 10,11"/>
            </svg>
          </button>
        </div>
        <div className="flex-1 text-center font-mono text-[11px] text-ink/55 dark:text-white/55 tracking-wide">
          lawandeconomics{path.length ? ' / ' + path.join(' / ') : ''}
        </div>
        <div className="w-12"/>
      </div>

      {/* Body — columns view */}
      {view === 'columns' && (
        <div className="grid"
             style={{ gridTemplateColumns: `repeat(${Math.max(columns.length, 1)}, minmax(180px, 1fr)) 1.4fr`, height: 540 }}>
          {columns.map((col, depth) => (
            <FinderColumn key={depth} col={col} depth={depth}
              selectedKey={path[depth] || null}
              onClick={(e) => onClickEntry(depth, e)}/>
          ))}
          <FinderPreview concept={selectedConcept} onOpen={onOpenItem}/>
        </div>
      )}

      {/* Body — tree view */}
      {view === 'tree' && (
        <div className="grid grid-cols-1 md:grid-cols-[1.2fr_1fr]" style={{ minHeight: 540 }}>
          <FinderTreeView tree={tree} onOpen={onOpenItem}/>
          <FinderPreview concept={selectedConcept || items[0]} onOpen={onOpenItem}/>
        </div>
      )}
    </div>

    {!ready && (
      <div className="text-center mt-6 text-[12px] font-mono text-ink/50 dark:text-white/50">
        Chargement de l'index…
      </div>
    )}
    {ready && items.length === 0 && (
      <div className="mt-6 px-6 py-4 rounded-xl border border-dashed border-black/15 dark:border-white/15 text-[13px] text-ink/65 dark:text-white/65 leading-relaxed">
        Aucun concept indexé pour le moment. Déposez vos dossiers dans <code className="font-mono text-ink dark:text-white">public/content/&lt;catégorie&gt;/&lt;slug&gt;/</code> avec un <code className="font-mono">meta.json</code> et des images <code className="font-mono">01.png, 02.png…</code>
      </div>
    )}
  </div>
</section>
);
}

function FinderColumn({ col, depth, selectedKey, onClick }) {
return (
<div className="border-r border-black/[0.06] dark:border-white/[0.08] flex flex-col overflow-hidden
                bg-paper-raised dark:bg-night-raised">
  <div className="px-3 py-2 text-[9.5px] tracking-[0.18em] uppercase font-semibold
                  text-ink/45 dark:text-white/45 font-mono
                  border-b border-black/[0.05] dark:border-white/[0.06]
                  bg-paper-sunk/50 dark:bg-night-sunk/50">
    {col.title}
  </div>
  <div className="flex-1 overflow-auto">
    {col.entries.length === 0 ? (
      <div className="p-5 text-[12px] text-ink/45 dark:text-white/45 italic">Vide.</div>
    ) : col.entries.map(e => {
      const active = e.key === selectedKey;
      return (
        <button key={e.key} onClick={() => onClick(e)}
          className={`w-full text-left px-3 py-2 flex items-center gap-2.5 text-[13px]
                      border-b border-black/[0.03] dark:border-white/[0.04]
                      transition-colors duration-100
                      ${active
                        ? 'bg-accent text-white'
                        : 'hover:bg-black/[0.04] dark:hover:bg-white/[0.06] text-ink dark:text-white/90'}`}>
          {e.type === 'category' || e.type === 'topic' ? (
            <FolderGlyph color={active ? '#fff' : (e.color || 'currentColor')} size={16}/>
          ) : (
            <FileGlyph color={active ? '#fff' : (e.color || 'currentColor')} size={14}/>
          )}
          <span className="flex-1 truncate">{e.label}</span>
          {e.denom && (
            <span className={`font-mono text-[9px] tracking-wider ${active ? 'text-white/70' : 'text-ink/40 dark:text-white/40'}`}>
              {e.denom}
            </span>
          )}
          {e.count > 0 && (
            <span className={`font-mono text-[10px] ${active ? 'text-white/75' : 'text-ink/45 dark:text-white/45'}`}>
              {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"
                 className={active ? 'text-white' : 'text-ink/40 dark:text-white/40'}>
              <polyline points="1,1 5,4.5 1,8"/>
            </svg>
          )}
        </button>
      );
    })}
  </div>
</div>
);
}

function FolderGlyph({ color = 'currentColor', size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 16 14" className="shrink-0">
  <path d="M1.5 1C0.67 1 0 1.67 0 2.5V11.5C0 12.33 0.67 13 1.5 13H14.5C15.33 13 16 12.33 16 11.5V4.5C16 3.67 15.33 3 14.5 3H8L6.5 1H1.5Z"
        fill={color} fillOpacity="0.22" stroke={color} strokeWidth="1"/>
</svg>
);
}

function FileGlyph({ color = 'currentColor', size = 14 }) {
return (
<svg width={size} height={size+2} viewBox="0 0 14 16" className="shrink-0">
  <path d="M1.5 0H9L13 4V14.5C13 15.33 12.33 16 11.5 16H1.5C0.67 16 0 15.33 0 14.5V1.5C0 0.67 0.67 0 1.5 0Z"
        fill={color} fillOpacity="0.18" stroke={color} strokeWidth="0.9"/>
  <path d="M9 0V4H13" stroke={color} strokeWidth="0.9" fill="none"/>
</svg>
);
}

/* Vue arbre — alternative façon explorateur de code */
function FinderTreeView({ tree, onOpen }) {
return (
<div className="border-r border-black/[0.06] dark:border-white/[0.08] overflow-auto p-3 font-mono text-[13px]"
     style={{ maxHeight: 540 }}>
  <div className="flex items-center gap-2 pb-3 mb-2 border-b border-black/[0.07] dark:border-white/[0.08]">
    <span className="text-[10px] uppercase tracking-[0.18em] font-semibold text-ink/50 dark:text-white/50">
      lawandeconomics /
    </span>
  </div>
  {Object.values(tree).length === 0 ? (
    <div className="text-ink/45 dark:text-white/45 italic px-2 py-6 text-center text-[12px] sans">
      Aucun fichier.
    </div>
  ) : (
    <ul className="space-y-0.5">
      {CATS.map(c => tree[c.key] && (
        <TreeNode key={c.key} node={tree[c.key]} name={c.key} depth={0} catColor={c.hex} onOpen={onOpen} forceColor={c.hex}/>
      ))}
    </ul>
  )}
</div>
);
}

function TreeNode({ node, name, depth, catColor, forceColor, onOpen }) {
const [open, setOpen] = useState(depth < 1);
const [hover, setHover] = useState(false);
const isConcept = !!node._concept;
const children = Object.entries(node._children || {});
const color = forceColor || catColor;

return (
<li>
  <button
    onClick={() => isConcept ? onOpen(node._concept) : setOpen(o => !o)}
    onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
    className="group w-full flex items-center gap-2 py-1 px-2 rounded-md transition-colors text-left
               hover:bg-black/[0.05] dark:hover:bg-white/[0.06]"
    style={{ paddingLeft: depth * 14 + 8 }}>
    <span className="w-3 flex justify-center">
      {!isConcept && children.length > 0 ? (
        <svg width="6" height="8" viewBox="0 0 6 8" fill="none" stroke="currentColor" strokeWidth="1.5"
             className={`text-ink/55 dark:text-white/55 transition-transform ${open ? 'rotate-90' : ''}`}>
          <polyline points="1,1 5,4 1,7"/>
        </svg>
      ) : <span className="w-1 h-1 rounded-full bg-current opacity-30"/>}
    </span>
    {isConcept
      ? <FileGlyph color={color} size={13}/>
      : <FolderGlyph color={color} size={14}/>}
    <span className={`text-[12.5px] ${hover ? 'text-ink dark:text-white' : 'text-ink/85 dark:text-white/85'}`}>
      {isConcept ? node._concept.title.replace(/\n/g,' ') : name}
    </span>
    {!isConcept && children.length > 0 && (
      <span className="ml-auto text-[10px] text-ink/40 dark:text-white/40">{children.length}</span>
    )}
  </button>
  {!isConcept && open && children.length > 0 && (
    <ul className="space-y-0.5">
      {children.map(([k, child]) => (
        <TreeNode key={k} node={child} name={k} depth={depth+1} catColor={catColor} forceColor={forceColor} onOpen={onOpen}/>
      ))}
    </ul>
  )}
</li>
);
}

function FinderPreview({ concept, onOpen }) {
if (!concept) return (
<div className="p-8 bg-paper-sunk dark:bg-night-sunk flex items-center justify-center text-center text-[13px] text-ink/50 dark:text-white/50">
  Sélectionnez un concept pour afficher son aperçu.
</div>
);
const cat = CAT_BY_KEY[concept.category];
const color = cat?.hex || '#999';
const imgs = concept.images || [];
return (
<div className="p-6 bg-paper-sunk dark:bg-night-sunk overflow-auto flex flex-col gap-4 max-h-[540px]">
  <div className="flex items-center justify-between">
    <div className="text-[10.5px] tracking-[0.22em] uppercase font-semibold" style={{ color }}>
      {cat?.label}
    </div>
    <span className="font-mono text-[10px] text-ink/40 dark:text-white/40">{cat?.denom}</span>
  </div>
  <h3 className="font-serif text-[22px] leading-[1.16] tracking-[-0.012em] text-ink dark:text-white whitespace-pre-line">
    {concept.title}
  </h3>
  {concept.summary && (
    <p className="text-[13px] leading-[1.6] text-ink/65 dark:text-white/65">{concept.summary}</p>
  )}

  <div className="rounded-xl overflow-hidden border border-black/[0.07] dark:border-white/[0.1] aspect-[3/2]
                  bg-paper-raised dark:bg-night-raised flex items-center justify-center">
    {imgs.length > 0 ? (
      <img src={imgs[0]} alt="" className="w-full h-full object-cover"/>
    ) : (
      <ConceptPlaceholder color={color} title={concept.title}/>
    )}
  </div>

  {imgs.length > 1 && (
    <div>
      <div className="text-[10px] tracking-[0.16em] uppercase font-semibold text-ink/55 dark:text-white/55 mb-2">
        {imgs.length} slides
      </div>
      <div className="grid grid-cols-4 gap-1.5">
        {imgs.slice(0, 8).map((src, i) => (
          <div key={i} className="aspect-square rounded-md overflow-hidden border border-black/[0.07] dark:border-white/[0.1]">
            <img src={src} alt="" className="w-full h-full object-cover"/>
          </div>
        ))}
      </div>
    </div>
  )}

  {concept.tags?.length > 0 && (
    <div className="flex gap-1.5 flex-wrap">
      {concept.tags.map(tag => (
        <span key={tag} className="px-2 py-0.5 rounded-full text-[10px] font-mono tracking-wide
                                   bg-black/5 dark:bg-white/10 text-ink/70 dark:text-white/70">
          {tag}
        </span>
      ))}
    </div>
  )}

  <button onClick={() => onOpen(concept)}
    className="mt-auto inline-flex items-center justify-center gap-3 px-5 py-3 rounded-full
               bg-ink dark:bg-white text-paper dark:text-ink font-medium text-[11px] tracking-[0.18em] uppercase
               hover:translate-y-[-1px] transition-transform shadow-md">
    Ouvrir le carrousel
    <svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="currentColor" strokeWidth="1.4">
      <line x1="0" y1="4" x2="11" y2="4"/><polyline points="9,1 12,4 9,7"/>
    </svg>
  </button>
</div>
);
}

/* Placeholder typographique — pas de gravure SVG, juste de la typo */
function ConceptPlaceholder({ color, title }) {
const initial = title.replace(/[^a-zA-ZÀ-ÿ]/g,'').slice(0,2).toUpperCase();
return (
<div className="w-full h-full relative flex items-center justify-center"
     style={{ background: `linear-gradient(135deg, ${color}10 0%, ${color}28 100%)` }}>
  <span className="font-serif italic text-[6vw] md:text-[5rem] font-light"
        style={{ color: color, opacity: 0.5 }}>
    {initial}
  </span>
  <div className="absolute bottom-2 right-3 font-mono text-[9px] tracking-[0.16em] uppercase"
       style={{ color, opacity: 0.6 }}>
    Aperçu
  </div>
</div>
);
}

/* ============================================================
11. INFO CARD fan-out (composant que tu m'as fourni, adapté)
============================================================ */

function InfoCardFanout({ item, onOpen }) {
const [hover, setHover] = useState(false);
const [overflow, setOverflow] = useState(false);
const imgs = (item.images || []).slice(0, 3);
const count = imgs.length;

useEffect(() => {
if (hover) {
  const t = setTimeout(() => setOverflow(true), 100);
  return () => clearTimeout(t);
}
setOverflow(false);
}, [hover]);

const rot = (i) => !hover || count === 1 ? 0 : (i - (count===2?0.5:1)) * 5;
const tx  = (i) => !hover || count === 1 ? 0 : (i - (count===2?0.5:1)) * 22;
const ty  = (i) => !hover ? 0 : count===1 ? -6 : (i===0 ? -12 : i===1 ? -6 : 0);
const sc  = (i) => !hover ? 1 : count===1 ? 1 : 0.95 + i*0.025;

return (
<button
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
  onClick={() => onOpen(item)}
  className="group relative text-left rounded-2xl border border-black/[0.08] dark:border-white/[0.1]
             bg-paper-raised dark:bg-night-raised
             p-5 flex flex-col w-full
             transition-all duration-300
             hover:border-ink/30 dark:hover:border-white/30
             hover:-translate-y-0.5"
  style={{
    boxShadow: hover ? `0 18px 40px ${item.color}25, 0 4px 12px rgba(0,0,0,.06)` : '0 1px 2px rgba(0,0,0,.04)',
  }}>
  {/* Top accent stripe */}
  <span className="absolute top-0 left-5 right-5 h-[2px] rounded-b-full"
        style={{ background: item.color }}/>

  {/* Header */}
  <div className="flex items-baseline justify-between mb-3 mt-1">
    <span className="text-[10px] tracking-[0.22em] uppercase font-semibold" style={{ color: item.color }}>
      {item.categoryLabel}
    </span>
    <span className="font-mono text-[10px] text-ink/45 dark:text-white/45">
      {CAT_BY_KEY[item.category]?.denom}
    </span>
  </div>

  {/* Title + summary */}
  <h3 className="font-serif text-[19px] leading-[1.18] tracking-[-0.01em] text-ink dark:text-white whitespace-pre-line">
    {item.title}
  </h3>
  {item.summary && (
    <p className="mt-2 text-[12.5px] leading-[1.55] text-ink/60 dark:text-white/60 line-clamp-3">
      {item.summary}
    </p>
  )}

  {/* Media zone — fan-out OR placeholder */}
  <div
    className={`relative mt-4 rounded-lg ${overflow ? 'overflow-visible' : 'overflow-hidden'}`}
    style={{
      height: count > 0 ? (hover ? 165 : 90) : 90,
      transition: 'height 380ms cubic-bezier(0.34,1.2,0.64,1)',
    }}>
    {count > 0 ? (
      <div className="relative h-[90px]">
        {imgs.map((src, i) => (
          <div key={src}
            className="absolute inset-x-0 transition-transform duration-[400ms]"
            style={{
              transform: `translate(${tx(i)}px, ${ty(i)}px) rotate(${rot(i)}deg) scale(${sc(i)})`,
              transitionTimingFunction: 'cubic-bezier(0.34,1.3,0.64,1.1)',
              zIndex: 10 - i,
            }}>
            <img src={src} alt=""
                 className="w-full h-[120px] object-cover rounded-md
                            border border-black/[0.08] dark:border-white/[0.12]
                            shadow-lg bg-paper-sunk dark:bg-night-sunk"/>
          </div>
        ))}
        <div
          className="absolute inset-x-0 bottom-0 h-10 pointer-events-none transition-opacity duration-300
                     bg-gradient-to-b from-transparent to-paper-raised dark:to-night-raised"
          style={{ opacity: hover ? 0 : 1 }}/>
      </div>
    ) : (
      <ConceptPlaceholder color={item.color} title={item.title}/>
    )}
  </div>

  {/* Footer */}
  <div className="mt-auto pt-4 flex items-center justify-between text-[10px] font-mono">
    <span className="text-ink/45 dark:text-white/45 tracking-wide">
      {count > 0 ? `${item.imageCount || count} slides` : 'Concept'}
    </span>
    <span className="inline-flex items-center gap-1.5 transition-all"
          style={{ color: hover ? item.color : 'currentColor', transform: hover ? 'translateX(3px)' : 'translateX(0)' }}>
      Ouvrir
      <svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="currentColor" strokeWidth="1.4">
        <line x1="0" y1="4" x2="11" y2="4"/><polyline points="9,1 12,4 9,7"/>
      </svg>
    </span>
  </div>
</button>
);
}

/* ============================================================
12. PUBLICATIONS — grille filtrable
============================================================ */

function Publications({ onOpenItem }) {
const { items, ready } = useContent();
const [filter, setFilter] = useState('all');
const [sort, setSort] = useState('recent');

const list = useMemo(() => {
let src = items;
if (filter !== 'all') src = src.filter(it => it.category === filter);
if (sort === 'alpha') src = [...src].sort((a,b) => a.title.localeCompare(b.title));
return src;
}, [items, filter, sort]);

return (
<section id="concepts" className="relative px-6 py-20 border-t border-black/[0.06] dark:border-white/[0.08]">
  <AuroraBackdrop/>
  <div className="max-w-7xl mx-auto">
    <div className="flex items-end justify-between mb-10 flex-wrap gap-6">
      <div>
        <div className="text-[11px] tracking-[0.22em] uppercase font-semibold text-accent mb-3">
          Bibliothèque
        </div>
        <h2 className="font-serif font-light text-4xl md:text-5xl tracking-[-0.025em] text-ink dark:text-white">
          Concepts publiés.
        </h2>
      </div>
      <div className="flex items-center gap-2 flex-wrap">
        <FilterChip label="Toutes" active={filter==='all'} onClick={() => setFilter('all')}
          count={items.length} dot="#15151a"/>
        {CATS.map(c => {
          const cnt = items.filter(it => it.category === c.key).length;
          return (
            <FilterChip key={c.key} label={c.label} active={filter===c.key}
              onClick={() => setFilter(c.key)} count={cnt} dot={c.hex}/>
          );
        })}
      </div>
    </div>

    {!ready ? (
      <PubsSkeleton/>
    ) : list.length === 0 ? (
      <EmptyPubs filter={filter}/>
    ) : (
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
        {list.map((it, i) => (
          <div key={it.id} style={{ animation: `cardIn 540ms ${Math.min(i,8)*45}ms cubic-bezier(.4,0,.2,1) both` }}>
            <InfoCardFanout item={it} onOpen={onOpenItem}/>
          </div>
        ))}
      </div>
    )}
  </div>
  <style>{`@keyframes cardIn { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: translateY(0) } }`}</style>
</section>
);
}

function FilterChip({ label, active, onClick, count, dot }) {
return (
<button onClick={onClick}
  className={`group inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full
              text-[10.5px] tracking-[0.16em] uppercase font-semibold transition-all
              ${active
                ? 'bg-ink text-paper dark:bg-white dark:text-night border border-ink dark:border-white'
                : 'border border-black/12 dark:border-white/15 text-ink/75 dark:text-white/75 hover:border-ink dark:hover:border-white hover:text-ink dark:hover:text-white'}`}>
  <span className="w-1.5 h-1.5 rounded-full" style={{ background: dot }}/>
  <span>{label}</span>
  {count != null && (
    <span className={`font-mono text-[9.5px] ${active ? 'opacity-70' : 'opacity-50'}`}>{count}</span>
  )}
</button>
);
}

function PubsSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
  {Array.from({length: 8}).map((_, i) => (
    <div key={i} className="rounded-2xl border border-black/[0.08] dark:border-white/[0.1] p-5 h-[280px] relative overflow-hidden">
      <div className="h-3 w-24 bg-black/5 dark:bg-white/5 rounded mb-4"/>
      <div className="h-5 w-3/4 bg-black/8 dark:bg-white/8 rounded mb-3"/>
      <div className="h-3 w-full bg-black/5 dark:bg-white/5 rounded mb-2"/>
      <div className="h-3 w-5/6 bg-black/5 dark:bg-white/5 rounded mb-6"/>
      <div className="h-20 w-full bg-black/4 dark:bg-white/4 rounded"/>
      <div className="absolute inset-0 -translate-x-full animate-shimmer
                      bg-gradient-to-r from-transparent via-black/[0.04] dark:via-white/[0.04] to-transparent"/>
    </div>
  ))}
</div>
);
}

function EmptyPubs({ filter }) {
return (
<div className="border border-dashed border-black/12 dark:border-white/15 rounded-2xl p-14 text-center
                bg-paper-raised/40 dark:bg-night-raised/40">
  <div className="font-serif italic text-3xl text-ink/30 dark:text-white/25 mb-4">∅</div>
  <h3 className="font-serif text-2xl text-ink dark:text-white tracking-[-0.01em]">
    Rien à afficher pour le moment.
  </h3>
  <p className="mt-3 text-[13px] text-ink/60 dark:text-white/60 max-w-md mx-auto leading-relaxed">
    {filter === 'all'
      ? <>Déposez vos premiers concepts dans <code className="font-mono">public/content/&lt;catégorie&gt;/</code>, puis poussez sur Cloudflare. L'index se reconstruit automatiquement au build.</>
      : <>Aucun concept dans cette catégorie. Choisissez « Toutes » ou ajoutez du contenu.</>}
  </p>
</div>
);
}

/* ============================================================
13. MANIFESTE
============================================================ */

const MANIFESTE = [
{ num: '01', title: 'Densité visuelle', body: 'Une diapositive condense ce qu\'un long article dilue. La cognition spatiale fait le reste.' },
{ num: '02', title: 'Mémorisation accrue', body: 'Le format séquentiel mobilise la mémoire de travail mieux qu\'un texte continu. Une notion par fiche.' },
{ num: '03', title: 'Référence rapide', body: 'Chaque carrousel est une unité atomique citable, partageable, archivable. Pas de scroll fatigue.' },
{ 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() {
return (
<section id="manifeste" className="relative px-6 py-24 bg-paper-sunk dark:bg-night-sunk
                                   border-y border-black/[0.06] dark:border-white/[0.08]">
  <div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-[1fr_1.6fr] gap-14">
    <div className="lg:sticky lg:top-32 lg:self-start">
      <div className="text-[11px] tracking-[0.22em] uppercase font-semibold text-accent mb-4">
        Manifeste
      </div>
      <h2 className="font-serif font-light text-[clamp(2rem,4.5vw,3.5rem)] leading-[1.04] tracking-[-0.025em] text-ink dark:text-white">
        Pourquoi le format <em className="not-italic text-accent font-normal">carrousel</em> ?
      </h2>
      <p className="mt-5 text-[14px] leading-[1.7] text-ink/70 dark:text-white/70 max-w-md">
        Les longs articles ne sont plus lus. Les fiches visuelles, oui. Le format impose la rigueur :
        une idée par slide, une source par citation, une notion par fiche.
      </p>
      <blockquote className="mt-8 pt-6 border-t border-black/10 dark:border-white/10
                             font-mono text-[12.5px] text-ink/55 dark:text-white/55 leading-[1.6] tracking-[0.01em]">
        « La densité visuelle est l'arme moderne de la doctrine. »
      </blockquote>
    </div>
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
      {MANIFESTE.map(p => (
        <div key={p.num} className="rounded-2xl bg-paper-raised dark:bg-night-raised
                                    border border-black/[0.07] dark:border-white/[0.1]
                                    p-7 transition-transform hover:-translate-y-0.5">
          <div className="font-mono text-[10px] tracking-[0.14em] text-accent mb-4">{p.num} —</div>
          <h3 className="font-serif text-[20px] leading-[1.2] tracking-[-0.012em] text-ink dark:text-white">
            {p.title}
          </h3>
          <p className="mt-3 text-[13px] leading-[1.65] text-ink/65 dark:text-white/65">{p.body}</p>
        </div>
      ))}
    </div>
  </div>
</section>
);
}

/* ============================================================
14. FOOTER
============================================================ */

function Footer() {
return (
<footer id="about" className="relative px-6 py-20 bg-paper-raised dark:bg-night-raised">
  <div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-[1.6fr_1fr_1fr_1fr] gap-10">
    <div>
      <div className="font-medium text-[15px] tracking-tight">
        lawandeconomics<span className="opacity-40">.ch</span>
      </div>
      <p className="mt-4 text-[13px] leading-[1.7] text-ink/65 dark:text-white/65 max-w-xs">
        Plateforme de vulgarisation juridique et économique. Notions clés expliquées en carrousels visuels.
      </p>
      <div className="mt-6 flex gap-2">
        {['linkedin','x','mail','rss'].map(k => <SocialIcon key={k} kind={k}/>)}
      </div>
    </div>
    {[
      { title: 'Thématiques', items: CATS.map(c => c.label) },
      { title: 'Ressources', items: ['Index', 'Bibliographie', 'Glossaire', 'Citations'] },
      { title: 'À propos',  items: ['Le projet', 'Contact', 'Mentions légales', 'CC BY-NC 4.0'] },
    ].map(col => (
      <div key={col.title}>
        <div className="font-mono text-[9.5px] tracking-[0.18em] uppercase font-semibold text-ink/50 dark:text-white/50 mb-5">
          {col.title}
        </div>
        <ul className="space-y-2.5">
          {col.items.map(it => (
            <li key={it}>
              <a href="#" className="text-[12.5px] text-ink/70 dark:text-white/70 hover:text-accent dark:hover:text-accent transition-colors">
                {it}
              </a>
            </li>
          ))}
        </ul>
      </div>
    ))}
  </div>
  <div className="max-w-7xl mx-auto mt-14 pt-6 border-t border-black/[0.07] dark:border-white/[0.1]
                  flex justify-between items-center font-mono text-[10px] tracking-[0.06em]
                  text-ink/45 dark:text-white/45">
    <span>© 2026 LAWANDECONOMICS.CH — GENÈVE</span>
    <span>CC BY-NC 4.0</span>
  </div>
</footer>
);
}

function SocialIcon({ kind }) {
const path = {
linkedin: <><rect x="3" y="3" width="18" height="18" rx="1"/><path d="M7 10v7M7 7v.01" strokeLinecap="round"/><path d="M11 17v-4a2 2 0 0 1 4 0v4M11 10v7"/></>,
x:        <><line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/></>,
mail:     <><rect x="3" y="5" width="18" height="14" rx="1"/><polyline points="3,7 12,14 21,7"/></>,
rss:      <><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1.5"/></>,
}[kind];
return (
<a href="#" aria-label={kind}
   className="w-9 h-9 rounded-full border border-black/10 dark:border-white/15
              flex items-center justify-center text-ink/65 dark:text-white/65
              hover:bg-ink hover:text-paper dark:hover:bg-white dark:hover:text-night
              transition-colors">
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">{path}</svg>
</a>
);
}

/* ============================================================
15. CAROUSEL VIEWER (modale)
============================================================ */

function CarouselViewer({ open, item, onClose }) {
const [idx, setIdx] = useState(0);
useEffect(() => { setIdx(0); }, [item?.id]);
useEffect(() => {
if (!open || !item) return;
const onKey = (e) => {
  if (e.key === 'Escape') onClose();
  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);
document.body.style.overflow = 'hidden';
return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, [open, item, onClose]);

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

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-8"
     style={{ animation: 'fadeIn 240ms ease both' }}
     onClick={onClose}>
  <div className="absolute inset-0 bg-black/72 backdrop-blur-md"/>
  <div onClick={e => e.stopPropagation()}
       className="relative w-full max-w-5xl max-h-[92vh] flex flex-col rounded-2xl overflow-hidden
                  bg-paper dark:bg-night
                  border border-black/10 dark:border-white/15
                  shadow-2xl"
       style={{ animation: 'modalIn 360ms cubic-bezier(.34,1.2,.64,1) both' }}>
    <div className="flex items-center justify-between px-5 py-3 border-b border-black/[0.07] dark:border-white/[0.1]">
      <div className="flex items-center gap-3 min-w-0">
        <span className="w-2 h-2 rounded-full" style={{ background: color }}/>
        <span className="text-[10.5px] tracking-[0.22em] uppercase font-semibold" style={{ color }}>
          {item.categoryLabel}
        </span>
        <span className="text-ink/30 dark:text-white/30 hidden sm:inline">·</span>
        <span className="font-serif text-[16px] text-ink dark:text-white truncate">
          {item.title.replace(/\n/g, ' ')}
        </span>
      </div>
      <button onClick={onClose} aria-label="Fermer"
        className="w-9 h-9 rounded-full flex items-center justify-center
                   text-ink/65 dark:text-white/65
                   hover:bg-ink hover:text-paper dark:hover:bg-white dark:hover:text-night transition-colors">
        <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
          <line x1="2" y1="2" x2="12" y2="12"/><line x1="12" y1="2" x2="2" y2="12"/>
        </svg>
      </button>
    </div>

    <div className="flex-1 relative bg-paper-sunk dark:bg-night-sunk min-h-[420px] flex items-center justify-center p-6 sm:p-10">
      {has ? (
        <img key={imgs[idx]} src={imgs[idx]} alt=""
             className="max-w-full max-h-[70vh] object-contain rounded-md
                        border border-black/[0.07] dark:border-white/[0.1] shadow-xl"
             style={{ animation: 'fadeIn 280ms ease both' }}/>
      ) : (
        <div className="text-center max-w-md">
          <ConceptPlaceholder color={color} title={item.title}/>
          <p className="mt-6 text-[13px] text-ink/60 dark:text-white/60 leading-relaxed">
            Carrousel à venir. Ajoutez des images dans
            <code className="font-mono text-[11px] text-ink dark:text-white block mt-1">
              public/content/{item.category}/{item.slug}/
            </code>
          </p>
        </div>
      )}

      {has && total > 1 && (
        <>
          <button onClick={() => setIdx(i => Math.max(0, i-1))} disabled={idx === 0}
            className="absolute left-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full
                       bg-paper-raised dark:bg-night-raised border border-black/10 dark:border-white/15
                       flex items-center justify-center disabled:opacity-30
                       hover:bg-ink hover:text-paper dark:hover:bg-white dark:hover:text-night transition-colors">
            <svg width="14" height="11" viewBox="0 0 14 11" fill="none" stroke="currentColor" strokeWidth="1.4">
              <polyline points="6,1 1,5.5 6,10"/><line x1="1" y1="5.5" x2="13" y2="5.5"/>
            </svg>
          </button>
          <button onClick={() => setIdx(i => Math.min(total-1, i+1))} disabled={idx === total-1}
            className="absolute right-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full
                       bg-paper-raised dark:bg-night-raised border border-black/10 dark:border-white/15
                       flex items-center justify-center disabled:opacity-30
                       hover:bg-ink hover:text-paper dark:hover:bg-white dark:hover:text-night transition-colors">
            <svg width="14" height="11" viewBox="0 0 14 11" fill="none" stroke="currentColor" strokeWidth="1.4">
              <line x1="1" y1="5.5" x2="13" y2="5.5"/><polyline points="8,1 13,5.5 8,10"/>
            </svg>
          </button>
        </>
      )}
    </div>

    {has && (
      <div className="flex items-center justify-between gap-4 px-5 py-3 border-t border-black/[0.07] dark:border-white/[0.1]
                      font-mono text-[10.5px] text-ink/55 dark:text-white/55">
        <span>{String(idx+1).padStart(2,'0')} / {String(total).padStart(2,'0')}</span>
        <div className="flex gap-1 flex-wrap justify-center">
          {imgs.map((_, i) => (
            <button key={i} onClick={() => setIdx(i)} aria-label={`Slide ${i+1}`}
              className="h-[3px] transition-all rounded-full"
              style={{
                width: i === idx ? 28 : 12,
                background: i === idx ? color : 'currentColor',
                opacity: i === idx ? 1 : 0.3,
              }}/>
          ))}
        </div>
        <span className="tracking-[0.06em] uppercase hidden sm:inline">← → ESC</span>
      </div>
    )}
  </div>
  <style>{`
    @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
    @keyframes modalIn { from { opacity: 0; transform: translateY(20px) scale(.97) } to { opacity: 1; transform: translateY(0) scale(1) } }
  `}</style>
</div>
);
}

/* ============================================================
16. APP
============================================================ */

function App() {
const [openItem, setOpenItem] = useState(null);
const [presetCat, setPresetCat] = useState(null);

const handleJump = (id, cat) => {
if (cat) setPresetCat(cat);
requestAnimationFrame(() => {
  const el = document.getElementById(id);
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
};

return (
<ThemeProvider>
  <ContentProvider>
    <div className="relative min-h-screen bg-paper dark:bg-night text-ink dark:text-white selection:bg-accent selection:text-white">
      <Navbar onJump={handleJump}/>
      <Hero onJump={handleJump}/>
      <CategoriesRail onJump={handleJump}/>
      <FinderExplorer presetCategory={presetCat} onOpenItem={setOpenItem}/>
      <Publications onOpenItem={setOpenItem}/>
      <Manifeste/>
      <Footer/>
      <CarouselViewer open={!!openItem} item={openItem} onClose={() => setOpenItem(null)}/>
    </div>
  </ContentProvider>
</ThemeProvider>
);
}

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