/* ============================================================
   components.jsx — shared UI primitives + icon set
   Exposed on window for the other babel scripts.
   ============================================================ */
const { useState, useEffect, useRef, useCallback, useMemo } = React;

/* ---------------- Icons ---------------- */
const I = {
  ring: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="8.2" stroke="currentColor" strokeWidth="1.7"/><circle cx="12" cy="12" r="3.4" fill="currentColor"/></svg>),
  search: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="1.8"/><path d="m20 20-3.2-3.2" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>),
  sun: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="4.2" stroke="currentColor" strokeWidth="1.7"/><path d="M12 3v2.2M12 18.8V21M3 12h2.2M18.8 12H21M5.6 5.6l1.6 1.6M16.8 16.8l1.6 1.6M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>),
  moon: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M20 14.5A8 8 0 1 1 9.5 4a6.3 6.3 0 0 0 10.5 10.5Z" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round"/></svg>),
  github: (p) => (<svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49v-1.7c-2.78.62-3.37-1.22-3.37-1.22-.46-1.18-1.11-1.5-1.11-1.5-.9-.64.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.9 1.57 2.34 1.12 2.91.85.09-.66.35-1.12.63-1.37-2.22-.26-4.55-1.14-4.55-5.05 0-1.12.39-2.03 1.03-2.74-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.71 1.03 1.62 1.03 2.74 0 3.92-2.34 4.79-4.57 5.04.36.32.68.94.68 1.9v2.81c0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"/></svg>),
  menu: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>),
  plus: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></svg>),
  grip: (p) => (<svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>),
  x: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>),
  chevron: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="m6 9 6 6 6-6" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  chevronR: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="m9 6 6 6-6 6" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  arrowR: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  copy: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="9" y="9" width="11" height="11" rx="2.2" stroke="currentColor" strokeWidth="1.7"/><path d="M5 15.5A2 2 0 0 1 3.5 13.5V5A1.5 1.5 0 0 1 5 3.5h8.5A2 2 0 0 1 15.5 5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>),
  check: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="m5 13 4 4 10-11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  hash: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M9 4 7 20M17 4l-2 16M5 9h15M4 15h15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  link: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M9.5 14.5 14.5 9.5M8 12l-2 2a3.5 3.5 0 0 0 5 5l2-2M16 12l2-2a3.5 3.5 0 0 0-5-5l-2 2" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  info: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.7"/><path d="M12 11v5M12 8h.01" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></svg>),
  bulb: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.8 10.6c.5.4.8 1 .8 1.6v.3h6v-.3c0-.6.3-1.2.8-1.6A6 6 0 0 0 12 3Z" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  warn: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 3.5 22 19.5H2L12 3.5Z" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round"/><path d="M12 10v4M12 17h.01" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></svg>),
  flame: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 22c4 0 6.5-2.6 6.5-6 0-3.7-2.8-5.4-2-9.5C13 8 11.5 9 11.5 9S10 7 10.5 3C6 6 5.5 9.7 5.5 12c0 3.6 2.5 6 6.5 6Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  image: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="3" y="4" width="18" height="16" rx="2.5" stroke="currentColor" strokeWidth="1.6"/><circle cx="8.5" cy="9.5" r="1.8" stroke="currentColor" strokeWidth="1.5"/><path d="m4 17 5-4.5 4 3.5 3-2.5 4 3.5" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  doc: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 3h8l4 4v14H6V3Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M14 3v4h4M9 13h6M9 16.5h6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  // system glyphs
  run: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="14" cy="5" r="2" stroke="currentColor" strokeWidth="1.7"/><path d="M9 21l2.5-5 .5-3 3 2 3 1M8 11l3-2 3 .5 2 2.5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  gamepad: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="2" y="7" width="20" height="10" rx="5" stroke="currentColor" strokeWidth="1.7"/><path d="M7 11v3M5.5 12.5h3M15.5 11.5h.01M18 13.5h.01" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>),
  camera: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="3" y="6" width="14" height="12" rx="2.5" stroke="currentColor" strokeWidth="1.7"/><path d="M17 10l4-2.5v9L17 14" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round"/></svg>),
  anim: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="3" y="3" width="18" height="18" rx="3" stroke="currentColor" strokeWidth="1.6"/><path d="M3 8h18M8 3v18M3 16h18" stroke="currentColor" strokeWidth="1.4"/></svg>),
  sword: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M14.5 3H20v5.5L9 19.5l-3-.5-.5-3L14.5 3Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="m5.5 16-2 2 2.5 2.5 2-2M16 5l3 3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  heart: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 20s-7-4.4-7-9.5A4 4 0 0 1 12 7a4 4 0 0 1 7 3.5C19 15.6 12 20 12 20Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  target: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="8.5" stroke="currentColor" strokeWidth="1.6"/><circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.6"/><path d="M12 1.5v3M12 19.5v3M1.5 12h3M19.5 12h3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  user: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="1.7"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>),
  lock: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="4.5" y="10.5" width="15" height="10" rx="2.4" stroke="currentColor" strokeWidth="1.7"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3" stroke="currentColor" strokeWidth="1.7"/><circle cx="12" cy="15.5" r="1.4" fill="currentColor"/></svg>),
  network: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="5" r="2.5" stroke="currentColor" strokeWidth="1.6"/><circle cx="5" cy="18" r="2.5" stroke="currentColor" strokeWidth="1.6"/><circle cx="19" cy="18" r="2.5" stroke="currentColor" strokeWidth="1.6"/><path d="M12 7.5v3m0 0L6.5 16M12 10.5 17.5 16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  save: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M5 4h12l3 3v13H5V4Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M8 4v5h7M9 20v-5h6v5" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  hud: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><rect x="3" y="4" width="18" height="16" rx="2.5" stroke="currentColor" strokeWidth="1.6"/><path d="M6 8h7M6 11h4M15 16h3" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>),
  bag: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 8h12l-1 12H7L6 8Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M9 8V6a3 3 0 0 1 6 0v2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  cube: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M4 7.5 12 12l8-4.5M12 12v9" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  skull: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 3a8 8 0 0 0-5 14v3h10v-3a8 8 0 0 0-5-14Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><circle cx="9" cy="12" r="1.4" fill="currentColor"/><circle cx="15" cy="12" r="1.4" fill="currentColor"/><path d="M11 17h2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>),
  book: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2V5Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M4 19a2 2 0 0 1 2-2h13" stroke="currentColor" strokeWidth="1.6"/></svg>),
  layers: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 3 3 8l9 5 9-5-9-5ZM3 13l9 5 9-5M3 17l9 5 9-5" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  bolt: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  rocket: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M5 15c-1 1-1.5 4-1.5 4s3-.5 4-1.5M14 4c3 0 6 3 6 6 0 4-5 8-8 9l-3-3c1-3 5-8 9-8M9.5 14.5 8 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/><circle cx="15" cy="9" r="1.5" stroke="currentColor" strokeWidth="1.5"/></svg>),
  cog: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="3.1" stroke="currentColor" strokeWidth="1.6"/><path d="M19.4 13a1.65 1.65 0 0 0 .33 1.82l.05.05a1.9 1.9 0 1 1-2.69 2.69l-.05-.05a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a1.9 1.9 0 0 1-3.8 0v-.06a1.65 1.65 0 0 0-1.08-1.51 1.65 1.65 0 0 0-1.82.33l-.05.05a1.9 1.9 0 1 1-2.69-2.69l.05-.05a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a1.9 1.9 0 0 1 0-3.8h.06a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0-.33-1.82l-.05-.05a1.9 1.9 0 1 1 2.69-2.69l.05.05a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a1.9 1.9 0 0 1 3.8 0v.06a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.05-.05a1.9 1.9 0 1 1 2.69 2.69l-.05.05a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a1.9 1.9 0 0 1 0 3.8h-.06a1.65 1.65 0 0 0-1.51 1Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>),
  flag: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M5 21V4M5 4h11l-2 4 2 4H5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  inbox: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M3 13l3-8h12l3 8v6H3v-6Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M3 13h5a2 2 0 0 0 4 0 2 2 0 0 0 4 0h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  trash: (p) => (<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M4 7h16M9 7V5a1.5 1.5 0 0 1 1.5-1.5h3A1.5 1.5 0 0 1 15 5v2M6 7l1 13h10l1-13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>),
};

