From 5e3f378d054794d7b0e11e7d472d463b22086759 Mon Sep 17 00:00:00 2001 From: Burin Choomnuan <19825136+burinc@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:07:20 +1000 Subject: [PATCH] feat: build this site with the shared docs-engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generator lived here, as a trimmed port of a private single-author engine. It has since been extracted to jlt-commons/docs-engine and generalized to serve every project site in the organization, so keeping a second copy here means a markdown fix has to land twice and the two drift in between. What leaves: src/, test/, and the engine's own templates and static assets. What stays is what belongs to this site — its pages, its homepage template, its mark, and now docs/site.edn for the configuration that used to be src/site/config.clj. Content moves to the engine's conventions: content/guide -> docs/guide, and the mark to docs/img, copied into the build by :asset-dirs rather than by living among the engine's own static files. bb.edn keeps only site:build / site:serve / site:clean, which shell into an engine checkout for local preview and print the clone command when they cannot find one. CI checks the engine out itself, pinned to v0.2.0. Verified against the live build rather than by inspection: the output is byte-identical to what this repo produced before, except for blank lines left by a now-false {% if mermaid %} and three added files (.nojekyll and the mermaid bundle, which ships but is never referenced here). `diff -Bw` across all four pages is clean. The engine loads mermaid only on pages that have a diagram, as of v0.2.0. This site has none, so it gained nothing but the vendored file, and the build now checks the bundle stays unreferenced — a regression in that gating would otherwise add 3.4 MB to every page here silently. The README's claim that mermaid is unsupported is no longer true and has been rewritten along with the build instructions. --- .github/workflows/site.yml | 68 +- README.md | 85 +- bb.edn | 69 +- {content => docs}/guide/index.md | 0 {content => docs}/guide/projects.md | 0 {resources/static => docs}/img/mark.svg | 0 docs/site.edn | 26 + {resources => docs}/templates/home.html | 0 resources/static/css/print.css | 66 - resources/static/css/screen.css | 1035 -------------- resources/static/vendor/highlightjs/LICENSE | 29 - .../vendor/highlightjs/highlight.min.js | 1244 ----------------- .../highlightjs/languages/clojure.min.js | 25 - .../highlightjs/languages/scheme.min.js | 20 - resources/templates/404.html | 8 - resources/templates/base.html | 121 -- resources/templates/docs.html | 29 - src/site/config.clj | 18 - src/site/core.clj | 237 ---- src/site/markdown.clj | 505 ------- test/site/core_test.clj | 84 -- test/site/markdown_test.clj | 355 ----- 22 files changed, 177 insertions(+), 3847 deletions(-) rename {content => docs}/guide/index.md (100%) rename {content => docs}/guide/projects.md (100%) rename {resources/static => docs}/img/mark.svg (100%) create mode 100644 docs/site.edn rename {resources => docs}/templates/home.html (100%) delete mode 100644 resources/static/css/print.css delete mode 100644 resources/static/css/screen.css delete mode 100644 resources/static/vendor/highlightjs/LICENSE delete mode 100644 resources/static/vendor/highlightjs/highlight.min.js delete mode 100644 resources/static/vendor/highlightjs/languages/clojure.min.js delete mode 100644 resources/static/vendor/highlightjs/languages/scheme.min.js delete mode 100644 resources/templates/404.html delete mode 100644 resources/templates/base.html delete mode 100644 resources/templates/docs.html delete mode 100644 src/site/config.clj delete mode 100644 src/site/core.clj delete mode 100644 src/site/markdown.clj delete mode 100644 test/site/core_test.clj delete mode 100644 test/site/markdown_test.clj diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 5fdc3c3..ce891ac 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -1,5 +1,13 @@ name: Site +# Builds the organization site on every pull request, and publishes it to +# GitHub Pages from main. The generator lives in jlt-commons/docs-engine, +# shared with every project site; this repo owns its content in docs/, its +# config in docs/site.edn, and its homepage template. +# +# The engine is pinned to a tag rather than tracking its main branch, so a +# change over there cannot take this site down without someone choosing it. + on: push: branches: [main] @@ -9,11 +17,16 @@ on: permissions: contents: read -# Never let two deploys race. Do not cancel a running deploy, since a -# half-applied Pages deployment is worse than a slightly stale one. +# Deploys must not race, and a half-applied Pages deployment is worse than +# a slightly stale one, so main is never cancelled. Pull request builds are +# scoped per ref and cancel their own earlier runs, so a third queued run +# cannot supersede a second one and show a contributor a cancelled check. concurrency: - group: pages - cancel-in-progress: false + group: site-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + DOCS_ENGINE_REF: v0.2.0 jobs: build: @@ -21,22 +34,53 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Check out the docs engine + uses: actions/checkout@v7 + with: + repository: jlt-commons/docs-engine + ref: ${{ env.DOCS_ENGINE_REF }} + path: .docs-engine + - name: Install babashka uses: DeLaGuardo/setup-clojure@13.6.1 with: bb: latest - - name: Test - run: bb test - - name: Build - run: bb build + working-directory: .docs-engine + run: bb build "$GITHUB_WORKSPACE" - - name: Check the build is not empty + - name: Check the build run: | - test -f _site/index.html || { echo "no index.html generated"; exit 1; } - test -f _site/css/screen.css || { echo "static assets missing"; exit 1; } - ! grep -q '{{site-base}}' _site/index.html || { echo "unrendered template variable"; exit 1; } + set -euo pipefail + out=_site + + test -f "$out/index.html" || { echo "no homepage generated"; exit 1; } + test -f "$out/guide/index.html" || { echo "no guide page generated"; exit 1; } + test -f "$out/css/screen.css" || { echo "static assets missing"; exit 1; } + test -f "$out/img/mark.svg" || { echo "the mark was not copied — check :asset-dirs"; exit 1; } + + ! grep -rq '{{site-base}}' "$out"/index.html "$out"/guide/*.html \ + || { echo "unrendered template variable"; exit 1; } + + # This site is served at the domain ROOT, so its URLs are correctly + # root-absolute. That is the opposite of a project site, where a bare + # /css/... would resolve here instead of inside the project. The + # check that belongs here is that no project's base path leaked in. + if grep -ohE '(href|src)="/[a-z-]+/' "$out"/index.html "$out"/guide/*.html \ + | sort -u | grep -vE '="/(css|img|guide|vendor)/'; then + echo "the paths above are not this site's own" + exit 1 + fi + + # The engine loads mermaid only where a diagram exists. This site has + # none, and the bundle is 3.4 MB, so its appearance means the gating + # regressed rather than that someone added a diagram — which would + # also need this line updated, deliberately. + ! grep -rq 'mermaid.min.js' "$out"/index.html "$out"/guide/*.html \ + || { echo "mermaid loaded on a site with no diagrams"; exit 1; } + + echo "build looks correct" - name: Upload the Pages artifact if: github.ref == 'refs/heads/main' diff --git a/README.md b/README.md index 18cbe49..2a248a6 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,71 @@ # jlt-commons.github.io -The [jlt-commons](https://github.com/jlt-commons) website, and the small static-site -generator that builds it. +The [jlt-commons](https://github.com/jlt-commons) website. Live at **https://jlt-commons.github.io**. -## Building it +## What's here -You need [babashka](https://babashka.org). Nothing else; the only external dependency is -`markdown-clj`, fetched on first run. +Content, and nothing else. The generator moved out to +[jlt-commons/docs-engine](https://github.com/jlt-commons/docs-engine), which is +shared with every project site in the organization. -```bash -bb test # run the test suite -bb build # generate the site into _site/ -bb serve # build, then serve at http://localhost:3000 -bb clean # delete _site/ ``` - -`_site/` is generated and is not committed. GitHub Actions builds and deploys on every -push to `main`, and runs the tests on every pull request. +docs/ + site.edn # configuration + guide/ # pages, in markdown + templates/home.html # the homepage + img/ # the mark +``` ## Editing content -Pages are markdown under `content/guide/`. Add a file and it appears in the nav -automatically, with `index.md` pinned first. The homepage is a hand-written template at -`resources/templates/home.html`. +Pages are markdown under `docs/guide/`. Add a file and it appears in the nav +automatically, with `index.md` pinned first. The homepage is a hand-written +template at `docs/templates/home.html`. -Governance text is deliberately **not** duplicated here. This site links to the canonical -documents in [`meta`](https://github.com/jlt-commons/meta), because two copies of a rule -become two different rules. +Governance text is deliberately **not** duplicated here. This site links to the +canonical documents in [`meta`](https://github.com/jlt-commons/meta), because two +copies of a rule become two different rules. -Mermaid fences are not supported. `markdown.clj` still rewrites ```` ```mermaid ```` blocks -into `
`, but the mermaid.js bundle that would render them isn't
-vendored in this site, so a diagram renders as plain, unstyled source text.
+## Building it
 
-## Using this generator for your own project
+CI builds the site on every pull request and deploys it from `main`, so a merged
+change goes live without anyone running anything. That is the authority.
 
-Any jlt-commons project is welcome to. Copy `src/`, `resources/`, `test/` and `bb.edn`,
-then set `:base-path` in `src/site/config.clj` to your repo name:
+To preview locally, clone the engine alongside this repo:
 
-```clojure
-:base-path "/your-repo"
+```bash
+git clone https://github.com/jlt-commons/docs-engine ../docs-engine
+bb site:serve    # build, then serve at http://localhost:3000
+bb site:build    # build into _site/ without serving
+bb site:clean    # delete _site/
 ```
 
