#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { socialCardPng } from "./social-card.mjs";
const root = process.cwd();
const docsDir = path.join(root, "docs");
const outDir = path.join(root, "dist", "docs-site");
const repoUrl = "https://github.com/openclaw/clawsweeper";
const repoEditBase = `${repoUrl}/edit/main/docs`;
const customDomain = "clawsweeper.bot";
const sections = [
["Start", ["scheduler.md", "work-lane.md"]],
[
"Lanes",
[
"commit-sweeper.md",
"commit-dispatcher.md",
"target-dispatcher.md",
"pr-review-comments.md",
"openclaw-event-hooks.md",
],
],
[
"Repair",
[
"repair/README.md",
"repair/operations.md",
"repair/auto-update-prs.md",
"repair/automerge-flow.md",
"repair/internal-features.md",
],
],
];
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(outDir, { recursive: true });
const pages = allMarkdown(docsDir).map((file) => {
const rel = path.relative(docsDir, file).replaceAll(path.sep, "/");
const markdown = fs.readFileSync(file, "utf8");
const title = firstHeading(markdown) || titleize(path.basename(rel, ".md"));
return { file, rel, title, outRel: outPath(rel), markdown, synthetic: false };
});
const homePage = {
file: null,
rel: "__home",
title: "ClawSweeper",
outRel: "index.html",
markdown: "",
synthetic: true,
};
pages.unshift(homePage);
const pageMap = new Map(pages.map((page) => [page.rel, page]));
const nav = sections
.map(([name, rels]) => ({
name,
pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean),
}))
.filter((section) => section.pages.length);
const sectionByRel = new Map();
for (const section of nav)
for (const page of section.pages) sectionByRel.set(page.rel, section.name);
const orderedPages = [homePage, ...nav.flatMap((s) => s.pages)];
for (const page of pages) {
const html = page.synthetic ? "" : markdownToHtml(page.markdown, page.rel);
const toc = page.synthetic ? "" : tocFromHtml(html);
const idx = orderedPages.findIndex((p) => p.rel === page.rel);
const prev = idx > 0 ? orderedPages[idx - 1] : null;
const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null;
const sectionName = sectionByRel.get(page.rel) || "Docs";
const pageOut = path.join(outDir, page.outRel);
fs.mkdirSync(path.dirname(pageOut), { recursive: true });
fs.writeFileSync(pageOut, layout({ page, html, toc, prev, next, sectionName }), "utf8");
}
fs.writeFileSync(path.join(outDir, "clawsweeper.svg"), clawSvg(), "utf8");
fs.writeFileSync(path.join(outDir, "favicon.svg"), faviconSvg(), "utf8");
fs.writeFileSync(path.join(outDir, "social-card.png"), socialCardPng());
fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
fs.writeFileSync(path.join(outDir, "CNAME"), `${customDomain}\n`, "utf8");
fs.writeFileSync(
path.join(outDir, "robots.txt"),
`User-agent: *\nAllow: /\nSitemap: https://${customDomain}/sitemap.xml\n`,
"utf8",
);
fs.writeFileSync(path.join(outDir, "sitemap.xml"), sitemap(orderedPages), "utf8");
console.log(`built docs site: ${path.relative(root, outDir)} (${pages.length} pages)`);
function allMarkdown(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return allMarkdown(full);
return entry.name.endsWith(".md") ? [full] : [];
})
.sort();
}
function outPath(rel) {
if (rel === "README.md") return "index.html";
if (rel.endsWith("/README.md")) return rel.replace(/README\.md$/, "index.html");
return rel.replace(/\.md$/, ".html");
}
function firstHeading(markdown) {
return markdown
.match(/^#\s+(.+)$/m)?.[1]
?.trim()
.replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\s+/u, "");
}
function titleize(input) {
return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase());
}
function markdownToHtml(markdown, currentRel) {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const html = [];
let paragraph = [];
let list = null;
let fence = null;
const flushParagraph = () => {
if (!paragraph.length) return;
html.push(`
${inline(paragraph.join(" "), currentRel)}
`);
paragraph = [];
};
const closeList = () => {
if (!list) return;
html.push(`${list}>`);
list = null;
};
const splitRow = (line) =>
line
.replace(/^\s*\|/, "")
.replace(/\|\s*$/, "")
.split("|")
.map((s) => s.trim());
const isDivider = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith("${escapeHtml(fence.lines.join("\n"))}`,
);
fence = null;
} else {
fence = { lang: fenceMatch[1] || "text", lines: [] };
}
continue;
}
if (fence) {
fence.lines.push(line);
continue;
}
if (!line.trim()) {
flushParagraph();
closeList();
continue;
}
const heading = line.match(/^(#{1,4})\s+(.+)$/);
if (heading) {
flushParagraph();
closeList();
const level = heading[1].length;
const text = heading[2].trim().replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\s+/u, "");
const id = slug(text);
const inner = inline(text, currentRel);
if (level === 1) {
html.push(`${inner} `);
} else {
html.push(
`# ${inner} `,
);
}
continue;
}
if (
line.trimStart().startsWith("|") &&
line.includes("|", line.indexOf("|") + 1) &&
isDivider(lines[i + 1] || "")
) {
flushParagraph();
closeList();
const header = splitRow(line);
const aligns = splitRow(lines[i + 1]).map((cell) => {
const left = cell.startsWith(":");
const right = cell.endsWith(":");
return right && left ? "center" : right ? "right" : left ? "left" : "";
});
i += 1;
const rows = [];
while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("|")) {
i += 1;
rows.push(splitRow(lines[i]));
}
const th = header
.map(
(c, idx) =>
`${inline(c, currentRel)} `,
)
.join("");
const tb = rows
.map(
(r) =>
`${r.map((c, idx) => `${inline(c, currentRel)} `).join("")} `,
)
.join("");
html.push(``);
continue;
}
const blockquote = line.match(/^>\s?(.*)$/);
if (blockquote) {
flushParagraph();
closeList();
const buf = [blockquote[1]];
while (i + 1 < lines.length && /^>\s?/.test(lines[i + 1])) {
i += 1;
buf.push(lines[i].replace(/^>\s?/, ""));
}
html.push(`${inline(buf.join(" "), currentRel)}
`);
continue;
}
const bullet = line.match(/^\s*-\s+(.+)$/);
const numbered = line.match(/^\s*\d+\.\s+(.+)$/);
if (bullet || numbered) {
flushParagraph();
const tag = bullet ? "ul" : "ol";
if (list && list !== tag) closeList();
if (!list) {
list = tag;
html.push(`<${tag}>`);
}
html.push(`${inline((bullet || numbered)[1], currentRel)} `);
continue;
}
paragraph.push(line.trim());
}
flushParagraph();
closeList();
return html.join("\n");
}
function inline(text, currentRel) {
const stash = [];
let out = text.replace(/`([^`]+)`/g, (_, code) => {
stash.push(`${escapeHtml(code)}`);
return ` ${stash.length - 1} `;
});
out = escapeHtml(out)
.replace(/\*\*([^*]+)\*\*/g, "$1 ")
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_, label, href) => `${label} `,
);
return out.replace(/ (\d+) /g, (_, i) => stash[Number(i)]);
}
function rewriteHref(href, currentRel) {
if (/^(https?:|mailto:|#)/.test(href)) return href;
const [raw, hash = ""] = href.split("#");
if (!raw) return `#${hash}`;
if (!raw.endsWith(".md")) return href;
const from = path.posix.dirname(currentRel);
const target = path.posix.normalize(path.posix.join(from, raw));
let rewritten = outPath(target);
const currentOut = outPath(currentRel);
rewritten = path.posix.relative(path.posix.dirname(currentOut), rewritten) || "index.html";
return `${rewritten}${hash ? `#${hash}` : ""}`;
}
function tocFromHtml(html) {
const items = [];
const re = /([\s\S]*?)<\/h[23]>/g;
let m;
while ((m = re.exec(html))) {
const text = m[3]
.replace(/]*>.*?<\/a>/, "")
.replace(/<[^>]+>/g, "")
.trim();
items.push({ level: Number(m[1]), id: m[2], text });
}
if (items.length < 2) return "";
return `On this page ${items
.map((i) => `${escapeHtml(i.text)} `)
.join("")} `;
}
function layout({ page, html, toc, prev, next, sectionName }) {
const depth = page.outRel.split("/").length - 1;
const rootPrefix = depth ? "../".repeat(depth) : "";
const editUrl = page.synthetic ? "" : `${repoEditBase}/${page.rel}`;
const isHome = page.synthetic;
const prevNext = !isHome && (prev || next) ? pageNavHtml(prev, next, rootPrefix) : "";
const heroBlock = isHome ? landingHero(rootPrefix) : standardHero(page, sectionName, editUrl);
const articleBlock = isHome ? landingBody() : `${html}${prevNext} `;
const tocBlock = isHome ? "" : toc;
const description = isHome
? "ClawSweeper is the conservative maintenance bot for OpenClaw repositories. It reviews issues, pull requests, and commits — and only acts when the evidence is strong."
: `${page.title} - ClawSweeper docs`;
return `
${escapeHtml(page.title)}${isHome ? " - Conservative maintenance bot" : " - ClawSweeper Docs"}
${heroBlock}
${articleBlock}
${tocBlock}
`;
}
function standardHero(page, sectionName, editUrl) {
return ``;
}
function landingHero(rootPrefix) {
return ``;
}
function landingBody() {
const features = [
[
"One report per item",
"Every reviewed issue and PR becomes records/<repo>/items/<n>.md: decision, evidence, proposed comment, runtime metadata, and snapshot hash.",
"report",
],
[
"Durable review comments",
"ClawSweeper edits a single marker-backed comment per item instead of stacking new ones. Maintainers get one source of truth, not noise.",
"comment",
],
[
"Conservative apply",
"A close is only proposed when the item is implemented, unreproducible, duplicate, incoherent, or obviously stale. Maintainer-authored stays open.",
"shield",
],
[
"Four operational lanes",
"Review, apply, repair, and commit review run as separate lanes. Each lane has its own state, gates, and GitHub Actions path.",
"lanes",
],
[
"Targeted dispatch",
"Target repos forward repository_dispatch for low-latency single-item review or commit-range review without polling.",
"bolt",
],
[
"Repair, gated",
"Opted-in PRs can run through review, fix, re-review, and merge. Strict reproducible bug issues can open one guarded generated PR.",
"wrench",
],
];
const cards = features
.map(
([title, body, icon]) =>
`${featureIcon(icon)}
${escapeHtml(title)} ${body}
`,
)
.join("");
const lanes = [
{
name: "Review Lane",
href: "scheduler.html",
desc: "Scheduled and event-driven issue/PR reviews. Planner paths: exact event, hot intake, normal backfill.",
},
{
name: "Apply Lane",
href: "scheduler.html#apply-lane",
desc: "Guarded comment and close mutations. Re-fetches live GitHub state before every write.",
},
{
name: "Repair Lane",
href: "repair/",
desc: 'Bounded "review, fix, re-review, merge" loop for opted-in PRs and strict generated bug PRs.',
},
{
name: "Commit Review Lane",
href: "commit-sweeper.html",
desc: "Reviews code-bearing commits on main. Skips non-code commits cheaply. Optional Check Runs.",
},
];
const laneCards = lanes
.map(
(l) =>
` -> ${l.name} ${l.desc}
`,
)
.join("");
return `
A sweep, in motion
Read, write, propose. Never the other way round.
ClawSweeper does not act on raw model output. Every decision lands in the report repo first; every comment is gated by a marker; every mutation is replayed against live GitHub state before the API call.
Read - GitHub snapshot, prior report, repository profile, paired issue/PR state.
Write - one markdown report per item or commit, with a hashed snapshot.
Act - one durable comment, guarded apply, and repair only through explicit trusted gates.
$ pnpm run plan -- --target-repo openclaw/openclaw --shard-count 100
$ pnpm run review -- --target-repo openclaw/openclaw --artifact-dir artifacts/reviews
$ pnpm run apply-decisions -- --target-repo openclaw/openclaw --limit 20
$ pnpm commit-reports -- --since 24h --findings
Four lanes, one engine
${laneCards}
Guardrails
A close is allowed only when the item is clearly one of these. Maintainer-authored items are never auto-closed.
Implemented on current main
Not reproducible on current main
Better suited for ClawHub skill / plugin work
Duplicate or superseded by a canonical item
Concrete but not actionable in this source repo
Incoherent enough that no action can be taken
Stale issue older than 60 days with too little data to verify
`;
}
function pageNavHtml(prev, next, rootPrefix) {
const cell = (page, dir) => {
if (!page) return "";
return `${dir === "prev" ? "Previous" : "Next"} ${escapeHtml(page.title)} `;
};
return `${cell(prev, "prev")}${cell(next, "next")} `;
}
function navHtml(currentRel, rootPrefix) {
const homeActive = currentRel === "__home" ? " active" : "";
const homeLink = ``;
const sectionLinks = nav
.map(
(section) =>
`${section.name} ${section.pages
.map((page) => {
const href = rootPrefix + page.outRel;
const active = page.rel === currentRel ? " active" : "";
return `${escapeHtml(page.title)} `;
})
.join("")} `,
)
.join("");
return homeLink + sectionLinks;
}
function sitemap(pages) {
const urls = pages
.map((p) => {
const path = p.outRel === "index.html" ? "" : p.outRel;
return ` https://${customDomain}/${path} `;
})
.join("\n");
return `\n\n${urls}\n \n`;
}
function css() {
return `
:root{
--ink:#06181c;
--paper:#fdf6e9;
--shell:#f4ead7;
--reef:#0b3a3f;
--tide:#0a6a72;
--kelp:#13848e;
--coral:#ec5b3c;
--crab:#d9472b;
--sun:#f4a93a;
--sand:#e9d7b1;
--line:#dccfb6;
--line-soft:#ece1c8;
--muted:#56625e;
--code-bg:#0a1d20;
--code-fg:#f1e3c8;
--shadow:0 24px 60px -28px rgba(6,24,28,.35);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:24px}
body{margin:0;background:var(--shell);color:var(--ink);font-family:Inter,"IBM Plex Sans",system-ui,sans-serif;line-height:1.65;overflow-x:hidden;-webkit-font-smoothing:antialiased;font-feature-settings:"ss01","cv11"}
body.home{background:linear-gradient(180deg,#fbf1de 0%,#f4ead7 38%,#ecdec0 100%)}
body:before{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(800px 500px at 110% -10%,rgba(236,91,60,.08),transparent 60%),radial-gradient(700px 500px at -10% 110%,rgba(10,106,114,.10),transparent 60%);z-index:-1}
::selection{background:var(--coral);color:var(--paper)}
a{color:var(--tide);text-decoration-thickness:.07em;text-underline-offset:.18em;transition:color .15s}
a:hover{color:var(--coral)}
.shell{display:grid;grid-template-columns:288px minmax(0,1fr);min-height:100vh}
/* sidebar */
.sidebar{position:sticky;top:0;height:100vh;overflow:auto;padding:24px 20px 14px;background:rgba(253,246,233,.78);border-right:1px solid var(--line);backdrop-filter:blur(20px);scrollbar-width:thin;scrollbar-color:var(--line) transparent;display:flex;flex-direction:column}
.sidebar::-webkit-scrollbar{width:6px}
.sidebar::-webkit-scrollbar-thumb{background:var(--line);border-radius:6px}
.brand{display:flex;align-items:center;gap:11px;color:var(--ink);text-decoration:none;margin-bottom:22px}
.brand img{width:46px;height:46px;filter:drop-shadow(0 4px 10px rgba(217,71,43,.25))}
.brand strong{display:block;font-family:Fraunces,Georgia,serif;font-size:1.36rem;line-height:1;letter-spacing:-.005em;font-weight:700}
.brand small{display:block;color:var(--muted);font-size:.74rem;margin-top:5px;letter-spacing:.01em}
.search{display:block;margin:0 0 22px}
.search span{display:block;color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:7px}
.search input{width:100%;border:1px solid var(--line);background:var(--paper);border-radius:9px;padding:10px 12px;font:inherit;font-size:.92rem;color:var(--ink);outline:none;transition:border-color .15s,box-shadow .15s}
.search input:focus{border-color:var(--coral);box-shadow:0 0 0 3px rgba(236,91,60,.20)}
nav section{margin:0 0 18px}
nav h2{font-size:.66rem;color:var(--muted);text-transform:uppercase;letter-spacing:.13em;margin:0 0 6px;font-weight:700}
.nav-link{display:block;color:var(--ink);text-decoration:none;border-radius:7px;padding:6px 10px;margin:1px 0;font-size:.91rem;line-height:1.4;border-left:2px solid transparent;transition:background .12s,color .12s}
.nav-link:hover{background:rgba(236,91,60,.08);color:var(--reef)}
.nav-link.active{background:#f0e0bf;color:var(--reef);border-left-color:var(--coral);font-weight:600}
.sidebar-foot{margin-top:auto;padding-top:12px;border-top:1px solid var(--line-soft);font-size:.78rem;color:var(--muted);display:flex;gap:8px;align-items:center}
.sidebar-foot a{color:var(--muted);text-decoration:none}
.sidebar-foot a:hover{color:var(--coral)}
/* main */
main{min-width:0;padding:28px clamp(20px,4.5vw,60px) 36px;max-width:1240px;margin:0 auto;width:100%;display:flex;flex-direction:column;min-height:100vh}
.page-foot{margin-top:auto;padding:24px 0 0;display:flex;justify-content:space-between;color:var(--muted);font-size:.8rem;flex-wrap:wrap;gap:8px;border-top:1px dashed var(--line);margin-top:48px}
.page-foot a{color:var(--muted)}
.page-foot a:hover{color:var(--coral)}
.hero{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;border-bottom:1px solid var(--line);padding:18px 0 22px;position:relative;flex-wrap:wrap}
.hero:after{content:"";position:absolute;left:0;bottom:-1px;width:96px;height:3px;background:linear-gradient(90deg,var(--coral),var(--sun),var(--kelp));border-radius:3px}
.hero-text{min-width:0;flex:1 1 320px}
.eyebrow{margin:0 0 8px;color:var(--coral);font-weight:700;text-transform:uppercase;letter-spacing:.14em;font-size:.72rem}
.hero h1{font-family:Fraunces,Georgia,serif;font-size:clamp(1.9rem,3.4vw,2.85rem);line-height:1.05;letter-spacing:-.005em;margin:0;font-weight:700;color:var(--ink)}
.hero-meta{display:flex;gap:8px;flex:0 0 auto}
.repo,.edit{border:1px solid var(--line);color:var(--ink);text-decoration:none;border-radius:9px;padding:7px 12px;font-weight:600;font-size:.84rem;background:var(--paper);transition:border-color .15s,color .15s}
.repo:hover,.edit:hover{border-color:var(--coral);color:var(--coral)}
.edit{color:var(--muted)}
/* landing hero */
.hero-home{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);gap:48px;align-items:center;border-bottom:0;padding:24px 0 12px}
.hero-home:after{display:none}
.hero-home .eyebrow{margin-bottom:14px}
.hero-home h1{font-size:clamp(2.2rem,5vw,4rem);line-height:1.0;letter-spacing:-.018em;font-weight:700;margin:0 0 18px;max-width:18ch}
.hero-home h1 em{font-style:italic;color:var(--coral);font-weight:600}
.lede{margin:0 0 22px;color:#293836;font-size:clamp(1rem,1.25vw,1.13rem);line-height:1.55;max-width:48ch}
.lede.small{font-size:.96rem;color:#3b4a48}
.lede code{background:#f0e0bf;border:1px solid #e2cf9f;border-radius:5px;padding:.04em .3em;font-size:.86em}
.cta{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px}
.cta-primary,.cta-secondary{display:inline-flex;align-items:center;border-radius:10px;padding:11px 18px;font-weight:600;font-size:.95rem;text-decoration:none;transition:transform .15s,box-shadow .15s,background .15s,border-color .15s,color .15s}
.cta-primary{background:var(--ink);color:var(--paper);border:1px solid var(--ink)}
.cta-primary:hover{background:var(--reef);border-color:var(--reef);color:var(--paper);transform:translateY(-1px);box-shadow:0 8px 22px rgba(11,58,63,.3)}
.cta-secondary{border:1px solid var(--ink);color:var(--ink);background:transparent}
.cta-secondary:hover{border-color:var(--coral);color:var(--coral);transform:translateY(-1px)}
.cta-foot{margin:6px 0 0;color:var(--muted);font-size:.84rem;font-style:italic}
.hero-art{position:relative;display:flex;align-items:center;justify-content:center;min-height:340px}
.hero-art svg{width:min(440px,90%);height:auto;filter:drop-shadow(0 30px 50px rgba(217,71,43,.22))}
.hero-art .crab-body{transform-origin:center;animation:sway 6s ease-in-out infinite}
.hero-art .claw-l{transform-origin:88px 142px;animation:snip-l 4s ease-in-out infinite}
.hero-art .claw-r{transform-origin:312px 142px;animation:snip-r 4s ease-in-out infinite}
.hero-art .bubble{animation:bubble 5s ease-in infinite;opacity:0}
.hero-art .bubble.b2{animation-delay:1.4s}
.hero-art .bubble.b3{animation-delay:2.8s}
@keyframes sway{0%,100%{transform:translateX(0) rotate(-1.2deg)}50%{transform:translateX(8px) rotate(1.2deg)}}
@keyframes snip-l{0%,40%,100%{transform:rotate(0deg)}20%{transform:rotate(-14deg)}}
@keyframes snip-r{0%,40%,100%{transform:rotate(0deg)}20%{transform:rotate(14deg)}}
@keyframes bubble{0%{opacity:0;transform:translateY(0) scale(.8)}30%{opacity:.7}100%{opacity:0;transform:translateY(-90px) scale(1.2)}}
@media(prefers-reduced-motion:reduce){.hero-art .crab-body,.hero-art .claw-l,.hero-art .claw-r,.hero-art .bubble{animation:none}}
/* feature cards on landing */
.features-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin:42px 0 6px}
.feature{background:rgba(253,246,233,.86);border:1px solid var(--line-soft);border-radius:14px;padding:22px 22px 20px;transition:border-color .15s,transform .15s,box-shadow .15s;position:relative;overflow:hidden}
.feature:hover{border-color:var(--coral);transform:translateY(-2px);box-shadow:0 16px 30px -16px rgba(11,58,63,.25)}
.feature-icon{display:inline-flex;width:38px;height:38px;border-radius:10px;background:linear-gradient(135deg,#fbe2cf,#f4a93a55);align-items:center;justify-content:center;margin-bottom:12px;color:var(--reef)}
.feature-icon svg{width:22px;height:22px}
.feature h3{font-family:Fraunces,Georgia,serif;font-size:1.12rem;margin:0 0 6px;font-weight:600;letter-spacing:-.005em;line-height:1.2}
.feature p{margin:0;color:#293836;font-size:.94rem;line-height:1.55}
.feature code{font-size:.82em;background:#f0e0bf;border:1px solid #e2cf9f;border-radius:5px;padding:.04em .3em;font-family:"JetBrains Mono",ui-monospace,monospace}
/* snippet row */
.snippet-row{margin:42px 0 0;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:32px;align-items:center}
.snippet-text h2{font-family:Fraunces,Georgia,serif;font-size:clamp(1.4rem,2.1vw,1.85rem);margin:0 0 12px;line-height:1.15;letter-spacing:-.005em;font-weight:600}
.snippet-text p{margin:0 0 14px;color:#293836}
.snippet-list{margin:0;padding-left:18px;color:#293836}
.snippet-list li{margin:6px 0}
.snippet{margin:0;background:var(--code-bg);color:var(--code-fg);border-radius:14px;padding:24px 24px;font:500 .9rem/1.65 "JetBrains Mono",ui-monospace,monospace;border:1px solid #06141660;box-shadow:0 24px 50px -22px rgba(6,24,28,.4);overflow:hidden}
.snippet code{background:transparent;border:0;padding:0;color:inherit;font:inherit;display:block;white-space:pre}
.snippet .prompt{color:var(--sun)}
.snippet .comment{color:#7e948f}
/* lanes row */
.lanes-row{margin:48px 0 0}
.lanes-row h2{font-family:Fraunces,Georgia,serif;font-size:clamp(1.4rem,2.1vw,1.85rem);margin:0 0 16px;line-height:1.15;letter-spacing:-.005em;font-weight:600}
.lanes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
.lane{display:block;background:linear-gradient(180deg,rgba(253,246,233,.96),rgba(244,234,215,.6));border:1px solid var(--line);border-radius:14px;padding:22px 22px 22px;text-decoration:none;color:var(--ink);position:relative;overflow:hidden;transition:transform .15s,border-color .15s,box-shadow .15s}
.lane:hover{transform:translateY(-2px);border-color:var(--coral);box-shadow:0 16px 30px -14px rgba(217,71,43,.22)}
.lane-arrow{position:absolute;top:18px;right:20px;color:var(--coral);font-family:"JetBrains Mono",monospace;font-weight:700;font-size:1.05rem;transition:transform .2s}
.lane:hover .lane-arrow{transform:translateX(4px)}
.lane h3{font-family:Fraunces,Georgia,serif;font-size:1.18rem;margin:0 0 6px;font-weight:600;letter-spacing:-.005em}
.lane p{margin:0;color:#3b4a48;font-size:.94rem;line-height:1.55}
.lane code{background:#f0e0bf;border:1px solid #e2cf9f;border-radius:5px;padding:.04em .3em;font-size:.84em;font-family:"JetBrains Mono",monospace}
/* rules */
.rules{margin:48px 0 8px;padding:28px 28px 26px;background:linear-gradient(135deg,rgba(11,58,63,.06),rgba(236,91,60,.04));border:1px solid var(--line);border-radius:18px}
.rules h2{font-family:Fraunces,Georgia,serif;font-size:clamp(1.4rem,2.1vw,1.85rem);margin:0 0 8px;line-height:1.15;letter-spacing:-.005em;font-weight:600}
.rules-list{list-style:none;padding:0;margin:14px 0 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:8px 18px;color:#293836}
.rules-list li{position:relative;padding-left:22px;line-height:1.5}
.rules-list li:before{content:"";position:absolute;left:0;top:.55em;width:10px;height:10px;background:var(--coral);clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);transform:rotate(0deg)}
.rules-list code{background:#f0e0bf;border:1px solid #e2cf9f;border-radius:5px;padding:.04em .3em;font-size:.86em;font-family:"JetBrains Mono",monospace}
/* layout: doc + toc */
.doc-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:36px;margin-top:30px}
.doc-grid-home{margin-top:18px}
.doc-home{background:transparent;box-shadow:none;border:0;padding:0;max-width:none;width:100%}
@media(min-width:1180px){.doc-grid{grid-template-columns:minmax(0,74ch) 200px;justify-content:start}.doc-grid-home{grid-template-columns:minmax(0,1fr)}}
.doc{min-width:0;max-width:74ch;background:rgba(253,246,233,.86);box-shadow:var(--shadow);border:1px solid var(--line-soft);border-radius:14px;padding:clamp(22px,3.6vw,44px);overflow-wrap:break-word}
.doc h1{display:none}
.doc h2{font-family:Fraunces,Georgia,serif;font-size:1.7rem;line-height:1.15;margin:1.9em 0 .5em;font-weight:600;letter-spacing:-.005em;position:relative}
.doc h3{font-size:1.14rem;margin:1.6em 0 .3em;position:relative;font-weight:600}
.doc h4{font-size:.99rem;margin:1.3em 0 .2em;color:var(--reef);position:relative;font-weight:600}
.doc h2:first-child,.doc h3:first-child,.doc h4:first-child{margin-top:0}
.doc :is(h2,h3,h4) .anchor{position:absolute;left:-1em;top:0;color:var(--muted);opacity:0;text-decoration:none;font-weight:400;padding-right:.3em;transition:opacity .12s,color .12s}
.doc :is(h2,h3,h4):hover .anchor{opacity:.55}
.doc :is(h2,h3,h4) .anchor:hover{opacity:1;color:var(--coral)}
.doc p{margin:0 0 1.05em}
.doc ul,.doc ol{padding-left:1.35rem;margin:0 0 1.2em}
.doc li{margin:.25em 0}
.doc li>p{margin:0 0 .4em}
.doc strong{font-weight:600}
.doc code{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.86em;background:#f0e0bf;border:1px solid #e2cf9f;border-radius:5px;padding:.08em .34em}
.doc pre{position:relative;overflow:auto;background:var(--code-bg);color:var(--code-fg);border-radius:11px;padding:16px 20px;border:1px solid #06141660;box-shadow:inset 0 0 0 1px rgba(255,255,255,.03);margin:1.35em 0;font-size:.88em;scrollbar-width:thin;scrollbar-color:#3a4a47 transparent}
.doc pre::-webkit-scrollbar{height:8px}
.doc pre::-webkit-scrollbar-thumb{background:#3a4a47;border-radius:8px}
.doc pre code{background:transparent;border:0;color:inherit;padding:0;font-size:1em}
.doc pre .copy{position:absolute;top:8px;right:8px;background:rgba(253,246,233,.06);color:var(--code-fg);border:1px solid rgba(253,246,233,.18);border-radius:6px;padding:3px 9px;font:600 .7rem/1 Inter,sans-serif;cursor:pointer;opacity:0;transition:opacity .15s,background .15s,border-color .15s}
.doc pre:hover .copy,.doc pre .copy:focus{opacity:1}
.doc pre .copy:hover{background:rgba(253,246,233,.14)}
.doc pre .copy.copied{background:var(--coral);border-color:var(--coral);opacity:1}
.doc blockquote{margin:1.4em 0;padding:12px 16px;border-left:3px solid var(--coral);background:#f3e3c5;border-radius:0 9px 9px 0;color:var(--ink)}
.doc blockquote p:last-child{margin-bottom:0}
.doc table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:.94em}
.doc th,.doc td{border-bottom:1px solid var(--line);padding:9px 10px;text-align:left}
.doc th{font-weight:600;color:var(--reef)}
.doc hr{border:0;border-top:1px solid var(--line);margin:2em 0}
/* toc */
.toc{position:sticky;top:24px;align-self:start;font-size:.85rem;padding-left:14px;border-left:1px solid var(--line);max-height:calc(100vh - 48px);overflow:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
.toc::-webkit-scrollbar{width:5px}
.toc::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px}
.toc h2{font-size:.66rem;color:var(--muted);text-transform:uppercase;letter-spacing:.13em;margin:0 0 10px;font-weight:700}
.toc a{display:block;color:var(--muted);text-decoration:none;padding:4px 0 4px 10px;line-height:1.35;border-left:2px solid transparent;margin-left:-12px;transition:color .12s,border-color .12s}
.toc a:hover{color:var(--ink)}
.toc a.active{color:var(--reef);border-left-color:var(--coral);font-weight:600}
.toc-l3{padding-left:22px!important;font-size:.94em}
@media(max-width:1179px){.toc{display:none}}
/* prev/next pager */
.page-nav{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:48px}
.page-nav>a{display:block;border:1px solid var(--line);background:var(--paper);border-radius:11px;padding:14px 18px;text-decoration:none;color:var(--ink);transition:border-color .15s,transform .15s,box-shadow .15s}
.page-nav>a:hover{border-color:var(--coral);transform:translateY(-1px);box-shadow:0 6px 18px rgba(11,58,63,.10)}
.page-nav small{display:block;color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.12em;margin-bottom:5px;font-weight:700}
.page-nav span{display:block;font-weight:600;line-height:1.3}
.page-nav-prev{text-align:left}
.page-nav-next{text-align:right;grid-column:2}
.page-nav-prev:only-child{grid-column:1}
/* mobile nav toggle */
.nav-toggle{display:none;position:fixed;top:14px;right:14px;z-index:20;width:42px;height:42px;border-radius:10px;background:var(--paper);border:1px solid var(--line);cursor:pointer;padding:11px 10px;flex-direction:column;justify-content:space-between;box-shadow:0 6px 18px rgba(6,24,28,.14)}
.nav-toggle span{display:block;height:2px;background:var(--ink);border-radius:2px;transition:transform .2s,opacity .2s}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(8px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-8px) rotate(-45deg)}
/* mobile */
@media(max-width:980px){
.shell{display:block}
.sidebar{position:fixed;inset:0 25% 0 0;max-width:340px;height:100vh;z-index:15;transform:translateX(-100%);transition:transform .25s ease;box-shadow:0 18px 40px rgba(6,24,28,.18);background:var(--paper)}
.sidebar.open{transform:translateX(0)}
.nav-toggle{display:flex}
main{padding:64px 18px 32px}
.hero{padding-top:8px}
.hero h1{font-size:clamp(1.7rem,7vw,2.2rem)}
.hero-meta{width:100%;justify-content:flex-start}
.hero-home{grid-template-columns:1fr;gap:18px}
.hero-home h1{font-size:clamp(2rem,8vw,2.7rem);max-width:none}
.hero-art{min-height:240px;order:-1}
.hero-art svg{width:min(280px,80%)}
.features-row{grid-template-columns:1fr;margin-top:30px}
.snippet-row{grid-template-columns:1fr;margin-top:32px;gap:18px}
.snippet{font-size:.78rem;padding:18px}
.lanes{grid-template-columns:1fr}
.rules{padding:22px}
.doc{padding:22px;border-radius:11px}
.doc-home{padding:0}
.doc-grid{margin-top:22px;gap:24px}
.doc :is(h2,h3,h4) .anchor{display:none}
}
@media(max-width:520px){
main{padding:60px 14px 28px}
.doc{padding:18px 16px}
.doc pre{margin-left:-16px;margin-right:-16px;border-radius:0;border-left:0;border-right:0}
}
`;
}
function js() {
return `
const sidebar=document.querySelector('.sidebar');
const toggle=document.querySelector('.nav-toggle');
toggle?.addEventListener('click',()=>{const open=sidebar.classList.toggle('open');toggle.setAttribute('aria-expanded',open?'true':'false')});
document.addEventListener('click',(e)=>{if(!sidebar?.classList.contains('open'))return;if(sidebar.contains(e.target)||toggle.contains(e.target))return;sidebar.classList.remove('open');toggle.setAttribute('aria-expanded','false')});
const input=document.getElementById('doc-search');
input?.addEventListener('input',()=>{const q=input.value.trim().toLowerCase();document.querySelectorAll('nav section').forEach(sec=>{let any=false;sec.querySelectorAll('.nav-link').forEach(a=>{const m=!q||a.textContent.toLowerCase().includes(q);a.style.display=m?'block':'none';if(m)any=true});sec.style.display=any?'block':'none'})});
document.querySelectorAll('.doc pre').forEach(pre=>{const btn=document.createElement('button');btn.type='button';btn.className='copy';btn.textContent='Copy';btn.addEventListener('click',async()=>{const code=pre.querySelector('code')?.textContent??'';try{await navigator.clipboard.writeText(code);btn.textContent='Copied';btn.classList.add('copied');setTimeout(()=>{btn.textContent='Copy';btn.classList.remove('copied')},1400)}catch{btn.textContent='Failed';setTimeout(()=>{btn.textContent='Copy'},1400)}});pre.appendChild(btn)});
const tocLinks=document.querySelectorAll('.toc a');
if(tocLinks.length){const map=new Map();tocLinks.forEach(a=>{const id=a.getAttribute('href').slice(1);const el=document.getElementById(id);if(el)map.set(el,a)});const setActive=l=>{tocLinks.forEach(x=>x.classList.remove('active'));l.classList.add('active')};const obs=new IntersectionObserver(entries=>{const visible=entries.filter(e=>e.isIntersecting).sort((a,b)=>a.boundingClientRect.top-b.boundingClientRect.top);if(visible.length){const link=map.get(visible[0].target);if(link)setActive(link)}},{rootMargin:'-15% 0px -65% 0px',threshold:0});map.forEach((_,el)=>obs.observe(el))}
`;
}
function heroCrab() {
return `
`;
}
function featureIcon(kind) {
const icons = {
report: ` `,
comment: ` `,
shield: ` `,
lanes: ` `,
bolt: ` `,
wrench: ` `,
};
return icons[kind] || icons.report;
}
function clawSvg() {
return `
`;
}
function faviconSvg() {
return `
`;
}
function slug(text) {
return text
.toLowerCase()
.replace(/`/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
function escapeHtml(value) {
return String(value).replace(
/[&<>"']/g,
(char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char],
);
}
function escapeAttr(value) {
return escapeHtml(value);
}