Page not found
+Nothing lives at this address. The whole guide is on a single page.
+Go to the guide or jump to the support matrix.
+diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 4fb9d4d..808ee0c 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -18,8 +18,11 @@ jobs:
steps:
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ # Full history, so the page's "Updated" date is the last commit that touched the content.
+ fetch-depth: 0
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- - run: uv run --with zensical zensical build --clean
+ - run: uv run build.py
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 06fff19..217a23c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,7 +23,12 @@ jobs:
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- name: Markdown lint
- run: uv run --with mdlint mdlint check docs/index.md README.md
+ run: uv run --with mdlint mdlint check README.md
+
+ - name: Python lint
+ run: |
+ uv run --with ruff ruff check
+ uv run --with ruff ruff format --check
- name: Validate docs
- run: uv run --with zensical zensical build
+ run: uv run build.py
diff --git a/.mdlint.toml b/.mdlint.toml
index 37fc984..0cceb2f 100644
--- a/.mdlint.toml
+++ b/.mdlint.toml
@@ -1,6 +1,4 @@
ignore = ["MD051", "MD060"] # MD060: table rows are not kept pipe-aligned
-# Content is included from README.md via pymdownx.snippets; nothing to lint here.
-exclude = ["docs/index.md"]
[rules.MD013]
line_length = 120
diff --git a/AGENTS.md b/AGENTS.md
index 2a20c94..dc7f6dc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,8 +8,22 @@ script (`cooldowns.sh`) that automates checking and configuring cooldowns.
## Conventions
- `README.md` is the single source of truth for documentation content.
- `docs/index.md` includes it via `pymdownx.snippets` (`--8<-- "README.md"`)
- and only adds frontmatter. Edit `README.md`, not `docs/index.md`.
+ `build.py` renders it into the single-page site in `site/`, using the
+ Jinja2 templates in `docs/templates/` and the CSS, JS, and self-hosted
+ fonts (Red Hat Text and Red Hat Mono, with their licenses) in `docs/static/`.
+
+- `docs/tools.toml` holds per-tool metadata the README can't express
+ (support level, first version, default cooldown, unit, three-day value).
+ It drives the sidebar, the per-tool support cards, and the support matrix.
+ When adding, renaming, or removing a tool section in `README.md`, update
+ `docs/tools.toml` and the README quick reference table together. A new H2
+ section must be added to an `[[ecosystem]]` or `[[group]]`. The build fails
+ if these disagree, or if a tool's `three_days` value no longer appears in
+ its README section.
+
+- `docs/anchors.txt` lists heading ids that inbound links depend on. Renaming
+ a heading changes its id and fails the build; only remove an entry if you
+ mean to break links to it.
- `cooldowns.sh` must work on both Linux and macOS. Use portable constructs
(e.g. `awk` instead of `grep -oP`, pure-bash version comparison instead
@@ -25,14 +39,29 @@ script (`cooldowns.sh`) that automates checking and configuring cooldowns.
- Run `shellcheck cooldowns.sh` after each change to the script.
+- Lint and format `build.py` with ruff after each change to it (settings
+ in `ruff.toml`):
+
+ ```sh
+ uv run --with ruff ruff check
+ uv run --with ruff ruff format
+ ```
+
- Lint markdown after each change to the documentation files:
```sh
uv run --with mdlint mdlint check
```
-- Build and validate docs after each change to README.md:
+- Build and validate docs after each change to README.md or `docs/`:
+
+ ```sh
+ uv run build.py
+ ```
+
+ To view the result locally, build and serve it on
{code}(Note|Important):", r'
\1:', body)
+
+ css = (DOCS / "static/style.css").read_text(encoding="utf-8")
+ dark = (DOCS / "static/dark-tokens.css").read_text(encoding="utf-8").strip()
+ # The OS dark preference applies unless the toggle chose light; the toggle's dark always applies.
+ dark_css = (
+ f'@media (prefers-color-scheme: dark) {{\n :root:not([data-theme="light"]) {{\n{dark}\n }}\n}}\n\n'
+ f':root[data-theme="dark"] {{\n{dark}\n}}'
+ )
+ css = css.replace("/* @dark-tokens */", dark_css)
+ css += "\n" + highlight_css()
+ js = (DOCS / "static/app.js").read_text(encoding="utf-8")
+ ctx = {"nav": nav, "body": Markup(body), "title": SITE["name"]}
+
+ page = env.get_template("page.html.j2").render(**ctx, base="", home="#dependency-cooldowns")
+ count = check_links(readme, page, DOCS / "anchors.txt")
+ shutil.rmtree(OUT, ignore_errors=True)
+ (OUT / "assets").mkdir(parents=True)
+ (OUT / "index.html").write_text(page, encoding="utf-8")
+ (OUT / "404.html").write_text(env.get_template("404.html.j2").render(**ctx, base="/", home="/"), encoding="utf-8")
+ (OUT / "assets/style.css").write_text(css, encoding="utf-8")
+ (OUT / "assets/app.js").write_text(js, encoding="utf-8")
+ for font in re.findall(r"url\(fonts/([^)]+)\)", css):
+ check((DOCS / "static/fonts" / font).is_file(), f"style.css references missing font {font}")
+ shutil.copytree(DOCS / "static/fonts", OUT / "assets/fonts")
+ shutil.copy(DOCS / "static/favicon.svg", OUT / "favicon.svg")
+ shutil.copy(DOCS / "CNAME", OUT / "CNAME")
+ (OUT / ".nojekyll").write_text("")
+ print(f"site/: {len(headings)} headings, {count} ids, anchors and internal links OK")
+
+ if args.serve:
+ serve(OUT, args.serve)
+
+
+def serve(directory, port):
+ """For viewing the site locally; rerun the build after editing README.md or docs/."""
+ handler = functools.partial(SimpleHTTPRequestHandler, directory=directory)
+ with ThreadingHTTPServer(("127.0.0.1", port), handler) as httpd, contextlib.suppress(KeyboardInterrupt):
+ print(f"Serving site/ at http://127.0.0.1:{port}/ (Ctrl+C to stop)", flush=True)
+ httpd.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/anchors.txt b/docs/anchors.txt
new file mode 100644
index 0000000..03c27f7
--- /dev/null
+++ b/docs/anchors.txt
@@ -0,0 +1,63 @@
+# Heading ids that existed when cooldowns.dev moved off zensical. Inbound links
+# (cooldowns.dev/#uv and so on) rely on them, so build.py fails if one goes missing.
+# Renaming a heading changes its id; only remove an entry here if you mean to break it.
+h1 dependency-cooldowns
+h2 does-it-actually-work
+h2 python-ecosystem
+h3 uv
+h3 pip
+h4 pip-261
+h3 pipenv
+h3 pipx
+h3 poetry
+h3 pdm
+h3 conda
+h4 micromamba
+h3 pixi
+h3 private-pypi-registries
+h2 javascript-ecosystem
+h3 npm-javascriptnodejs
+h3 pnpm-javascriptnodejs
+h4 pnpm-v11
+h4 pnpm-v10
+h3 yarn-javascriptnodejs
+h3 bun-javascriptnodejs
+h3 deno-javascripttypescript
+h3 npm-check-updates-javascriptnodejs
+h2 rust-ecosystem
+h3 cargo
+h2 ruby-ecosystem
+h3 bundler
+h2 elixir-ecosystem
+h3 hex
+h2 scala-jvm-ecosystem
+h3 scala-steward
+h2 php-ecosystem
+h3 composer
+h2 tool-managers
+h3 mise
+h2 ide-extensions
+h3 vs-code
+h3 cursor
+h2 github-actions
+h3 actions-up
+h2 other-ecosystems
+h2 dependency-update-bots
+h2 security-scanners-and-pr-checks
+h2 registry-level-proxies
+h2 container-images
+h3 relative-durations
+h3 absolute-timestamps
+h2 cooldownssh
+h3 setting-cooldowns
+h3 checking-cooldowns
+h3 usage-in-containers
+h2 quick-reference
+h2 bypassing-cooldowns
+h2 faq
+h3 how-long-should-my-cooldown-be
+h3 would-cooldowns-still-work-if-everyone-adopted-them-am-i-offloading-my-risks-onto-others
+h3 dont-cooldowns-block-security-fixes
+h3 should-cooldowns-replace-lockfiles
+h2 conclusion
+h2 changelog
diff --git a/docs/images/shield.svg b/docs/images/shield.svg
deleted file mode 100644
index 77ef4fb..0000000
--- a/docs/images/shield.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index cb313ab..0000000
--- a/docs/index.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-hide:
- - navigation
-search:
- exclude: true
----
-
---8<-- "README.md"
diff --git a/docs/js/no-search.js b/docs/js/no-search.js
deleted file mode 100644
index 5f919e9..0000000
--- a/docs/js/no-search.js
+++ /dev/null
@@ -1,6 +0,0 @@
-document.addEventListener("keydown", function (e) {
- if ((e.ctrlKey || e.metaKey) && e.key === "k") {
- e.preventDefault();
- e.stopPropagation();
- }
-}, true);
diff --git a/docs/static/app.js b/docs/static/app.js
new file mode 100644
index 0000000..c67f158
--- /dev/null
+++ b/docs/static/app.js
@@ -0,0 +1,326 @@
+// Progressive enhancement only: theme toggle, nav drawer, nav filter, collapsible
+// groups, scrollspy, and copy buttons. Every section stays readable without it.
+(() => {
+ const root = document.documentElement;
+ const $ = (sel, el = document) => el.querySelector(sel);
+ const $$ = (sel, el = document) => [...el.querySelectorAll(sel)];
+ const live = $("#live");
+ const say = (msg) => {
+ live.textContent = "";
+ setTimeout(() => (live.textContent = msg), 60);
+ };
+
+ // Theme: three modes. "system" follows the OS; "light" and "dark" are remembered. The cycle goes
+ // system -> the opposite of the OS theme -> the OS theme, so the first click always changes the look.
+ const themeBtn = $(".theme-btn");
+ const systemDark = matchMedia("(prefers-color-scheme: dark)");
+ const mode = () => root.getAttribute("data-theme") || "system";
+ const nextMode = () => {
+ const os = systemDark.matches ? "dark" : "light";
+ const order = ["system", os === "dark" ? "light" : "dark", os];
+ return order[(order.indexOf(mode()) + 1) % order.length];
+ };
+ const labelTheme = () => {
+ const label = `Theme: ${mode()}. Switch to ${nextMode()}.`;
+ themeBtn.dataset.mode = mode();
+ themeBtn.setAttribute("aria-label", label);
+ themeBtn.setAttribute(themeBtn.hasAttribute("data-tip") ? "data-tip" : "title", label);
+ };
+ themeBtn.addEventListener("click", () => {
+ const next = nextMode();
+ if (next === "system") root.removeAttribute("data-theme");
+ else root.setAttribute("data-theme", next);
+ try {
+ if (next === "system") localStorage.removeItem("theme");
+ else localStorage.setItem("theme", next);
+ } catch {}
+ labelTheme();
+ });
+ systemDark.addEventListener("change", labelTheme);
+ labelTheme();
+
+ // Copy buttons on code blocks.
+ const COPY_ICON =
+ '';
+ for (const block of $$(".code")) {
+ const btn = document.createElement("button");
+ btn.type = "button";
+ btn.className = "copy-btn";
+ btn.innerHTML = `${COPY_ICON}Copy`;
+ const label = btn.lastChild;
+ btn.addEventListener("click", async () => {
+ const text = $("code", block).innerText.replace(/\n$/, "");
+ let ok = true;
+ try {
+ await navigator.clipboard.writeText(text);
+ } catch {
+ ok = false;
+ }
+ label.textContent = ok ? "Copied" : "Copy failed";
+ say(ok ? "Copied to clipboard" : "Copy failed. Select the code and copy it manually.");
+ setTimeout(() => (label.textContent = "Copy"), 1600);
+ });
+ $(".code-head", block).append(btn);
+ }
+
+ // Star and fork counts from the GitHub API, like a README badge. The result is cached for the
+ // tab session to stay far below the API's limit of 60 unauthenticated requests per hour per IP.
+ const repo = $(".repo[data-api]");
+ if (repo) {
+ const compact = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k`.replace(".0k", "k") : String(n));
+ const show = ({ stars, forks }) => {
+ if (!Number.isFinite(stars) || !Number.isFinite(forks)) return;
+ $('[data-stat="stars"]', repo).textContent = compact(stars);
+ $('[data-stat="forks"]', repo).textContent = compact(forks);
+ $(".repo-stats", repo).hidden = false;
+ repo.setAttribute("aria-label", `Source on GitHub: ${compact(stars)} stars, ${compact(forks)} forks`);
+ };
+ let cached = null;
+ try {
+ cached = JSON.parse(sessionStorage.getItem("repo-stats"));
+ } catch {}
+ if (cached) {
+ show(cached);
+ } else {
+ fetch(repo.dataset.api, { headers: { Accept: "application/vnd.github+json" } })
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
+ .then((d) => {
+ const stats = { stars: d.stargazers_count, forks: d.forks_count };
+ try {
+ sessionStorage.setItem("repo-stats", JSON.stringify(stats));
+ } catch {}
+ show(stats);
+ })
+ .catch(() => {}); // Leave the counts hidden.
+ }
+ }
+
+ // Tooltips from title attributes: a short delay for the first, then instant while warm.
+ const tip = Object.assign(document.createElement("div"), { className: "tooltip", id: "tooltip" });
+ tip.setAttribute("role", "tooltip");
+ document.body.append(tip);
+ let shownFor = null;
+ let pendingFor = null;
+ let openTimer = 0;
+ let coolTimer = 0;
+ let warm = false;
+ const placeTip = (el) => {
+ const at = el.getBoundingClientRect();
+ const left = Math.min(at.left + at.width / 2 - tip.offsetWidth / 2, root.clientWidth - tip.offsetWidth - 4);
+ let top = at.bottom + 6;
+ if (top > root.clientHeight - tip.offsetHeight - 4) top = at.top - tip.offsetHeight - 6;
+ tip.style.left = `${Math.round(Math.max(4, left))}px`;
+ tip.style.top = `${Math.round(Math.max(4, top))}px`;
+ };
+ const showTip = (el) => {
+ clearTimeout(coolTimer);
+ pendingFor = null;
+ tip.textContent = el.dataset.tip;
+ tip.dataset.instant = String(warm);
+ tip.style.left = tip.style.top = "0px";
+ placeTip(el);
+ tip.dataset.show = "true";
+ el.setAttribute("aria-describedby", tip.id);
+ shownFor = el;
+ warm = true;
+ };
+ const hideTip = () => {
+ clearTimeout(openTimer);
+ pendingFor = null;
+ if (!shownFor) return;
+ tip.dataset.show = "false";
+ tip.textContent = "";
+ shownFor.removeAttribute("aria-describedby");
+ shownFor = null;
+ clearTimeout(coolTimer);
+ coolTimer = setTimeout(() => (warm = false), 300);
+ };
+ const tipFor = (node) => node?.closest?.("[data-tip]");
+ document.addEventListener("mouseover", (e) => {
+ const el = tipFor(e.target);
+ if (!el) {
+ if (!shownFor?.matches(":focus-visible")) hideTip();
+ return;
+ }
+ if (el === shownFor || el === pendingFor) return;
+ clearTimeout(openTimer);
+ if (warm) {
+ showTip(el);
+ } else {
+ pendingFor = el;
+ openTimer = setTimeout(() => showTip(el), 200);
+ }
+ });
+ document.addEventListener("mouseleave", hideTip);
+ document.addEventListener("focusin", (e) => {
+ const el = tipFor(e.target);
+ if (el?.matches(":focus-visible")) showTip(el);
+ });
+ document.addEventListener("focusout", hideTip);
+ document.addEventListener("click", hideTip);
+ document.addEventListener("toggle", hideTip, true);
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") hideTip();
+ });
+ const reflowTip = () => {
+ if (!shownFor) return;
+ const at = shownFor.getBoundingClientRect();
+ if (at.bottom < 0 || at.top > root.clientHeight) hideTip();
+ else placeTip(shownFor);
+ };
+ addEventListener("scroll", reflowTip, { passive: true });
+ addEventListener("resize", reflowTip);
+ const canHover = matchMedia("(hover: hover)");
+ const adoptTitles = () => {
+ for (const el of $$("[title]")) {
+ el.dataset.tip = el.title;
+ el.removeAttribute("title");
+ }
+ };
+ const restoreTitles = () => {
+ hideTip();
+ for (const el of $$("[data-tip]")) {
+ el.title = el.dataset.tip;
+ delete el.dataset.tip;
+ }
+ };
+ canHover.addEventListener("change", () => (canHover.matches ? adoptTitles() : restoreTitles()));
+ if (canHover.matches) adoptTitles();
+
+ const sidebar = $("#sidebar");
+ if (!sidebar) return; // 404 page has no navigation.
+
+ // Mobile drawer.
+ const menuBtn = $(".menu-btn");
+ const narrow = matchMedia("(max-width: 959px)");
+ const isOpen = () => root.classList.contains("drawer-open");
+ const setDrawer = (open) => {
+ root.classList.toggle("drawer-open", open);
+ menuBtn.setAttribute("aria-expanded", String(open));
+ menuBtn.setAttribute("aria-label", open ? "Close navigation" : "Open navigation");
+ for (const el of [$("main"), $(".site-footer")]) el.inert = open;
+ if (open) sidebar.focus({ preventScroll: true });
+ };
+ menuBtn.addEventListener("click", () => setDrawer(!isOpen()));
+ $(".backdrop").addEventListener("click", () => setDrawer(false));
+ sidebar.addEventListener("click", (e) => {
+ if (e.target.closest("a") && narrow.matches) setDrawer(false);
+ });
+ narrow.addEventListener("change", () => setDrawer(false));
+
+ // Collapsible ecosystem groups.
+ for (const btn of $$(".eco-toggle")) {
+ btn.addEventListener("click", () => {
+ const open = btn.getAttribute("aria-expanded") !== "true";
+ btn.setAttribute("aria-expanded", String(open));
+ btn.closest(".nav-eco").classList.toggle("collapsed", !open);
+ });
+ }
+
+ // Nav filter: matches tool names, heading and subsection text, and ecosystem names.
+ const filter = $("#nav-filter");
+ const empty = $(".nav-empty");
+ filter.addEventListener("input", () => {
+ const q = filter.value.trim().toLowerCase();
+ sidebar.classList.toggle("filtering", q !== "");
+ let total = 0;
+ for (const list of $$(".nav-list", sidebar)) {
+ let shown = 0;
+ for (const li of $$(":scope > li", list)) {
+ let hit = !q || li.dataset.terms.includes(q);
+ if (li.classList.contains("nav-eco")) {
+ const groupHit = hit;
+ for (const t of $$(":scope > ul > li", li)) {
+ t.hidden = !(groupHit || t.dataset.terms.includes(q));
+ hit ||= !t.hidden;
+ }
+ }
+ li.hidden = !hit;
+ shown += hit;
+ }
+ list.hidden = list.previousElementSibling.hidden = shown === 0;
+ total += shown;
+ }
+ empty.hidden = total > 0;
+ });
+ filter.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ const first = $$("li:not([hidden]) > a", sidebar).find((a) => a.offsetParent !== null);
+ if (first) {
+ e.preventDefault();
+ first.click();
+ }
+ } else if (e.key === "Escape" && filter.value) {
+ e.stopPropagation();
+ filter.value = "";
+ filter.dispatchEvent(new Event("input"));
+ }
+ });
+
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ if (isOpen()) {
+ setDrawer(false);
+ menuBtn.focus();
+ }
+ } else if (e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey) {
+ if (e.target.closest("input, textarea, select, [contenteditable]")) return;
+ e.preventDefault();
+ if (narrow.matches) setDrawer(true);
+ filter.focus();
+ }
+ });
+
+ // Scrollspy: highlight the current section in the sidebar and unfold its subsections.
+ // Several tools can share one anchor (Other ecosystems); keep only the first link.
+ const navLinks = new Map();
+ for (const a of $$('a[href^="#"]', sidebar)) if (!navLinks.has(a.hash.slice(1))) navLinks.set(a.hash.slice(1), a);
+ // Headings the sidebar doesn't link leave the previous section highlighted.
+ const heads = $$(".doc :is(h1, h2, h3, h4)[id]").filter((h) => navLinks.has(h.id));
+ const navScroll = $(".nav-scroll");
+ const bar = $(".progress span");
+ const keepVisible = (box, el) => {
+ if (!el?.offsetParent || box.scrollHeight <= box.clientHeight) return;
+ const b = box.getBoundingClientRect();
+ const r = el.getBoundingClientRect();
+ if (r.top < b.top + 24 || r.bottom > b.bottom - 24) box.scrollTop += r.top - b.top - b.height / 3;
+ };
+ let active = null;
+ const spy = () => {
+ const y = (parseFloat(getComputedStyle(root).scrollPaddingTop) || 80) + 8;
+ // At the very bottom, short final sections can't reach the top line; take the last visible heading.
+ const limit = innerHeight + scrollY >= root.scrollHeight - 2 ? innerHeight : y;
+ let h = heads[0];
+ for (const el of heads) {
+ if (el.getBoundingClientRect().top > limit) break;
+ h = el;
+ }
+ if (h !== active) {
+ active = h;
+ for (const a of $$("[aria-current]", sidebar)) a.removeAttribute("aria-current");
+ for (const li of $$(".nav-parent.open", sidebar)) li.classList.remove("open");
+ const link = navLinks.get(h.id);
+ link.setAttribute("aria-current", "location");
+ link.closest(".nav-parent")?.classList.add("open");
+ // In a collapsed group the link is hidden; keep the group's heading in view instead.
+ keepVisible(navScroll, link.offsetParent ? link : link.closest(".nav-eco")?.querySelector(".eco-head a"));
+ }
+ const max = root.scrollHeight - innerHeight;
+ bar.style.transform = `scaleX(${max > 0 ? Math.min(1, scrollY / max) : 0})`;
+ };
+ let queued = false;
+ addEventListener(
+ "scroll",
+ () => {
+ if (queued) return;
+ queued = true;
+ requestAnimationFrame(() => {
+ queued = false;
+ spy();
+ });
+ },
+ { passive: true },
+ );
+ addEventListener("resize", spy);
+ spy();
+})();
diff --git a/docs/static/dark-tokens.css b/docs/static/dark-tokens.css
new file mode 100644
index 0000000..284fe7c
--- /dev/null
+++ b/docs/static/dark-tokens.css
@@ -0,0 +1,40 @@
+ /* Dark palette. build.py emits this block twice: under prefers-color-scheme
+ (unless the toggle chose light) and under [data-theme="dark"]. */
+ color-scheme: dark;
+ --bg: #232529;
+ --bg-side: #1e2023;
+ --surface: #2a2c31;
+ --surface-2: #2c2e33;
+ --code-bg: #2b2d32;
+ --code-head: #303238;
+ --ink: #ece6ea;
+ --ink-2: #c8bfc5;
+ --ink-3: #a69ca3;
+ --line: #383a40;
+ --line-strong: #4b4e55;
+ --link: #d9a441;
+ --link-hover: #f0d298;
+ --link-line: rgba(217, 164, 65, 0.45);
+ --focus: #f0d298;
+ --hover-bg: #2e3035;
+ --active-bg: #3a2b36;
+ --active-ink: #f3d9a6;
+ --callout-bg: #2e2a25;
+ --callout-line: #5a4a30;
+ --ok: #79d4a2;
+ --ok-bg: #1d3a2c;
+ --warn: #f2a66e;
+ --warn-bg: #45301f;
+ --info: #9fbcf3;
+ --info-bg: #27334e;
+ --off: #b7adb3;
+ --off-bg: #36383d;
+ --hl-comment: #a89fa5;
+ --hl-keyword: #e9a3d7;
+ --hl-string: #e8b965;
+ --hl-number: #82d5c4;
+ --hl-name: #a3c1f5;
+ --hl-func: #f2a983;
+ --hl-punct: #c9c0c6;
+ --shadow-pop: 0 14px 36px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
+ --backdrop: rgba(8, 4, 8, 0.6);
\ No newline at end of file
diff --git a/docs/static/favicon.svg b/docs/static/favicon.svg
new file mode 100644
index 0000000..b48c64f
--- /dev/null
+++ b/docs/static/favicon.svg
@@ -0,0 +1,11 @@
+
diff --git a/docs/static/fonts/LICENSE-red-hat-mono.txt b/docs/static/fonts/LICENSE-red-hat-mono.txt
new file mode 100644
index 0000000..d0a53ff
--- /dev/null
+++ b/docs/static/fonts/LICENSE-red-hat-mono.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) RedHatMono-Italic[wght].ttf: Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/docs/static/fonts/LICENSE-red-hat-text.txt b/docs/static/fonts/LICENSE-red-hat-text.txt
new file mode 100644
index 0000000..5db8d6a
--- /dev/null
+++ b/docs/static/fonts/LICENSE-red-hat-text.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) RedHatText-Italic[wght].ttf: Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/docs/static/fonts/red-hat-mono-latin-ext-wght-italic.woff2 b/docs/static/fonts/red-hat-mono-latin-ext-wght-italic.woff2
new file mode 100644
index 0000000..5a2f3c1
Binary files /dev/null and b/docs/static/fonts/red-hat-mono-latin-ext-wght-italic.woff2 differ
diff --git a/docs/static/fonts/red-hat-mono-latin-ext-wght-normal.woff2 b/docs/static/fonts/red-hat-mono-latin-ext-wght-normal.woff2
new file mode 100644
index 0000000..6c2775b
Binary files /dev/null and b/docs/static/fonts/red-hat-mono-latin-ext-wght-normal.woff2 differ
diff --git a/docs/static/fonts/red-hat-mono-latin-wght-italic.woff2 b/docs/static/fonts/red-hat-mono-latin-wght-italic.woff2
new file mode 100644
index 0000000..c291c94
Binary files /dev/null and b/docs/static/fonts/red-hat-mono-latin-wght-italic.woff2 differ
diff --git a/docs/static/fonts/red-hat-mono-latin-wght-normal.woff2 b/docs/static/fonts/red-hat-mono-latin-wght-normal.woff2
new file mode 100644
index 0000000..ee5d5f3
Binary files /dev/null and b/docs/static/fonts/red-hat-mono-latin-wght-normal.woff2 differ
diff --git a/docs/static/fonts/red-hat-text-latin-ext-wght-italic.woff2 b/docs/static/fonts/red-hat-text-latin-ext-wght-italic.woff2
new file mode 100644
index 0000000..6dbfded
Binary files /dev/null and b/docs/static/fonts/red-hat-text-latin-ext-wght-italic.woff2 differ
diff --git a/docs/static/fonts/red-hat-text-latin-ext-wght-normal.woff2 b/docs/static/fonts/red-hat-text-latin-ext-wght-normal.woff2
new file mode 100644
index 0000000..2d36087
Binary files /dev/null and b/docs/static/fonts/red-hat-text-latin-ext-wght-normal.woff2 differ
diff --git a/docs/static/fonts/red-hat-text-latin-wght-italic.woff2 b/docs/static/fonts/red-hat-text-latin-wght-italic.woff2
new file mode 100644
index 0000000..e88ae81
Binary files /dev/null and b/docs/static/fonts/red-hat-text-latin-wght-italic.woff2 differ
diff --git a/docs/static/fonts/red-hat-text-latin-wght-normal.woff2 b/docs/static/fonts/red-hat-text-latin-wght-normal.woff2
new file mode 100644
index 0000000..ea28f1c
Binary files /dev/null and b/docs/static/fonts/red-hat-text-latin-wght-normal.woff2 differ
diff --git a/docs/static/style.css b/docs/static/style.css
new file mode 100644
index 0000000..11d4ad8
--- /dev/null
+++ b/docs/static/style.css
@@ -0,0 +1,1140 @@
+/* Layout: two-column reference docs. Sticky grouped tool nav | reading column with a margin rail
+ that holds each tool's support card. Below 1200px cards move inline; below 960px the nav becomes a drawer. */
+
+/* Self-hosted Red Hat Text and Red Hat Mono (SIL OFL 1.1, from Fontsource 5.3.0; licenses in fonts/).
+ Variable weight 300 to 700, latin and latin-ext only; other scripts fall back to system fonts. */
+@font-face {
+ font-family: "Red Hat Text";
+ font-style: normal;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-text-latin-wght-normal.woff2) format("woff2");
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
+}
+
+@font-face {
+ font-family: "Red Hat Text";
+ font-style: normal;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-text-latin-ext-wght-normal.woff2) format("woff2");
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "Red Hat Text";
+ font-style: italic;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-text-latin-wght-italic.woff2) format("woff2");
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
+}
+
+@font-face {
+ font-family: "Red Hat Text";
+ font-style: italic;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-text-latin-ext-wght-italic.woff2) format("woff2");
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "Red Hat Mono";
+ font-style: normal;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-mono-latin-wght-normal.woff2) format("woff2");
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
+}
+
+@font-face {
+ font-family: "Red Hat Mono";
+ font-style: normal;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-mono-latin-ext-wght-normal.woff2) format("woff2");
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "Red Hat Mono";
+ font-style: italic;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-mono-latin-wght-italic.woff2) format("woff2");
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
+}
+
+@font-face {
+ font-family: "Red Hat Mono";
+ font-style: italic;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url(fonts/red-hat-mono-latin-ext-wght-italic.woff2) format("woff2");
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
+}
+
+:root {
+ color-scheme: light;
+ /* Brand */
+ --plum: #3d0f35;
+ --amber: #b3651d;
+ --amber-light: #f0d298;
+ --brand-gradient: linear-gradient(90deg, #3d0f35 0%, #b3651d 100%);
+ /* Neutrals, tinted toward plum */
+ --bg: #fcfafb;
+ --bg-side: #f6f1f4;
+ --surface: #ffffff;
+ --surface-2: #f4eef1;
+ --code-bg: #f9f5f7;
+ --code-head: #f2ebef;
+ --ink: #241a21;
+ --ink-2: #4f434b;
+ --ink-3: #6a5c65;
+ --line: #e7dee3;
+ --line-strong: #d3c5cd;
+ /* Interaction */
+ --link: #8f6210;
+ --link-hover: #7a4a0c;
+ --link-line: rgba(154, 106, 18, 0.4);
+ --focus: #8a2a78;
+ --hover-bg: #efe6eb;
+ --active-bg: #f1e0ea;
+ --active-ink: #561449;
+ --callout-bg: #fbf3e4;
+ --callout-line: #ead3a6;
+ /* Support status: shape and label carry meaning, color reinforces it */
+ --ok: #1d7a4a;
+ --ok-bg: #e2f2e8;
+ --warn: #a3521a;
+ --warn-bg: #fbeadb;
+ --info: #3558a8;
+ --info-bg: #e6ecf8;
+ --off: #6d5f67;
+ --off-bg: #eee8eb;
+ /* Syntax highlighting (mapped from Pygments token types in build.py) */
+ --hl-comment: #6c5f67;
+ --hl-keyword: #8b1d6f;
+ --hl-string: #7f5100;
+ --hl-number: #0d6e64;
+ --hl-name: #2f55a0;
+ --hl-func: #9b3412;
+ --hl-punct: #5a4d55;
+ /* Misc */
+ --shadow-pop: 0 14px 36px rgba(61, 15, 53, 0.16), 0 2px 6px rgba(61, 15, 53, 0.08);
+ --backdrop: rgba(29, 10, 25, 0.45);
+ /* Type */
+ --font-sans: "Red Hat Text", system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", "Helvetica Neue", Arial,
+ sans-serif;
+ --font-mono: "Red Hat Mono", ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas,
+ "Adwaita Mono", "Liberation Mono", monospace;
+ /* Metrics */
+ --header-h: 56px;
+ --safe-top: env(safe-area-inset-top, 0px);
+ --gutter-l: max(16px, env(safe-area-inset-left, 0px));
+ --gutter-r: max(16px, env(safe-area-inset-right, 0px));
+ --measure: 46rem;
+ --rail-w: 13.5rem;
+ --rail-gap: 2.5rem;
+ --doc-w: var(--measure);
+}
+
+/* @dark-tokens */
+
+/* ---------- Base ---------- */
+
+[hidden] { display: none !important; }
+*, *::before, *::after { box-sizing: border-box; }
+
+html {
+ scroll-padding-top: calc(var(--header-h) + var(--safe-top) + 20px);
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ /* No rubber-band bounce: it reveals a strip of page background above the gradient header. */
+ overscroll-behavior-y: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ html { scroll-behavior: smooth; }
+}
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--ink);
+ font: 16px/1.65 var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Paint the phone's top inset in the header color. */
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0 0 auto;
+ height: var(--safe-top);
+ z-index: 60;
+ background: var(--brand-gradient);
+}
+
+::selection { background: rgba(179, 101, 29, 0.28); }
+
+:focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
+}
+
+a { color: var(--link); }
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
+html:not(.js) .js-only { display: none !important; }
+
+.i { flex: none; }
+
+.skip {
+ position: absolute;
+ top: calc(var(--safe-top) + 8px);
+ left: var(--gutter-l);
+ z-index: 100;
+ padding: 8px 14px;
+ border-radius: 8px;
+ background: var(--surface);
+ color: var(--ink);
+ font-weight: 600;
+ box-shadow: var(--shadow-pop);
+ transform: translateY(-300%);
+}
+
+.skip:focus { transform: none; }
+
+/* ---------- Header ---------- */
+
+.site-header {
+ position: sticky;
+ top: var(--safe-top);
+ z-index: 50;
+ background: var(--brand-gradient);
+ color: #fff;
+}
+
+/* A faint top sheen keeps the band from looking flat. (No glow at the warm end: it pushed the
+ white GitHub label below AA contrast.) */
+.site-header::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(rgba(255, 255, 255, 0.07), transparent 60%);
+ pointer-events: none;
+}
+
+.header-inner {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ max-width: 1480px;
+ height: var(--header-h);
+ margin: 0 auto;
+ padding-left: var(--gutter-l);
+ padding-right: var(--gutter-r);
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 4px 6px 4px 2px;
+ border-radius: 8px;
+ color: #fff;
+ font-size: 1.08rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ text-decoration: none;
+}
+
+.brand .tld { font-weight: 500; opacity: 0.8; }
+.logo-shield { fill: #fff; }
+.logo-ring { stroke: var(--plum); }
+.logo-sweep { fill: var(--plum); }
+
+.header-actions {
+ display: flex;
+ gap: 2px;
+ margin-left: auto;
+}
+
+.icon-btn {
+ display: inline-grid;
+ place-items: center;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ border: 0;
+ border-radius: 8px;
+ background: transparent;
+ color: #fff;
+ cursor: pointer;
+}
+
+.icon-btn:hover { background: rgba(255, 255, 255, 0.16); }
+
+/* Repository link with star and fork counts (fetched at build time). */
+.repo {
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+ height: 40px;
+ margin-right: 4px;
+ padding: 0 10px 0 8px;
+ border-radius: 8px;
+ color: #fff;
+ font-size: 0.8rem;
+ line-height: 1.25;
+ text-decoration: none;
+}
+
+.repo:hover { background: rgba(255, 255, 255, 0.16); }
+.repo .i-github { width: 22px; height: 22px; }
+.repo-text { display: grid; }
+.repo-name { font-weight: 600; }
+
+.repo-stats {
+ display: flex;
+ gap: 10px;
+ opacity: 0.85;
+ font-variant-numeric: tabular-nums;
+}
+
+.repo-stats > span { display: inline-flex; align-items: center; gap: 3px; }
+.repo-stats .i { width: 12px; height: 12px; }
+
+/* Phones: just the GitHub icon; the counts stay in the link's aria-label. */
+@media (max-width: 479px) {
+ .repo { margin-right: 0; padding-inline: 8px; }
+ .repo-text { display: none; }
+}
+.site-header :focus-visible { outline-color: #fff; }
+/* The theme button shows the current mode, which app.js sets: system, light, or dark. */
+.theme-btn .i { display: none; }
+.theme-btn[data-mode="system"] .i-auto,
+.theme-btn[data-mode="light"] .i-sun,
+.theme-btn[data-mode="dark"] .i-moon { display: block; }
+
+/* Reading progress along the bottom edge of the header. */
+.progress {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ height: 2px;
+}
+
+.progress span {
+ display: block;
+ height: 100%;
+ background: var(--amber-light);
+ transform: scaleX(0);
+ transform-origin: left;
+}
+
+/* ---------- Layout ---------- */
+
+.layout {
+ display: grid;
+ grid-template-columns: 17rem minmax(0, 1fr);
+ grid-template-areas: "nav main" "nav footer";
+ max-width: 1480px;
+ margin: 0 auto;
+}
+
+main {
+ grid-area: main;
+ min-width: 0;
+ padding: 44px 48px 96px;
+}
+
+main:focus { outline: none; }
+
+/* ---------- Sidebar ---------- */
+
+.sidebar {
+ grid-area: nav;
+ position: sticky;
+ top: calc(var(--header-h) + var(--safe-top));
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - var(--header-h) - var(--safe-top));
+ height: calc(100dvh - var(--header-h) - var(--safe-top));
+ border-right: 1px solid var(--line);
+ background: var(--bg-side);
+}
+
+.sidebar:focus { outline: none; }
+
+.nav-filter {
+ position: relative;
+ padding: 16px 14px 8px;
+}
+
+.nav-filter .i-search {
+ position: absolute;
+ top: 26px;
+ left: 25px;
+ width: 16px;
+ height: 16px;
+ color: var(--ink-3);
+ pointer-events: none;
+}
+
+.nav-filter input {
+ width: 100%;
+ height: 36px;
+ padding: 0 34px;
+ border: 1px solid var(--line-strong);
+ border-radius: 8px;
+ background: var(--surface);
+ color: var(--ink);
+ font: inherit;
+ font-size: 0.9rem;
+}
+
+.nav-filter input::placeholder { color: var(--ink-3); opacity: 1; }
+.nav-filter input:focus-visible { outline-offset: 0; border-color: var(--focus); }
+.nav-filter input::-webkit-search-cancel-button { display: none; }
+
+.nav-filter kbd {
+ position: absolute;
+ top: 24px;
+ right: 22px;
+ min-width: 20px;
+ padding: 0 5px;
+ border: 1px solid var(--line-strong);
+ border-radius: 4px;
+ color: var(--ink-3);
+ font: 600 0.72rem/18px var(--font-mono);
+ text-align: center;
+}
+
+.nav-scroll {
+ flex: 1;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: 0 10px 40px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--line-strong) transparent;
+}
+
+.nav-title {
+ margin: 18px 8px 4px;
+ color: var(--ink-3);
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+}
+
+.nav-list,
+.nav-list ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.nav-list a {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 4px 8px;
+ border-radius: 6px;
+ color: var(--ink-2);
+ font-size: 0.9rem;
+ line-height: 1.4;
+ text-decoration: none;
+}
+
+.nav-list a:hover { background: var(--hover-bg); color: var(--ink); }
+
+.nav-list a[aria-current] {
+ background: var(--active-bg);
+ color: var(--active-ink);
+ font-weight: 600;
+}
+
+.eco-head {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-top: 2px;
+}
+
+.eco-head a {
+ flex: 1;
+ color: var(--ink);
+ font-weight: 600;
+}
+
+.eco-count {
+ min-width: 18px;
+ color: var(--ink-3);
+ font: 500 0.72rem var(--font-mono);
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+}
+
+html:not(.js) .eco-count { padding-right: 8px; }
+
+.eco-toggle {
+ display: grid;
+ place-items: center;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border: 0;
+ border-radius: 6px;
+ background: none;
+ color: var(--ink-3);
+ cursor: pointer;
+}
+
+.eco-toggle:hover { background: var(--hover-bg); color: var(--ink); }
+.eco-toggle .i { width: 16px; height: 16px; transition: transform 0.15s; }
+.eco-toggle[aria-expanded="false"] .i { transform: rotate(-90deg); }
+
+.nav-eco > ul {
+ margin: 1px 0 6px 13px;
+ padding-left: 6px;
+ border-left: 1px solid var(--line);
+}
+
+.nav-eco.collapsed > ul { display: none; }
+.filtering .nav-eco.collapsed > ul { display: block; }
+
+/* Subsections of a nav item. Without JS they always show; with JS only the current item's. */
+.nav-list .nav-sub {
+ margin: 1px 0 4px 21px;
+ padding-left: 8px;
+ border-left: 1px solid var(--line);
+}
+
+.nav-list .nav-sub a {
+ padding-block: 3px;
+ color: var(--ink-3);
+ font-size: 0.84rem;
+}
+
+.js .nav-sub { display: none; }
+.js .nav-parent.open > .nav-sub { display: block; }
+.filtering .nav-parent.open > .nav-sub { display: none; }
+.nav-parent.open > a:not([aria-current]) { color: var(--ink); font-weight: 600; }
+
+.nav-empty {
+ margin: 12px 8px;
+ color: var(--ink-3);
+ font-size: 0.88rem;
+}
+
+/* ---------- Status dots and chips ---------- */
+
+.dot {
+ flex: none;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+.dot-native { background: var(--ok); }
+.dot-partial {
+ background: linear-gradient(90deg, var(--warn) 50%, transparent 50%);
+ box-shadow: inset 0 0 0 1.5px var(--warn);
+}
+.dot-third-party { box-shadow: inset 0 0 0 1.5px var(--info); }
+.dot-none { height: 2px; border-radius: 1px; background: var(--off); }
+.dot-blank { visibility: hidden; }
+
+.chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 9px 0 8px;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 650;
+ line-height: 1.7;
+ white-space: nowrap;
+}
+
+.chip-native { background: var(--ok-bg); color: var(--ok); }
+.chip-partial { background: var(--warn-bg); color: var(--warn); }
+.chip-third-party { background: var(--info-bg); color: var(--info); }
+.chip-none { background: var(--off-bg); color: var(--off); }
+
+/* ---------- Document ---------- */
+
+.doc {
+ max-width: var(--measure);
+ margin: 0 auto;
+}
+
+.doc h1,
+.doc h2,
+.doc h3,
+.doc h4 {
+ color: var(--ink);
+ text-wrap: balance;
+}
+
+.doc h1 {
+ margin: 0 0 14px;
+ font-size: clamp(2.1rem, 1.6rem + 1.8vw, 2.9rem);
+ font-weight: 800;
+ line-height: 1.08;
+ letter-spacing: -0.03em;
+}
+
+.doc h2 {
+ margin: 72px 0 16px;
+ padding-top: 36px;
+ border-top: 1px solid var(--line);
+ font-size: 1.7rem;
+ font-weight: 750;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+}
+
+.doc h3 {
+ margin: 44px 0 12px;
+ font-size: 1.28rem;
+ font-weight: 700;
+ line-height: 1.3;
+ letter-spacing: -0.01em;
+}
+
+.doc h4 {
+ margin: 32px 0 10px;
+ font-size: 1.05rem;
+ font-weight: 700;
+}
+
+.anchor {
+ margin-left: 0.4rem;
+ color: var(--ink-3);
+ font-weight: 400;
+ text-decoration: none;
+ opacity: 0;
+}
+
+.doc :is(h2, h3, h4):hover .anchor { opacity: 1; }
+
+/* Zero specificity beyond .doc, so generated components can override these without !important. */
+.doc :where(p, ul, ol) { margin: 0 0 16px; }
+.doc :where(ul, ol) { padding-left: 1.35rem; }
+.doc :where(li + li) { margin-top: 6px; }
+.doc li::marker { color: var(--ink-3); }
+
+.doc a {
+ text-decoration-line: underline;
+ text-decoration-color: var(--link-line);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 2px;
+}
+
+.doc a:hover { color: var(--link-hover); text-decoration-color: currentColor; }
+
+.doc :not(pre) > code {
+ padding: 0.1em 0.38em;
+ border: 1px solid var(--line);
+ border-radius: 5px;
+ background: var(--surface-2);
+ font-family: var(--font-mono);
+ font-size: 0.85em;
+ overflow-wrap: anywhere;
+ white-space-collapse: preserve;
+}
+
+.doc strong { font-weight: 650; }
+
+.doc-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 20px;
+ margin: 0 0 32px;
+ padding-bottom: 22px;
+ border-bottom: 1px solid var(--line);
+ color: var(--ink-3);
+ font-size: 0.88rem;
+}
+
+.doc-meta time { font-family: var(--font-mono); font-size: 0.84rem; font-variant-numeric: tabular-nums; }
+
+.callout {
+ padding: 12px 16px;
+ border: 1px solid var(--callout-line);
+ border-radius: 8px;
+ background: var(--callout-bg);
+}
+
+/* Per-tool support card generated from tools.toml. Narrow screens: a strip under the
+ heading. Wide screens (see below): a sticky card in the margin beside its section. */
+.tool-sec { position: relative; }
+
+.tool-card {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px 28px;
+ margin: 0 0 22px;
+ padding: 10px 16px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--surface-2);
+}
+
+.tc-title { display: none; }
+
+.tc-support {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ font-size: 0.88rem;
+ font-weight: 650;
+}
+
+.tc-native { color: var(--ok); }
+.tc-partial { color: var(--warn); }
+.tc-third-party { color: var(--info); }
+
+.tc-rows {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px 28px;
+ margin: 0;
+}
+
+.tc-rows > div { display: grid; gap: 1px; min-width: 0; }
+
+.tc-rows dt {
+ color: var(--ink-3);
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+}
+
+.tc-rows dt:is([title], [data-tip]) {
+ text-decoration: underline dotted var(--line-strong);
+ text-underline-offset: 3px;
+}
+
+.tc-rows dd { margin: 0; font-size: 0.9rem; }
+.tool-card .mono { font-family: var(--font-mono); font-size: 0.86rem; font-variant-numeric: tabular-nums; }
+.tc-note { flex: 1 1 14rem; }
+.tc-note dd { color: var(--ink-2); }
+
+@media (min-width: 1200px) {
+ :root { --doc-w: calc(var(--measure) + var(--rail-gap) + var(--rail-w)); }
+
+ /* Reserve a margin rail to the right of the reading measure. */
+ .doc {
+ max-width: var(--doc-w);
+ padding-right: calc(var(--rail-gap) + var(--rail-w));
+ }
+
+ /* The rail spans the whole section, so the sticky card inside it stops at the section end. */
+ .tool-rail {
+ position: absolute;
+ top: 6px;
+ bottom: 0;
+ left: calc(100% + var(--rail-gap));
+ width: var(--rail-w);
+ }
+
+
+ .tool-card {
+ position: sticky;
+ top: calc(var(--header-h) + var(--safe-top) + 28px);
+ display: block;
+ margin: 0;
+ padding: 14px 0 0;
+ border: 0;
+ border-top: 1px solid var(--line-strong);
+ border-radius: 0;
+ background: none;
+ }
+
+ .tc-title {
+ display: block;
+ margin: 0 0 6px;
+ color: var(--ink);
+ font-size: 1rem;
+ font-weight: 700;
+ line-height: 1.3;
+ }
+
+ .tc-rows {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 9px 18px;
+ margin-top: 14px;
+ }
+
+ .tc-rows > div { display: contents; }
+ .tc-rows dt { padding-top: 3px; }
+ .tc-note dt, .tc-note dd { grid-column: 1 / -1; }
+ .tc-note dt { margin-top: 2px; }
+ .tc-note dd { margin-top: -5px; font-size: 0.86rem; line-height: 1.45; }
+
+ /* Tables may use the slack beside the reading measure; outside tool sections, also the rail's width. */
+ .doc .table-wrap { margin-left: -40px; }
+ .doc > .table-wrap,
+ .doc > .matrix > .table-wrap { margin-right: calc(-1 * (var(--rail-gap) + var(--rail-w))); }
+}
+
+/* Tools of an ecosystem, under its heading. */
+.eco-index {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin: 0 0 8px;
+ padding: 0;
+ list-style: none;
+}
+
+.eco-index li { margin: 0; }
+
+.doc .eco-index a {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 2px 11px 2px 10px;
+ border: 1px solid var(--line-strong);
+ border-radius: 999px;
+ background: var(--surface);
+ color: var(--ink-2);
+ font-size: 0.86rem;
+ text-decoration: none;
+}
+
+.doc .eco-index a:hover { border-color: var(--link); color: var(--ink); }
+
+/* Code blocks */
+.code {
+ margin: 16px 0 20px;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--code-bg);
+}
+
+.code-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 32px;
+ padding: 0 6px 0 14px;
+ border-bottom: 1px solid var(--line);
+ background: var(--code-head);
+}
+
+.code-lang {
+ color: var(--ink-3);
+ font: 600 0.7rem var(--font-mono);
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.code pre {
+ margin: 0;
+ padding: 12px 16px 14px;
+ overflow-x: auto;
+ color: var(--ink);
+ font: 0.84rem/1.6 var(--font-mono);
+ tab-size: 4;
+}
+
+.code pre:focus-visible { outline-offset: -2px; }
+.code code { font: inherit; }
+
+.copy-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ height: 24px;
+ padding: 0 8px;
+ border: 0;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--ink-3);
+ font: 600 0.74rem var(--font-sans);
+ cursor: pointer;
+}
+
+.copy-btn:hover { background: var(--hover-bg); color: var(--ink); }
+
+/* Tables */
+.table-wrap {
+ margin: 16px 0 24px;
+ overflow-x: auto;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--surface);
+}
+
+.table-wrap:focus-visible { outline-offset: 2px; }
+
+.doc table {
+ width: 100%;
+ min-width: 36rem;
+ border-collapse: collapse;
+ font-size: 0.88rem;
+ line-height: 1.5;
+}
+
+.doc th,
+.doc td {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--line);
+ text-align: left;
+ vertical-align: top;
+}
+
+.doc thead th {
+ background: var(--surface-2);
+ color: var(--ink-3);
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.doc tbody:last-child tr:last-child > * { border-bottom: 0; }
+
+/* Support matrix (replaces the README quick reference table). */
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 18px;
+ margin: 4px 0 12px;
+ padding: 0;
+ list-style: none;
+}
+
+.legend li {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ margin: 0;
+}
+
+.legend-n {
+ color: var(--ink-2);
+ font: 600 0.85rem var(--font-mono);
+ font-variant-numeric: tabular-nums;
+}
+
+.matrix-table tr.group th {
+ padding: 7px 12px 6px;
+ background: var(--bg-side);
+ color: var(--ink-3);
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+}
+
+.doc .matrix-table tr.group a { color: inherit; text-decoration: none; }
+.doc .matrix-table tr.group a:hover { color: var(--link-hover); }
+
+.matrix-table th[scope="row"] { font-weight: 600; white-space: nowrap; }
+.doc .matrix-table th[scope="row"] a { color: var(--ink); text-decoration-color: var(--line-strong); }
+.doc .matrix-table th[scope="row"] a:hover { color: var(--link-hover); text-decoration-color: currentColor; }
+.matrix-table .m-support { color: var(--ink-2); min-width: 11rem; }
+.matrix-table tbody tr:not(.group):hover { background: var(--hover-bg); }
+.doc .matrix-table .m-config code { white-space: nowrap; overflow-wrap: normal; }
+
+/* Changelog */
+.doc details {
+ margin: 16px 0;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--surface);
+}
+
+.doc summary {
+ padding: 12px 16px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-weight: 650;
+}
+
+.doc details > ul {
+ margin: 0;
+ padding: 4px 20px 18px 36px;
+ font-size: 0.92rem;
+}
+
+.doc details li strong {
+ font-family: var(--font-mono);
+ font-size: 0.86rem;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+.notfound { padding-block: 80px 120px; }
+
+/* ---------- Footer ---------- */
+
+.site-footer {
+ padding-block: 24px 32px;
+ padding-left: var(--gutter-l);
+ padding-right: var(--gutter-r);
+ border-top: 1px solid var(--line);
+ color: var(--ink-3);
+ font-size: 0.85rem;
+}
+
+/* On the docs page the footer sits in the content column, aligned with the text. */
+.layout > .site-footer {
+ grid-area: footer;
+ min-width: 0;
+ padding-inline: 48px;
+}
+
+.layout > .site-footer .footer-inner { max-width: var(--doc-w); }
+
+.footer-inner {
+ max-width: 1480px;
+ margin: 0 auto;
+}
+
+/* ---------- Medium: slimmer sidebar ---------- */
+
+@media (max-width: 1279px) {
+ .layout { grid-template-columns: 16rem minmax(0, 1fr); }
+}
+
+/* ---------- Narrow: nav becomes a drawer ---------- */
+
+@media (max-width: 959px) {
+ .layout {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas: "main" "footer";
+ }
+
+ html:not(.js) .layout { grid-template-areas: "nav" "main" "footer"; }
+
+ html:not(.js) .sidebar {
+ position: static;
+ height: auto;
+ border-right: 0;
+ border-bottom: 1px solid var(--line);
+ }
+
+ .js .sidebar {
+ position: fixed;
+ top: calc(var(--header-h) + var(--safe-top));
+ bottom: 0;
+ left: 0;
+ z-index: 45;
+ width: min(20rem, 86vw);
+ height: auto;
+ padding-left: env(safe-area-inset-left, 0px);
+ transform: translateX(-100%);
+ visibility: hidden;
+ transition: transform 0.22s ease, visibility 0s linear 0.22s;
+ }
+
+ .drawer-open .sidebar {
+ transform: none;
+ visibility: visible;
+ box-shadow: var(--shadow-pop);
+ transition: transform 0.22s ease;
+ }
+
+ .backdrop {
+ position: fixed;
+ inset: calc(var(--header-h) + var(--safe-top)) 0 0;
+ z-index: 44;
+ background: var(--backdrop);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0s linear 0.2s;
+ }
+
+ .drawer-open .backdrop { opacity: 1; visibility: visible; transition: opacity 0.2s; }
+ .drawer-open body { overflow: hidden; }
+
+ main { padding: 28px var(--gutter-r) 72px var(--gutter-l); }
+ .layout > .site-footer { padding-left: var(--gutter-l); padding-right: var(--gutter-r); }
+ .doc h2 { margin-top: 56px; padding-top: 28px; font-size: 1.5rem; }
+ .doc h3 { margin-top: 36px; }
+}
+
+@media (min-width: 960px) {
+ .menu-btn, .backdrop { display: none; }
+}
+
+/* ---------- Phone: matrix rows stack ---------- */
+
+@media (max-width: 639px) {
+ .matrix .table-wrap { overflow: visible; }
+ .doc .matrix-table { min-width: 0; }
+ .matrix-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
+ .matrix-table tbody, .matrix-table tr.group, .matrix-table tr.group th { display: block; }
+
+ .matrix-table tr:not(.group) {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 4px 12px;
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--line);
+ }
+
+ .doc .matrix-table tr:not(.group) > * { padding: 0; border: 0; }
+ .matrix-table .m-support, .matrix-table .m-config { grid-column: 1 / -1; min-width: 0; }
+ .doc tbody:last-child tr:last-child { border-bottom: 0; }
+ .tool-card, .tc-rows { gap: 8px 22px; }
+}
+
+.tooltip {
+ position: fixed;
+ z-index: 70;
+ max-width: calc(100vw - 1rem);
+ padding: 4px 8px;
+ overflow: hidden;
+ border-radius: 5px;
+ background: var(--ink);
+ color: var(--bg);
+ font-size: 0.75rem;
+ line-height: 1.45;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0ms;
+}
+
+.tooltip[data-show="true"] {
+ opacity: 1;
+ transition: opacity 120ms ease-out;
+}
+
+.tooltip[data-show="true"][data-instant="true"] { transition-duration: 0ms; }
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { transition: none !important; }
+}
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
deleted file mode 100644
index 9ab16f1..0000000
--- a/docs/stylesheets/extra.css
+++ /dev/null
@@ -1,84 +0,0 @@
-.md-search {
- display: none;
-}
-
-/* Same gradient in both schemes, so pin the foreground to white. */
-.md-header {
- background: linear-gradient(90deg, #3d0f35 0%, #b3651d 100%);
- color: #fff;
-}
-
-/* Black-stroked SVG loaded as an Nothing lives at this address. The whole guide is on a single page. Go to the guide or jump to the support matrix.. */
-.md-header .md-logo img,
-[data-md-color-scheme="slate"] .md-logo img {
- filter: invert(1);
-}
-
-/* Zensical's slate background is near black; soften it. */
-[data-md-color-scheme="slate"] {
- --md-default-bg-color: #232529;
- --md-default-bg-color--light: #2325298a;
- --md-default-bg-color--lighter: #23252942;
- --md-default-bg-color--lightest: #23252912;
- --md-code-bg-color: #2f3137;
- --md-code-bg-color--light: #2f3137e6;
- --md-code-bg-color--lighter: #2f31378a;
- --md-footer-bg-color: #1a1b1ede;
- --md-footer-bg-color--dark: #16171a;
-}
-
-/* Amber links glare on dark and fail contrast on white; accent is the hover
- color. The body prefix outranks [data-md-color-primary=amber]. */
-body[data-md-color-scheme="slate"] {
- --md-typeset-a-color: #d9a441;
- --md-accent-fg-color: #f0d298;
- --md-accent-fg-color--transparent: #f0d2981a;
- --md-accent-bg-color: #000000de;
-}
-
-body[data-md-color-scheme="default"] {
- --md-typeset-a-color: #9a6a12;
- --md-accent-fg-color: #7a4a0c;
- --md-accent-fg-color--transparent: #7a4a0c1a;
- --md-accent-bg-color: #ffffff;
-}
-
-/* Title duplicates the header banner. Kept in the DOM for the outline and the
- "Skip to content" target. */
-.md-content__inner > h1:first-child {
- position: absolute;
- width: 1px;
- height: 1px;
- overflow: hidden;
- clip-path: inset(50%);
- white-space: nowrap;
-}
-
-/* Site name and page title are identical, so don't swap them on scroll. */
-.md-header__topic + .md-header__topic {
- display: none;
-}
-
-.md-header__title--active .md-header__topic {
- z-index: 0;
- pointer-events: auto;
- opacity: 1;
- transform: none;
-}
-
-/* Close the gap the hidden title leaves, and align content with the TOC. */
-.md-main__inner {
- margin-top: 0.4rem;
-}
-
-.md-content__inner > h1:first-child + * {
- margin-top: 0;
-}
-
-.md-sidebar--secondary {
- padding-top: 0.9rem;
-}
-
-code {
- white-space-collapse: preserve;
-}
diff --git a/docs/templates/404.html.j2 b/docs/templates/404.html.j2
new file mode 100644
index 0000000..e5c5506
--- /dev/null
+++ b/docs/templates/404.html.j2
@@ -0,0 +1,12 @@
+{% extends "base.html.j2" %}
+{% set title = "Page not found · " ~ site.name %}
+{% block layout %}
+
Page not found
+
{{ dot(t.support) }}{{ labels[t.support] }} support
+| {{ head[0] }} | Status | {{ head[1] }} | {{ head[2] }} |
|---|---|---|---|
| {{ g.name }} | |||
| {{ r.name }} | {{ chip(r.tool.support) }} | {{ r.support }} | {{ r.config }} |