/* ============================================================
   acl_docs.jsx — in-app ACL reference, embedded as a lens inside
   Access controls (not a separate route). Left rail of collapsible
   sections + scrollable content, search with highlight, copyable +
   "Insert into policy" examples that merge into the live PolicyModel.
   Content is original reference text, rendered verbatim.
   ============================================================ */

/* ---- content model: sections of typed blocks (searchable + highlightable) ---- */
const DOCS = [
  {
    id: "how", title: "How a Headscale policy works", blocks: [
      { p: "The policy is one HuJSON document — JSON that additionally allows `//` line comments and trailing commas. It has up to six top-level blocks: `groups`, `tagOwners`, `hosts`, `acls`, `ssh`, `autoApprovers`. None are individually addressable over the API — the whole document is read with `GET /api/v1/policy`, written with `PUT /api/v1/policy`, and validated with `POST /api/v1/policy/check`." },
      { note: "Default-deny is the single most important concept. An empty policy ({}, or a policy with no acls block) means allow everything — every node can reach every node. The moment you add an acls array, the model flips to default-deny: only the traffic you explicitly accept is allowed; everything else is dropped. There are no deny rules in Headscale — you express intent purely by what you accept." },
      { p: "Policy modes. Headscale runs the policy in either `file` mode (the document is owned by a file on disk; the API/console can read it but not write it) or `database` mode (stored in Headscale's DB and writable via `PUT`). The console is read-only in `file` mode." },
    ],
  },
  {
    id: "vocab", title: "The selector vocabulary (src / dst)", blocks: [
      { p: "Everywhere a rule names a \"who\" or a \"what,\" it uses one of these selectors:" },
      { table: { head: ["Selector", "Looks like", "Means"], rows: [
        ["User", "alice@", "A specific user. The trailing @ marks it as a user (vs a host/tag)."],
        ["Group", "group:engineers", "A named set of users, defined in groups."],
        ["Tag", "tag:server", "A role/label on devices, declared in tagOwners."],
        ["Host", "db-primary", "A friendly alias for an IP/CIDR, defined in hosts."],
        ["CIDR / IP", "10.0.0.0/8", "A literal address or subnet."],
        ["Autogroup", "autogroup:internet", "A built-in dynamic set (see below)."],
        ["Wildcard", "*", "Everything."],
      ] } },
      { p: "Only `dst` selectors carry a port spec (`target:ports`); `src` selectors never do." },
      { h: "Autogroups (built-in, dynamic)" },
      { ul: [
        "`autogroup:internet` — the public internet, reached via an exit node. Use it as a dst to permit exit-node usage.",
        "`autogroup:member` — every user-owned (non-tagged) device.",
        "`autogroup:self` — for each source user, their own devices (enables per-user isolation; see the performance note).",
        "`autogroup:tagged` — every tagged device.",
        "`autogroup:nonroot` — in an SSH rule's users list, means \"any login user except root.\"",
      ] },
    ],
  },
  {
    id: "groups", title: "groups", blocks: [
      { p: "A naming layer over users." },
      { code: `{
  "groups": {
    "group:engineers": ["alice@", "bob@"],
    "group:ops":       ["carol@"]
  }
}`, insert: true },
      { p: "Rules: keys must be `group:`-prefixed; members are users only (`user@`) — a group cannot contain another group and cannot contain tags. Groups are then usable anywhere a selector is allowed (`acls`, `ssh`, `tagOwners`, `autoApprovers`)." },
    ],
  },
  {
    id: "tagowners", title: "tagOwners and tags", blocks: [
      { p: "Tags are device roles. Before a tag can be used, you must declare who may apply it in `tagOwners`:" },
      { code: `{
  "tagOwners": {
    "tag:server": ["group:ops"],
    "tag:ci":     ["alice@"]
  }
}`, insert: true },
      { p: "Two ways a device gets a tag:" },
      { ul: [
        "Forced tag — an admin sets it via the API (`POST /api/v1/node/{id}/tags`). Authoritative.",
        "Requested tag — a device asks for it at registration (`tailscale up --advertise-tags=tag:server`); it only sticks if the device's owner is listed in that tag's tagOwners.",
      ] },
      { note: "Ownership flips when a device is tagged. A tagged device is owned by its tags, not by a user (it appears under the tagged-devices owner). A device is therefore either tagged or user-owned, never both — and user-based rules (alice@) will not match a tagged device. This is the most common policy surprise: once you tag a server, rules that referenced it by its old user stop applying; reference it by tag: instead." },
    ],
  },
  {
    id: "hosts", title: "hosts", blocks: [
      { p: "Friendly aliases for addresses, so rules read in names instead of IPs:" },
      { code: `{
  "hosts": {
    "db-primary":   "100.64.0.10/32",
    "internal-net": "192.168.50.0/24"
  }
}`, insert: true },
      { p: "Values are CIDRs (`/32` for a single host). Aliases are usable in `acls` `src`/`dst`." },
    ],
  },
  {
    id: "acls", title: "acls — the access rules", blocks: [
      { p: "Each rule is `{ \"action\": \"accept\", \"src\": [...], \"dst\": [...] }`. `action` is always `accept`. `dst` entries are `target:ports`, where ports is `*`, a single port, a comma list, or a range:" },
      { code: `{
  "acls": [
    // engineers may reach the dev server fleet on web ports
    { "action": "accept", "src": ["group:engineers"], "dst": ["tag:dev:80,443"] },

    // ops have full access to everything
    { "action": "accept", "src": ["group:ops"], "dst": ["*:*"] },

    // anyone may reach the database alias, Postgres only
    { "action": "accept", "src": ["autogroup:member"], "dst": ["db-primary:5432"] }
  ]
}`, insert: true },
      { p: "Optional `proto` narrows by protocol (e.g. `\"proto\": \"tcp\"`, `\"udp\"`, `\"icmp\"`, or a numeric protocol number). Omit it to allow all protocols on the listed ports. Rule order does not matter (accept-only, default-deny)." },
    ],
  },
  {
    id: "ssh", title: "ssh — Tailscale SSH rules", blocks: [
      { p: "Governs `tailscale ssh` access (separate from network ACLs). Each rule:" },
      { code: `{
  "ssh": [
    {
      "action": "check",              // "accept" = allow silently; "check" = require periodic re-auth
      "src": ["group:engineers"],
      "dst": ["tag:server"],          // destinations are typically tags or autogroup:self
      "users": ["autogroup:nonroot"], // which OS login users are permitted
      "checkPeriod": "12h"            // for "check": how long an approval lasts
    }
  ]
}`, insert: true },
      { ul: [
        "`action: \"accept\"` connects without interaction; `action: \"check\"` forces the user to re-authenticate in the browser every checkPeriod.",
        "`users` lists allowed login names; `autogroup:nonroot` permits any user except root.",
        "`dst` must resolve to devices where Tailscale SSH is enabled; `autogroup:self` lets users SSH only into their own machines.",
      ] },
    ],
  },
  {
    id: "autoapprovers", title: "autoApprovers — hands-off route/exit approval", blocks: [
      { p: "Subnet routes and exit nodes are inert until approved. `autoApprovers` approves them automatically when advertised by trusted owners:" },
      { code: `{
  "autoApprovers": {
    "routes": {
      "10.0.0.0/8":      ["tag:router"],
      "192.168.50.0/24": ["group:ops"]
    },
    "exitNode": ["tag:exit"]
  }
}`, insert: true },
      { p: "`routes` is a map of CIDR → approver selectors (tags/groups); `exitNode` is a flat list of approver selectors. This connects the ACL page to the Routes page — an auto-approved route is labeled as such there." },
    ],
  },
  {
    id: "advanced", title: "Advanced / complicated setups", blocks: [
      { h: "Tag-based microsegmentation" },
      { p: "Treat tags, not users, as the unit of policy for infrastructure. Tag servers (`tag:web`, `tag:db`, `tag:ci`), then write rules between tags (`tag:web → tag:db:5432`). Because tagged devices have no user identity, this decouples policy from who registered the box and survives staff changes. Pair with `autoApprovers` so a freshly tagged subnet router self-approves." },
      { h: "Per-user device isolation (autogroup:self)" },
      { p: "Let every user reach only their own devices and nothing else:" },
      { code: `{ "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"] }` },
      { note: "Performance caveat: autogroup:self is evaluated per-user and can be expensive on large tailnets — use it deliberately, not as a default." },
      { h: "Controlling exit-node usage" },
      { p: "Allowing exit nodes is just permitting `autogroup:internet` as a destination; restrict which users may egress by scoping `src`. To pin specific users to specific exit nodes, alias the exit nodes as hosts and write per-user rules:" },
      { code: `{
  "hosts": { "exit-eu": "100.64.0.1/32", "exit-us": "100.64.0.2/32" },
  "acls": [
    { "action": "accept", "src": ["alice@"], "dst": ["exit-eu:*"] },
    { "action": "accept", "src": ["bob@"],   "dst": ["exit-us:*"] }
  ]
}`, insert: true },
      { h: "Subnet routers + reaching services behind them" },
      { p: "A subnet router advertises a CIDR; clients reach services on that CIDR, and you reference the service address, not the router. For high availability, run several routers advertising the same prefix; Headscale elects one primary and fails over automatically (visible as subnetRoutes vs approvedRoutes). Combine with `autoApprovers.routes` so new routers approve without manual steps." },
      { h: "Port + protocol granularity" },
      { code: `{ "action": "accept", "src": ["tag:web"], "dst": ["tag:db:5432"], "proto": "tcp" }` },
      { h: "SSH into a tagged fleet with re-auth" },
      { code: `{ "action": "check", "src": ["group:oncall"], "dst": ["tag:server"], "users": ["autogroup:nonroot"], "checkPeriod": "4h" }` },
    ],
  },
  {
    id: "mistakes", title: "Common mistakes / gotchas", blocks: [
      { ul: [
        "Allow-all surprise: adding your first acls rule silently switches the whole tailnet to default-deny — everything not accepted now breaks. Plan the full rule set before saving the first rule.",
        "Tagged devices and user rules don't mix: after tagging a node, `alice@`-style rules no longer match it. Re-express by tag.",
        "Forgetting tagOwners: a tag used in acls/ssh but never declared in tagOwners is invalid — /policy/check will reject it.",
        "Ports on src: ports belong only on dst. A src with a port is invalid.",
        "Locking yourself out: removing the rule that grants your own user/group access can strand you. Keep a broad admin accept (`group:admin → *:*`) and use the lockout guard.",
        "Mismatched user reference: users are `name@` (trailing @); dropping it makes the token parse as a host/tag and silently fail to match.",
        "autogroup:self everywhere: convenient but costly at scale.",
      ] },
    ],
  },
  {
    id: "example", title: "A full annotated example", blocks: [
      { code: `{
  // ---- People ----
  "groups": {
    "group:admin":     ["alice@"],
    "group:engineers": ["bob@", "dana@"],
    "group:oncall":    ["bob@"]
  },

  // ---- Device roles (and who may assign them) ----
  "tagOwners": {
    "tag:web":    ["group:admin"],
    "tag:db":     ["group:admin"],
    "tag:router": ["group:admin"],
    "tag:exit":   ["group:admin"]
  },

  // ---- Friendly names ----
  "hosts": { "office-net": "192.168.10.0/24" },

  // ---- Network access (default-deny once this block exists) ----
  "acls": [
    { "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },
    { "action": "accept", "src": ["tag:web"], "dst": ["tag:db:5432"], "proto": "tcp" },
    { "action": "accept", "src": ["group:engineers"], "dst": ["tag:web:443", "office-net:*"] },
    { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:internet:*"] }
  ],

  // ---- SSH ----
  "ssh": [
    { "action": "check", "src": ["group:oncall"], "dst": ["tag:web", "tag:db"], "users": ["autogroup:nonroot"], "checkPeriod": "4h" }
  ],

  // ---- Hands-off approval ----
  "autoApprovers": {
    "routes":   { "192.168.10.0/24": ["tag:router"] },
    "exitNode": ["tag:exit"]
  }
}` },
    ],
  },
  {
    id: "validation", title: "Validation & safe rollout", blocks: [
      { ul: [
        "Validate before every save with `POST /api/v1/policy/check` (HTTP 200 = valid); the console mirrors this as the \"All references resolve\" badge.",
        "Roll out additively: add the broad admin accept first, confirm you still have access, then tighten.",
        "Use the Review-&-save diff and the lockout guard before PUT.",
        "In file mode the document is read-only here; edit the file on the server and reload.",
      ] },
    ],
  },
];

