/* ============================================================
   UI kit — icons, social glyphs, generative domain artwork,
   placeholders, scribble underline, stars.
   ============================================================ */
const { useState: useStateU, useMemo: useMemoU, useRef: useRefU, useEffect: useEffectU } = React;

/* ---------- generic line icons ---------- */
function Icon({ name, size = 22, stroke = 2, style }) {
  const p = { width: size, height: size, viewBox: "0 0 24 24", fill: "none",
    stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", style };
  switch (name) {
    case "menu":   return <svg {...p}><path d="M3 6h18M3 12h18M3 18h18"/></svg>;
    case "close":  return <svg {...p}><path d="M6 6l12 12M18 6L6 18"/></svg>;
    case "arrow":  return <svg {...p}><path d="M5 12h14M13 6l6 6-6 6"/></svg>;
    case "left":   return <svg {...p}><path d="M15 6l-6 6 6 6"/></svg>;
    case "right":  return <svg {...p}><path d="M9 6l6 6-6 6"/></svg>;
    case "down":   return <svg {...p}><path d="M6 9l6 6 6-6"/></svg>;
    case "up":     return <svg {...p}><path d="M6 15l6-6 6 6"/></svg>;
    case "plus":   return <svg {...p}><path d="M12 5v14M5 12h14"/></svg>;
    case "edit":   return <svg {...p}><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>;
    case "trash":  return <svg {...p}><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>;
    case "save":   return <svg {...p}><path d="M5 3h12l4 4v14H5zM8 3v6h8M8 21v-6h8v6"/></svg>;
    case "filter": return <svg {...p}><path d="M3 5h18l-7 8v6l-4 2v-8z"/></svg>;
    case "grip":   return <svg {...p}><circle cx="9" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="18" r="1"/></svg>;
    case "lock":   return <svg {...p}><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>;
    case "user":   return <svg {...p}><circle cx="12" cy="8" r="4"/><path d="M4 20a8 8 0 0 1 16 0"/></svg>;
    case "bell":   return <svg {...p}><path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6M10 20a2 2 0 0 0 4 0"/></svg>;
    case "chart":  return <svg {...p}><path d="M4 20V4M4 20h16M8 16v-5M12 16V8M16 16v-7"/></svg>;
    case "msg":    return <svg {...p}><path d="M4 5h16v11H8l-4 4z"/></svg>;
    case "star":   return <svg {...p}><path d="M12 3l2.7 5.5 6 .9-4.3 4.2 1 6-5.4-2.8-5.4 2.8 1-6L3.3 9.4l6-.9z"/></svg>;
    case "starF":  return <svg {...p} fill="currentColor" stroke="none"><path d="M12 3l2.7 5.5 6 .9-4.3 4.2 1 6-5.4-2.8-5.4 2.8 1-6L3.3 9.4l6-.9z"/></svg>;
    case "play":   return <svg {...p}><circle cx="12" cy="12" r="9"/><path d="M10 9l5 3-5 3z" fill="currentColor"/></svg>;
    case "ext":    return <svg {...p}><path d="M14 5h5v5M19 5l-8 8M11 5H5v14h14v-6"/></svg>;
    case "home":   return <svg {...p}><path d="M4 11l8-7 8 7M6 10v10h12V10"/></svg>;
    case "compass":return <svg {...p}><circle cx="12" cy="12" r="9"/><path d="M15 9l-2 5-5 2 2-5z" fill="currentColor" stroke="none"/></svg>;
    case "globe":  return <svg {...p}><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18"/></svg>;
    case "search": return <svg {...p}><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg>;
    case "check":  return <svg {...p}><path d="M5 13l4 4L19 7"/></svg>;
    case "music":  return <svg {...p}><path d="M9 18V5l11-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="17" cy="16" r="3"/></svg>;
    case "headphones": return <svg {...p}><path d="M4 14v-2a8 8 0 0 1 16 0v2"/><rect x="3" y="14" width="4" height="6" rx="1.4"/><rect x="17" y="14" width="4" height="6" rx="1.4"/></svg>;
    case "doc":    return <svg {...p}><path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5M10 13h6M10 17h6"/></svg>;
    case "rss":    return <svg {...p}><path d="M5 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2" fill="currentColor" stroke="none"/><path d="M5 11a8 8 0 0 1 8 8M5 5a14 14 0 0 1 14 14"/></svg>;
    default: return null;
  }
}

/* ---------- social glyphs (generic placeholders, not brand marks) ---------- */
function Social({ id, size = 20 }) {
  const c = "#fff";
  const p = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: c, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" };
  switch (id) {
    case "linkedin": return <svg {...p}><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M7 10v6M7 7v.01M11 16v-4a2 2 0 0 1 4 0v4M11 16v-6" stroke={c}/></svg>;
    case "github":   return <svg {...p}><path d="M9 19c-4 1.4-4-2-6-2.5M15 21v-3.5c0-1 .1-1.4-.5-2 2.8-.3 4.5-1.4 4.5-5a3.9 3.9 0 0 0-1.1-2.7 3.6 3.6 0 0 0-.1-2.7s-1-.3-3 1a10 10 0 0 0-5 0c-2-1.3-3-1-3-1a3.6 3.6 0 0 0-.1 2.7A3.9 3.9 0 0 0 5 10c0 3.5 1.7 4.7 4.5 5-.4.4-.5.9-.5 2V21"/></svg>;
    case "youtube":  return <svg {...p}><rect x="2.5" y="6" width="19" height="12" rx="3.5"/><path d="M10 9.5l5 2.5-5 2.5z" fill={c} stroke="none"/></svg>;
    case "instagram":return <svg {...p}><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="3.6"/><circle cx="17" cy="7" r="1" fill={c} stroke="none"/></svg>;
    case "x":        return <svg {...p}><path d="M4 4l16 16M20 4L4 20"/></svg>;
    default:         return <svg {...p}><circle cx="12" cy="12" r="9"/></svg>;
  }
}

/* social pill button */
function SocialChip({ s, size = 38 }) {
  return (
    <a className="socialchip" href={s.link} target="_blank" rel="noreferrer" title={s.name}
       style={{ width: size, height: size, background: s.color || "#211e1a" }}>
      <Social id={s.id} size={size * 0.5} />
    </a>
  );
}

/* ---------- scribble underline ---------- */
function Scribble({ children, color }) {
  return (
    <span className="scribble">
      {children}
      <svg viewBox="0 0 300 24" preserveAspectRatio="none" aria-hidden="true">
        <path d="M4 14 C 60 6, 120 20, 180 12 S 280 8, 296 16" style={color ? { stroke: color } : null}/>
      </svg>
    </span>
  );
}

/* ---------- content-kind metadata — single source of truth for icon / label / noun ----------
   Used by section grids, menus, mode dashboards and the content detail page so every
   content type (blog · video · podcast · music · audio · document) renders consistently. */
const KIND = {
  blog:     { icon: "edit",       word: "article",  words: "articles",  label: "Article",  player: false },
  video:    { icon: "play",       word: "video",    words: "videos",    label: "Video",    player: true  },
  podcast:  { icon: "msg",        word: "podcast",  words: "podcasts",  label: "Podcast",  player: true  },
  music:    { icon: "music",      word: "track",    words: "tracks",    label: "Track",    player: true  },
  audio:    { icon: "headphones", word: "audio",    words: "audio",     label: "Audio",    player: true  },
  sound:    { icon: "headphones", word: "album",    words: "albums",    label: "Album",    player: true  },
  image:    { icon: "compass",    word: "gallery",  words: "galleries", label: "Gallery",  player: false },
  document: { icon: "doc",        word: "document", words: "documents", label: "Document", player: false },
};
function kindMeta(k) { return KIND[k] || KIND.blog; }
function iconForKind(k) { return kindMeta(k).icon; }

/* ---------- language availability chips (EN / FR) ----------
   Small badges shown on covers everywhere — they list ALL authored languages,
   not just the one currently displayed. `has` is a (lang)=>bool predicate. */
function LangChips({ has, className }) {
  const langs = ["en", "fr"].filter(l => has(l));
  if (!langs.length) return null;
  return (
    <div className={"lang-chips" + (className ? " " + className : "")}>
      {langs.map(l => <span key={l} className="lc-badge">{l.toUpperCase()}</span>)}
    </div>
  );
}

/* ---------- rating stars ---------- */
function Stars({ n = 0, size = 14 }) {
  return (
    <span className="row" style={{ gap: 2, color: "var(--c-amber)" }}>
      {[1,2,3,4,5].map(i => <Icon key={i} name={i <= n ? "starF" : "star"} size={size} stroke={1.6} />)}
    </span>
  );
}

/* ---------- placeholder ---------- */
function Placeholder({ label = "illustration", tag = "replaceable", style, className = "" }) {
  return (
    <div className={"ph " + className} style={style}>
      <span style={{ maxWidth: "80%" }}>{label}</span>
      {tag ? <span className="ph-tag">{tag}</span> : null}
    </div>
  );
}

/* ---------- image upload (reads a File to a data URL) ---------- */
function ImageUpload({ value, onChange, label = "upload image", height = 90, round = false, aspect }) {
  const ref = useRefU(null);
  const pick = (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => onChange(reader.result);
    reader.readAsDataURL(file);
  };
  return (
    <div className={"img-upload" + (round ? " round" : "")} style={{ height: aspect ? "auto" : height, aspectRatio: aspect || undefined }}
      onClick={() => ref.current && ref.current.click()}
      onDragOver={e => e.preventDefault()}
      onDrop={e => { e.preventDefault(); pick(e.dataTransfer.files[0]); }}>
      {value
        ? <img src={value} alt="" />
        : <span className="iu-label">{label}<span className="ph-tag">img</span></span>}
      <input ref={ref} type="file" accept="image/*" style={{ display: "none" }} onChange={e => pick(e.target.files && e.target.files[0])} />
      {value && <button type="button" className="iu-clear" onClick={e => { e.stopPropagation(); onChange(null); }}><Icon name="close" size={13} /></button>}
    </div>
  );
}

/* ---------- inline rich text: **bold**, __underline__, [text](url),
   internal [text](#/route) / [text](/route), [[term|definition]] bubbles,
   and single \n line breaks ---------- */
function renderInline(text) {
  const out = [];
  let key = 0;
  const lines = String(text == null ? "" : text).split("\n");
  const re = /(\*\*([^*]+)\*\*)|(__([^_]+)__)|(\[\[([^\]|]+)\|([^\]]+)\]\])|(\[([^\]]+)\]\(([^)]+)\))/;
  lines.forEach((line, li) => {
    if (li > 0) out.push(<br key={"br" + (key++)} />);
    let rest = line, m;
    while ((m = rest.match(re))) {
      if (m.index > 0) out.push(rest.slice(0, m.index));
      if (m[2] != null) out.push(<strong key={key++}>{m[2]}</strong>);
      else if (m[4] != null) out.push(<u key={key++}>{m[4]}</u>);
      else if (m[6] != null) out.push(
        <span key={key++} className="defterm" tabIndex={0} role="button" aria-label={m[6] + ": " + m[7]}>
          {m[6]}<span className="def-bubble">{m[7]}</span>
        </span>
      );
      else {
        const url = m[10];
        const internal = /^#|^\//.test(url);
        const href = url.charAt(0) === "/" ? "#" + url : url;
        out.push(<a key={key++} href={href} className="rt-link" {...(internal ? {} : { target: "_blank", rel: "noreferrer" })}>{m[9]}</a>);
      }
      rest = rest.slice(m.index + m[0].length);
    }
    if (rest) out.push(rest);
  });
  return out;
}

