carlos / carloku Public

Merge pull request #12 from carlosframework/docs-site

🤖 /docs: ten customer pages, the pipeline, and a ship runbook that keeps them
Paul Campbell pushed by claude agent, operated by paul@keymail.dev a71ce0670d05a9a2f8ca6e267abe04327c5eefb2 parent 6c95332parent 9cb139a
38 files changed, +3815 −38
  • .gitignore +2 −0
  • AGENTS.md +219 −0
  • CLAUDE.md +5 −35
  • PRODUCT.md +1 −1
  • README.md +19 −2
  • eleventy.config.js +178 −0
  • hack/check-docs.mjs +175 −0
  • package-lock.json +1725 −0
  • package.json +15 −0
  • src/_data/docsanchors.json +15 −0
  • src/_data/docsnav.json +24 −0
  • src/_includes/docs.njk +85 −0
  • src/docs-index.njk +20 −0
  • src/docs.css +450 −0
  • src/docs.njk +13 −0
  • src/docs/channels.md +101 −0
  • src/docs/config.md +78 −0
  • src/docs/console.md +89 −0
  • src/docs/deploying.md +89 −0
  • src/docs/docs.json +5 −0
  • src/docs/domains.md +70 −0
  • src/docs/index.md +13 −0
  • src/docs/limits.md +76 −0
  • src/docs/logs.md +67 −0
  • src/docs/members.md +109 −0
  • src/docs/sleeping.md +86 −0
  • src/docs/storage.md +77 −0
  • src/docsmd.njk +9 −0
  • favicon.svg => src/favicon.svg +0 −0
  • {fonts => src/fonts}/Geist-Variable.woff2 binary
  • {fonts => src/fonts}/GeistMono-Variable.woff2 binary
  • {fonts => src/fonts}/LICENSE-OFL.txt +0 −0
  • {get-started => src/get-started}/index.html +0 −0
  • {img => src/img}/dashboard-dark.png binary
  • {img => src/img}/dashboard-light.png binary
  • index.html => src/index.html +0 −0
  • site.css => src/site.css +0 −0
  • site.js => src/site.js +0 −0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3a799fb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+_site/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..caabe96
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,219 @@
+# 🤖 AGENTS.md
+
+The canonical agent-instructions file for this repo — keep it current here,
+not in a tool-specific file. (Claude Code reads it via the `CLAUDE.md`
+pointer.)
+
+The marketing site for **Carloku** — the hosted CARLOS platform
+(console.carloku.com). Brand boundary: Carloku is the *hosted platform*
+brand; CARLOS is the framework/CLI/console name. Never rename CARLOS things
+to Carloku here.
+
+**Voice lives in PRODUCT.md** — the one feeling is lightness ("finally,
+simple"); the landing page appeals to the heart, /get-started carries the
+tech. Read it before touching copy.
+
+Rules inherited from the `website` repo, which governs the sibling site:
+
+- **Small site, nothing fetched.** `/` and `/get-started/` are hand-written
+ and share one `site.css`. No frameworks, no analytics, and nothing pulled
+ from another origin — the two Geist faces in `fonts/` are self-hosted. The
+ only script is `site.js`: 2KB, hand-written, and pure enhancement (the hero
+ terminal types itself, the header grows a hairline on scroll). The pages
+ read fine with it blocked; keep it that way, and don't add a second one.
+- **`/docs` is built; the rest of the site is not.** Docs pages come from the
+ markdown in `src/docs/` through Eleventy, the same pipeline rastrillo.org
+ uses, run here before you ship. `_site/` is gitignored and never committed;
+ Eleventy and markdown-it are devDependencies that no visitor ever meets.
+ See "Building the docs" below.
+- **Light and dark** via `prefers-color-scheme` — keep both working.
+- **AI authorship is always marked** with a visible 🤖 (cascades from a
+ heading); person-emoji (👨/👤/🧑) blocks are certified human and
+ off-limits to LLM edits. Baseline: everything here is AI-written unless
+ marked otherwise.
+- **Don't overclaim.** The console requires Keymail sign-in; pricing does
+ not exist yet, so the site says nothing about it.
+
+## Building the docs
+
+`/docs` is ten pages of markdown under `src/docs/`, rendered by Eleventy into
+`_site/` before you ship. **The corpus is authored here, by hand.** That is the
+opposite of carlosframework.com, whose `/docs` corpus is vendored out of the
+platform repo by a sync script: there is no sync step in this repo, nothing
+under `src/docs/` or `src/_data/docsnav.json` is generated, and nothing will
+overwrite an edit you make. The markdown IS the source — edit it here.
+
+```
+npm install
+npm run check # builds, then gates the rendered output
+npm run serve # localhost:8080, live reload
+```
+
+`npm run check` is `eleventy` followed by `hack/check-docs.mjs`, which reads
+`_site/` after the build: the hand-written pages and shared assets were
+emitted at all; every nav entry has a built page and a built `.md` twin; every
+internal `/docs` href resolves to a built file, and to a real `id="…"` when it
+carries a fragment; and `slugify` in `eleventy.config.js` agrees with Go's
+`internal/docsite.Anchor` on every case in `src/_data/docsanchors.json`. That
+last one is why the anchor fixture is vendored rather than retyped: a heading
+whose fragment the platform's Go gate accepts cannot 404 in a browser here
+because the two slug rules drifted.
+
+**Eleventy does not clean stale output.** Delete or rename a page and its old
+built HTML stays in `_site/` until you remove it — and the gate will *not*
+notice, because every check it runs is satisfied by a file that should no
+longer exist. `_site/` is gitignored, so a stale page never shows up in a diff
+either. **Build clean whenever you are verifying anything:**
+
+```
+rm -rf _site && npm run check
+```
+
+The deploy sequence below gets this for free: it builds inside a fresh
+`git archive` export, which has no `_site/` in it at all.
+
+`index.html` and `get-started/index.html` are hand-written and pass through the
+build byte-identical — `templateFormats` excludes `html` on purpose, so
+Eleventy copies them instead of rendering them as templates. If either changes
+in `_site/` without you editing its source, something is wrong.
+
+## Deploying
+
+Merging does **not** publish. `carloku.com` is a bucket-mode CARLOS app on the
+flagship, and the live site is stale until someone ships.
+
+**There is a build step now, so what ships is `_site/` — never the repo tree.**
+A tree ship has no `_site/` in it: it would publish `index.html` buried under
+`src/`, no built pages, and no `/docs` at all.
+
+The deployment coordinates, confirmed against the bucket on 2026-08-27:
+
+| | |
+| --- | --- |
+| app | `carloku` |
+| account (sqid) | `bab` |
+| channel | `canary/rehearsal` — the app's only channel |
+| kind | `static` |
+| bucket | `carlos-flagship-271376211898` |
+| AWS profile / region | `keymail` / `eu-west-1` |
+
+```
+SHA=$(git rev-parse --short HEAD) # the sha you are shipping
+
+# 1. Clean export. Never ship a working checkout — PackDir packs every
+# regular file it sees, including .git, node_modules/ and .claude/.
+rm -rf /tmp/carloku-ship && mkdir -p /tmp/carloku-ship
+git archive "$SHA" --prefix=export/ | tar -x -C /tmp/carloku-ship
+cd /tmp/carloku-ship/export
+
+# 2. Build, and gate the rendered output. `check` runs eleventy, then
+# hack/check-docs.mjs over what it produced.
+npm ci
+npm run check
+
+# 3. Cache-bust BOTH stylesheets across every built page — see Caching
+# below; this is not optional.
+find _site -name '*.html' -exec sed -i \
+ -e "s/\.css\"/.css?v=$SHA\"/g" \
+ -e "s/\.css?v=[^\"]*\"/.css?v=$SHA\"/g" {} +
+
+grep -rho '\.css?v=[^"]*"' _site --include='*.html' | sort -u # ONE line: your sha
+grep -rn '\.css"' _site --include='*.html' \
+ && echo "UNBUSTED LINK — do not ship" || echo "cache-bust ok"
+
+# 4. Ship the BUILT OUTPUT, then promote. The pinfra scripts are the
+# canonical form for this app: the signing key is read from SSM inside
+# the script and never appears in a terminal or a transcript.
+cd ~/github.com/carlosframework/platform-infrastructure
+CARLOS_BIN=$(command -v carlos) scripts/ship-app.sh \
+ keymail carlos-flagship-271376211898 \
+ carloku "$SHA" canary/rehearsal static /tmp/carloku-ship/export/_site
+```
+
+`ship-app.sh` takes `<aws-profile> <bucket> <app> <version> <channel> <kind>
+<file-or-dir>` and does both halves. The equivalent by hand, if you are not in
+the pinfra checkout, is:
+
+```
+export AWS_PROFILE=keymail AWS_REGION=eu-west-1 \
+ CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898
+carlos ship --app carloku --kind static --version "$SHA" _site
+CARLOS_RELEASE_KEY=$(aws ssm get-parameter --name /carlos/release-key \
+ --with-decryption --query Parameter.Value --output text) \
+ carlos promote --app carloku "$SHA" canary/rehearsal
+```
+
+The env matters: without `CARLOS_DEPLOYMENT_BUCKET` the CLI goes through the
+console API, where this app was never registered, and fails with "not found".
+This is bucket mode. Use a current `carlos` binary — the ship/promote pair is
+the platform's own release path, and an old binary on your `PATH` is a real
+hazard here.
+
+**Step 3 explained, because both halves are load-bearing.** The links in `src/`
+are plain (`href="/site.css"`), so the first expression stamps a token onto a
+bare link and the second re-stamps a link that already carries one. Both end at
+the closing quote on purpose, and so does the verification grep. A sha
+beginning with `0` — this app has already shipped `01b5365` — makes
+`?v=01b5365` contain the literal substring `?v=0`, so an unanchored grep for
+`?v=0` calls every freshly bumped file stale (13 of 13, measured), and an
+unanchored `sed` run twice yields `?v=01b536501b5365`. Anchoring on `"` fixes
+both and makes step 3 idempotent: run it again and again, the answer is the
+same. Today that is 24 links across 13 built pages — 13 × `site.css`, 11 ×
+`docs.css`. Don't hard-code those counts anywhere; the corpus grows, which is
+also why the `find` walks `_site` instead of naming files.
+
+**Caching: the edge sends no `Cache-Control` and no `ETag` on this static
+route — only `Last-Modified`** (confirmed live on carloku.com 2026-08-27).
+Browsers therefore apply HEURISTIC caching, roughly 10% of the age since
+`Last-Modified`, so a returning visitor can hold a stale page for days. This
+bit the sibling site the day its redesign shipped: one browser served the whole
+old page, another served the NEW html against the OLD stylesheet. The real fix
+is server-side `Cache-Control` on static routes — platform issue
+**carlosframework/platform#234**. Until that lands, **step 3 is a required part
+of deploying this site**, and it is required for `docs.css` exactly as much as
+for `site.css`: the docs pages link both, and a docs page rendered against a
+stale `docs.css` loses its whole layout.
+
+**Convergence is seconds, not minutes** (platform PR #108, live 2026-08-08): a
+promote is picked up by the edge within ~2s. Verify by header and by content,
+and verify `/docs` **specifically** — the landing page looks right whether or
+not the built docs made it into the artifact:
+
+```
+# by header — --resolve so a stale DNS answer cannot satisfy it. 99.81.104.219
+# is the flagship, where the carloku.com A record points.
+curl -sI --resolve carloku.com:443:99.81.104.219 \
+ https://carloku.com/docs/ | grep -i x-carlos-version
+
+# by content
+curl -s https://carloku.com/ | grep -i "<something from the change>"
+curl -s https://carloku.com/docs/ | grep -o 'docs\.css?v=[^"]*' # your sha
+```
+
+Static routes do carry `X-Carlos-Version` (platform#112). `-I` is fine for that
+header, but it sends a HEAD, and HEAD is excluded from edge compression —
+anything about `Content-Encoding` needs a GET.
+
+**`carlos channels` cannot verify this app.** It is direct-bucket and invisible
+to the console API: `carlos channels -app carloku` answers "not found"
+(re-confirmed 2026-08-27). Verify by header or content as above, or read the
+pointer out of the bucket
+(`aws s3 cp s3://carlos-flagship-271376211898/apps/bab/carloku/channels/canary/rehearsal.json -`).
+
+Rollback is a pointer move, like any CARLOS app — promote the previous good sha
+back onto the same channel; the edge picks it up in seconds:
+
+```
+cd ~/github.com/carlosframework/platform-infrastructure
+CARLOS_BIN=$(command -v carlos) scripts/promote-app.sh \
+ keymail carlos-flagship-271376211898 bab carloku <previous-sha> canary/rehearsal
+```
+
+That previous release is still in the bucket under
+`apps/bab/carloku/releases/<sha>/`, cache-busted as it was shipped, so a
+rollback restores a self-consistent page-plus-stylesheet pair.
+
+(The channel is `canary/rehearsal`, not `stable`, for the same reason the
+sibling site's is: `stable` bakes 72h on a box's *first* sighting of a channel
+head, which would mean 72h of downtime for a never-before-served route. A
+future `stable` flip is optional cleanup, not required.)
diff --git a/CLAUDE.md b/CLAUDE.md
index 2e7dd85..270f5c9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,36 +1,6 @@
-# 🤖 CLAUDE.md
+# CLAUDE.md
-The marketing site for **Carloku** — the hosted CARLOS platform
-(console.carloku.com). Brand boundary: Carloku is the *hosted platform*
-brand; CARLOS is the framework/CLI/console name. Never rename CARLOS things
-to Carloku here.
-
-**Voice lives in PRODUCT.md** — the one feeling is lightness ("finally,
-simple"); the landing page appeals to the heart, /get-started carries the
-tech. Read it before touching copy.
-
-Rules inherited from the `website` repo, which governs the sibling site:
-
-- **Small site, zero dependencies.** Two pages (`/`, `/get-started/`) and
- one shared `site.css`. No frameworks, no fetched fonts, no analytics, no
- JavaScript, no build step.
-- **Light and dark** via `prefers-color-scheme` — keep both working.
-- **AI authorship is always marked** with a visible 🤖 (cascades from a
- heading); person-emoji (👨/👤/🧑) blocks are certified human and
- off-limits to LLM edits. Baseline: everything here is AI-written unless
- marked otherwise.
-- **Don't overclaim.** The console requires Keymail sign-in; pricing does
- not exist yet, so the site says nothing about it.
-
-Deploys: bucket-mode CARLOS app `carloku` on flagship — ship a clean
-`git archive` export via the pinfra `scripts/ship-app.sh` / `promote-app.sh`
-(see the platform repo's CLAUDE.md "Deploy reality"). Merging does NOT
-publish; the site is stale until someone ships. Since platform PR #108
-(2026-08-08), the fleet converges within seconds of ship+promote — no
-need to wait for the next adoption pass. Verify by content, e.g.
-`curl -s https://carloku.com/ | grep -i "<something from the change>"`,
-or with `carlos channels -app carloku` showing the new sha promoted —
-static routes carry `X-Carlos-Version` too (since the platform#112
-roll — verified live 2026-08-23: the header reports the promoted sha),
-so `curl -sI` is also proof. The pinfra ship+promote scripts remain the
-operator default, and for this app that pair IS the deploy.
+Guidance for AI coding agents working in this repository lives in
+**AGENTS.md**. Claude Code: read AGENTS.md. Update AGENTS.md — not this
+file — going forward; this file is only a pointer so Claude Code finds
+it.
diff --git a/PRODUCT.md b/PRODUCT.md
index 0183a33..bb4603c 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -46,7 +46,7 @@ division of labor between the pages.
- Never on the landing page: immutable, content-addressed, canary,
signature/signed, provision, TLS, converge, infra, artifact, registry.
All of it lives happily on /get-started ("Under the hood") or in docs.
-- No pricing language until billing exists (CLAUDE.md rule). Describe
+- No pricing language until billing exists (AGENTS.md rule). Describe
hibernation by behavior, never by cost.
- No claims we can't cash today. Where the product is young, say young —
honesty reads as confidence.
diff --git a/README.md b/README.md
index 74d15ab..dbec513 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,22 @@
# carloku.com
-🤖 Marketing site for Carloku, the hosted CARLOS platform. One static page,
-no build step. See CLAUDE.md for the rules; see
+🤖 Marketing site for Carloku, the hosted CARLOS platform, plus the customer
+docs at /docs. The marketing pages are hand-written static HTML; the docs are
+markdown under `src/docs/`, built by Eleventy before shipping.
+
+**There is a build step, and it decides what the site serves.** Eleventy reads
+`src/` and writes `_site/`, and `_site/` is what ships — never the repo tree.
+So: **if a URL should return it, the file goes under `src/`.** A file at the
+repo root is a repo file, not a page, and will not be served. Anything the site
+needs — a page, a stylesheet, a font, an image — lives under `src/`, and a
+non-template asset also needs an `addPassthroughCopy` line in
+`eleventy.config.js` to be emitted at all.
+
+```
+npm install
+npm run check # build, then gate the rendered output
+npm run serve # localhost:8080
+```
+
+See AGENTS.md for the deploy runbook and the rules of the repo; see
https://carlosframework.com for the framework itself.
diff --git a/eleventy.config.js b/eleventy.config.js
new file mode 100644
index 0000000..05d185a
--- /dev/null
+++ b/eleventy.config.js
@@ -0,0 +1,178 @@
+// 🤖 Build-time only — the served site ships no JavaScript beyond site.js.
+import markdownIt from "markdown-it";
+import markdownItAnchor from "markdown-it-anchor";
+
+// slugify must match internal/docsite's Anchor in the platform repo
+// exactly, or an internal fragment link passes that repo's Go gate and
+// 404s in a browser. Both sides are deliberately plain — lowercase
+// alphanumerics, everything else a separator — because
+// markdown-it-anchor's own default URI-encodes punctuation, which would
+// anchor a heading naming `db.Open` differently on each side.
+// src/_data/docsanchors.json (vendored from docs/site/anchors.json) is
+// the contract both sides are checked against.
+export function slugify(text) {
+ let out = "";
+ let pendingSep = false;
+ for (const ch of String(text).toLowerCase()) {
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9")) {
+ if (pendingSep && out.length > 0) out += "-";
+ pendingSep = false;
+ out += ch;
+ } else {
+ pendingSep = true;
+ }
+ }
+ return out;
+}
+
+// fenceDelim/sameFence port internal/docsite/docsite.go's fence-matching
+// exactly, for docsSections below: a trimmed line opens or closes a
+// fence, and a closer only closes if it is the same character as the
+// opener and at least as long a run.
+function fenceDelim(line) {
+ let r = null;
+ if (line.startsWith("```")) r = "`";
+ else if (line.startsWith("~~~")) r = "~";
+ else return null;
+ let n = 0;
+ while (n < line.length && line[n] === r) n++;
+ return { mark: line.slice(0, n), info: line.slice(n).trim() };
+}
+
+function sameFence(closing, opening) {
+ if (!closing || !opening || closing[0] !== opening[0]) return false;
+ return closing.length >= opening.length;
+}
+
+export default function (eleventyConfig) {
+ eleventyConfig.addPassthroughCopy({ "src/index.html": "index.html" });
+ eleventyConfig.addPassthroughCopy({ "src/get-started": "get-started" });
+ eleventyConfig.addPassthroughCopy({ "src/site.css": "site.css" });
+ eleventyConfig.addPassthroughCopy({ "src/docs.css": "docs.css" });
+ eleventyConfig.addPassthroughCopy({ "src/site.js": "site.js" });
+ eleventyConfig.addPassthroughCopy({ "src/favicon.svg": "favicon.svg" });
+ eleventyConfig.addPassthroughCopy({ "src/fonts": "fonts" });
+ eleventyConfig.addPassthroughCopy({ "src/img": "img" });
+
+ const md = markdownIt({ html: true, linkify: false, typographer: false }).use(
+ markdownItAnchor,
+ { slugify, level: [2, 3, 4], tabIndex: false },
+ );
+ eleventyConfig.setLibrary("md", md);
+
+ // The docs corpus is authored here, by hand, under src/docs/ — there
+ // is no vendoring and no sync step. Pages are addressed by slug —
+ // "cli" — which is also how src/_data/docsnav.json (the corpus's
+ // hand-maintained table of contents) and every internal /docs link
+ // name them.
+ eleventyConfig.addCollection("docs", (api) =>
+ api
+ .getFilteredByGlob("src/docs/**/*.md")
+ .map((page) => {
+ const slug = page.filePathStem.replace(/^\/docs\//, "");
+ return { slug, page };
+ })
+ .filter(({ slug }) => slug !== "index"),
+ );
+
+ // The /docs index page's own prose, as its own collection.
+ //
+ // docs-index.njk renders this page's content above the cards. It must
+ // NOT reach for collections.all to find it: that collection contains
+ // docs-index.njk itself, and Eleventy rejects a template that reads
+ // templateContent from a collection it belongs to as a circular
+ // reference. A one-page collection it is not a member of is the way
+ // to read another template's rendered output.
+ eleventyConfig.addCollection("docsIndex", (api) =>
+ api.getFilteredByGlob("src/docs/index.md"),
+ );
+
+ // Section headings of one page, for the sidebar's sub-nav. Read from
+ // the source rather than the rendered HTML so it needs no DOM.
+ //
+ // Fence tracking is ported from internal/docsite/docsite.go's
+ // fenceDelim/sameFence in the platform repo, not invented here: a
+ // closing marker must match the opener's character and be at least as
+ // long, so a ``` block nested inside an outer ~~~ fence stays inside
+ // that outer fence instead of prematurely toggling out of it (which
+ // would both hide real "##" headings after the true close and expose
+ // fence-internal "##" lines as fake ones).
+ eleventyConfig.addFilter("docsSections", (raw) => {
+ const out = [];
+ let inFence = false;
+ let fenceMark = "";
+ for (const line of String(raw).split("\n")) {
+ const trimmed = line.trim();
+ const fd = fenceDelim(trimmed);
+ if (fd) {
+ if (!inFence) {
+ inFence = true;
+ fenceMark = fd.mark;
+ continue;
+ } else if (sameFence(fd.mark, fenceMark)) {
+ inFence = false;
+ continue;
+ }
+ // A fence-looking line that doesn't close the current fence
+ // (different character, or a shorter run) is body content of
+ // the still-open fence, same as the Go side.
+ }
+ if (inFence) continue;
+ const m = /^##\s+(.+?)\s*#*$/.exec(trimmed);
+ if (m) out.push({ title: m[1], anchor: slugify(m[1]) });
+ }
+ return out;
+ });
+
+ eleventyConfig.addFilter("docsSlugify", slugify);
+
+ // A page's own "# " title. The docs markdown carries no Eleventy front
+ // matter — it's plain prose with a leading "# " heading, authored that
+ // way on purpose so the corpus reads the same whether you're in an
+ // editor, on GitHub, or fetched raw via docsmd.njk — so the title is
+ // read from the source rather than declared twice.
+ eleventyConfig.addFilter("docsTitle", (raw) => {
+ for (const line of String(raw).split("\n")) {
+ const m = /^#\s+(.+?)\s*#*$/.exec(line);
+ if (m) return m[1].replace(/^🤖\s*/, "");
+ }
+ return "";
+ });
+
+ // A page's raw markdown, looked up from the collection by slug.
+ //
+ // NOT passed through eleventyComputed: a computed value in Nunjucks
+ // front matter is a string interpolation, which HTML-escapes it, and
+ // an escaped apostrophe turns "carlos's" into "carlos-39-s" in every
+ // anchor derived from it. The headings themselves are fine —
+ // markdown-it-anchor reads the raw token — so the result was a
+ // sub-nav whose links pointed at ids no page had.
+ eleventyConfig.addFilter("docsRawFor", (docs, slug) => {
+ for (const doc of docs) {
+ if (doc.slug === slug) return doc.page.rawInput;
+ }
+ return "";
+ });
+
+ // A page's one-line blurb, from nav.json — the same string the /docs
+ // index renders on its card, reused as the meta description so the
+ // two cannot drift.
+ eleventyConfig.addFilter("docsBlurb", (nav, slug) => {
+ for (const section of nav.sections) {
+ for (const entry of section.pages) {
+ if (entry.slug === slug) return entry.blurb;
+ }
+ }
+ return "";
+ });
+
+ return {
+ // "html" is deliberately excluded: index.html and get-started/index.html
+ // are hand-written and must ship byte-identical. If html stayed a
+ // template format, Eleventy's default html engine (liquid) would
+ // render them as templates in addition to the passthrough copies
+ // above, and any literal "{{" in their inline SVG/script would break.
+ templateFormats: ["njk", "md"],
+ dir: { input: "src", includes: "_includes", data: "_data", output: "_site" },
+ };
+}
diff --git a/hack/check-docs.mjs b/hack/check-docs.mjs
new file mode 100644
index 0000000..ef3ae9d
--- /dev/null
+++ b/hack/check-docs.mjs
@@ -0,0 +1,175 @@
+// 🤖 Fails the build when the built docs site is internally broken.
+//
+// The platform repo gates the corpus as markdown: nav and files agree,
+// links resolve, anchors exist, symbols are covered. This checks what
+// that cannot — that the rendering actually produced the pages and ids
+// those links point at. A link can be correct in the source and still
+// 404 in a browser if the renderer disagrees with the gate about how a
+// heading becomes an anchor, which is exactly the failure worth
+// catching here.
+import { readdirSync, readFileSync, statSync } from "node:fs";
+import { join, relative } from "node:path";
+
+const site = "_site";
+const docs = join(site, "docs");
+const nav = JSON.parse(readFileSync("src/_data/docsnav.json", "utf8"));
+
+const problems = [];
+const fail = (m) => problems.push(m);
+
+function walk(dir, match) {
+ const out = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const p = join(dir, entry.name);
+ if (entry.isDirectory()) out.push(...walk(p, match));
+ else if (match(entry.name)) out.push(p);
+ }
+ return out;
+}
+
+const exists = (p) => {
+ try {
+ statSync(p);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+// 1. The site as a whole was actually built. Everything below this check
+// is scoped to _site/docs — deliberately, that's the docs-specific
+// gate this file owns — but nothing else in the pipeline verifies
+// that the hand-written pages and shared assets were emitted at all.
+// Ported from the website's check-docs.mjs, which paid for this the
+// hard way: a passthrough file list going stale there dropped a live
+// page and shipped a 404ing stylesheet, both silently, because
+// npm run check only ever looked under _site/docs and exited 0
+// either time. A build missing its homepage should fail before
+// anyone reads about anchors, so this runs first.
+// When one of these goes missing, it is almost always because an
+// addPassthroughCopy line in eleventy.config.js was removed or
+// renamed — check there first.
+const requiredSitePaths = [
+ "index.html",
+ "get-started/index.html",
+ "site.css",
+ "site.js",
+ "favicon.svg",
+ "docs.css",
+ "fonts/Geist-Variable.woff2",
+ "fonts/GeistMono-Variable.woff2",
+ "img/dashboard-light.png",
+ "img/dashboard-dark.png",
+];
+for (const rel of requiredSitePaths) {
+ if (!exists(join(site, rel))) {
+ fail(`_site/${rel} was not built — check the addPassthroughCopy lines in eleventy.config.js`);
+ }
+}
+
+// 2. Every nav entry has a built page and a built .md twin.
+//
+// docsnav.json is hand-maintained here — there is no generator to blame
+// or re-run, so every failure below is an edit someone made. The floor
+// after the loop is the point of the whole gate: a loop over an empty nav
+// asserts nothing, so a nav trimmed down to {"sections": []} would print
+// "docs ok — 0 pages" and exit 0 on a corpus that had vanished. Same shape
+// as the pages.length guard below.
+const slugs = [];
+const sections = Array.isArray(nav.sections) ? nav.sections : [];
+if (!Array.isArray(nav.sections)) fail("docsnav.json has no sections array at all");
+for (const section of sections) {
+ if (!Array.isArray(section.pages)) {
+ fail(`docsnav.json section ${JSON.stringify(section.title ?? "?")} has no "pages" key — every section in src/_data/docsnav.json needs one, listing that section's pages`);
+ continue;
+ }
+ for (const entry of section.pages) {
+ slugs.push(entry.slug);
+ if (!exists(join(docs, entry.slug, "index.html"))) fail(`no built page for /docs/${entry.slug}/`);
+ if (!exists(join(docs, `${entry.slug}.md`))) fail(`no markdown twin for /docs/${entry.slug}.md`);
+ if (!entry.blurb) fail(`nav entry ${entry.slug} has no blurb`);
+ if (!entry.label) fail(`nav entry ${entry.slug} has no label`);
+ }
+}
+if (slugs.length === 0) {
+ fail("src/_data/docsnav.json lists no pages — every page in src/docs/ needs an entry there; nothing in check 2 can fail against an empty nav");
+}
+if (!exists(join(docs, "index.html"))) fail("no built /docs/ index");
+
+// 3. Every internal href resolves — to a built page, and to a real id
+// when it carries a fragment.
+const pages = walk(docs, (n) => n.endsWith(".html"));
+if (pages.length === 0) fail("no built docs pages at all");
+
+const idsFor = new Map();
+for (const p of pages) {
+ const html = readFileSync(p, "utf8");
+ idsFor.set(p, new Set([...html.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1])));
+}
+
+// Docs links are "/docs", "/docs/", "/docs/<slug>/" or "/docs/<slug>.md".
+// Anchored to a slash (or end) on purpose: this site links a stylesheet
+// as /docs.css, and a bare /docs prefix match claimed that as a page and
+// failed the build on it.
+const docsHref = /href="(\/docs(?:\/[^"]*)?)"/g;
+
+const pageFor = (href) => {
+ const clean = href.replace(/[?#].*$/, "");
+ if (clean !== "/docs" && !clean.startsWith("/docs/")) return null;
+ const rel = clean.replace(/^\/docs\/?/, "").replace(/\/$/, "");
+ if (rel === "") return join(docs, "index.html");
+ if (rel.endsWith(".md")) return join(docs, rel);
+ return join(docs, rel, "index.html");
+};
+
+for (const p of pages) {
+ const html = readFileSync(p, "utf8");
+ const where = relative(site, p);
+ for (const m of html.matchAll(docsHref)) {
+ const href = m[1];
+ const target = pageFor(href);
+ if (!target || !exists(target)) {
+ fail(`${where}: href="${href}" does not resolve to a built file`);
+ continue;
+ }
+ const hash = href.includes("#") ? href.split("#")[1] : "";
+ if (hash && !(idsFor.get(target) ?? new Set()).has(hash)) {
+ fail(`${where}: href="${href}" — ${relative(site, target)} has no id="${hash}"`);
+ }
+ }
+ // Same-page fragments.
+ const ids = idsFor.get(p);
+ for (const m of html.matchAll(/href="#([^"]+)"/g)) {
+ if (!ids.has(m[1])) fail(`${where}: href="#${m[1]}" has no matching id`);
+ }
+}
+
+// 4. The anchor rule agrees on both sides. The Go gate accepted these
+// fragments; if the renderer slugifies differently they are dead
+// links no earlier check can see. The fixture is the platform repo's
+// docs/site/anchors.json, vendored — both languages read this file
+// rather than each other's implementation.
+//
+// The "//" key is not a case: it is generated-by metadata some fixture
+// writers prepend to every JSON file they emit. Skip it by name rather
+// than by loosening the comparison — the strictness IS this file's
+// purpose.
+const { slugify } = await import("../eleventy.config.js");
+const fixture = JSON.parse(readFileSync("src/_data/docsanchors.json", "utf8"));
+const cases = Object.entries(fixture).filter(([input]) => input !== "//");
+if (cases.length < 5) {
+ fail("src/_data/docsanchors.json has too few cases to pin anything — it has been emptied or truncated; restore it from the platform repo's docs/site/anchors.json");
+}
+for (const [input, want] of cases) {
+ const got = slugify(input);
+ if (got !== want) {
+ fail(`slugify(${JSON.stringify(input)}) = ${JSON.stringify(got)}, want ${JSON.stringify(want)} — the renderer and internal/docsite.Anchor disagree`);
+ }
+}
+
+if (problems.length) {
+ console.error(`docs check failed (${problems.length}):`);
+ for (const p of problems) console.error(` ${p}`);
+ process.exit(1);
+}
+console.log(`docs ok — ${slugs.length} pages, ${pages.length} built, ${cases.length} anchor cases agree`);
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..490bb04
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1725 @@
+{
+ "name": "carloku",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "carloku",
+ "devDependencies": {
+ "@11ty/eleventy": "^3.0.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^9.2.0"
+ }
+ },
+ "node_modules/@11ty/dependency-tree": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz",
+ "integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@11ty/eleventy-utils": "^2.0.1"
+ }
+ },
+ "node_modules/@11ty/dependency-tree-esm": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz",
+ "integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@11ty/eleventy-utils": "^2.0.7",
+ "acorn": "^8.15.0",
+ "dependency-graph": "^1.0.0",
+ "normalize-path": "^3.0.0"
+ }
+ },
+ "node_modules/@11ty/eleventy": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.6.tgz",
+ "integrity": "sha512-ZlSiR1PLdS2lv7TelBgWAhcvMiLNZkPBlLEb+lh7kGYZ+Mk0bo9qcYgVsewvw9W7Em0RH3wd01h5fAstNDh0zA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@11ty/dependency-tree": "^4.0.2",
+ "@11ty/dependency-tree-esm": "^2.0.4",
+ "@11ty/eleventy-dev-server": "^2.0.8",
+ "@11ty/eleventy-plugin-bundle": "^3.0.7",
+ "@11ty/eleventy-utils": "^2.0.7",
+ "@11ty/lodash-custom": "^4.17.21",
+ "@11ty/posthtml-urls": "^1.0.3",
+ "@11ty/recursive-copy": "^4.0.4",
+ "@sindresorhus/slugify": "^2.2.1",
+ "bcp-47-normalize": "^2.3.0",
+ "chokidar": "^3.6.0",
+ "debug": "^4.4.3",
+ "dependency-graph": "^1.0.0",
+ "entities": "^6.0.1",
+ "filesize": "^10.1.6",
+ "gray-matter": "^4.0.3",
+ "iso-639-1": "^3.1.5",
+ "js-yaml": "^4.1.1",
+ "kleur": "^4.1.5",
+ "liquidjs": "^10.27.0",
+ "luxon": "^3.7.2",
+ "markdown-it": "^14.2.0",
+ "minimist": "^1.2.8",
+ "moo": "0.5.2",
+ "node-retrieve-globals": "^6.0.1",
+ "nunjucks": "^3.2.4",
+ "picomatch": "^4.0.4",
+ "please-upgrade-node": "^3.2.0",
+ "posthtml": "^0.16.7",
+ "posthtml-match-helper": "^2.0.3",
+ "semver": "^7.8.1",
+ "slugify": "^1.6.9",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "eleventy": "cmd.cjs"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/eleventy-dev-server": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz",
+ "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@11ty/eleventy-utils": "^2.0.1",
+ "chokidar": "^3.6.0",
+ "debug": "^4.4.0",
+ "finalhandler": "^1.3.1",
+ "mime": "^3.0.0",
+ "minimist": "^1.2.8",
+ "morphdom": "^2.7.4",
+ "please-upgrade-node": "^3.2.0",
+ "send": "^1.1.0",
+ "ssri": "^11.0.0",
+ "urlpattern-polyfill": "^10.0.0",
+ "ws": "^8.18.1"
+ },
+ "bin": {
+ "eleventy-dev-server": "cmd.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/eleventy-plugin-bundle": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz",
+ "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@11ty/eleventy-utils": "^2.0.2",
+ "debug": "^4.4.0",
+ "posthtml-match-helper": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/eleventy-utils": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz",
+ "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/lodash-custom": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz",
+ "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/posthtml-urls": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz",
+ "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "evaluate-value": "^2.0.0",
+ "http-equiv-refresh": "^2.0.1",
+ "list-to-array": "^1.1.0",
+ "parse-srcset": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@11ty/recursive-copy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.4.tgz",
+ "integrity": "sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "errno": "^1.0.0",
+ "junk": "^3.1.0",
+ "minimatch": "^3.1.5",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sindresorhus/slugify": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
+ "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/transliterate": "^1.0.0",
+ "escape-string-regexp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sindresorhus/transliterate": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz",
+ "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/a-sync-waterfall": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
+ "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bcp-47": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz",
+ "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47-match": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
+ "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47-normalize": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz",
+ "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bcp-47": "^2.0.0",
+ "bcp-47-match": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dependency-graph": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
+ "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/dom-serializer/node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/errno": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz",
+ "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esm-import-transformer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz",
+ "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.15.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/evaluate-value": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz",
+ "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/filesize": {
+ "version": "10.1.6",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
+ "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 10.4.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.15.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
+ "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz",
+ "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.2",
+ "domutils": "^2.8.0",
+ "entities": "^3.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
+ "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/http-equiv-refresh": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz",
+ "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-json": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz",
+ "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/iso-639-1": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.6.tgz",
+ "integrity": "sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/junk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz",
+ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
+ "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/liquidjs": {
+ "version": "10.29.0",
+ "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.29.0.tgz",
+ "integrity": "sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^10.0.0"
+ },
+ "bin": {
+ "liquid": "bin/liquid.js",
+ "liquidjs": "bin/liquid.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/liquidjs"
+ }
+ },
+ "node_modules/list-to-array": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz",
+ "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/luxon": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
+ "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
+ "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.5.0",
+ "linkify-it": "^5.0.2",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it-anchor": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz",
+ "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==",
+ "dev": true,
+ "license": "Unlicense",
+ "peerDependencies": {
+ "@types/markdown-it": "*",
+ "markdown-it": "*"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/moo": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
+ "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/morphdom": {
+ "version": "2.7.8",
+ "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz",
+ "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-retrieve-globals": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz",
+ "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.14.1",
+ "acorn-walk": "^8.3.4",
+ "esm-import-transformer": "^3.0.3"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nunjucks": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
+ "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "a-sync-waterfall": "^1.0.0",
+ "asap": "^2.0.3",
+ "commander": "^5.1.0"
+ },
+ "bin": {
+ "nunjucks-precompile": "bin/precompile"
+ },
+ "engines": {
+ "node": ">= 6.9.0"
+ },
+ "peerDependencies": {
+ "chokidar": "^3.3.0"
+ },
+ "peerDependenciesMeta": {
+ "chokidar": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nunjucks/node_modules/commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "node_modules/posthtml": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
+ "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "posthtml-parser": "^0.11.0",
+ "posthtml-render": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/posthtml-match-helper": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz",
+ "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "posthtml": "^0.16.6"
+ }
+ },
+ "node_modules/posthtml-parser": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz",
+ "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "htmlparser2": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/posthtml-render": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz",
+ "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-json": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slugify": {
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
+ "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ssri": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz",
+ "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/urlpattern-polyfill": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz",
+ "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..059597f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "carloku",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "eleventy",
+ "serve": "eleventy --serve",
+ "check": "eleventy && node hack/check-docs.mjs"
+ },
+ "devDependencies": {
+ "@11ty/eleventy": "^3.0.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^9.2.0"
+ }
+}
diff --git a/src/_data/docsanchors.json b/src/_data/docsanchors.json
new file mode 100644
index 0000000..d2f9211
--- /dev/null
+++ b/src/_data/docsanchors.json
@@ -0,0 +1,15 @@
+{
+ "Getting started": "getting-started",
+ "The `carlos ship` contract": "the-carlos-ship-contract",
+ "404, never 403": "404-never-403",
+ "Routes & instances": "routes-instances",
+ " Leading and trailing ": "leading-and-trailing",
+ "Hyphen-joined words": "hyphen-joined-words",
+ "`-app` and `--app`": "app-and-app",
+ "canary/rehearsal": "canary-rehearsal",
+ "X-Carlos-Version": "x-carlos-version",
+ "-app": "app",
+ "--app": "app",
+ "Ship — then promote": "ship-then-promote",
+ "Montréal edge": "montr-al-edge"
+}
diff --git a/src/_data/docsnav.json b/src/_data/docsnav.json
new file mode 100644
index 0000000..bb07b74
--- /dev/null
+++ b/src/_data/docsnav.json
@@ -0,0 +1,24 @@
+{
+ "sections": [
+ {
+ "title": "Running your app",
+ "pages": [
+ { "slug": "deploying", "label": "Deploying", "blurb": "Ship a version, put it in front of people, and know it landed." },
+ { "slug": "channels", "label": "Channels", "blurb": "How a release reaches production, and how to put it back." },
+ { "slug": "domains", "label": "Custom domains", "blurb": "Your own hostname on your app, and the DNS record it needs." },
+ { "slug": "config", "label": "Env and secrets", "blurb": "Two stores for configuration, and which one a value belongs in." },
+ { "slug": "storage", "label": "Object storage", "blurb": "A bucket per app: creating one, granting access, rotating keys." },
+ { "slug": "logs", "label": "Logs", "blurb": "One timeline of your app's output and the platform's events about it." },
+ { "slug": "sleeping", "label": "Restarting and sleeping", "blurb": "Cycling an app, and what hibernation means for your visitors." }
+ ]
+ },
+ {
+ "title": "Your account",
+ "pages": [
+ { "slug": "console", "label": "The console", "blurb": "What each page shows, and what you can do from it." },
+ { "slug": "members", "label": "Members and service credentials", "blurb": "Who can act on your apps, and credentials a server holds." },
+ { "slug": "limits", "label": "Limits and getting help", "blurb": "The rate limits you can hit, and where to ask when stuck." }
+ ]
+ }
+ ]
+}
diff --git a/src/_includes/docs.njk b/src/_includes/docs.njk
new file mode 100644
index 0000000..4e44386
--- /dev/null
+++ b/src/_includes/docs.njk
@@ -0,0 +1,85 @@
+{#
+ 🤖 The documentation shell: sidebar, section sub-nav, article column.
+
+ One layout serves the index and every page, because they share the
+ whole chrome. Zero client-side JavaScript beyond site.js's existing
+ enhancements (both no-ops here, guarded by querySelector) — there is
+ no search box, and findability is the nav, the card index, and the
+ per-page section list.
+
+ The sidebar precedes the article in document order, which on a narrow
+ viewport is what puts the nav above the content without a toggle.
+
+ The sidebar is a <nav>, not an <aside>: with no search, it IS this
+ site's whole navigation model, not supplementary content. The article
+ is wrapped in <main> so the skip link (first focusable element in
+ <body>) has a landmark to target — with dozens of sidebar links ahead
+ of it on every page, a keyboard user needs a way past them.
+#}
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{ title }} &mdash; Carloku docs</title>
+<meta name="description" content="{{ description }}">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
+<meta name="theme-color" content="#0e0c13" media="(prefers-color-scheme: dark)">
+<meta property="og:title" content="{{ title }} &mdash; Carloku docs">
+<meta property="og:description" content="{{ description }}">
+<meta property="og:type" content="article">
+<meta property="og:url" content="https://carloku.com/docs/{% if not isIndex %}{{ docsSlug }}/{% endif %}">
+<link rel="preload" href="/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="stylesheet" href="/site.css">
+<link rel="stylesheet" href="/docs.css">
+</head>
+<body class="docs-body">
+<a class="skip" href="#main">Skip to content</a>
+<p class="brandline"><a href="/"><b>Carloku</b></a> <span aria-hidden="true">/</span> <span class="here">docs</span><span class="folio" aria-hidden="true">carloku.com/docs</span></p>
+
+<div class="docs-shell">
+ <nav class="docs-nav" aria-label="Documentation">
+ <p class="docs-nav-title"><a href="/docs/"{% if isIndex %} class="active" aria-current="page"{% endif %}>Documentation</a></p>
+ {% for section in docsnav.sections %}
+ <p class="docs-nav-section">{{ section.title }}</p>
+ <ul>
+ {% for entry in section.pages %}
+ <li>
+ <a href="/docs/{{ entry.slug }}/"{% if entry.slug == docsSlug %} class="active" aria-current="page"{% endif %}>{{ entry.label }}</a>
+ {% if entry.slug == docsSlug %}
+ {% set sections = collections.docs | docsRawFor(docsSlug) | docsSections %}
+ {% if sections.length %}
+ <ul class="docs-subnav">
+ {% for s in sections %}<li><a href="#{{ s.anchor }}">{{ s.title }}</a></li>{% endfor %}
+ </ul>
+ {% endif %}
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ {% endfor %}
+ </nav>
+
+ <main id="main">
+ <article class="docs-article">
+ {{ content | safe }}
+
+ {% if not isIndex %}
+ <p class="docs-foot">
+ <a href="/docs/{{ docsSlug }}.md">Read this page as markdown</a> &mdash;
+ exact, unstyled, and cheap for an agent to fetch.
+ </p>
+ {% endif %}
+ </article>
+ </main>
+</div>
+
+<footer class="docs-sitefoot">
+ <p>🤖 These pages were written by an LLM, on the ideas, instruction, and editing of humans. AI-written text here is always marked, visibly &mdash; that's <a href="https://11factor.org/#x">factor X</a>.</p>
+ <p class="docs-provenance">These pages are written and maintained in the <a href="https://github.com/carlosframework/carloku">carloku</a> repo, alongside the rest of this site.</p>
+</footer>
+<script src="/site.js" defer></script>
+</body>
+</html>
diff --git a/src/docs-index.njk b/src/docs-index.njk
new file mode 100644
index 0000000..fc17a38
--- /dev/null
+++ b/src/docs-index.njk
@@ -0,0 +1,20 @@
+---
+layout: docs.njk
+title: Documentation
+description: "Carloku documentation: how to ship, promote, and run apps on the hosted CARLOS platform."
+isIndex: true
+permalink: /docs/index.html
+---
+{{ collections.docsIndex[0].templateContent | safe }}
+
+{% for section in docsnav.sections %}
+<h2 id="{{ section.title | docsSlugify }}">{{ section.title }}</h2>
+<ul class="docs-cards">
+ {% for entry in section.pages %}
+ <li>
+ <a href="/docs/{{ entry.slug }}/"><strong>{{ entry.label }}</strong></a>
+ <span>{{ entry.blurb }}</span>
+ </li>
+ {% endfor %}
+</ul>
+{% endfor %}
diff --git a/src/docs.css b/src/docs.css
new file mode 100644
index 0000000..2e5feed
--- /dev/null
+++ b/src/docs.css
@@ -0,0 +1,450 @@
+/* 🤖 carloku.com/docs — the documentation skin.
+
+ Loaded after site.css, never instead of it. Every colour, measure,
+ type stack, focus ring, scrollbar and selection colour here comes
+ from that file's tokens, so both schemes come along for free: the
+ whole palette swaps under `prefers-color-scheme: dark`, which is the
+ one theme mechanism this site has. There is no [data-theme] switch to
+ match, and inventing a second, half-supported one for these pages
+ only would put the docs out of step with / and /get-started.
+
+ What this file adds is the shape site.css has no page for — a
+ sidebar, a reference column, and fenced code. It stays inside the
+ house rules in DESIGN.md while it does it:
+
+ - hairlines divide, boxes do not enclose (no bordered cards, and
+ no border heavier than 1px anywhere below);
+ - one purple carries links, focus and the you-are-here mark, over
+ an accent-soft wash — the same wash the code chips and the quiet
+ button's hover already use;
+ - a fence wears the terminal's clothes and stays dark in both
+ schemes, because anything terminal-shaped does (--term-*);
+ - mono carries commands and addresses, never prose;
+ - weights come off the variable axis at the tuned steps
+ (480/560/620/640/650/680), and tracking tightens as size grows.
+
+ Same build rules as the rest of the site: nothing fetched, no
+ @import, no JavaScript. Motion is colour-only and short, so the
+ global prefers-reduced-motion kill switch in site.css covers it. */
+
+/* ---------------------------------------------------------------- tokens -- */
+
+:root {
+ /* The two measures site.css lacks, both computed from the real Geist
+ metrics rather than borrowed.
+
+ Body copy sets at 1rem (site.css `body`). Geist's prose advance —
+ its letters weighted by English frequency, plus a space every 5.7
+ characters — is 0.467em, so a 65-75 character line wants 30.4rem
+ to 35.0rem. --docs-measure sits near the top of that band at
+ roughly 73 characters, and is also site.css's existing lede width,
+ so the docs read at the same measure the rest of the site does.
+
+ The column is wider than the measure because a reference page also
+ carries command lines. The longest in the corpus is 66 characters;
+ in Geist Mono (a true 0.6em advance) at the 0.88rem fences use,
+ that is 34.9rem, and 42rem of column leaves it 4.6rem of room
+ inside the fence's padding. So the ordinary command sits in its box
+ and only a genuinely long one has to scroll. 42rem is also what
+ site.css already caps /get-started's prose at. */
+ --docs-measure: 34rem;
+ --docs-column: 42rem;
+}
+
+/* ------------------------------------------------------------ page frame -- */
+
+/* .docs-body and .language-sh carry nothing of their own, deliberately.
+ site.css's `body` rule already sets the ground, the type and the
+ voice; and the site ships no highlighter, so a fence's language class
+ has nothing to say. Inventing a declaration so the hooks appear in
+ this file would be worse than leaving them alone. */
+
+/* Where you are, in words, above a hairline. The drawn wordmark and the
+ sticky translucent header belong to the pages that are a destination;
+ a reference page gets a line of text and gets out of the way. */
+.brandline {
+ display: flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ max-width: var(--wrap);
+ margin-inline: auto;
+ padding: 1.3rem 1.5rem 0.85rem;
+ border-bottom: 1px solid var(--line);
+ font-size: 0.9rem;
+ color: var(--muted);
+}
+.brandline a { color: var(--muted); text-decoration: none; transition: color 120ms ease-out; }
+.brandline a:hover { color: var(--accent); }
+.brandline b { font-weight: 640; letter-spacing: -0.01em; color: var(--ink); }
+.brandline a:hover b { color: inherit; }
+.brandline .here { color: var(--ink); font-weight: 560; }
+
+/* The folio is an address, so it sets as data — mono, the way every
+ other identifier on this site does — and it is the first thing a
+ narrow viewport gives up. */
+.folio {
+ margin-left: auto;
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ letter-spacing: 0.01em;
+ color: var(--faint);
+}
+@media (max-width: 40rem) { .folio { display: none; } }
+
+/* The skip link is site.css's .skip, whole: parked at top: -3rem and
+ slid into view on :focus-visible. It is the site's own idiom and it
+ already does what these pages need — with 40+ sidebar links ahead of
+ the article and no search to route around them, a keyboard user needs
+ the way past. Restating it here would only let the two drift. */
+
+/* ----------------------------------------------------------------- shell -- */
+
+.docs-shell {
+ max-width: var(--wrap);
+ margin-inline: auto;
+ padding: clamp(1.75rem, 4vw, 2.75rem) 1.5rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 2.25rem;
+ align-items: start;
+}
+
+/* Both children are grid items, so both get min-width: 0. Without it a
+ grid child's min-width: auto refuses to shrink below its widest
+ unbreakable content, and a long command line would scroll the page
+ sideways instead of scrolling inside its own fence. */
+.docs-shell > * { min-width: 0; }
+
+/* Two columns once there is room for both. Below this the nav keeps its
+ document order — it is written before the article — so it simply
+ stacks above the content, with no toggle and no script. */
+@media (min-width: 60rem) {
+ .docs-shell {
+ grid-template-columns: 15rem minmax(0, 1fr);
+ gap: clamp(2.5rem, 5vw, 4rem);
+ }
+ .docs-nav {
+ position: sticky;
+ top: 1.75rem;
+ max-height: calc(100vh - 3.5rem);
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ }
+}
+
+/* ------------------------------------------------------------------- nav -- */
+
+/* Every link is padded rather than pulled out with a negative margin:
+ the sidebar becomes a scroll container at the two-column breakpoint,
+ and an overhang inside `overflow-y: auto` would give the nav its own
+ horizontal scrollbar. The labels above are padded to match, so the
+ column still aligns with itself. */
+.docs-nav { font-size: 0.94rem; }
+.docs-nav ul { list-style: none; padding: 0; }
+.docs-nav li { margin-top: 0.1rem; }
+
+.docs-nav a {
+ display: block;
+ padding: 0.26rem 0.55rem;
+ border-radius: 0.4rem;
+ color: var(--muted);
+ font-weight: 480;
+ text-decoration: none;
+ transition: color 120ms ease-out, background 120ms ease-out;
+}
+.docs-nav a:hover { color: var(--ink); }
+
+/* The page you are on, marked with the accent over the accent-soft wash
+ — the same pair the code chips, the step numerals and the quiet
+ button's hover already use, so the mark is house vocabulary rather
+ than a new device. Keyed to both the class the template stamps and
+ the aria-current it stamps beside it, so the mark cannot outlive
+ either one. */
+.docs-nav a.active,
+.docs-nav a[aria-current="page"] {
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-weight: 560;
+}
+
+.docs-nav-title {
+ margin-bottom: 1.15rem;
+ padding-inline: 0.55rem;
+ font-size: 1.05rem;
+ font-weight: 640;
+ letter-spacing: -0.01em;
+}
+.docs-nav-title a { color: var(--ink); font-weight: inherit; }
+.docs-nav-title a:hover { color: var(--accent); }
+
+/* A section label, not a heading and not data: it stays in the voice
+ face at a quiet weight above a hairline. This site never sets tracked
+ small caps, and importing them here to say "documentation" would be
+ borrowing a convention rather than using ours. */
+.docs-nav-section {
+ margin-top: 1.6rem;
+ padding-top: 0.9rem;
+ margin-bottom: 0.5rem;
+ padding-inline: 0.55rem;
+ border-top: 1px solid var(--line);
+ font-size: 0.82rem;
+ font-weight: 560;
+ color: var(--faint);
+}
+.docs-nav-title + .docs-nav-section { margin-top: 0; padding-top: 0; border-top: 0; }
+
+/* The open page's own sections, indented under it against a hairline. */
+.docs-subnav {
+ margin: 0.35rem 0 0.7rem 0.85rem;
+ padding-left: 0.5rem;
+ border-left: 1px solid var(--line);
+ font-size: 0.875rem;
+}
+.docs-subnav li { margin-top: 0; }
+.docs-subnav a {
+ padding: 0.16rem 0.45rem;
+ font-weight: 400;
+ color: var(--faint);
+}
+.docs-subnav a:hover { color: var(--accent); }
+
+/* --------------------------------------------------------------- article -- */
+
+.docs-article { max-width: var(--docs-column); min-width: 0; }
+
+.docs-article h1 {
+ font-size: clamp(2rem, 4.4vw, 2.7rem);
+ line-height: 1.06;
+ letter-spacing: -0.032em;
+ font-weight: 680;
+ text-wrap: balance;
+ /* site.css's element-level h1/h2 rules carry a margin-bottom for the
+ landing page's rhythm. Here the following element owns the gap, so
+ the heading gives it up rather than adding to it. */
+ margin-bottom: 0;
+}
+.docs-article h1 + p {
+ margin-top: 1.15rem;
+ font-size: 1.12rem;
+ line-height: 1.65;
+ max-width: var(--docs-measure);
+}
+
+/* A group opens on a full hairline — the same divider /get-started uses
+ between its sections. The reference reads as a run of divided
+ entries, never as a stack of boxes. */
+.docs-article h2 {
+ margin-top: clamp(2.75rem, 5.5vw, 4rem);
+ padding-top: clamp(1.6rem, 3.2vw, 2.25rem);
+ border-top: 1px solid var(--line);
+ font-size: clamp(1.35rem, 2.4vw, 1.7rem);
+ line-height: 1.2;
+ letter-spacing: -0.025em;
+ font-weight: 650;
+ text-wrap: balance;
+ margin-bottom: 0;
+ /* No measure cap: the hairline is the divider, so it runs the width of
+ the column rather than stopping short of the fences beneath it. The
+ longest heading in the corpus is 37 characters — 29rem at this size
+ — so nothing wraps for want of the cap. */
+}
+
+/* h3/h4 and the block elements further down are the floor for markdown
+ this corpus does not use yet — it is authored in this repo and grows
+ by hand. Kept deliberately short: enough that a new page cannot land
+ unstyled, not so much that it becomes a theme nobody asked for. */
+.docs-article h3 {
+ margin-top: 2.2rem;
+ font-size: 1.08rem;
+ font-weight: 620;
+ letter-spacing: -0.01em;
+ color: var(--ink);
+}
+.docs-article h4 {
+ margin-top: 1.8rem;
+ font-size: 0.98rem;
+ font-weight: 620;
+ letter-spacing: -0.01em;
+ color: var(--ink);
+}
+
+.docs-article :is(h1, h2, h3, h4) { scroll-margin-top: 1.5rem; }
+
+.docs-article p {
+ margin-top: 1.05rem;
+ max-width: var(--docs-measure);
+ color: var(--muted);
+ text-wrap: pretty;
+}
+.docs-article :is(h2, h3, h4) + p { margin-top: 0.85rem; }
+.docs-article strong { color: var(--ink); font-weight: 620; }
+.docs-article em { color: var(--ink); }
+
+.docs-article ul:not(.docs-cards),
+.docs-article ol {
+ margin-top: 1rem;
+ padding-left: 1.2rem;
+ max-width: var(--docs-measure);
+ color: var(--muted);
+}
+/* Scoped with a child combinator rather than `.docs-article li + li`,
+ which would outrank `.docs-cards li` below and break the index
+ grid's row alignment. */
+.docs-article ul:not(.docs-cards) > li + li,
+.docs-article ol > li + li { margin-top: 0.45rem; }
+.docs-article blockquote {
+ margin-top: 1.15rem;
+ padding-left: 1.1rem;
+ border-left: 1px solid var(--line-strong);
+ max-width: var(--docs-measure);
+ color: var(--faint);
+ font-size: 0.95rem;
+}
+.docs-article hr {
+ margin-block: 2.25rem;
+ border: 0;
+ border-top: 1px solid var(--line);
+}
+.docs-article table {
+ margin-top: 1.25rem;
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.95rem;
+}
+.docs-article :is(th, td) {
+ text-align: left;
+ padding: 0.6rem 0.9rem 0.6rem 0;
+ border-top: 1px solid var(--line);
+ color: var(--muted);
+ vertical-align: baseline;
+}
+.docs-article th { color: var(--ink); font-weight: 620; }
+
+.docs-article a {
+ color: var(--accent);
+ text-decoration: none;
+}
+.docs-article a:hover { text-decoration: underline; text-underline-offset: 0.2em; }
+
+/* ------------------------------------------------------------------ code -- */
+
+/* A fence is a box that scrolls itself: the page must never scroll
+ sideways because a command was long. It wears the terminal's clothes
+ and keeps them in both schemes, under the rule that anything
+ terminal-shaped sits on Terminal Black — the same clothes
+ /get-started's blocks already wear, and the reason a Carloku page
+ reads as a Carloku page. */
+.docs-article pre {
+ margin: 1.15rem 0 0;
+ max-width: 100%;
+ min-width: 0;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ -webkit-overflow-scrolling: touch;
+ padding: 1rem 1.2rem;
+ border: 1px solid var(--term-line);
+ border-radius: 0.7rem;
+ background: var(--term-bg);
+ color: var(--term-ink);
+ font-family: var(--mono);
+ font-size: 0.88rem;
+ line-height: 1.7;
+}
+.docs-article p + pre { margin-top: 0.9rem; }
+
+/* Inline code is a chip: mono on an accent-soft wash, the way the
+ commands appear in prose everywhere else on this site. Inside a fence
+ that chip would tile the whole block, so it is undone there. */
+.docs-article code {
+ font-family: var(--mono);
+ font-size: 0.88em;
+ background: var(--accent-soft);
+ color: var(--accent-ink);
+ padding: 0.12em 0.4em;
+ border-radius: 0.3em;
+}
+.docs-article pre code {
+ display: block;
+ padding: 0;
+ border-radius: 0;
+ background: none;
+ color: inherit;
+ font-size: inherit;
+ white-space: pre;
+}
+.docs-article a code { color: inherit; }
+
+/* ------------------------------------------------------------- the index -- */
+
+/* The index's entries: a label and a line saying what is inside. They
+ are not cards and they are not boxed — this site separates siblings
+ with a single hairline and whitespace, the way the feature list and
+ the guide's sections do. Two ruled columns of contents, nothing to
+ lift off the page. */
+.docs-cards {
+ margin-top: 1.25rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ column-gap: 2.5rem;
+ list-style: none;
+ padding: 0;
+}
+@media (min-width: 46rem) {
+ .docs-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+.docs-cards li {
+ min-width: 0;
+ margin-top: 0;
+ padding: 1.05rem 0 1.15rem;
+ border-top: 1px solid var(--line);
+}
+.docs-cards a { display: inline-block; text-decoration: none; }
+.docs-cards a strong {
+ font-size: 1.05rem;
+ font-weight: 620;
+ letter-spacing: -0.01em;
+ color: var(--ink);
+ transition: color 120ms ease-out;
+}
+.docs-cards a:hover strong { color: var(--accent); }
+.docs-cards a:hover { text-decoration: none; }
+.docs-cards span {
+ display: block;
+ margin-top: 0.25rem;
+ font-size: 0.95rem;
+ color: var(--muted);
+ text-wrap: pretty;
+}
+
+/* The index's h2s head a grid rather than prose, so they need less air
+ under them and no measure cap. */
+.docs-article h2 + .docs-cards { margin-top: 1.4rem; }
+
+/* ------------------------------------------------------------------ feet -- */
+
+/* Qualified so it outranks `.docs-article p` — this is a <p> inside the
+ article, and the unqualified class would lose the colour and the top
+ margin to it. */
+.docs-article .docs-foot {
+ margin-top: clamp(2.75rem, 5.5vw, 4rem);
+ padding-top: 1.15rem;
+ border-top: 1px solid var(--line);
+ max-width: var(--docs-measure);
+ font-size: 0.9rem;
+ color: var(--faint);
+}
+.docs-article .docs-foot a { color: var(--faint); }
+.docs-article .docs-foot a:hover { color: var(--accent); }
+
+/* site.css's <footer> already gives the rule, the muted voice and the
+ block padding; these pages have no .wrap element inside it, so the
+ measure and the gutters are what is left to supply. */
+.docs-sitefoot {
+ max-width: var(--wrap);
+ margin-inline: auto;
+ padding-inline: 1.5rem;
+}
+.docs-provenance { font-size: 0.85rem; color: var(--faint); }
+.docs-provenance a { color: var(--faint); }
+.docs-provenance a:hover { color: var(--accent); }
diff --git a/src/docs.njk b/src/docs.njk
new file mode 100644
index 0000000..d909dbe
--- /dev/null
+++ b/src/docs.njk
@@ -0,0 +1,13 @@
+---
+pagination:
+ data: collections.docs
+ size: 1
+ alias: doc
+layout: docs.njk
+eleventyComputed:
+ title: "{{ doc.page.rawInput | docsTitle }}"
+ description: "{{ docsnav | docsBlurb(doc.slug) }}"
+ docsSlug: "{{ doc.slug }}"
+permalink: "/docs/{{ doc.slug }}/index.html"
+---
+{{ doc.page.templateContent | safe }}
diff --git a/src/docs/channels.md b/src/docs/channels.md
new file mode 100644
index 0000000..39d535d
--- /dev/null
+++ b/src/docs/channels.md
@@ -0,0 +1,101 @@
+# Channels
+
+A channel is a pointer with a name. It says which shipped version an app
+is serving, and promoting is the act of moving it. Nothing is rebuilt
+between one channel and the next — the bytes that were tested are the
+bytes that go live, which is the whole reason the platform separates
+shipping from promoting.
+
+## You start with one
+
+Every app is created with a single channel called `edge`, and
+`carlos deploy` lands there. That is enough for most apps: you deploy,
+and it is live. To see where things stand:
+
+```sh
+carlos channels -app hello
+```
+
+Nothing is ever promoted *onto* the entry channel. Deploys land there, and
+promotion moves a version from it to whatever comes next — so with one
+channel there is no promotion step at all.
+
+## Adding a stage in front of production
+
+When you want a version to sit somewhere before a wider audience sees it,
+declare a pipeline. The starter template adds a `production` channel
+behind `edge`:
+
+```sh
+carlos pipeline init -app hello -template edge-production
+```
+
+That gives `production` a 24-hour bake window, a passkey requirement on
+promotion, and an approval on any change to those rules — so the
+protection cannot be quietly removed later either. The other template,
+`full-ladder`, sets up `edge → beta → stable` with bakes of 24 and 72
+hours and the passkey on `stable`.
+
+Once a pipeline exists, deploys still land on `edge` and you move a
+version forward by naming it:
+
+```sh
+carlos promote -app hello a1b2c3d production
+```
+
+A channel is entered from the one immediately before it and no other, so
+"it ran on edge for a day first" is a fact about the bytes rather than a
+claim about process. Promoting the same version twice succeeds and
+changes nothing.
+
+The rules a channel can carry — bake windows, passkeys, approval counts —
+are all editable after the fact with
+[carlos pipeline](https://carlosframework.com/docs/cli/#carlos-pipeline),
+and [carlos promote](https://carlosframework.com/docs/cli/#carlos-promote)
+documents the rest of its own flags.
+
+## Putting a version back
+
+Rollback points a channel at whatever it was serving before:
+
+```sh
+carlos rollback -app hello production
+```
+
+The outgoing version goes onto the channel's history as it leaves, so
+running the same command again returns you to where you started — useful
+when the rollback turns out to be the wrong call. A bake window does not
+delay it: a box already has the version it is going back to, so there is
+nothing to hold.
+
+Two things are worth doing before you need this. Give your ships a
+`-label` people can read, because that is what you will be squinting at
+during a rollback. And get familiar with
+[carlos releases](https://carlosframework.com/docs/cli/#carlos-releases)
+while nothing is on fire: it answers "what is there to run", where
+`carlos channels` answers "what is running".
+
+## What reaching stable means
+
+The `full-ladder` shape has a `stable` channel at the top, and a
+promotion onto it can cut a semver tag: one name pointing at one release,
+with the changelog of every iteration that led there compiled into it.
+
+That happens when the version being promoted is an iteration of a target
+rather than a commit sha, so set the target once:
+
+```sh
+carlos version target -app hello 0.5.0
+```
+
+From then on `carlos ship` with no `-version` mints the next iteration
+under it. Promote a sha-named build to stable instead and the promotion
+still lands. There is no tag to cut, and the promotion says so.
+
+Tags are a naming convenience and an audit record, nothing more. No part
+of getting your app running reads one, so an app that never uses them
+loses nothing.
+
+Full flags for the commands on this page:
+[channels](https://carlosframework.com/docs/cli/#carlos-channels) and
+[rollback](https://carlosframework.com/docs/cli/#carlos-rollback).
diff --git a/src/docs/config.md b/src/docs/config.md
new file mode 100644
index 0000000..9de659f
--- /dev/null
+++ b/src/docs/config.md
@@ -0,0 +1,78 @@
+# Env and secrets
+
+Your app's configuration lives in two stores. Both arrive at your process
+as ordinary environment variables, and from inside the app you cannot
+tell them apart. The difference is what happens to the value on the way
+there, and it is worth getting right the first time.
+
+Plain config goes in `carlos env`:
+
+```sh
+carlos env set -app hello LOG_LEVEL=debug
+```
+
+Credentials go in `carlos secrets`. Same shape:
+
+```sh
+carlos secrets set -app hello STRIPE_KEY=sk_live_example
+```
+
+## Which store a value belongs in
+
+Ask whether you would mind the value appearing in a screenshot. A log
+level, a feature flag, the public URL of a service you call — nobody
+cares, and `carlos env` keeps them readable, which is what you want when
+you are working out why staging behaves oddly.
+
+An API key, a database URL with a password in it, a signing secret,
+anything a third party issued you: `carlos secrets`. If in doubt, the
+sealed store costs you nothing except the ability to read the value back,
+so put it there.
+
+## What sealing actually does
+
+`carlos secrets set` seals the value on your machine, using the
+deployment's public key, before anything leaves your terminal. What gets
+stored is an envelope. The private half of that key lives on the box that
+runs your app and never leaves it, so the console holds a secret it
+cannot itself open.
+
+The consequence is the one thing people trip over: nothing can show you
+the value again. `carlos secrets list` prints key names and stops there.
+Lose the value and your only move is setting a new one.
+
+One practical wrinkle. The value is on your command line when you type
+it, so your shell history keeps a copy of it. Clear that out on a machine
+other people use.
+
+## Getting the new value into your app
+
+Changing config converges on its own. The write bumps a marker, every box
+serving the app notices within seconds, rewrites the environment file for
+each of the app's routes, and cycles the process so it comes back holding
+the new value. There is no separate deploy, and no restart for you to
+remember.
+
+Two things follow from the cycling. A config change is a small
+interruption for a running app, so batch related changes into one call —
+`carlos env set` takes several `KEY=value` pairs at once. And a change
+that appears not to have taken effect is more likely to be your app
+reading the variable once at startup than the platform failing to deliver
+it.
+
+To see what is set:
+
+```sh
+carlos env list -app hello
+```
+
+That prints values as well as names, and the sealed store's `list` prints
+names alone. Both families also take `unset` with a key name, and the
+reference pages for
+[carlos env](https://carlosframework.com/docs/cli/#carlos-env) and
+[carlos secrets](https://carlosframework.com/docs/cli/#carlos-secrets)
+have their remaining sub-verbs.
+
+If you take one thing from this page: put the credential in the sealed
+store on the day you first set it. Moving it later means rotating it,
+because by then it has been sitting somewhere readable.
diff --git a/src/docs/console.md b/src/docs/console.md
new file mode 100644
index 0000000..7834a10
--- /dev/null
+++ b/src/docs/console.md
@@ -0,0 +1,89 @@
+# The console
+
+[console.carloku.com](https://console.carloku.com) is the same platform your
+terminal talks to, with a browser in front of it. You sign in with a passkey
+through Keymail, and the account you sign in to is the one
+`carlos auth login` connects a terminal to — the short code the CLI prints
+is approved in this browser.
+
+Most weeks you will barely open it. Deploys, config, logs and domains are all
+commands, and the console reads the records those commands write. Where it
+earns its place is the handful of jobs with no command behind them, and
+looking at an app somebody else on your team deployed.
+
+## What each page shows
+
+The bar across the top is per account: Apps, Fleet, Team and Settings, plus
+a switcher for moving between accounts when you belong to more than one.
+
+Apps is the list: each app's name, the address it answers on, a health chip,
+and the version live right now. A Trash link appears under it once something
+is in it. Opening an app gives you six tabs.
+
+Overview answers *is it up* in a sentence — the hostname, the running version,
+when it was published and by whom — then tiles for running copies, CPU and
+memory, web addresses and versions, and a left-to-right diagram of the
+channels a version travels through. [Channels](/docs/channels/) explains what
+that diagram is drawing.
+
+Versions is the release history. Every version you have shipped with its
+label, kind and build time and a promote control per row; underneath, the
+channels, what each holds, when a baked version goes live, and rollback.
+Canaries and semver tags sit at the bottom of the same page.
+
+Running is the copies themselves — address, health, version, channel, CPU,
+memory — and where the app is placed. A copy reading *asleep* is annotated
+there as waking on the next request, which [Restarting and
+sleeping](/docs/sleeping/) explains at more length.
+
+Logs is the window `carlos logs` prints, with a form for narrowing it.
+Activity is the other one: when, who, what, for every change made to this
+app. That record has no CLI verb, so the console is where you read it.
+
+Settings carries the app's name and rename, its domains with their DNS and
+TLS state alongside the attach form, its object store, its email
+configuration, a link through to the pipeline rules, and deletion.
+
+Back at account level, Fleet answers where this account's work actually runs:
+the shared platform boxes your apps are placed on, and any boxes of your own.
+Team and Settings are the subject of [Members and service
+credentials](/docs/members/).
+
+## Jobs that only happen here
+
+Approving a terminal is the first one you meet: `carlos auth login` prints a
+code, and nothing is granted until you type it into the console and confirm.
+
+The rest cluster around authority and permanence. Inviting a teammate,
+removing one and handing over ownership live on the Team page; minting,
+rotating and revoking a credential a server holds live on Account settings.
+
+Deletion is the other cluster. A deleted app goes to Trash and stays
+restorable for 30 days, keeping its releases, channels and history.
+Restoring gives all of that back with no routes, so the app comes back with
+nowhere to serve from until you make one:
+
+```sh
+carlos instances create -app hello -host hello.bdf.oncarlos.com
+```
+
+That is the same command [Deploying](/docs/deploying/) names when a channel
+has no instance following it. The console's own confirmation currently tells
+you to run `carlos add` instead — don't. That verb edits a box's route
+registry from the box, and your app doesn't come with a box you can log in
+to.
+
+An owner can also destroy an app immediately from the same page; that asks
+for the app's name typed out and then for your passkey.
+
+And when the day's app-creation budget is spent, the console's Create app
+form is the way through — it has no limit of its own. [Limits and getting
+help](/docs/limits/) has the numbers.
+
+## Your own settings
+
+Your address in the header opens a page listing every terminal signed in as
+you: where it signed in from, when, when it was last used, and when it
+expires. Each row has a revoke control. A laptop that went missing is the
+reason this page exists — revoking there kills that terminal's access
+without touching anyone else's.
diff --git a/src/docs/deploying.md b/src/docs/deploying.md
new file mode 100644
index 0000000..9d95de7
--- /dev/null
+++ b/src/docs/deploying.md
@@ -0,0 +1,89 @@
+# Deploying
+
+Deploying is getting a build off your machine and onto the URL people
+use, and then knowing — not hoping — that the URL is serving it. One
+command covers all of that.
+
+```sh
+carlos deploy -app hello ./hello
+```
+
+It prints where it is deploying to, then `shipped`, then `promoted`, and
+finally `live` against each hostname. That last line is the one that
+matters. The others say a write succeeded; only `live` says a visitor
+would get the new build.
+
+## What the one command is doing
+
+Three steps, and you can run them separately when you want the gap
+between them.
+
+Shipping hashes your artifact, stores it under its content address, and
+records a manifest that never changes afterwards. Nothing you serve
+changes yet. Promoting points a channel at that version — see
+[Channels](/docs/channels/) for what a channel is and when you would have
+more than one. Then the command watches: it polls the live hostname for
+the `X-Carlos-Version` header until that header names the version you
+shipped, over a 2xx. It gives up after two minutes by default, and
+`-timeout` moves that.
+
+The header is also how you check by hand later, and it is worth
+preferring over anything else you might reach for. A page that looks
+right can be a cached copy of the old build:
+
+```sh
+curl -sI https://hello.bdf.oncarlos.com | grep -i x-carlos-version
+```
+
+## What you ship
+
+The version defaults to `git rev-parse --short HEAD`, so a deploy from a
+clean checkout is already labelled with the commit it came from.
+`-version` overrides it.
+
+Binaries are the common case, and `-target` says what you built for. It
+defaults to `linux-arm64`, which is what Carloku runs, so a Go build for
+this platform is:
+
+```sh
+GOOS=linux GOARCH=arm64 go build -o hello .
+```
+
+A directory of files ships as a site, with `-kind static`:
+
+```sh
+carlos deploy -app hello-site -kind static ./public
+```
+
+Run any of these once from your project and the CLI offers to remember
+the app and the artifact in `.carlos/config`. Say yes and the whole
+release becomes `carlos deploy` with no arguments, which is the form to
+put in a Makefile or a git hook.
+
+## When it doesn't go live
+
+A deploy that stops short usually stops for a reason it will tell you.
+
+If the channel you are promoting into needs approvals, the ship still
+lands and the promote becomes a proposal: the CLI says how many
+confirmations it needs and where to confirm them. If the channel has a
+bake window, the promote lands and the fleet adopts when the window
+expires — the command exits successfully, because the hold is policy
+rather than a failure. And if no instance follows the channel yet, it
+says so instead of watching a URL that nobody serves, and names
+`carlos instances create` as the step that gives the version somewhere to
+run.
+
+A watch that times out is a different situation. The version is published
+and the pointer has moved, so your app is either failing to start or
+failing its first request. Go to the logs:
+
+```sh
+carlos logs -app hello -since 10m
+```
+
+Full flag lists for
+[deploy](https://carlosframework.com/docs/cli/#carlos-deploy),
+[ship](https://carlosframework.com/docs/cli/#carlos-ship) and
+[promote](https://carlosframework.com/docs/cli/#carlos-promote) are in
+the CLI reference, and `carlos deploy -h` prints them at your terminal.
diff --git a/src/docs/docs.json b/src/docs/docs.json
new file mode 100644
index 0000000..7708084
--- /dev/null
+++ b/src/docs/docs.json
@@ -0,0 +1,5 @@
+{
+ "//": "This directory's markdown is authored here, in place — there is no vendoring and no sync step (see eleventy.config.js). Each file is input for src/docs.njk (rendered page) and src/docsmd.njk (raw .md fetch), paginated over the docs collection by slug; it is not a page of its own, which is why permalink is false here rather than in each file's front matter.",
+ "permalink": false,
+ "eleventyExcludeFromCollections": false
+}
diff --git a/src/docs/domains.md b/src/docs/domains.md
new file mode 100644
index 0000000..26bc375
--- /dev/null
+++ b/src/docs/domains.md
@@ -0,0 +1,70 @@
+# Custom domains
+
+Your app already answers on a hostname the platform gave it, something
+like `hello.bdf.oncarlos.com`. Attaching your own domain is two moves:
+tell Carloku the hostname is yours, then point a DNS record at it. The
+platform hostname keeps working afterwards, so you can do all of this
+before you switch any traffic over.
+
+```sh
+carlos domains attach -app hello www.example.com
+```
+
+Leave `-route` off when the app has one instance — the command works out
+what to point the hostname at. With several, name the one you mean.
+
+## The DNS record
+
+Attach finishes by printing the record to create and the hostname's
+current DNS state, so you need neither a second command nor a trip to the
+console to know what is left. Usually that is an A record at this
+deployment's edge addresses, named for you rather than left to look up.
+Where the deployment offers a steering hostname, a subdomain is offered a
+CNAME at that instead: visitors then reach the nearest healthy edge, and
+a failed edge drops out of the answer on its own. An apex domain —
+`example.com` with nothing in front of it — always takes the A record,
+because a CNAME cannot legally sit at the apex.
+
+Create the record with whoever runs your DNS. Checks run every few
+minutes, so there is nothing to trigger.
+
+## Watching it come live
+
+```sh
+carlos domains list -app hello
+```
+
+Each row is one claim joined to what the fleet actually observes: which
+route it points at, whether the hostname resolves to the edge, when its
+certificate expires, and — while it isn't pointing yet — the same
+instruction attach printed. Certificates are minted and renewed for you;
+there is no step here you own.
+
+The claim itself reaches the boxes within seconds of the attach, so a row
+that still reads `pending` a minute later is unusual. The DNS column is
+the slow half. It stays unhelpful until your record has propagated, and
+nothing on this side can hurry that along.
+
+## Taking one off
+
+```sh
+carlos domains detach -app hello www.example.com
+```
+
+The boxes serving the app notice within seconds and stop answering for
+the hostname, the same way they picked it up when you attached it. Your
+DNS record is yours to clean up afterwards.
+
+## Wildcards
+
+Two flags on
+[attach](https://carlosframework.com/docs/cli/#carlos-domains) cover the
+give-every-customer-a-subdomain case. `-wildcard` enables a delegated
+wildcard certificate for the hostname; `-catchall` derives a `*.<host>`
+alias onto the same route. Both need DNS delegation in place, and attach
+refuses with the reason when it isn't. Attaching one marketing domain
+needs neither.
+
+Neither flag has a default position either. Naming one changes it and
+leaving it off leaves it alone, so you can turn a wildcard on months
+later without restating anything else about the claim.
diff --git a/src/docs/index.md b/src/docs/index.md
new file mode 100644
index 0000000..25dcc3a
--- /dev/null
+++ b/src/docs/index.md
@@ -0,0 +1,13 @@
+# Documentation
+
+Carloku is the hosted CARLOS platform. You deploy a binary, get a URL, and
+the platform handles certificates, restarts, and putting a quiet app to
+sleep until traffic comes back.
+
+These pages cover running your app day-to-day, and the account settings
+that go with it.
+
+Haven't deployed yet? [Get started](/get-started/) is where to begin.
+
+Platform internals, self-hosting, and the full `carlos` CLI reference live
+at [carlosframework.com/docs](https://carlosframework.com/docs).
diff --git a/src/docs/limits.md b/src/docs/limits.md
new file mode 100644
index 0000000..27f13d5
--- /dev/null
+++ b/src/docs/limits.md
@@ -0,0 +1,76 @@
+# Limits and getting help
+
+The numbers on this page exist to make a stolen token boring, not to ration
+ordinary work. You are unlikely to meet any of them by hand. An automated
+loop is another matter, so here they are, along with what the refusal looks
+like when one does bite.
+
+## Shipping
+
+Two uploads a minute, counted per terminal token. That is past what an honest
+build-test-ship loop can produce, since a build worth shipping takes longer
+than half a minute to make.
+
+A refusal is a 429 that names the wait in seconds and repeats it in a
+`Retry-After` header, so a script has a number to sleep on instead of a
+guess. The window is a minute for exactly that reason: the worst a refused
+caller is ever asked for is sixty seconds. Sleep, retry, carry on.
+
+Size is the other ship limit. One request carries 256 MiB across everything
+in it. A static release is a single archive; a binary release can carry up to
+eight targets at once, which is well past what a normal build produces.
+
+## Everything else you type
+
+Ordinary mutations — env, secrets, promotes, rollbacks, restarts,
+schedules — get thirty a minute, counted against your address and not your
+terminal, so opening a second shell does not double it. The instance verbs
+get sixty a minute on their own budget, because a burst of signups creating
+instances is the workload that API exists for.
+
+Claiming an app name is the tight one: ten attempts a day per address, and
+a try that gets refused still spends one of them. A claimed name persists
+whether or not you ever use it, which is why this ceiling runs over a day and
+the others run over a minute. When you hit it, the message points at the
+console's Create app form, which has no limit of its own. That is the
+intended way through.
+
+## Credentials a server holds
+
+A credential minted with `carlos services` draws on its own budgets, larger
+than a person's and entirely separate: ten ships a minute, a hundred and
+twenty other writes a minute, a hundred app claims a day. A CI fleet cutting
+releases back to back therefore cannot starve the person who minted it, and
+the person's noisy afternoon cannot stop CI.
+
+The counters key on the credential's name, so rotating its secret does not
+hand it a fresh budget. [Members and service
+credentials](/docs/members/) covers the rest of how they work.
+
+## Things that expire
+
+Logs go back seven days, with a size ceiling per app that drops the oldest
+hour first — [Logs](/docs/logs/) has the detail. A deleted app sits in Trash
+for thirty days before the platform destroys it. A terminal you have signed
+in stays valid for ninety days, and you can revoke it sooner from your own
+settings page. An invitation to join an account lasts seven days and works
+once.
+
+## When something is refused
+
+Read the message before anything else. The platform's refusals are written to
+be acted on: a rate limit names the seconds to wait, an app you cannot claim
+today names the console form that will let you, an attached domain that is
+not resolving prints the DNS record it wants. Very few need interpreting.
+
+After that, in rough order of how often it helps: `carlos <verb> -h` for the
+flags, [the CLI reference](https://carlosframework.com/docs/cli/) for the
+full page, and `carlos logs -app <app> -since 1h` for anything where your app
+started and then misbehaved. If a deploy looks stuck, check
+`X-Carlos-Version` on the live hostname, not the page in your browser —
+[Deploying](/docs/deploying/) explains why the header is the honest answer.
+
+If none of that gets you there, open an issue on
+[the carloku repo](https://github.com/carlosframework/carloku/issues). These
+pages are written and kept there, so a question worth asking is usually a
+page worth fixing, and both happen in the same place.
diff --git a/src/docs/logs.md b/src/docs/logs.md
new file mode 100644
index 0000000..a47f8bd
--- /dev/null
+++ b/src/docs/logs.md
@@ -0,0 +1,67 @@
+# Logs
+
+`carlos logs` is one timeline rather than two. Your app's own stdout and
+stderr come back interleaved with the platform's account of what happened to
+it — which instance woke and on what version, when something restarted it,
+and when a wake failed, with the reason.
+
+```sh
+carlos logs -app hello -since 1h
+```
+
+Lines print oldest to newest: a UTC timestamp, the stream, the instance, the
+message, and any structured fields your app attached, dimmed at the end.
+
+## Following
+
+`-f` leaves the command running and polls for new lines every two seconds.
+
+```sh
+carlos logs -app hello -f
+```
+
+It runs close to live, not exactly live. Boxes ship what they have collected
+every ten seconds or so, so a follow arrives in small batches instead of
+scrolling a character at a time. Start one before a deploy and you will see
+the wake and the first requests.
+
+## Cutting it down
+
+An hour of a busy app is more than anyone reads. `-since` takes a duration
+back from now (`15m`, `24h`) or an RFC3339 timestamp, and `-until` closes the
+far end of the window. `-grep` matches an RE2 pattern against the message.
+`-host` narrows to a single instance when the app runs more than one. `-n`
+caps how many lines come back — a thousand by default, five thousand at most
+— and when the window has more than that, it tells you it is showing the
+newest.
+
+There is also `-stream`. `-stream app` drops the platform's commentary and
+leaves your own output; `-stream platform` does the opposite.
+
+## The half you can't get any other way
+
+Your app's output you could always have arranged for yourself. The platform
+stream is the part that needs a view from outside the process: `instance woke
+(version a1b2c3)`, `instance restarting (carlos restart)`, `instance
+stopping`, and — when a request got a 503 you cannot reproduce — a wake
+failure written in terms you can act on, like a socket left behind by a
+previous run, or nothing promoted on the channel this instance follows.
+
+A static site has no process and so has no app stream at all. Its platform
+events still show up, and they are the reason to run `carlos logs` against a
+site.
+
+## How far back it goes
+
+Seven days, and a size ceiling per app that drops the oldest hour first if
+an app is unusually chatty. If you need a longer record than that, have the
+app ship its own copy somewhere; the platform's retention is sized for
+debugging this week.
+
+An empty result is usually honest, and worth a second look. Platform
+events only happen when something happens — a wake, a restart, a failure —
+so a quiet hour of a quiet app really does have nothing in it. Widen to
+`-since 24h` before you conclude that logging is broken.
+
+The remaining flags, including `-level` and the repeatable `-field`, are in
+[carlos logs](https://carlosframework.com/docs/cli/#carlos-logs).
diff --git a/src/docs/members.md b/src/docs/members.md
new file mode 100644
index 0000000..f5a450d
--- /dev/null
+++ b/src/docs/members.md
@@ -0,0 +1,109 @@
+# Members and service credentials
+
+Two kinds of thing act on your apps. A person is a Keymail address with a
+role in the account. A credential is something a server holds — a CI runner,
+a job, a router — with its reach decided when it was minted and no human
+attached to it. They are administered in different places, and the mistakes
+they invite are different too.
+
+## People
+
+Invites go out from the Team page in the console, addressed to a Keymail
+address. The invitation lands in that person's Keymail Apps inbox — the same
+sealed path a sign-in confirmation takes — so inviting somebody sends them no
+email. It works once and expires after seven days, and an owner can revoke a
+pending one before it is used. If the delivery itself fails the console hands
+you the link to pass on yourself; the invitation is real by then either way.
+
+There are two roles. A member does the day-to-day work on the account's apps
+— deploy, promote, roll back, config, domains, logs. An owner does all of
+that plus everything that changes who can act or ends something permanently:
+inviting, removing a member, handing ownership over, deleting an app or the
+account, and every credential in the next section. Handing over ownership is
+a single transaction that steps you down to member as it lifts them up, so
+there is no moment with two owners or none.
+
+Some of those actions ask for your passkey again even though you are already
+signed in — removing a member, transferring ownership, deleting the account,
+approving a terminal or revoking one, minting or rotating a credential. The
+gesture has to be within the last five minutes. A session cookie proves who
+you are and not that you are still at the keyboard, which is the whole
+question when the action is irreversible.
+
+None of this has a `carlos` verb. Membership is console work, deliberately:
+the passkey prompt is the point of it, and a terminal has nowhere to put one.
+
+## Credentials a server holds
+
+```sh
+carlos services create -role publish -app hello ci-runner
+```
+
+The secret prints once, right there in your terminal, and only its hash is
+stored. There is no page and no command that will show it to you again — a
+lost secret means rotating, not re-running. Get it into the app the way you
+get any credential in:
+
+```sh
+carlos secrets set -app hello CARLOS_SERVICE_TOKEN=<the secret>
+```
+
+Pick the narrowest role that does the job. `publish` is the deploy loop:
+claim an app, ship, promote, roll back, manage hostnames, retire. `operate`
+is runtime work with no release authority — restart, config, logs. `instance`
+is the older app-token shape, kept for what already depends on it. `admin` is
+the union of those plus ledgers, storage and placement. It also grants
+object-store access, and that mints a real IAM user — hand one out only when
+you mean to hand over.
+
+`-app hello` binds the credential to one app; it cannot claim new ones.
+Leave it off for an account-wide credential. Either way the reach is fixed at
+mint: if you join another account next month, the credential you minted does
+not follow you there. Its rate budgets are its own, separate from yours — see
+[Limits](/docs/limits/) — and it never expires, so revoking is the control.
+
+## Rotate replaces, revoke stops
+
+This is the distinction to have straight before you need it.
+
+```sh
+carlos services rotate ci-runner
+```
+
+Rotation mints a fresh secret and prints it once, keeping the name, the role,
+the scope and the history. The old secret is refused on the credential's
+*next* call — work already in flight is unaffected. That makes rotation a
+hygiene move: something you schedule.
+
+```sh
+carlos services revoke ci-runner
+```
+
+Revocation is the hard stop: the credential's next call fails whatever secret
+it is holding. It is idempotent, so revoking a name twice, or one that never
+existed, answers the same way. And unlike minting and rotating, it does not
+ask for your passkey — one step, at whatever hour you find out.
+
+If you suspect a secret has leaked, revoke. Then mint a replacement under a
+new name if the work still needs doing.
+
+To see what exists:
+
+```sh
+carlos services ls
+```
+
+Live credentials only — name, role, scope, when it was minted, when its
+current secret began, when it was last used, and who minted it. A revoked one
+disappears from the list entirely, so this is inventory, not history.
+
+Every verb here is owner-only, and closed to service credentials themselves:
+no credential can mint, rotate or revoke any credential, not even its own.
+A leaked one therefore cannot quietly issue itself a quieter sibling. The
+console's Account settings page carries the same list, the same mint form,
+and rotate and revoke controls per row.
+
+Each verb takes `-account` when you belong to more than one; with a single
+account you can leave it off.
+[carlos services](https://carlosframework.com/docs/cli/#carlos-services) has
+the rest of the flags.
diff --git a/src/docs/sleeping.md b/src/docs/sleeping.md
new file mode 100644
index 0000000..505f9d2
--- /dev/null
+++ b/src/docs/sleeping.md
@@ -0,0 +1,86 @@
+# Restarting and sleeping
+
+Your app's process gets cycled for reasons that have nothing to do with new
+code. You ask for it, a config change forces it, or the platform puts a quiet
+app to sleep and starts it again when somebody turns up. This page covers the
+first and the last.
+
+```sh
+carlos restart -app hello
+```
+
+The reply says *restart requested*, which is the accurate word for what
+happened: the console wrote one small object, and every box serving the app
+notices it within seconds and cycles its own routes. No version moves, no
+channel is consulted, nothing is pushed at a box.
+
+What cycling looks like depends on how the instance runs. A long-running one
+stops and comes straight back. An on-demand one stops within seconds and
+starts again on the next request that needs it — for a quiet app, that may
+be a while. An instance that is already asleep is left asleep and
+starts fresh whenever it next wakes.
+
+Reach for restart when a process is wedged and you have no change to make. A
+deploy already cycles the app, and so does a config change — see
+[Deploying](/docs/deploying/) and [Env and secrets](/docs/config/) — so
+neither of those needs a restart after it.
+
+## What hibernation is
+
+An app nobody has talked to for a while is checkpointed to its bucket and its
+process stopped. The idle window belongs to the deployment; fifteen minutes
+is the default. The app's state was living in the bucket the whole time, so
+there is nothing to lose in the transition — the process was only ever the
+part that reads it.
+
+```sh
+carlos instances list -app hello
+```
+
+The health column reads `asleep` for those instances and `running` for the
+rest. For anything without steady traffic, `asleep` is the ordinary
+condition, not a fault to clear.
+
+## What a visitor sees
+
+A request for a sleeping app is held at the edge while the instance wakes,
+then handed to it. The visitor waits longer for that page and then gets it;
+there is no error page and nothing for them to retry. Requests that arrive
+together during a wake all join the same one instead of each starting their
+own.
+
+The exception is a wake that takes an unusual length of time. Past about
+thirty seconds the request is answered with a 503 and a `Retry-After`, and a
+wake that fails outright ends the same way. In
+both cases the reason is one line in the platform stream:
+
+```sh
+carlos logs -app hello -stream platform -since 15m
+```
+
+## Traffic that doesn't wake anything
+
+The edge keeps its own cache of responses and checks it before it goes
+anywhere near the wake path, so a crawler asking for the same public page all
+afternoon can be answered without your app running at all. What makes a
+response eligible is your app saying so: a 2xx GET with `Cache-Control:
+max-age=…` and no `Set-Cookie`. Whatever number you set, the edge caps the
+entry at five minutes, so this cannot strand a page for long. Setting that
+header on your public pages is the single most useful thing you can do for an
+app that hibernates.
+
+A TLS handshake by itself never wakes anything either — only a request that
+would reach your app does.
+
+## Work that still happens
+
+A timetable set with [carlos
+schedule](https://carlosframework.com/docs/cli/#carlos-schedule) survives
+hibernation. Before an instance goes to sleep the platform records when its
+next job falls due, and wakes it when that time arrives; the job runs, and the
+usual idle window puts it back. You do not have to keep the app awake to keep
+its schedule.
+
+Flags for [carlos restart](https://carlosframework.com/docs/cli/#carlos-restart)
+and [carlos instances](https://carlosframework.com/docs/cli/#carlos-instances)
+are in the CLI reference.
diff --git a/src/docs/storage.md b/src/docs/storage.md
new file mode 100644
index 0000000..295fe12
--- /dev/null
+++ b/src/docs/storage.md
@@ -0,0 +1,77 @@
+# Object storage
+
+Uploads, generated files, anything your app wants to still have next week —
+none of it belongs on the box's local disk, because the process that wrote
+it there is not guaranteed to be the one serving your next request. Each app
+can have a bucket of its own instead, and the credentials for it arrive in
+your process as environment variables.
+
+```sh
+carlos store create -app hello
+```
+
+That records the request and nothing more. No bucket exists yet: creating
+declares that the app wants one, and provisioning is a step on the Carloku
+side. `carlos store status` is where you watch for it landing.
+
+## What your app ends up with
+
+Five variables under a prefix, `CARLOS_STORE` unless you name another:
+`CARLOS_STORE_BUCKET`, `_REGION`, `_ACCESS_KEY` and `_SECRET_KEY`, plus
+`_ENDPOINT` where the backend has one. Your app reads them like any other
+config and talks to the bucket directly, with whichever S3 client its
+language already has. Nothing proxies that traffic.
+
+The bucket, region and endpoint are ordinary values — `carlos env list` will
+show them to you. The two keys are delivered through the sealed store, so
+they reach your process and stop being readable to anyone, including you.
+[Env and secrets](/docs/config/) explains why that is one-way.
+
+If your app already expects a particular set of names, say so at create
+time and skip the adapter code:
+
+```sh
+carlos store create -app hello -env-prefix BLOBS
+```
+
+## Watching it arrive
+
+```sh
+carlos store status -app hello
+```
+
+You get what you declared, what was granted, and what was provisioned —
+bucket, region, the user the credential belongs to, and the key id currently
+in use. Two of its lines are worth knowing before you meet them. One says a
+delivered variable has not reached your app's config yet. That is the
+ordinary state for a few seconds after any change, and a problem only if it
+persists. The other says a rotation was started and never finished.
+
+## Rotating the key
+
+```sh
+carlos store rotate -app hello
+```
+
+A fresh credential is minted and delivered, and the previous one keeps
+working. Your app picks the new values up the way it picks up any config
+change: the box rewrites the environment for the app's routes and cycles the
+process, within seconds. Leaving the old key alive is the point of doing it
+in two halves — an upload that started under it gets to finish, and a second
+instance that has not converged yet keeps serving.
+
+Close the window when you are satisfied nothing is still using it:
+
+```sh
+carlos store rotate -finish -app hello
+```
+
+Until you do, `status` keeps telling you a rotation is pending.
+
+One habit worth forming. The secret never appears at your terminal — rotate
+prints the new key's id and that is the whole of what you see — so if the
+value ever turns up somewhere it shouldn't, it came out of the app. Whatever
+your app dumps at startup is the first place to look.
+
+[carlos store](https://carlosframework.com/docs/cli/#carlos-store) has the
+sub-verbs and the rest of their flags.
diff --git a/src/docsmd.njk b/src/docsmd.njk
new file mode 100644
index 0000000..64ab68a
--- /dev/null
+++ b/src/docsmd.njk
@@ -0,0 +1,9 @@
+---
+pagination:
+ data: collections.docs
+ size: 1
+ alias: doc
+permalink: "/docs/{{ doc.slug }}.md"
+eleventyExcludeFromCollections: true
+---
+{{ doc.page.rawInput | safe }}
diff --git a/favicon.svg b/src/favicon.svg
similarity index 100%
rename from favicon.svg
rename to src/favicon.svg
diff --git a/fonts/Geist-Variable.woff2 b/src/fonts/Geist-Variable.woff2
similarity index 100%
rename from fonts/Geist-Variable.woff2
rename to src/fonts/Geist-Variable.woff2
diff --git a/fonts/GeistMono-Variable.woff2 b/src/fonts/GeistMono-Variable.woff2
similarity index 100%
rename from fonts/GeistMono-Variable.woff2
rename to src/fonts/GeistMono-Variable.woff2
diff --git a/fonts/LICENSE-OFL.txt b/src/fonts/LICENSE-OFL.txt
similarity index 100%
rename from fonts/LICENSE-OFL.txt
rename to src/fonts/LICENSE-OFL.txt
diff --git a/get-started/index.html b/src/get-started/index.html
similarity index 100%
rename from get-started/index.html
rename to src/get-started/index.html
diff --git a/img/dashboard-dark.png b/src/img/dashboard-dark.png
similarity index 100%
rename from img/dashboard-dark.png
rename to src/img/dashboard-dark.png
diff --git a/img/dashboard-light.png b/src/img/dashboard-light.png
similarity index 100%
rename from img/dashboard-light.png
rename to src/img/dashboard-light.png
diff --git a/index.html b/src/index.html
similarity index 100%
rename from index.html
rename to src/index.html
diff --git a/site.css b/src/site.css
similarity index 100%
rename from site.css
rename to src/site.css
diff --git a/site.js b/src/site.js
similarity index 100%
rename from site.js
rename to src/site.js