-That matters. A project site is served at `jlt-commons.github.io/your-repo/`, and without
-a base path every stylesheet and nav link resolves against the organization site instead
-of yours. The page still renders, which is what makes the bug easy to miss, so
-`site.core-test` covers it.
+`_site/` is generated and is not committed. The tasks print the clone command if
+they cannot find an engine checkout; CI needs none of this, because it checks the
+engine out itself at a pinned tag.
+
+## Mermaid
+
+Supported. A ```` ```mermaid ```` fence in a guide page renders as a diagram,
+themed to match the reader's light or dark setting. The engine loads the bundle
+only on pages that actually have one, since it is 3.4 MB against a typical page
+of a few kilobytes.
+
+This site currently has no diagrams, and its build checks that the bundle stays
+absent. Adding one means updating that check, deliberately, in
+`.github/workflows/site.yml`.
 
-Leave `:base-path` as `""` only for a site served at a domain root.
+## Using the engine for your own project
 
-## Credit
+Any jlt-commons project can. See the
+[engine's README](https://github.com/jlt-commons/docs-engine#onboarding-a-project);
+[`raylib-jlt`](https://github.com/jlt-commons/raylib-jlt) is a working example
+with a bespoke homepage and an image gallery.
 
-The generator is a trimmed port of a private engine by the same author, reduced to a
-single site and extended with base-path support.
+The one thing to get right is `:base-path`. This site sets `""` because it is
+served at the domain root. A project site is served at
+`jlt-commons.github.io//` and sets `:base-path "/"`; without it every
+stylesheet and nav link resolves against this site instead. The page still
+renders, wearing the wrong clothes, which is what makes the bug easy to ship.
diff --git a/bb.edn b/bb.edn
index 0e1c0cf..ae8870c 100644
--- a/bb.edn
+++ b/bb.edn
@@ -1,29 +1,50 @@
-;; bb.edn - the jlt-commons website generator.
+;; bb.edn — local preview for the jlt-commons organization site.
 ;;