/* ---------- tiny markdown render (block-level wrapper) ---------- */
function RichText({ text = "", className, style, as }) {
  const parts = useMemoU(() => renderInline(text), [text]);
  const Tag = as || "p";
  return <Tag className={className} style={style}>{parts}</Tag>;
}

/* ============================================================
   Generative domain artwork — abstract data-art stand-ins.
   Deterministic from a seed so it never reflows.
   ============================================================ */
function rng(seed) {
  let s = seed >>> 0;
  return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}

function DomainArt({ kind = "network", accent = "#2f6bdb", animate = true, bodyOnly = false }) {
  const id = useMemoU(() => "art" + Math.random().toString(36).slice(2, 7), []);
  const W = 520, H = 420;

  let body = null;

  if (kind === "network") {
    const r = rng(7);
    const nodes = Array.from({ length: 22 }, (_, i) => ({
      x: 60 + r() * (W - 120), y: 50 + r() * (H - 100), s: 3 + r() * 7, i,
    }));
    const edges = [];
    nodes.forEach((n, i) => {
      const m = nodes.slice(i + 1).sort((a, b) => Math.hypot(a.x-n.x,a.y-n.y) - Math.hypot(b.x-n.x,b.y-n.y)).slice(0, 2);
      m.forEach(t => edges.push([n, t]));
    });
    body = (
      <>
        {edges.map(([a, b], i) => (
          <line key={i} x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={accent} strokeWidth="1" opacity="0.28" />
        ))}
        {nodes.map((n, i) => (
          <circle key={i} cx={n.x} cy={n.y} r={n.s} fill={i % 4 === 0 ? accent : "none"} stroke={accent} strokeWidth="1.6">
            {animate && <animate attributeName="opacity" values="0.5;1;0.5" dur={`${2.5 + (i % 5)}s`} repeatCount="indefinite" />}
          </circle>
        ))}
      </>
    );
  } else if (kind === "blueprint") {
    body = (
      <g stroke={accent} fill="none" strokeWidth="1.6">
        <rect x="80" y="90" width="360" height="240" rx="6" opacity="0.35" strokeDasharray="4 6" />
        <circle cx="200" cy="210" r="70" />
        <circle cx="200" cy="210" r="44" />
        {Array.from({ length: 12 }).map((_, i) => {
          const a = (i / 12) * Math.PI * 2;
          return <line key={i} x1={200 + Math.cos(a) * 44} y1={210 + Math.sin(a) * 44} x2={200 + Math.cos(a) * 82} y2={210 + Math.sin(a) * 82} opacity="0.7" />;
        })}
        <circle cx="200" cy="210" r="14" fill={accent} />
        <circle cx="350" cy="160" r="36" />
        <circle cx="350" cy="160" r="20" />
        <path d="M236 210 H300" strokeDasharray="3 4" />
        <rect x="300" y="250" width="110" height="60" rx="4" opacity="0.7" />
        <path d="M300 280 H410 M330 250 V310 M380 250 V310" opacity="0.5" />
      </g>
    );
  } else if (kind === "tools") {
    const r = rng(31);
    body = (
      <g stroke={accent} fill="none" strokeWidth="1.8">
        <path d="M120 300 L300 120" strokeWidth="10" opacity="0.18" />
        {/* wrench */}
        <path d="M150 290 l120 -120 a26 26 0 1 0 -22 -22 l-120 120 a16 16 0 0 0 22 22 z" strokeWidth="2.2" />
        {/* bolts scattered */}
        {Array.from({ length: 7 }).map((_, i) => {
          const cx = 320 + r() * 140, cy = 120 + r() * 220, rad = 12 + r() * 10;
          const pts = Array.from({ length: 6 }, (_, k) => {
            const a = (k / 6) * Math.PI * 2 + 0.5;
            return `${cx + Math.cos(a) * rad},${cy + Math.sin(a) * rad}`;
          }).join(" ");
          return <g key={i}><polygon points={pts} /><circle cx={cx} cy={cy} r={rad * 0.45} /></g>;
        })}
        <path d="M90 360 h340" strokeDasharray="2 8" opacity="0.5" />
      </g>
    );
  } else if (kind === "contour") {
    body = (
      <g fill="none" stroke={accent}>
        {Array.from({ length: 9 }).map((_, i) => {
          const k = i * 18;
          return (
            <path key={i} opacity={0.25 + i * 0.07} strokeWidth={i === 8 ? 2.4 : 1.3}
              d={`M40 ${360 - k * 0.4}
                  C 140 ${300 - k}, 200 ${230 - k * 1.2}, 270 ${150 - k * 0.6}
                  S 420 ${250 - k}, 480 ${330 - k * 0.4}`} />
          );
        })}
        <circle cx="270" cy="92" r="4" fill={accent} />
        <path d="M270 96 v40" strokeDasharray="2 5" />
      </g>
    );
  } else if (kind === "human") {
    // A clear human (woman) head: a calm face, hair, a split mind — intuition (organic
    // swirl + heart) on one side, data / intelligence (a small network) on the other,
    // and "esprit" rising as thought nodes. Deliberately organic — not a robot.
    const cx = 250, cy = 215, R = 96;
    const nodes = Array.from({ length: 7 }, (_, i) => {
      const a = (-0.85 + (i / 6) * 1.7) * Math.PI / 2;          // fan above the head
      const rad = R + 18 + (i % 3) * 26;
      return { x: cx + Math.cos(a) * rad * 0.9, y: cy - 28 - Math.sin(a) * rad, s: 2.6 + (i % 3) };
    });
    body = (
      <g fill="none" stroke={accent} strokeLinecap="round" strokeLinejoin="round">
        {/* hair (woman) — two outer arcs framing the head */}
        <path strokeWidth="2.2" opacity="0.7" d={`M${cx - R - 14} ${cy + 34} C ${cx - R - 30} ${cy - 64}, ${cx - 34} ${cy - R - 42}, ${cx} ${cy - R - 38}`} />
        <path strokeWidth="2.2" opacity="0.7" d={`M${cx + R + 14} ${cy + 34} C ${cx + R + 30} ${cy - 64}, ${cx + 34} ${cy - R - 42}, ${cx} ${cy - R - 38}`} />
        {/* head */}
        <circle cx={cx} cy={cy} r={R} strokeWidth="3" />
        {/* centre line — intuition (left) vs data (right) */}
        <path strokeWidth="1.4" opacity="0.35" strokeDasharray="2 6" d={`M${cx} ${cy - R + 20} L ${cx} ${cy + R - 20}`} />
        {/* left — intuition swirl (organic) */}
        <path strokeWidth="2" opacity="0.85" d={`M${cx - 18} ${cy - 28} C ${cx - 64} ${cy - 28}, ${cx - 64} ${cy + 28}, ${cx - 20} ${cy + 26} C ${cx - 46} ${cy + 24}, ${cx - 44} ${cy - 6}, ${cx - 24} ${cy - 4}`} />
        {/* right — small data grid (intelligence) */}
        {[0, 1, 2].map(r => [0, 1, 2].map(c => (
          <circle key={r + "-" + c} cx={cx + 24 + c * 20} cy={cy - 26 + r * 26} r={2.6} fill={(r + c) % 2 === 0 ? accent : "none"} strokeWidth="1.3" />
        )))}
        {/* eyes — calm, closed (presence / intuition) */}
        <path strokeWidth="2" d={`M${cx - 42} ${cy + 46} q12 -9 24 0`} opacity="0.8" />
        <path strokeWidth="2" d={`M${cx + 18} ${cy + 46} q12 -9 24 0`} opacity="0.8" />
        {/* heart — intuition, warmth */}
        <path strokeWidth="2.2" d={`M${cx} ${cy + R + 28} c -10 -16 -34 -10 -34 8 c 0 16 34 30 34 30 c 0 0 34 -14 34 -30 c 0 -18 -24 -24 -34 -8 z`} opacity="0.9" />
        {/* esprit — thought nodes rising from the mind */}
        {nodes.map((n, i) => (
          <g key={i}>
            <line x1={cx} y1={cy - R + 8} x2={n.x} y2={n.y} strokeWidth="0.9" opacity="0.18" />
            <circle cx={n.x} cy={n.y} r={n.s} strokeWidth="1.5" fill={i % 3 === 0 ? accent : "none"}>
              {animate && <animate attributeName="opacity" values="0.4;1;0.4" dur={`${2.4 + (i % 4)}s`} repeatCount="indefinite" />}
            </circle>
          </g>
        ))}
      </g>
    );
  }

  if (bodyOnly) return body;
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block", maxHeight: "100%" }} role="img">
      {body}
    </svg>
  );
}