/* ---------------- CodeBlock ---------------- */
function highlightCSharp(code) {
  try {
    if (window.Prism && Prism.languages.csharp) {
      return Prism.highlight(code, Prism.languages.csharp, "csharp");
    }
  } catch (e) {}
  return code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function CodeBlock({ code, file, lang = "csharp", highlight = [] }) {
  const [copied, setCopied] = useState(false);
  const src = (code || "").replace(/\n$/, "");
  const lines = src.split("\n");
  const hlSet = useMemo(() => new Set(highlight), [highlight.join(",")]);

  const html = useMemo(() => {
    if (lang === "csharp" || lang === "cs") return highlightCSharp(src);
    return src.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }, [src, lang]);

  // split highlighted html back into lines (Prism keeps newlines as text nodes)
  const htmlLines = useMemo(() => html.split("\n"), [html]);

  const onCopy = () => {
    navigator.clipboard?.writeText(src).then(() => {
      setCopied(true); setTimeout(() => setCopied(false), 1600);
    }).catch(() => {});
  };

  const dots = ["#ff5f57", "#febc2e", "#28c840"];
  return (
    <div className="codeblock">
      <div className="code-head">
        <div className="code-dots">{dots.map((c, i) => <span key={i} className="code-dot" style={{ background: c }} />)}</div>
        {file && <span className="code-file">{file}</span>}
        <span className="code-lang">{lang === "csharp" || lang === "cs" ? "C#" : lang}</span>
        <button className={"copy-btn" + (copied ? " copied" : "")} onClick={onCopy} aria-label="Copy code">
          {copied ? <I.check /> : <I.copy />}{copied ? "Copied" : "Copy"}
        </button>
      </div>
      <div className="code-scroll">
        <pre className="code-body"><code className={"language-" + lang}>
          {htmlLines.map((ln, i) => (
            <span key={i} className={"code-line" + (hlSet.has(i + 1) ? " hl" : "")}>
              <span className="ln">{i + 1}</span>
              <span dangerouslySetInnerHTML={{ __html: ln === "" ? "\u200b" : ln }} />
              {"\n"}
            </span>
          ))}
        </code></pre>
      </div>
    </div>
  );
}

/* ---------------- Inline markdown-ish renderer ---------------- */
function RichText({ text }) {
  // supports **bold**, `code`, [link](url)
  const parts = [];
  let rest = text;
  let key = 0;
  const re = /(\*\*([^*]+)\*\*)|(`([^`]+)`)|(\[([^\]]+)\]\(([^)]+)\))/;
  while (rest.length) {
    const m = rest.match(re);
    if (!m) { parts.push(rest); break; }
    if (m.index > 0) parts.push(rest.slice(0, m.index));
    if (m[1]) parts.push(<strong key={key++}>{m[2]}</strong>);
    else if (m[3]) parts.push(<code key={key++} className="inline">{m[4]}</code>);
    else if (m[5]) {
      const href = m[7];
      const internal = href.startsWith("#");
      parts.push(<a key={key++} className="link" href={href} target={internal ? undefined : "_blank"} rel={internal ? undefined : "noreferrer"}>{m[6]}</a>);
    }
    rest = rest.slice(m.index + m[0].length);
  }
  return <>{parts}</>;
}

/* ---------------- Callout ---------------- */
function Callout({ variant = "note", title, children, text }) {
  const icon = { note: <I.info />, tip: <I.bulb />, warn: <I.warn />, gold: <I.flame /> }[variant] || <I.info />;
  return (
    <div className={"callout " + variant}>
      <div className="co-icon">{icon}</div>
      <div className="co-body">
        {title && <div className="co-title">{title}</div>}
        {text ? <p><RichText text={text} /></p> : children}
      </div>
    </div>
  );
}

/* ---------------- Tabs ---------------- */
function Tabs({ tabs }) {
  const [i, setI] = useState(0);
  const t = tabs[i] || tabs[0];
  return (
    <div className="tabs">
      <div className="tab-row" role="tablist">
        {tabs.map((tb, idx) => (
          <button key={idx} role="tab" aria-selected={idx === i} className={"tab-btn" + (idx === i ? " active" : "")} onClick={() => setI(idx)}>{tb.label}</button>
        ))}
      </div>
      <div className="tab-panel">
        {t.blocks ? <Blocks blocks={t.blocks} /> : (t.code ? <CodeBlock code={t.code} file={t.file} lang={t.lang} highlight={t.highlight} /> : (t.text ? <p><RichText text={t.text} /></p> : null))}
      </div>
    </div>
  );
}

/* ---------------- Steps ---------------- */
function Steps({ steps }) {
  return (
    <ol className="steps">
      {steps.map((s, i) => (
        <li className="step" key={i}>
          <div className="step-title">{s.title}</div>
          <div className="step-body">{s.blocks ? <Blocks blocks={s.blocks} /> : <p><RichText text={s.text} /></p>}</div>
        </li>
      ))}
    </ol>
  );
}

/* ---------------- Property table ---------------- */
function PropTable({ rows, cols = ["Field", "Type", "Description"] }) {
  return (
    <table className="proptable">
      <thead><tr>{cols.map((c, i) => <th key={i}>{c}</th>)}</tr></thead>
      <tbody>
        {rows.map((r, i) => (
          <tr key={i}>
            <td className="name">{r.name}</td>
            <td className="type">{r.type}</td>
            <td className="desc"><RichText text={r.desc} /></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

/* ---------------- Placeholder / image ---------------- */
function Placeholder({ label, caption, src }) {
  if (src) {
    return (
      <figure className="doc-figure">
        <img src={src} alt={caption || label || ""} loading="lazy" />
        {caption && <figcaption>{caption}</figcaption>}
      </figure>
    );
  }
  return (
    <div className="placeholder">
      <div className="ph-icon"><I.image /></div>
      <div className="ph-label">{label || "image-placeholder"}</div>
      {caption && <div className="ph-cap">{caption}</div>}
    </div>
  );
}

/* ---------------- Embed (video / iframe) ---------------- */
function embedSrc(url) {
  if (!url) return null;
  let m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/);
  if (m) return "https://www.youtube.com/embed/" + m[1];
  m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
  if (m) return "https://player.vimeo.com/video/" + m[1];
  return url;
}
function Embed({ url, caption }) {
  const src = embedSrc(url);
  if (!src) return <Placeholder label="video: paste a YouTube or Vimeo URL" caption={caption} />;
  return (
    <figure className="embed">
      <div className="embed-frame">
        <iframe src={src} title={caption || "Embedded video"} loading="lazy"
          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
          allowFullScreen></iframe>
      </div>
      {caption && <figcaption className="embed-cap">{caption}</figcaption>}
    </figure>
  );
}

/* ---------------- Block renderer ---------------- */
function slugify(s) { return (s || "").toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-"); }

function Heading({ level, text }) {
  const id = slugify(text);
  const Tag = level === 3 ? "h3" : "h2";
  return (
    <Tag id={id}>
      {text}
      <a className="anchor" href={"#" + id} aria-label="Link to section" onClick={(e) => { e.preventDefault(); history.replaceState(null, "", "#" + id); document.getElementById(id)?.scrollIntoView ? window.scrollTo({ top: document.getElementById(id).getBoundingClientRect().top + window.scrollY - 80, behavior: "smooth" }) : null; }}><I.hash style={{ width: 15, height: 15, verticalAlign: "middle" }} /></a>
    </Tag>
  );
}

function Blocks({ blocks }) {
  return (<>{blocks.map((b, i) => <Block key={i} b={b} />)}</>);
}

function Block({ b }) {
  switch (b.type) {
    case "h2": return <Heading level={2} text={b.text} />;
    case "h3": return <Heading level={3} text={b.text} />;
    case "p": return <p><RichText text={b.text} /></p>;
    case "lede": return <p className="page-lede">{b.text}</p>;
    case "ul": return <ul>{(b.items || []).filter((it) => it && it.trim() !== "").map((it, i) => <li key={i}><RichText text={it} /></li>)}</ul>;
    case "ol": return <ol>{(b.items || []).filter((it) => it && it.trim() !== "").map((it, i) => <li key={i}><RichText text={it} /></li>)}</ol>;
    case "code": return <CodeBlock code={b.code} file={b.file} lang={b.lang} highlight={b.highlight} />;
    case "callout": return <Callout variant={b.variant} title={b.title} text={b.text}>{b.children ? <Blocks blocks={b.children} /> : null}</Callout>;
    case "tabs": return <Tabs tabs={b.tabs} />;
    case "steps": return <Steps steps={b.steps} />;
    case "props": return <PropTable rows={b.rows} cols={b.cols} />;
    case "placeholder": return <Placeholder label={b.label} caption={b.caption} src={b.src} />;
    case "embed": return <Embed url={b.url} caption={b.caption} />;
    case "hr": return <hr />;
    default: return null;
  }
}

/* Resolve an icon key to a component. An explicit "none" (or empty) returns null = no icon. */
function resolveIcon(key, fallbackKey) {
  if (key === "none" || key === "") return null;
  return I[key] || (fallbackKey ? I[fallbackKey] : null) || null;
}

Object.assign(window, {
  I, CodeBlock, RichText, Callout, Tabs, Steps, PropTable, Placeholder, Embed, Blocks, Block, Heading, slugify, highlightCSharp,
  resolveIcon,
});