-;; A trimmed port of a private engine by the same author, reduced to a
-;; single site. The one addition is base-path support (see
-;; site.core/base-path), which lets a jlt-commons member project build a
-;; site served at // rather than at a domain root.
-{:paths ["src" "test" "resources"]
- :deps  {markdown-clj/markdown-clj {:mvn/version "1.11.4"}}
- :tasks
- {:requires ([site.config :as config])
+;; The generator is not here. It lives in jlt-commons/docs-engine, shared
+;; with every project site in the organization, and CI checks it out at a
+;; pinned tag (.github/workflows/site.yml). These tasks exist only so the
+;; site can be previewed without pushing, and they are the one place that
+;; needs a checkout of the engine on disk.
+{:tasks
+ {:requires ([babashka.fs :as fs])
 
-  test  {:doc "Run the full test suite"
-         :requires ([clojure.test :as t]
-                    [site.markdown-test]
-                    [site.core-test])
-         :task (let [{:keys [fail error]} (t/run-tests 'site.markdown-test 'site.core-test)]
-                 (when (pos? (+ fail error)) (System/exit 1)))}
+  :init
+  (do
+    (def docs-engine-repo "https://github.com/jlt-commons/docs-engine")
 
-  build {:doc "Generate the site into _site/"
-         :requires ([site.core :as core])
-         :task (core/generate! (config/site))}
+    (defn docs-engine-dir
+      "Where the engine is checked out: $JLT_DOCS_ENGINE, else a sibling
+       directory, else the usual spot. nil when none of them exist."
+      []
+      (let [home (System/getProperty "user.home")]
+        (->> [(System/getenv "JLT_DOCS_ENGINE")
+              "../docs-engine"
+              (str home "/dev/jlt-commons/docs-engine")]
+             (remove nil?)
+             (filter (fn [d] (fs/exists? (fs/file d "bb.edn"))))
+             first)))
 
-  clean {:doc "Delete the _site/ build output"
-         :requires ([site.core :as core])
-         :task (core/clean! (config/site))}
+    (defn site-task
+      "Runs one of the engine's tasks against this site."
+      [task & args]
+      (if-let [engine (docs-engine-dir)]
+        (let [argv (into ["bb" task (str (fs/cwd))] (remove nil? args))
+              res  (apply shell {:dir (str engine) :continue true} argv)]
+          (when-not (zero? (:exit res)) (System/exit (:exit res))))
+        (do
+          (println "The docs engine is not checked out anywhere I looked:")
+          (println "  $JLT_DOCS_ENGINE, ../docs-engine, ~/dev/jlt-commons/docs-engine")
+          (println)
+          (println "  git clone" docs-engine-repo "../docs-engine")
+          (println)
+          (println "CI does not need this — it checks the engine out itself.")
+          (System/exit 1)))))
 
-  serve {:doc "Build, then serve _site/ locally (bb serve [port], default 3000)"
-         :requires ([site.core :as core])
-         :task (core/serve! (config/site) (first *command-line-args*))}}}
+  site:build {:doc "Generate the site into _site/"
+              :task (site-task "build")}
+
+  site:serve {:doc "Build, then serve _site/ locally (bb site:serve [port], default 3000)"
+              :task (site-task "serve" (first *command-line-args*))}
+
+  site:clean {:doc "Delete the _site/ build output"
+              :task (site-task "clean")}}}
diff --git a/content/guide/index.md b/docs/guide/index.md
similarity index 100%
rename from content/guide/index.md
rename to docs/guide/index.md
diff --git a/content/guide/projects.md b/docs/guide/projects.md
similarity index 100%
rename from content/guide/projects.md
rename to docs/guide/projects.md
diff --git a/resources/static/img/mark.svg b/docs/img/mark.svg
similarity index 100%
rename from resources/static/img/mark.svg
rename to docs/img/mark.svg
diff --git a/docs/site.edn b/docs/site.edn
new file mode 100644
index 0000000..d50a480
--- /dev/null
+++ b/docs/site.edn
@@ -0,0 +1,26 @@
+;; Configuration for the jlt-commons organization site, read by the shared
+;; docs-engine (https://github.com/jlt-commons/docs-engine).
+;;
+;; Build it locally with `bb site:serve`. CI builds and publishes it from
+;; .github/workflows/site.yml.
+{;; Empty, because this site is served at the domain root,
+ ;; https://jlt-commons.github.io. A member project is served at
+ ;; https://jlt-commons.github.io// and sets :base-path "/"
+ ;; instead. See site.core/base-path in the engine.
+ :base-path   ""
+
+ :title       "jlt-commons"
+ :description "A community-led home for Jolt libraries and tooling."
+
+ ;; The organization, not this repository. The nav's GitHub link should
+ ;; land on the org page rather than on the site's own source, which is
+ ;; why this overrides the engine's derive-from-origin default.
+ :github-url  "https://github.com/jlt-commons"
+
+ ;; docs/img holds the mark the homepage renders. Engine static assets
+ ;; (css, highlight.js, mermaid) come from the engine itself; anything
+ ;; belonging to this site lives here.
+ :asset-dirs  ["img"]
+
+ ;; Relative to docs/templates/.
+ :home-template "home.html"}
diff --git a/resources/templates/home.html b/docs/templates/home.html
similarity index 100%
rename from resources/templates/home.html
rename to docs/templates/home.html
diff --git a/resources/static/css/print.css b/resources/static/css/print.css
deleted file mode 100644
index da39a1d..0000000
--- a/resources/static/css/print.css
+++ /dev/null
@@ -1,66 +0,0 @@
-/* ============================================================
-   jlt-commons site print.css
-   Loaded only when printing (media="print" on its  in
-   base.html); never affects on-screen rendering. Forces the light
-   palette regardless of the reader's chosen screen theme (nobody
-   wants to print a black background), strips chrome that means
-   nothing on paper, and keeps code blocks/tables/headings from
-   splitting awkwardly across a page break.
-
-   The :root selector below is repeated 3 ways (bare, [data-theme=
-   "light"], [data-theme="dark"]) to match/exceed the specificity of
-   every selector screen.css might have set --bg/--text/etc. through.
-   A plain `:root { ... }` here would lose to screen.css's
-   :root[data-theme="dark"] on specificity even though this
-   stylesheet loads later.
-   ============================================================ */
-
-:root,
-:root[data-theme="light"],
-:root[data-theme="dark"] {
-  --bg: #ffffff;
-  --bg-raised: #f6f8fa;
-  --border: #d0d7de;
-  --text: #1f2328;
-  --text-dim: #59636e;
-  --accent: #c8430c;
-  --accent-hover: #9e340a;
-  --code-keyword: #cf222e;
-  --code-title: #8250df;
-  --code-attr: #0550ae;
-  --code-string: #0a3069;
-  --code-builtin: #953800;
-  --code-comment: #6e7781;
-  --code-name: #116329;
-  --code-section: #0550ae;
-  --code-bullet: #953800;
-  --code-addition-bg: #dafbe1;
-  --code-addition-fg: #24292f;
-  --code-deletion-bg: #ffebe9;
-  --code-deletion-fg: #24292f;
-}
-
-.site-nav,
-.docs-sidebar,
-nav.toc,
-.theme-toggle,
-.print-trigger,
-footer {
-  display: none !important;
-}
-
-.docs-layout {
-  display: block;
-}
-
-pre,
-.doc-content table,
-.doc-content img {
-  break-inside: avoid;
-  page-break-inside: avoid;
-}
-
-h1, h2, h3, h4, h5, h6 {
-  break-after: avoid;
-  page-break-after: avoid;
-}
diff --git a/resources/static/css/screen.css b/resources/static/css/screen.css
deleted file mode 100644
index 101a997..0000000
--- a/resources/static/css/screen.css
+++ /dev/null
@@ -1,1035 +0,0 @@
-/* ============================================================
-   jlt-commons site screen.css theme
-   Dark-by-default, code-forward theme with a light counterpart.
-   Written to be read and edited by maintainers, not shipped as
-   opaque minified CSS.
-
-   Theming: every color token below has three faces: the bare value
-   here (dark, the unconditional fallback), a light override under
-   `@media (prefers-color-scheme: light)`, and explicit
-   `[data-theme="light"|"dark"]` overrides that always win over the
-   media query. base.html's anti-flash script resolves and sets
-   `data-theme` before first paint. See print.css (a separate
-   stylesheet, loaded only when printing) for the one place that
-   forces light values regardless of `data-theme`.
-
-   Token system
-   ------------
-   Color:
-     --bg          #0d1117  Void      - chosen to match code blocks'
-                                         own background exactly, so
-                                         they blend into the page
-                                         instead of sitting in a
-                                         mismatched box.
-     --bg-raised   #131a24  Panel     - chrome surfaces: nav, sidebar,
-                                         TOC, footer.
-     --border      #25303d  Hairline  - dividers, card borders.
-     --text        #c9d1d9  Ink       - the page's own body-text color;
-                                         --code-comment and others are
-                                         chosen to read well against it.
-     --text-dim    #8b96a3  Ink, dim  - captions, secondary nav/UI.
-     --accent      #ff7a3d  Copper    - solder/wire: the material of
-                                         native FFI glue. Used for links,
-                                         selected states, inline code,
-                                         the brand parens. The one accent
-                                         hue on the page.
-     --accent-hover #ffa06b           - hover/brighter copper.
-
-   Code (13 --code-* tokens, one set per theme face above, see the
-   "Code highlighting" section further down this file for the full
-   list and the .hljs-* rules that consume them; code highlighting no
-   longer comes from a vendored per-theme stylesheet).
-
-   Type: one family (monospace) used for everything: code, prose,
-   chrome, the deliberate "code-forward" bet. Hierarchy comes from
-   size/weight/letter-spacing, not a second typeface:
-     - display role:  headings, brand: bold, tight tracking
-     - body role:     prose: regular weight, relaxed line-height
-     - utility role:  nav/sidebar/TOC/footer labels: small, uppercase,
-                       tracked out, dim ink
-   ============================================================ */
-
-:root {
-  --bg: #0d1117;
-  --bg-raised: #131a24;
-  --border: #25303d;
-  --text: #c9d1d9;
-  --text-dim: #8b96a3;
-  --accent: #ff7a3d;
-  --accent-hover: #ffa06b;
-
-  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Berkeley Mono",
-    "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono",
-    monospace;
-
-  --radius: 8px;
-  --radius-sm: 4px;
-  --content-measure: 70ch;
-  --nav-height: 3.5rem;
-}
-
-/* ---- Light palette ----
-   Used when the reader has no stored preference and their OS/browser
-   prefers light (prefers-color-scheme), OR when they've explicitly
-   toggled to light. data-theme, when present, always wins over the
-   media query in either direction; see base.html's anti-flash
-   script, which always resolves and sets a concrete data-theme value
-   (never leaves it absent), so in practice the media-query-only layer
-   below only matters for a reader with JavaScript disabled. */
-@media (prefers-color-scheme: light) {
-  :root:not([data-theme="dark"]) {
-    --bg: #ffffff;
-    --bg-raised: #f6f8fa;
-    --border: #d0d7de;
-    --text: #1f2328;
-    --text-dim: #59636e;
-    --accent: #c8430c;
-    --accent-hover: #9e340a;
-    --code-keyword: #cf222e;
-    --code-title: #8250df;
-    --code-attr: #0550ae;
-    --code-string: #0a3069;
-    --code-builtin: #953800;
-    --code-comment: #6e7781;
-    --code-name: #116329;
-    --code-section: #0550ae;
-    --code-bullet: #953800;
-    --code-addition-bg: #dafbe1;
-    --code-addition-fg: #24292f;
-    --code-deletion-bg: #ffebe9;
-    --code-deletion-fg: #24292f;
-  }
-}
-
-:root[data-theme="light"] {
-  --bg: #ffffff;
-  --bg-raised: #f6f8fa;
-  --border: #d0d7de;
-  --text: #1f2328;
-  --text-dim: #59636e;
-  --accent: #c8430c;
-  --accent-hover: #9e340a;
-  --code-keyword: #cf222e;
-  --code-title: #8250df;
-  --code-attr: #0550ae;
-  --code-string: #0a3069;
-  --code-builtin: #953800;
-  --code-comment: #6e7781;
-  --code-name: #116329;
-  --code-section: #0550ae;
-  --code-bullet: #953800;
-  --code-addition-bg: #dafbe1;
-  --code-addition-fg: #24292f;
-  --code-deletion-bg: #ffebe9;
-  --code-deletion-fg: #24292f;
-}
-
-:root[data-theme="dark"] {
-  --bg: #0d1117;
-  --bg-raised: #131a24;
-  --border: #25303d;
-  --text: #c9d1d9;
-  --text-dim: #8b96a3;
-  --accent: #ff7a3d;
-  --accent-hover: #ffa06b;
-  --code-keyword: #ff7b72;
-  --code-title: #d2a8ff;
-  --code-attr: #79c0ff;
-  --code-string: #a5d6ff;
-  --code-builtin: #ffa657;
-  --code-comment: #8b949e;
-  --code-name: #7ee787;
-  --code-section: #1f6feb;
-  --code-bullet: #f2cc60;
-  --code-addition-bg: #033a16;
-  --code-addition-fg: #aff5b4;
-  --code-deletion-bg: #67060c;
-  --code-deletion-fg: #ffdcd7;
-}
-
-/* ---- Reset ---- */
-
-*,
-*::before,
-*::after {
-  box-sizing: border-box;
-}
-
-html {
-  -webkit-text-size-adjust: 100%;
-  scroll-behavior: smooth;
-}
-
-body,
-h1,
-h2,
-h3,
-p,
-ul,
-ol,
-li {
-  margin: 0;
-}
-
-img {
-  max-width: 100%;
-  display: block;
-}
-
-@media (prefers-reduced-motion: reduce) {
-  html {
-    scroll-behavior: auto;
-  }
-  *,
-  *::before,
-  *::after {
-    animation-duration: 0.01ms !important;
-    transition-duration: 0.01ms !important;
-  }
-}
-
-/* ---- Base typography ---- */
-
-body {
-  background: var(--bg);
-  color: var(--text);
-  font-family: var(--font-mono);
-  font-size: 1rem;
-  line-height: 1.7;
-  -webkit-font-smoothing: antialiased;
-}
-
-::selection {
-  background: var(--accent);
-  color: var(--bg);
-}
-
-a {
-  color: inherit;
-}
-
-a:focus-visible,
-button:focus-visible {
-  outline: 2px solid var(--accent);
-  outline-offset: 2px;
-  border-radius: 2px;
-}
-
-code {
-  font-family: var(--font-mono);
-}
-
-/* ---- Site nav ---- */
-
-.site-nav {
-  background: var(--bg-raised);
-  border-bottom: 1px solid var(--border);
-  position: sticky;
-  top: 0;
-  z-index: 10;
-}
-
-.site-nav .inner {
-  max-width: 1120px;
-  margin: 0 auto;
-  padding: 0.9rem 1.5rem;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 1.5rem;
-  min-height: var(--nav-height);
-}
-
-.brand {
-  font-weight: 700;
-  font-size: 1.05rem;
-  letter-spacing: -0.01em;
-  color: var(--text);
-  text-decoration: none;
-  transition: color 0.15s ease;
-}
-
-.brand::before {
-  content: "(";
-  color: var(--accent);
-}
-
-.brand::after {
-  content: ")";
-  color: var(--accent);
-}
-
-.brand:hover {
-  color: var(--accent);
-}
-
-.links {
-  list-style: none;
-  display: flex;
-  gap: 1.75rem;
-}
-
-.links a {
-  display: inline-block;
-  color: var(--text-dim);
-  text-decoration: none;
-  font-size: 0.8125rem;
-  font-weight: 600;
-  letter-spacing: 0.06em;
-  text-transform: uppercase;
-  padding: 0.25rem 0;
-  border-bottom: 2px solid transparent;
-  transition: color 0.15s ease, border-color 0.15s ease;
-}
-
-.links a:hover {
-  color: var(--text);
-}
-
-.links .selected {
-  color: var(--accent);
-}
-
-.links .selected a {
-  color: var(--accent);
-  border-bottom-color: var(--accent);
-}
-
-.theme-toggle {
-  background: none;
-  border: none;
-  color: var(--text-dim);
-  font-size: 1rem;
-  line-height: 1;
-  cursor: pointer;
-  padding: 0.25rem 0;
-  font-family: inherit;
-  transition: color 0.15s ease;
-}
-
-.theme-toggle:hover {
-  color: var(--accent);
-}
-
-/* ---- Docs layout ---- */
-
-.docs-layout {
-  display: grid;
-  grid-template-columns: 260px minmax(0, 1fr);
-  max-width: 1120px;
-  margin: 0 auto;
-  align-items: start;
-}
-
-.docs-sidebar {
-  min-width: 0; /* grid items default to min-width:auto; a long,
-                   unbreakable generated title would otherwise stretch
-                   this track (and the page) past the viewport width */
-  background: var(--bg-raised);
-  border-right: 1px solid var(--border);
-  padding: 2rem 1.25rem;
-  position: sticky;
-  top: var(--nav-height);
-  align-self: start;
-  max-height: calc(100vh - var(--nav-height));
-  overflow-y: auto;
-}
-
-.docs-sidebar h3 {
-  font-size: 0.75rem;
-  font-weight: 600;
-  letter-spacing: 0.08em;
-  text-transform: uppercase;
-  color: var(--text-dim);
-  margin-bottom: 1rem;
-}
-
-/* Group headers within the sidebar; infra.html groups ~90 pages by
-   service directory; a plain h3-per-group would be too loud repeated
-   dozens of times, so this is smaller/dimmer and reads as a sub-label. */
-.docs-sidebar h4 {
-  font-size: 0.6875rem;
-  font-weight: 600;
-  letter-spacing: 0.04em;
-  text-transform: uppercase;
-  color: var(--text-dim);
-  opacity: 0.7;
-  margin: 1rem 0 0.35rem;
-}
-
-.docs-sidebar ul {
-  list-style: none;
-  display: flex;
-  flex-direction: column;
-  gap: 0.15rem;
-}
-
-.docs-sidebar a {
-  display: block;
-  margin-left: -0.6rem;
-  padding: 0.4rem 0.6rem;
-  border-left: 2px solid transparent;
-  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
-  color: var(--text-dim);
-  text-decoration: none;
-  font-size: 0.875rem;
-  line-height: 1.4;
-  overflow-wrap: anywhere; /* titles are generated content; guard against
-                               any single unbroken token overflowing */
-  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
-}
-
-.docs-sidebar a:hover {
-  color: var(--text);
-  background: rgba(201, 209, 217, 0.06);
-}
-
-.docs-sidebar li.selected {
-  background: rgba(232, 162, 89, 0.08);
-  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
-}
-
-.docs-sidebar li.selected a {
-  color: var(--accent);
-  border-left-color: var(--accent);
-  font-weight: 600;
-}
-
-.docs-main {
-  min-width: 0; /* prevent a wide 
 from blowing out the grid track */
-  padding: 2.5rem 2rem 4rem;
-}
-
-/* ---- Table of contents ----
-   Two elements carry class="toc" in the real markup: the outer
-   `