/* ---------- superposed multi-domain artwork ----------
   When a piece has no cover, its default thumbnail is the layered artwork of
   EVERY domain it belongs to, drawn into ONE svg that shares the exact same
   viewBox / sizing as a single-domain cover — so a superposed cover is never a
   different size than a plain one. Shapes keep their domain color; the neutral
   white/blue background is supplied by the container (see .thumb-art / .ah-art). */
function MultiDomainArt({ arts, animate = false }) {
  const list = (arts && arts.length) ? arts : [{ kind: "network", accent: "#2f6bdb" }];
  return (
    <svg viewBox="0 0 520 420" width="100%" style={{ display: "block", maxHeight: "100%" }} role="img">
      {list.map((a, i) => (
        <g key={i} opacity={list.length > 1 ? 0.9 : 1}>
          <DomainArt kind={a.kind} accent={a.accent} animate={animate} bodyOnly />
        </g>
      ))}
    </svg>
  );
}

/* ---------- language fallback "ball" switch ----------
   Shows the fallback CHOICE whenever exactly one language is filled
   (the other is missing). When both languages exist, no choice is
   needed — a confirmation line is shown instead. */
function LangFallbackToggle({ enOn, frOn, fallback, onChange, inline }) {
  if (enOn && frOn) {
    if (inline) return null;             // no choice needed — and inline lives inside the status block
    return (
      <div className="langfb done">
        <span className="lfb-ic"><Icon name="check" size={15} /></span>
        <span className="lfb-msg">Published in <b>EN</b> and <b>FR</b>.</span>
      </div>
    );
  }
  if (!enOn && !frOn) {
    if (inline) return null;
    return (
      <div className="langfb done warn">
        <span className="lfb-ic"><Icon name="bell" size={15} /></span>
        <span className="lfb-msg">Add content in at least one language.</span>
      </div>
    );
  }
  const haveCode = enOn ? "EN" : "FR";
  const missCode = enOn ? "FR" : "EN";
  const missFull = enOn ? "French" : "English";
  const strict = fallback === "strict";
  const switchBtn = (
    <button type="button" className={"lfb-switch" + (strict ? " right" : " left")}
      role="switch" aria-checked={!strict}
      onClick={() => onChange(strict ? "fallback" : "strict")}>
      <span className={"lfb-opt" + (!strict ? " on" : "")}>{haveCode} version published for {missFull}</span>
      <span className={"lfb-opt" + (strict ? " on" : "")}>No {missCode} version published</span>
      <span className="lfb-ball" aria-hidden="true" />
    </button>
  );
  if (inline) {
    return (
      <div className="langfb inline">
        <span className="lfb-title">{missCode} missing — choose behaviour</span>
        {switchBtn}
      </div>
    );
  }
  return (
    <div className="langfb">
      <div className="lfb-title">{missCode} is missing — choose behaviour</div>
      {switchBtn}
      <div className="lfb-note">{strict
        ? <>The {missCode} version stays an unpublished draft until you translate it.</>
        : <>{missFull} readers see the {haveCode} version with a small “not translated yet” notice.</>}</div>
    </div>
  );
}

