#!/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 = 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(`${th}${tb}
`); 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 ``; } 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}
    ClawSweeper - one of the OpenClaw tools ${escapeHtml(new Date().toISOString().slice(0, 10))} - source
    `; } function standardHero(page, sectionName, editUrl) { return `

    ${escapeHtml(sectionName)}

    ${escapeHtml(page.title)}

    `; } function landingHero(rootPrefix) { return `

    OpenClaw - maintenance bot

    Sideways through the backlog.
    Sweep what's safe.
    Leave the rest.

    ClawSweeper is the conservative maintenance bot for OpenClaw. It reviews issues, pull requests, and code-bearing commits; keeps one durable public comment per item; and turns narrow trusted findings into guarded repair or automerge work.

    No closes without evidence. No autocomments without a marker. No mutations without an audit.

    `; } 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 `
    ${cards}

    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.

    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 ``; } function navHtml(currentRel, rootPrefix) { const homeActive = currentRel === "__home" ? " active" : ""; const homeLink = `

    Home

    Overview
    `; 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); }