function docSectionText(sec) {
  let t = sec.title + " ";
  sec.blocks.forEach((b) => {
    if (b.p) t += b.p + " ";
    if (b.h) t += b.h + " ";
    if (b.note) t += b.note + " ";
    if (b.code) t += b.code + " ";
    if (b.ul) t += b.ul.join(" ") + " ";
    if (b.table) t += b.table.head.join(" ") + " " + b.table.rows.map((r) => r.join(" ")).join(" ");
  });
  return t.toLowerCase();
}

/* ---- inline rich text: `code` spans + search highlight ---- */
function Highlighted({ text, q }) {
  if (!q) return text;
  const tl = text.toLowerCase(), ql = q.toLowerCase();
  const out = []; let i = 0;
  while (true) { const j = tl.indexOf(ql, i); if (j < 0) { out.push(text.slice(i)); break; } out.push(text.slice(i, j)); out.push(<mark key={j} className="dochl">{text.slice(j, j + q.length)}</mark>); i = j + q.length; }
  return out;
}
function Rich({ text, q }) {
  return text.split(/(`[^`]+`)/g).map((p, i) => (p.startsWith("`") && p.endsWith("`"))
    ? <span key={i} className="code-inline">{p.slice(1, -1)}</span>
    : <React.Fragment key={i}><Highlighted text={p} q={q} /></React.Fragment>);
}

/* ---- merge an example snippet into the live policy ---- */
function insertSnippet(snippet) {
  const add = ACLM.parse(snippet);
  if (!add) return { ok: false, msg: "Example didn't parse" };
  let text = HS.aclPolicy.text;
  if (!ACLM.model(text)) return { ok: false, msg: "Current policy has a syntax error" };
  const order = ["groups", "tagOwners", "hosts", "acls", "ssh", "autoApprovers"];
  let firstTab = null;
  const tabFor = { groups: "groups", tagOwners: "tagOwners", hosts: "hosts", acls: "rules", ssh: "ssh", autoApprovers: "auto" };
  const write = (key, fn) => { const m = ACLM.model(text); if (!m) return; fn(m); text = ACLM.writers[key](text, m); };
  order.forEach((key) => {
    if (!(key in add)) return;
    firstTab = firstTab || tabFor[key];
    if (key === "groups" || key === "tagOwners" || key === "hosts") write(key, (m) => { m[key] = { ...m[key], ...add[key] }; });
    else if (key === "acls") write("acls", (m) => { m.acls = [...m.acls, ...add.acls.map((r) => ({ action: r.action || "accept", src: r.src || [], dst: r.dst || [], proto: r.proto, _extra: {}, representable: true }))]; });
    else if (key === "ssh") write("ssh", (m) => { m.ssh = [...m.ssh, ...add.ssh.map((r) => ({ action: r.action || "accept", src: r.src || [], dst: r.dst || [], users: r.users || [], checkPeriod: r.checkPeriod, _extra: {}, representable: true }))]; });
    else if (key === "autoApprovers") write("autoApprovers", (m) => { m.autoApprovers = { routes: { ...m.autoApprovers.routes, ...(add.autoApprovers.routes || {}) }, exitNode: [...new Set([...(m.autoApprovers.exitNode || []), ...(add.autoApprovers.exitNode || [])])] }; });
  });
  if (!ACLM.model(text)) return { ok: false, msg: "Merge produced invalid HuJSON" };
  HS.act.saveAcl(text);
  return { ok: true, firstTab };
}

/* ---- code example with Copy + Insert ---- */
function DocCode({ code, insert, q, readOnly, onInserted }) {
  const toast = useToast();
  const [copied, setCopied] = useState(false);
  const doInsert = () => {
    const r = insertSnippet(code);
    if (r.ok) { toast.push({ kind: "success", title: "Inserted into policy", msg: "Validated & saved via PUT /policy" }); onInserted && onInserted(r.firstTab); }
    else toast.push({ kind: "error", title: "Couldn't insert", msg: r.msg });
  };
  return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 9, overflow: "hidden", margin: "8px 0" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px 5px 12px", background: "var(--surface-2)", borderBottom: "1px solid var(--border)" }}>
        <span className="mono" style={{ fontSize: 11, color: "var(--text-faint)" }}>hujson</span>
        <div style={{ flex: 1 }} />
        <button className="btn icon sm ghost" title="Copy" onClick={() => { navigator.clipboard?.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1200); }}><Ic name={copied ? "check" : "copy"} size={14} /></button>
        {insert && !readOnly && <button className="btn sm" title="Merge this snippet into the current policy" onClick={doInsert}><Ic name="plus" size={13} />Insert into policy</button>}
      </div>
      <pre className="mono" style={{ margin: 0, padding: "12px 14px", fontSize: 12, lineHeight: 1.6, overflowX: "auto", color: "var(--text)", whiteSpace: "pre" }}>{q ? <Highlighted text={code} q={q} /> : code}</pre>
    </div>
  );
}

/* ---- block renderer ---- */
function DocBlock({ b, q, readOnly, onInserted }) {
  if (b.p) return <p style={{ fontSize: 13.5, lineHeight: 1.65, color: "var(--text-dim)", margin: "8px 0" }}><Rich text={b.p} q={q} /></p>;
  if (b.h) return <h4 style={{ fontSize: 14, fontWeight: 600, margin: "16px 0 4px" }}><Rich text={b.h} q={q} /></h4>;
  if (b.note) return <div style={{ display: "flex", gap: 9, padding: "11px 13px", background: "var(--accent-soft)", border: "1px solid var(--accent-line)", borderRadius: 9, margin: "10px 0", fontSize: 12.5, lineHeight: 1.6, color: "var(--text)" }}><Ic name="info" size={16} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} /><div><Rich text={b.note} q={q} /></div></div>;
  if (b.code) return <DocCode code={b.code} insert={b.insert} q={q} readOnly={readOnly} onInserted={onInserted} />;
  if (b.ul) return <ul style={{ margin: "8px 0", paddingLeft: 18, display: "flex", flexDirection: "column", gap: 6 }}>{b.ul.map((it, i) => <li key={i} style={{ fontSize: 13, lineHeight: 1.55, color: "var(--text-dim)" }}><Rich text={it} q={q} /></li>)}</ul>;
  if (b.table) return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 9, overflow: "hidden", margin: "10px 0" }}>
      <table className="tbl"><thead><tr>{b.table.head.map((h, i) => <th key={i}>{h}</th>)}</tr></thead>
        <tbody>{b.table.rows.map((r, i) => <tr key={i} style={{ cursor: "default" }}>{r.map((c, j) => <td key={j} style={{ fontSize: 12.5 }}>{j === 1 ? <span className="code-inline">{c}</span> : <Rich text={c} q={q} />}</td>)}</tr>)}</tbody>
      </table>
    </div>
  );
  return null;
}

/* ============================================================
   AclDocs — the Reference lens
   ============================================================ */
function AclDocs({ tab, setTab, mode, setMode }) {
  const s = useStore();
  const [q, setQ] = useState("");
  const [open, setOpen] = useState(() => new Set(DOCS.map((d) => d.id)));
  const scrollRef = useRef(null);
  const sectionRefs = useRef({});
  const readOnly = mode === "file";

  const matches = useMemo(() => {
    if (!q.trim()) return null;
    const ql = q.toLowerCase();
    return new Set(DOCS.filter((d) => docSectionText(d).includes(ql)).map((d) => d.id));
  }, [q]);
  const visible = DOCS.filter((d) => !matches || matches.has(d.id));

  // contextual deep-link: a lens set hs.acldocs → scroll there on mount
  useEffect(() => {
    const target = localStorage.getItem("hs.acldocs");
    if (target) { localStorage.removeItem("hs.acldocs"); setTimeout(() => goTo(target), 60); }
  }, []);
  // when searching, force-open matched sections
  useEffect(() => { if (matches) setOpen(new Set(matches)); }, [q]);

  const goTo = (id) => {
    setOpen((o) => new Set([...o, id]));
    const el = sectionRefs.current[id], cont = scrollRef.current;
    if (el && cont) setTimeout(() => { cont.scrollTo({ top: el.offsetTop - 10, behavior: "smooth" }); }, 30);
  };
  const toggle = (id) => setOpen((o) => { const n = new Set(o); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const onInserted = (t) => { if (t) setTab(t); };

  return (
    <div className="page wide fade-up" style={{ height: "100%", display: "flex", flexDirection: "column", paddingBottom: 24 }}>
      <div className="section-head">
        <div>
          <h2 style={{ fontSize: 19 }}>Access controls</h2>
          <div className="sub">Reference — how Headscale policy works, with copyable examples</div>
        </div>
        <div className="spacer" />
        <AclTabs tab={tab} setTab={setTab} mode={mode} setMode={setMode} />
        <span className="badge" style={{ height: 22 }}>targeting {s.serverInfo.targetVersion}</span>
      </div>

      <div className="docs-grid" style={{ display: "grid", gridTemplateColumns: "224px 1fr", gap: 14, flex: 1, minHeight: 0 }}>
        {/* rail */}
        <div className="card" style={{ display: "flex", flexDirection: "column", overflow: "hidden", minHeight: 0 }}>
          <div style={{ padding: 10, borderBottom: "1px solid var(--border)" }}>
            <div className="tb-search" style={{ width: "100%" }}>
              <Ic name="search" size={14} />
              <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search reference…" style={{ border: "none", background: "none", outline: "none", flex: 1, color: "var(--text)", fontSize: 12.5, minWidth: 0 }} />
              {q && <span className="copy-btn" onClick={() => setQ("")}><Ic name="x" size={13} /></span>}
            </div>
          </div>
          <div style={{ display: "flex", gap: 6, padding: "8px 10px", borderBottom: "1px solid var(--border)" }}>
            <button className="btn icon sm ghost" title="Expand all" onClick={() => setOpen(new Set(DOCS.map((d) => d.id)))} style={{ flex: 1 }}><Ic name="plus" size={13} /></button>
            <button className="btn icon sm ghost" title="Collapse all" onClick={() => setOpen(new Set())} style={{ flex: 1 }}><Ic name="x" size={13} /></button>
          </div>
          <div style={{ overflowY: "auto", flex: 1, padding: 6 }}>
            {visible.length === 0 && <div style={{ fontSize: 12, color: "var(--text-faint)", padding: 12, textAlign: "center" }}>No matches</div>}
            {visible.map((d, i) => (
              <div key={d.id} className="doc-rail-item" onClick={() => goTo(d.id)}>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--text-faint)", width: 22 }}>{String(i + 1).padStart(2, "0")}</span>
                <span style={{ fontSize: 12.5 }}><Highlighted text={d.title} q={q || ""} /></span>
              </div>
            ))}
          </div>
        </div>

        {/* content */}
        <div ref={scrollRef} className="card" style={{ overflowY: "auto", padding: "8px 22px 40px", minHeight: 0, position: "relative" }}>
          {readOnly && <div style={{ margin: "12px 0" }}><Banner cls="warn" icon="shield">Policy is <b>file-managed</b> — "Insert into policy" is disabled. Examples are still copyable.</Banner></div>}
          {visible.length === 0 && <div className="empty" style={{ padding: 50 }}><Ic name="search" size={34} /><h3>No matches for “{q}”</h3><div>Try a block name like <span className="code-inline">tagOwners</span> or a term like <span className="code-inline">exit node</span>.</div></div>}
          {visible.map((d, i) => (
            <div key={d.id} ref={(el) => (sectionRefs.current[d.id] = el)} style={{ borderBottom: "1px solid var(--border)", padding: "14px 0" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }} onClick={() => toggle(d.id)}>
                <Ic name={open.has(d.id) ? "chevDown" : "chevRight"} size={16} style={{ color: "var(--text-faint)" }} />
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--accent)" }}>{String(DOCS.indexOf(d) + 1).padStart(2, "0")}</span>
                <h3 style={{ fontSize: 15.5, fontWeight: 600 }}><Highlighted text={d.title} q={q || ""} /></h3>
              </div>
              <div className={"doc-body" + (open.has(d.id) ? " open" : "")}>
                {open.has(d.id) && <div style={{ paddingLeft: 26, paddingTop: 4 }}>{d.blocks.map((b, k) => <DocBlock key={k} b={b} q={q} readOnly={readOnly} onInserted={onInserted} />)}</div>}
              </div>
            </div>
          ))}
        </div>
      </div>

      <style>{`
        .doc-rail-item { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 7px; cursor: pointer; color: var(--text-dim); }
        .doc-rail-item:hover { background: var(--surface-2); color: var(--text); }
        mark.dochl { background: color-mix(in oklab, var(--accent) 30%, transparent); color: var(--text); border-radius: 3px; padding: 0 1px; }
        @media (prefers-reduced-motion: no-preference) { .doc-body.open { animation: docReveal .18s var(--ease); } }
        @keyframes docReveal { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
        @media (max-width: 860px) { .docs-grid { grid-template-columns: 1fr !important; } }
      `}</style>
    </div>
  );
}

Object.assign(window, { AclDocs, DOCS, insertSnippet });