/* ============================================================
   Interactive puzzle artwork — gradient descent (2 axes).
   Drag the weights θ across a loss surface to the GLOBAL minimum.
   Decoy local minima trap naive (1-axis / greedy) descent, so a bot
   sweeping a single axis can't stumble onto it — you must read the
   contours and get BOTH coordinates right.
   Responsive: pointer coords are mapped through the SVG viewBox.
   ============================================================ */
function NetworkPuzzle({ accent = "#2f6bdb", solved = false, onSolve = () => {} }) {
  const W = 460, H = 300;
  // Loss surface: three wells. The deepest (accent, most rings) is the GLOBAL min.
  // Two decoys sit in different rows/columns so an axis-wise sweep of a single
  // weight slides into a decoy — you must read the map to set BOTH weights right.
  const G  = { x: W * 0.60, y: H * 0.40, d: 1.00, s: 60, global: true };
  const D1 = { x: W * 0.22, y: H * 0.66, d: 0.60, s: 46 };
  const D2 = { x: W * 0.84, y: H * 0.70, d: 0.55, s: 42 };
  const wells = [G, D1, D2];
  const loss = (x, y) => {
    let s = 0;
    wells.forEach(w => { s += w.d * Math.exp(-(((x - w.x) ** 2) + ((y - w.y) ** 2)) / (2 * w.s * w.s)); });
    return Math.max(0, Math.min(1, 1 - s));
  };
  const WIN_R = 26;
  // learning-rate sweet-spot band (independent third axis — no loss gradient leads here)
  const ETA_LO = 34, ETA_HI = 52;

  // controls: two weights (0..100) + learning rate (0..100)
  const [t1, setT1] = useStateU(8);
  const [t2, setT2] = useStateU(12);
  const [eta, setEta] = useStateU(78);
  const [done, setDone] = useStateU(solved);
  const [sparks, setSparks] = useStateU([]);
  const doneRef = useRefU(solved);

  useEffectU(() => { if (solved) { setDone(true); doneRef.current = true; } }, [solved]);

  const px = 30 + (t1 / 100) * (W - 60);
  const py = 34 + (t2 / 100) * (H - 64);
  const dist = Math.hypot(px - G.x, py - G.y);
  const inWell = dist <= WIN_R;
  const etaOk = eta >= ETA_LO && eta <= ETA_HI;
  const curLoss = loss(px, py);

  const celebrate = () => {
    const burst = Array.from({ length: 16 }, (_, i) => {
      const a = (i / 16) * Math.PI * 2;
      return { id: i, x: G.x, y: G.y, vx: Math.cos(a) * (26 + Math.random() * 24), vy: Math.sin(a) * (26 + Math.random() * 24), c: [accent, "#e08a1e", "#1f9e72", "#e1543f", "#7a4fe0"][i % 5] };
    });
    setSparks(burst);
    setTimeout(() => setSparks([]), 1100);
  };

  useEffectU(() => {
    if (doneRef.current) return;
    if (inWell && etaOk) { setDone(true); doneRef.current = true; celebrate(); onSolve(); }
    // eslint-disable-next-line
  }, [t1, t2, eta]);

  const ringsFor = (w) => w.global ? [0.6, 1.1, 1.7, 2.3] : [0.8, 1.4];
  const conv = inWell && etaOk;
  const markC = done || conv ? "#1f9e72" : (inWell ? accent : "#8a8478");

  return (
    <div className="puzzle-wrap">
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" className="puzzle-map" role="img" style={{ display: "block" }}>
        <text x={16} y={22} fontFamily="var(--font-mono)" fontSize="11" fill="#8a8478">loss surface — tune the weights to the deepest basin</text>
        {/* contour rings */}
        {wells.map((w, wi) => ringsFor(w).map((m, ri) => (
          <circle key={wi + "-" + ri} cx={w.x} cy={w.y} r={w.s * m} fill="none"
            stroke={w.global ? accent : "#b8b0a2"} strokeWidth={w.global ? 1.4 : 1} opacity={(w.global ? 0.5 : 0.3) - ri * 0.07} />
        )))}
        {wells.map((w, wi) => (
          <g key={"c" + wi}>
            <circle cx={w.x} cy={w.y} r={w.global ? 4.5 : 3} fill={w.global ? accent : "#b8b0a2"} />
            {w.global && <text x={w.x} y={w.y - 12} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="9" fill={accent} opacity={done ? 1 : .8}>{done ? "global min ✓" : "global"}</text>}
          </g>
        ))}
        {/* guide lines from axes to marker */}
        <line x1={px} y1={34} x2={px} y2={py} stroke={markC} strokeWidth="1" strokeDasharray="3 4" opacity=".4" />
        <line x1={30} y1={py} x2={px} y2={py} stroke={markC} strokeWidth="1" strokeDasharray="3 4" opacity=".4" />
        {/* weights marker */}
        <g style={{ transition: "all .12s" }}>
          <circle cx={px} cy={py} r="14" fill={markC} opacity=".15" />
          <circle cx={px} cy={py} r="8" fill={markC} stroke="#fff" strokeWidth="2" />
        </g>
        {sparks.map(s => (
          <circle key={s.id} cx={s.x} cy={s.y} r="3.5" fill={s.c}>
            <animate attributeName="cx" from={s.x} to={s.x + s.vx} dur="0.9s" fill="freeze" />
            <animate attributeName="cy" from={s.y} to={s.y + s.vy} dur="0.9s" fill="freeze" />
            <animate attributeName="opacity" from="1" to="0" dur="0.9s" fill="freeze" />
            <animate attributeName="r" from="4.5" to="1" dur="0.9s" fill="freeze" />
          </circle>
        ))}
      </svg>

      <div className="puzzle-controls">
        <div className="pc-row">
          <span className="pc-lbl">θ₁ <span className="pc-sub">weight 1</span></span>
          <input type="range" min={0} max={100} value={t1} disabled={done} onChange={e => setT1(+e.target.value)} style={{ accentColor: accent }} />
        </div>
        <div className="pc-row">
          <span className="pc-lbl">θ₂ <span className="pc-sub">weight 2</span></span>
          <input type="range" min={0} max={100} value={t2} disabled={done} onChange={e => setT2(+e.target.value)} style={{ accentColor: accent }} />
        </div>
        <div className="pc-row">
          <span className="pc-lbl">η <span className="pc-sub">learn rate</span></span>
          <div className="pc-eta">
            <span className="pc-band" style={{ left: `${ETA_LO}%`, width: `${ETA_HI - ETA_LO}%` }} />
            <input type="range" min={0} max={100} value={eta} disabled={done} onChange={e => setEta(+e.target.value)} style={{ accentColor: etaOk ? "#1f9e72" : "#e1543f" }} />
          </div>
          <span className={"pc-flag" + (etaOk ? " ok" : "")}>{etaOk ? "stable" : "unstable"}</span>
        </div>
        <div className="pc-status">
          {done
            ? <span className="pc-ok"><span className="pc-dot ok" /> converged to the global minimum ✓</span>
            : <span className="pc-hint"><span className="pc-dot" style={{ background: inWell ? "#1f9e72" : accent }} /> loss <b>{curLoss.toFixed(2)}</b> · {inWell ? (etaOk ? "—" : "now stabilise the learning rate") : "read the contours: deepest basin wins"}</span>}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { Icon, Social, SocialChip, Scribble, Stars, Placeholder, DomainArt, MultiDomainArt, LangFallbackToggle, ImageUpload, RichText, renderInline, NetworkPuzzle, BilingualStatus, KIND, kindMeta, iconForKind, LangChips });

/* ---------- compact EN/FR status summary — just the two badges (used by every editor) ---------- */
function BilingualStatus({ enOn, frOn, fallback, editingLang }) {
  const Badge = ({ code, on }) => (
    <span className={"bl-badge" + (on ? " on" : " off")}>
      <Icon name={on ? "check" : "bell"} size={12} /> {code} {on ? "ready" : "missing"}
    </span>
  );
  return (
    <div className="bl-status compact">
      <div className="bl-badges">
        <Badge code="EN" on={enOn} />
        <Badge code="FR" on={frOn} />
      </div>
    </div>
  );
}
