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
-   `