diff --git a/.github/workflows/builder-image.yml b/.github/workflows/builder-image.yml
new file mode 100644
index 00000000..6d385453
--- /dev/null
+++ b/.github/workflows/builder-image.yml
@@ -0,0 +1,90 @@
+# NOT ACTIVE where it sits. Move it into place to enable:
+# git mv themes/vfb-nova/ci/builder-image.yml .github/workflows/builder-image.yml
+# Parked here because the token used to push this branch has no `workflow`
+# scope, and GitHub rejects any push touching .github/workflows/ without it.
+# Pushing from a checkout with normal credentials has no such problem.
+#
+# Until it is moved, virtualflybrain/vfb-static-builder DOES NOT EXIST and the
+# Rancher service has nothing to pull. This has to happen before the switchover.
+#
+# Publishes the image the Rancher build service runs. It lives here, next to
+# deploy.sh, because deploy.sh is what dictates the image's contents — bash,
+# rsync and findutils are in the Dockerfile for its sake. Splitting the two
+# across repos is how rcourt/docsy-builder:Feb2023 ended up frozen on a floating
+# base tag for two years.
+#
+# Uses the org-level Docker Hub secrets (DOCKER_HUB_USER / DOCKER_HUB_PASSWORD),
+# which vfb-status and vfbquery already use to publish into the virtualflybrain
+# namespace. Docker Hub creates the repository on first push.
+name: builder image
+
+on:
+ push:
+ branches: ["master"]
+ paths:
+ - "Dockerfile"
+ - "deploy.sh"
+ - ".github/workflows/builder-image.yml"
+ release:
+ types: [published]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USER }}
+ password: ${{ secrets.DOCKER_HUB_PASSWORD }}
+
+ - name: Metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: virtualflybrain/vfb-static-builder
+ tags: |
+ type=raw,value=latest,enable={{is_default_branch}}
+ type=ref,event=tag
+ type=sha,format=short
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ # Tests the first tag this run actually pushed. The previous version
+ # tested :latest, which metadata-action only emits on the default branch,
+ # so a tag-triggered run would have failed on an image that did not exist.
+ #
+ # Pin the Rancher service to the sha- tag printed at the end, not :latest.
+ - name: Smoke test the published image
+ env:
+ TAGS: ${{ steps.meta.outputs.tags }}
+ run: |
+ set -euo pipefail
+ image=$(printf '%s\n' "$TAGS" | head -n1)
+ echo "testing $image"
+ docker run --rm --entrypoint hugo "$image" version
+ docker run --rm --entrypoint /bin/sh "$image" -c '
+ set -e
+ command -v bash
+ command -v rsync
+ command -v find
+ test "$HUGO_CACHEDIR" = /tmp/hugo_cache
+ test "$(pwd)" = /src
+ test "$(id -u)" = 0
+ '
+ echo "pushed tags:"
+ printf '%s\n' "$TAGS"
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100755
index ad4f7db8..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,6 +0,0 @@
-[submodule "themes/hello-friend-ng"]
- path = themes/hello-friend-ng
- url = https://github.com/rhazdon/hugo-theme-hello-friend-ng
-[submodule "themes/docsy"]
- path = themes/docsy
- url = https://github.com/google/docsy.git
diff --git a/Dockerfile b/Dockerfile
index 4e653452..ce2aa5f9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,78 @@
-FROM klakegg/hugo:ext-alpine
+# Build image for the static site.
+#
+# docker build -t vfb-static-builder .
+# docker run --rm -v "$PWD:/src" -v vfb_cache:/tmp vfb-static-builder
+#
+# Pinned to Hugo 0.122.0 rather than the newest release. That is a deliberate
+# step backwards and the reason is measured, not suspected.
+#
+# The content tree is ~763k generated term pages, ~650k of them in a single
+# directory (content/en/blog/ontologies/vfb), reached over NFSv3 from a Synology
+# volume whose btrfs metadata sits on RAID5 spinning disks. Measured on that
+# volume: plain readdir runs at 5-29k entries/s, but readdir+stat runs at
+# 253/s — ~4ms per inode, a disk seek, nothing cached. NFSv3 uses READDIRPLUS,
+# so the server stats every entry it enumerates. Per-page filesystem cost is
+# therefore the dominant term in this build, far above template or render cost.
+#
+# Hugo's per-page filesystem cost rose at v0.123.0, which rewrote page assembly.
+# Counted with strace over an identical 25k-page corpus on local disk:
+#
+# 0.122.0 75,658 openat 76,997 newfstatat 3.06 stat/page
+# 0.128.0 100,788 openat 101,527 newfstatat 4.04 stat/page
+# 0.140.2 100,813 openat 101,485 newfstatat 4.03 stat/page
+# 0.164.0 100,844 openat 101,488 newfstatat 4.03 stat/page
+#
+# Flat from 0.128 onward, so this is one regression at 0.123, not a drift. At
+# 763k pages and 253 stat/s that extra stat per page is ~49 min on top of a
+# build already costing ~2.6h of pure metadata I/O.
+#
+# 0.122.0 is the last version known to complete this build: the live term pages
+# carry `generator: Hugo 0.122.0` and were written 2026-08-05 17:59. No 0.16x
+# build has ever finished — each ran 17h+ with one thread in uninterruptible
+# sleep on the vfb/ directory and not a single page written.
+#
+# This pin is a workaround. The fix is to stop holding 650k files in one
+# directory (shard vfbterms.py's output) and to pin the volume's btrfs metadata
+# to its SSD cache. When either lands, raise HUGO_VERSION — it is an ARG so that
+# is a one-line change.
+#
+# Debian rather than Alpine, and the tarball rather than ghcr.io/gohugoio/hugo:
+# that registry has no v0.122.0 tag (it begins around v0.140), and the extended
+# release binary is glibc-linked — `ldd` gives /lib64/ld-linux-x86-64.so.2 plus
+# libstdc++.so.6 — so musl with gcompat is not a safe host for it.
+#
+# Extended edition, matching the rcourt/docsy-builder:Feb2023 that produced the
+# last good site. The theme ships plain CSS and needs no Sass, so the standard
+# edition would also serve and is statically linked; keeping extended here
+# changes one variable at a time relative to the last build known to work.
+#
+# The theme declares min_version 0.128.0 for `[pagination] pagerSize`, so 0.122
+# emits one WARN and falls back to 10 items per page. Nothing on the site
+# paginates today, so that is latent rather than active.
+FROM debian:bookworm-slim
-RUN apk add git && \
- git config --global --add safe.directory /src
+ARG HUGO_VERSION=0.122.0
+
+# bash and find are already in the base image. rsync is for deploy.sh, which
+# stages the build and syncs it into the served directory rather than writing
+# there directly. libstdc++6 is what the extended Hugo binary links against.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends rsync ca-certificates libstdc++6 \
+ && rm -rf /var/lib/apt/lists/*
+
+ADD https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz /tmp/hugo.tgz
+RUN tar xzf /tmp/hugo.tgz -C /usr/local/bin hugo \
+ && rm -f /tmp/hugo.tgz \
+ && hugo version
+
+# Deliberately root. The published tree on the NFS volume is owned by root from
+# the klakegg-era builds, so an unprivileged rsync could not overwrite it.
+USER root
+
+# deploy.sh uses ${HUGO_CACHEDIR:-/tmp/hugo_cache}, so this must be set
+# explicitly: Rancher mounts the workspace at /src and the cache volume at /tmp.
+ENV HUGO_CACHEDIR=/tmp/hugo_cache
+WORKDIR /src
+
+ENTRYPOINT []
+CMD ["/bin/bash", "/src/deploy.sh"]
diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss
deleted file mode 100644
index cf839350..00000000
--- a/assets/scss/_styles_project.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- Project-level Docsy overrides.
-*/
-
-/* Image-map figures escape the main column at wide viewports because the
-
is given a fixed pixel width via inline CSS, but the surrounding
- .taxonomy-terms-cloud sidebar in