git » homepage.git » commit 09fa575

Refresh homepage and reproducible build

author Alan Dipert
2026-07-16 16:10:12 UTC
committer Alan Dipert
2026-07-16 16:10:12 UTC
parent 66d9dfb0effab7713264b6a60474b5c8300b855b

Refresh homepage and reproducible build

.containerignore +12 -0
.gitignore +1 -0
Containerfile +27 -0
Makefile +72 -11
README.md +40 -13
md/GridCalc.md +1 -1
md/Home.md +13 -12
md/HomepageDesign.md +5 -5
md/Klong.md +1 -1
md/ProgrammablePhones.md +3 -3
md/RandallRDipert.md +1 -2
md/TechWorks.md +0 -2
md/WhoIsGod.md +2 -4
tools/build_page.py +7 -0
tools/buildinfo.py +19 -8
tools/check_links.py +30 -14
tools/gen_index.py +2 -4
tools/normalize_output.py +43 -0
tools/verify_reproducible.py +64 -0
tpl/head.html +7 -2
tpl/style.css +396 -126

diff --git a/.containerignore b/.containerignore
new file mode 100644
index 0000000..4281de1
--- /dev/null
+++ b/.containerignore
@@ -0,0 +1,12 @@
+.git
+out
+OUT
+drafts
+vendor
+__pycache__
+*.pyc
+*~
+.#*
+#*#
+*.swp
+.DS_Store
diff --git a/.gitignore b/.gitignore
index 8126d18..a76c8e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ tramp
 .\#*
 
 /out/
+/.build/
 __pycache__/
diff --git a/Containerfile b/Containerfile
new file mode 100644
index 0000000..94a235d
--- /dev/null
+++ b/Containerfile
@@ -0,0 +1,27 @@
+FROM docker.io/library/debian:13-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd
+
+ARG DEBIAN_SNAPSHOT=20260715T000000Z
+
+RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/debian.sources \
+    && printf '%s\n' \
+       "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT} trixie main" \
+       > /etc/apt/sources.list \
+    && apt-get update \
+    && apt-get install --yes --no-install-recommends \
+       ca-certificates \
+       cmark-gfm \
+       coreutils \
+       curl \
+       git \
+       make \
+       python3 \
+       rsync \
+    && rm -rf /var/lib/apt/lists/*
+
+ENV LANG=C.UTF-8 \
+    LC_ALL=C.UTF-8 \
+    TZ=UTC
+
+WORKDIR /workspace
+
+CMD ["make", "fresh"]
diff --git a/Makefile b/Makefile
index f014fcf..0a06c67 100644
--- a/Makefile
+++ b/Makefile
@@ -1,26 +1,43 @@
-.PHONY: all assets clean deploy help tree check-git-clean check-links
+.PHONY: all assets clean deploy help tree check-git-clean check-links check-tools \
+	fresh container-image container-build container-check-links \
+	verify-reproducible verify-reproducible-inner
 
 SRC := md
-OUT := out
+OUT ?= out
 HEAD := tpl/head.html
 FOOT := tpl/foot.html
 CSS := tpl/style.css
-MD2HTML ?= /usr/bin/cmark-gfm
+MD2HTML ?= cmark-gfm
 CMARK_FLAGS := --to html --extension table --validate-utf8 --unsafe
 PYTHON ?= python3
 BUILDINFO := tools/buildinfo.py
 BUILD_PAGE := tools/build_page.py
 GEN_INDEX := tools/gen_index.py
+NORMALIZE_OUTPUT := tools/normalize_output.py
+VERIFY_OUTPUT := tools/verify_reproducible.py
 INDEX_HTML := $(OUT)/Index.html
 DEPLOY_HOST ?= arsien23i2@dreamhost:tailrecursion.com/~alan
 SITE_BASE_URL ?= https://tailrecursion.com/~alan/
+CONTAINER_ENGINE ?= podman
+BUILDER_IMAGE ?= localhost/homepage-builder:2026-07-15
+SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct HEAD 2>/dev/null)
+STAGE_OUT ?= /tmp/homepage-out
+
+export SOURCE_DATE_EPOCH
 
 MD_FILES := $(shell find $(SRC) -type f -name '*.md' | LC_ALL=C sort)
 HTML := $(patsubst $(SRC)/%.md,$(OUT)/%.html,$(MD_FILES))
 HOME_MD := md/Home.md
 UPDATES_FEED := $(OUT)/updates.xml
 
-all: assets $(OUT)/style.css $(HTML) $(INDEX_HTML) $(UPDATES_FEED)
+all: check-tools assets $(OUT)/style.css $(HTML) $(INDEX_HTML) $(UPDATES_FEED)
+
+check-tools:
+	@command -v "$(MD2HTML)" >/dev/null 2>&1 || { \
+		echo 'Missing Markdown renderer: $(MD2HTML)' >&2; \
+		echo 'Use make container-build, or install cmark-gfm and set MD2HTML.' >&2; \
+		exit 1; \
+	}
 
 $(OUT)/style.css: $(CSS)
 	mkdir -p $(OUT)
@@ -35,7 +52,9 @@ $(OUT)/%.html: $(SRC)/%.md $(HEAD) $(FOOT) tools/mdlink2html.py $(BUILD_PAGE) $(
 
 assets:
 	mkdir -p $(OUT)
-	rsync -a --include='*/' --exclude='*.md' --exclude='*.MD' --prune-empty-dirs $(SRC)/ $(OUT)/
+	rsync -a --include='*/' --exclude='*.md' --exclude='*.MD' \
+		--exclude='*~' --exclude='.#*' --exclude='#*#' --exclude='*.swp' \
+		--exclude='.DS_Store' --prune-empty-dirs $(SRC)/ $(OUT)/
 
 $(INDEX_HTML): $(HTML) $(GEN_INDEX) $(HEAD) $(FOOT) $(BUILDINFO) | $(OUT)
 	$(PYTHON) $(GEN_INDEX) $@ $(SRC)
@@ -44,7 +63,46 @@ $(UPDATES_FEED): $(HOME_MD) tools/gen_updates_rss.py tools/mdlink2html.py tools/
 	SITE_BASE_URL="$(SITE_BASE_URL)" $(PYTHON) tools/gen_updates_rss.py "$(HOME_MD)" "$@"
 	$(PYTHON) tools/validate_rss.py "$@"
 
-deploy: check-git-clean assets all
+fresh:
+	rm -rf "$(STAGE_OUT)"
+	$(MAKE) OUT="$(STAGE_OUT)" all
+	$(PYTHON) $(NORMALIZE_OUTPUT) "$(STAGE_OUT)" "$(SOURCE_DATE_EPOCH)"
+	mkdir -p "$(OUT)"
+	rsync -a --delete --chmod=F644,D755 "$(STAGE_OUT)/" "$(OUT)/"
+	rm -rf "$(STAGE_OUT)"
+
+container-image:
+	$(CONTAINER_ENGINE) build --pull=missing --tag "$(BUILDER_IMAGE)" --file Containerfile .
+
+container-build: container-image
+	mkdir -p "$(OUT)"
+	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
+		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
+		--env SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \
+		"$(BUILDER_IMAGE)" make fresh OUT="$(OUT)"
+
+container-check-links: container-image
+	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
+		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
+		"$(BUILDER_IMAGE)" make check-links
+
+verify-reproducible: container-image
+	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
+		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
+		--env SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \
+		"$(BUILDER_IMAGE)" make verify-reproducible-inner
+
+verify-reproducible-inner:
+	rm -rf /tmp/homepage-out-a /tmp/homepage-out-b
+	$(MAKE) OUT=/tmp/homepage-out-a all
+	$(MAKE) OUT=/tmp/homepage-out-b all
+	$(PYTHON) $(NORMALIZE_OUTPUT) /tmp/homepage-out-a "$(SOURCE_DATE_EPOCH)"
+	$(PYTHON) $(NORMALIZE_OUTPUT) /tmp/homepage-out-b "$(SOURCE_DATE_EPOCH)"
+	$(PYTHON) $(VERIFY_OUTPUT) /tmp/homepage-out-a /tmp/homepage-out-b
+	rm -rf /tmp/homepage-out-a /tmp/homepage-out-b
+
+deploy: check-git-clean
+	$(MAKE) container-build
 	@if [ -z "$(DEPLOY_HOST)" ]; then \
 		echo "DEPLOY_HOST is not set"; \
 		exit 1; \
@@ -61,11 +119,14 @@ clean:
 
 help:
 	@echo 'Static site generator targets:'
-	@echo '  make assets   - copy non-Markdown assets into $(OUT)/'
-	@echo '  make          - build HTML pages (same as make all)'
-	@echo '  make deploy   - rsync $(OUT)/ to $$DEPLOY_HOST'
-	@echo '  make tree     - count source and generated files'
-	@echo '  make clean    - remove $(OUT)/'
+	@echo '  make container-build       - reproducibly build $(OUT)/ with Podman'
+	@echo '  make verify-reproducible   - compare two isolated container builds'
+	@echo '  make container-check-links - check outbound links with container tools'
+	@echo '  make assets                - copy non-Markdown assets into $(OUT)/'
+	@echo '  make                       - build locally (requires cmark-gfm)'
+	@echo '  make deploy                - container-build, then publish from the host'
+	@echo '  make tree                  - count source and generated files'
+	@echo '  make clean                 - remove $(OUT)/'
 	@echo '  (deploy refuses to run with a dirty git worktree)'
 
 check-git-clean:
diff --git a/README.md b/README.md
index 04bf84e..bc547bb 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,52 @@
 # Homepage static site generator
 
-This converts Markdown from `md/` to HTML in `out/` and can deploy the result to your shared host.
+This converts Markdown from `md/` to a static site in `out/`. Podman provides the pinned build environment; the host only needs Podman, Make, and Git.
 
 > AI helpers: read `AGENTS.md` before making changes.
 
-## Usage
-make assets && make
+## Reproducible build
+
+```sh
+make container-build
 xdg-open out/Index.html  # or open on macOS
+```
 
-> Note: `make deploy` requires a clean git worktree. Commit or stash changes first if you plan to publish.
+The builder uses rootless Podman and leaves `out/` owned by the invoking user. It builds into a fresh staging directory, normalizes timestamps and permissions, and then replaces the generated tree so stale files cannot survive between builds.
 
-## Deploy
-make deploy  # uses rsync to upload to your shared host defined in DEPLOY_HOST
+To prove that two isolated builds produce identical files and metadata:
+
+```sh
+make verify-reproducible
+```
+
+The `Containerfile` pins both the Debian base-image digest and a dated Debian package snapshot. Update those two values together when intentionally refreshing the toolchain, rebuild with `make container-image`, and rerun the reproducibility check.
+
+## Local fallback
+
+The Make pipeline can still run without Podman when `cmark-gfm`, Python 3, rsync, Make, and Git are installed locally:
 
-## Build cmark
+```sh
+make assets && make
+```
+
+Set `MD2HTML` if the renderer is not on `PATH`:
+
+```sh
+make MD2HTML=bin/cmark-gfm
+```
+
+Outbound links can be checked with the containerized toolchain:
 
-This project defaults to `/usr/bin/cmark-gfm`. If it is missing on your system you can build a local copy:
+```sh
+make container-check-links
+```
+
+This check contacts external sites and is diagnostic; it is not part of the byte-for-byte build guarantee.
+
+## Deploy
 
-git clone https://github.com/github/cmark-gfm vendor/cmark-gfm
-cmake -S vendor/cmark-gfm -B vendor/cmark-gfm/build
-cmake --build vendor/cmark-gfm/build
-cp vendor/cmark-gfm/build/src/cmark-gfm bin/
+```sh
+make deploy
+```
 
-Then rerun make with `MD2HTML=bin/cmark-gfm`.
+Deploy first runs the reproducible container build, then uses host-side rsync and Git credentials to publish to `DEPLOY_HOST`. It refuses to run unless the Git worktree is clean. The container never receives SSH keys or Git credentials.
diff --git a/md/GridCalc.md b/md/GridCalc.md
index 902dda7..6adac97 100644
--- a/md/GridCalc.md
+++ b/md/GridCalc.md
@@ -24,7 +24,7 @@ The result is a grid where every input and operation becomes a cell. The grid is
 
 ## The seed: Forth and touchscreens
 
-I learned some [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)) around 2011, and what stuck with me was not just the stack but the flatness of the syntax: words, space-separated, linear, no parentheses, no precedence, a stream of tokens in plain sight.
+I learned some [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)) around 2011. What stuck with me was the flatness of the syntax as much as the stack: words, space-separated, linear, no parentheses, no precedence, a stream of tokens in plain sight.
 
 It felt like a language that wanted to be handled like physical tokens, where you drag words around, reorder them, insert one between two others, and treat the code as something you can literally move with your hands.
 
diff --git a/md/Home.md b/md/Home.md
index 81af383..ec35355 100644
--- a/md/Home.md
+++ b/md/Home.md
@@ -3,23 +3,24 @@
 
 Hello, and welcome to the personal [homepage](https://en.wikipedia.org/wiki/Home_page) and [wiki](https://en.wikipedia.org/wiki/Wiki) of Alan Dipert!
 
+## Explore
 
-* [ConsultingPractice](./ConsultingPractice.md) is about the professional software engineering consulting services I provide.
-* [PersonalBackground](./PersonalBackground.md) is about my personal history and interests.
-* [TechWorks](./TechWorks.md) is a list of technical presentations, workshops, papers, and other works I've created or helped to create.
-* [Writings](./Writings.md) is where I collect personal essays.
+* [Consulting](./ConsultingPractice.md)
+* [Background](./PersonalBackground.md)
+* [Technical work](./TechWorks.md)
+* [Essays](./Writings.md)
 
-The best way to contact me is by email at [alan@tailrecursion.com](mailto:alan@tailrecursion.com). I also maintain a [CoffeeTime](./CoffeeTime.md) to meet with folks. In addition, you can find me on the follow social media websites:
+## Connect
 
+* [Email](mailto:alan@tailrecursion.com)
+* [Coffee time](./CoffeeTime.md)
+* [GitHub](https://github.com/alandipert/)
+* [LinkedIn](https://www.linkedin.com/in/alandipert/)
+* [Hacker News](https://news.ycombinator.com/user?id=wooby)
+* [Twitter](https://twitter.com/alandipert)
 
-* [Twitter](https://twitter.com/alandipert) is where I post infrequently, usually about technology.
-* [GitHub](https://github.com/alandipert/) is where I typically collaborate on open source software and [star interesting projects](https://github.com/alandipert?tab=stars) I run across.
-* [LinkedIn](https://www.linkedin.com/in/alandipert/) is where I maintain my work history and professional connections.
-* [Hacker News](https://news.ycombinator.com/user?id=wooby) is where I occasionally submit links and engage The Internet in discussion.
 
-
-Updates
--------
+## Updates
 [Subscribe to the Updates RSS feed](./updates.xml).
 
 | Date       | Note                                                                                                                                       |
diff --git a/md/HomepageDesign.md b/md/HomepageDesign.md
index 505d765..424f44e 100644
--- a/md/HomepageDesign.md
+++ b/md/HomepageDesign.md
@@ -1,15 +1,15 @@
 # HomepageDesign
 
-This page explains how my homepage moved from a Zim-based wiki to the minimal static generator that now publishes these pages.
-
 ## Zim beginnings
 
 For years I maintained this site directly out of [Zim](https://zim-wiki.org/). Zim made it easy to sketch ideas, follow wiki links, and export the whole notebook as HTML whenever I wanted to share an update. The design and layout you see in older posts originated in that exported theme, including the ornate background and the habit of organizing pages like a personal wiki.
 
 ## Static site generator today
 
-Today the site is rebuilt with a tiny static site generator that lives right alongside the content. Everything under `md/` is authored in Markdown, and a `make` pipeline rewrites wiki links, runs [`cmark-gfm`](https://github.com/github/cmark-gfm) to render HTML, wraps the result in a shared template, and copies any non-Markdown assets into the `out/` directory. Deploys are a simple `make deploy`, which rsyncs the generated files to my DreamHost account. The build footer shows who published the site, when, and from which git revision.
+Today the site is rebuilt with a tiny static site generator that lives right alongside the content. Everything under `md/` is authored in Markdown. A `make` pipeline rewrites wiki links, runs [`cmark-gfm`](https://github.com/github/cmark-gfm) to render HTML, wraps the result in a shared template, and copies non-Markdown assets into the `out/` directory.
+
+The supported build runs inside a pinned Podman container. It fixes the renderer and operating-system packages, normalizes generated timestamps and permissions, and builds into a clean staging directory. The same commit produces the same output without installing the site toolchain on the host. Deploys remain a simple `make deploy`: the container builds the site, then the host rsyncs it to my DreamHost account. The footer records deterministic source revisions instead of the builder's machine and wall-clock time.
 
-## Why change?
+## Why I left Zim
 
-Moving to this generator means the whole site is version-controlled and scriptable. I can check diffs, branch drafts, and automate deployment from any machine (or even Cosmopolitan builds someday). It keeps Zim's information architecture while making the publishing workflow reproducible. If you're curious about the raw files or the build logic, the [homepage repo](https://tailrecursion.com/git-arr/r/homepage.git/) is public.
+Moving to this generator means the whole site is version-controlled and scriptable. I can check diffs, branch drafts, and automate deployment from any machine (or even Cosmopolitan builds someday). It keeps Zim's information architecture while making the publishing workflow reproducible. The [homepage repo](https://tailrecursion.com/git-arr/r/homepage.git/) is public.
diff --git a/md/Klong.md b/md/Klong.md
index cd96858..689dc32 100644
--- a/md/Klong.md
+++ b/md/Klong.md
@@ -30,7 +30,7 @@ A few weeks later I understood why people sometimes call array languages "write
 
 ## Finding Klong
 
-About five years ago I found [Klong](https://t3x.org/klong/), a small array language inspired by [K](https://en.wikipedia.org/wiki/K_(programming_language)). It had the dense expressions and whole-array operations I remembered liking.
+Later I found [Klong](https://t3x.org/klong/), a small array language inspired by [K](https://en.wikipedia.org/wiki/K_(programming_language)). It had the dense expressions and whole-array operations I remembered liking.
 
 Klong also addressed my specific objection. Its syntax is unambiguous: fold, convergence, iteration, and looping have distinct spellings. Klong came with a clear [reference manual](https://t3x.org/klong/klong-ref.txt.html), Holm's book, and a small, free C interpreter I could read and hack on.
 
diff --git a/md/ProgrammablePhones.md b/md/ProgrammablePhones.md
index 24a432b..1ebecd1 100644
--- a/md/ProgrammablePhones.md
+++ b/md/ProgrammablePhones.md
@@ -14,7 +14,7 @@ Until around December, most programmers I knew, myself included, had plenty of i
 
 I've always been a little frustrated by the fact that I carried a powerful computer on my person at almost all times, and yet I couldn't easily write software for it.
 
-That's changing. The device in my pocket is starting to feel less like a sealed appliance and more like a personal computer again: something I can shape around my own habits, sensors, workflows, and annoyances.
+That's changing. The device in my pocket is starting to behave like a personal computer I can shape around my own habits, sensors, workflows, and annoyances.
 
 ## The App I Actually Wanted
 
@@ -42,7 +42,7 @@ I love it. The computers we physically carry, physically touch, and stare at all
 
 ## The Hard Part Is Still Hard
 
-There are problems, of course, but as far as I can tell they are those inherent to AI coding generally. Currently, AI agents work well with established languages, toolkits, and platforms. But like any software, mobile apps are vulnerable to self-induced bloat that can eventually bring any software project, especially an aggressively AI-coded one, to a grinding halt, as state space and complexity exceed both the context window of the LLM and the time budget of even automated QA.
+The problems are the same ones I see in AI coding generally. Currently, AI agents work well with established languages, toolkits, and platforms. But like any software, mobile apps are vulnerable to self-induced bloat that can eventually bring any software project, especially an aggressively AI-coded one, to a grinding halt, as state space and complexity exceed both the context window of the LLM and the time budget of even automated QA.
 
 UI and interaction toolkits on mobile might be especially prone to this, due to added complexity from touch interaction, multiple-device support, and screen size and capability differences.
 
@@ -58,4 +58,4 @@ I'm also taking (another) hard look at [Backus's FL](https://worrydream.com/refs
 
 That is the tradeoff I keep coming back to. AI lowers the cost of getting something working, but it does not remove the need for taste, discipline, testing, and some theory of correctness.
 
-Still, this feels like a good trade. The next generation of mobile personal computing is not just better apps from companies. It is useful software made by the people carrying the devices, for the lives they are already living.
+Still, this feels like a good trade. The next generation of mobile personal computing includes useful software made by the people carrying the devices, for the lives they are already living.
diff --git a/md/RandallRDipert.md b/md/RandallRDipert.md
index ab0a283..b2ab6b0 100644
--- a/md/RandallRDipert.md
+++ b/md/RandallRDipert.md
@@ -1,10 +1,9 @@
 # RandallRDipert
 Created Tuesday 17 November 2020
 
-Randall Roy Dipert (1951-2019) was my father. This page honors his memory and legacy.
+Randall Roy Dipert (1951-2019) was my father.
 
 
 * [Randall Dipert on Wikipedia](https://en.wikipedia.org/wiki/Randall_Dipert)
 * [WellReadUndergrad](./WellReadUndergrad.md) is a list of philosophy readings my dad suggested for undergraduate students.
 * [RandallRDipertEulogy](./RandallRDipertEulogy.md) is the eulogy I delivered at his memorial service on July 5, 2019.
-
diff --git a/md/TechWorks.md b/md/TechWorks.md
index 1fc38e2..e62fc5a 100644
--- a/md/TechWorks.md
+++ b/md/TechWorks.md
@@ -1,5 +1,4 @@
 # TechWorks
-The following are technical presentations, workshops, papers, and other works I've created or helped to create.
 
 | Date       | Venue                                                                                         | Title                                                                                                                                                                                                                                                                       | Format         | Role               | With                       |
 |:-----------|:----------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------|:---------------------------|
@@ -37,4 +36,3 @@ The following are technical presentations, workshops, papers, and other works I'
 | 2011-08-25 | The Ruby Hoedown V                                                                            | Functional Programming with Ruby                                                                                                                                                                                                                                            | Presentation   | Presenter          |                            |
 | 2005-01-14 | Linux.com                                                                                     | [My workstation OS: NetBSD](https://web.archive.org/web/20080201010726/http://www.linux.com/articles/41523)                                                                                                                                                                 | Article        | Author             |                            |
 
-
diff --git a/md/WhoIsGod.md b/md/WhoIsGod.md
index 9177bde..ce683dc 100644
--- a/md/WhoIsGod.md
+++ b/md/WhoIsGod.md
@@ -2,15 +2,13 @@
 
 - Added November 19th 2025
 
-This page serves as the hub for the "Who is God?" study notes, collecting links to the reference tables on His names and covenants and showing how they fit together. It's written for quick scanning so you can jump into the resource that answers your immediate question.
-
-## Quick Links
+## Names and covenants
 
 * [NamesOfGod](./NamesOfGod.md) — master table of Hebrew, Aramaic, and Greek divine titles with NIV/ESV references, interpretations, and significance.
 * [CovenantNamesOfGod](./CovenantNamesOfGod.md) — focused list of the covenant-compound YHWH names (Jireh, Rapha, etc.) with their narrative moments and New Testament fulfillment.
 * [CovenantsOfGod](./CovenantsOfGod.md) — overview of the major biblical covenants, their signs, promises, and how Christ completes them.
 
-## How the resources relate
+## Reading order
 
 1. **Start with NamesOfGod** to see the breadth of God's self-revelation. Each title anchors a doctrine (holiness, provision, lordship, triune life) and includes linked Scriptures.
 2. **Drill into CovenantNamesOfGod** when you want the devotional names derived from specific redemptive episodes (Genesis 22, Exodus 17, Judges 6, etc.). These show God's character applied to urgent needs (provision, healing, peace, righteousness, presence, shepherding, sanctification).
diff --git a/tools/build_page.py b/tools/build_page.py
index 4a26fac..634ee94 100755
--- a/tools/build_page.py
+++ b/tools/build_page.py
@@ -6,6 +6,7 @@ from __future__ import annotations
 import argparse
 import html
 import os
+import re
 import shlex
 import subprocess
 import sys
@@ -161,10 +162,16 @@ def compute_root_prefix(output_path: Path, out_dir: Path) -> str:
     return "../" * depth
 
 
+def page_class(page_title: str) -> str:
+    slug = re.sub(r"[^a-z0-9]+", "-", page_title.lower()).strip("-")
+    return f"page-{slug or 'untitled'}"
+
+
 def render_template(template_path: Path, root_prefix: str, page_title: str, build_info_html: str) -> str:
     template = template_path.read_text()
     rendered = template.replace("@ROOT@", root_prefix)
     rendered = rendered.replace("@TITLE@", html.escape(page_title))
+    rendered = rendered.replace("@PAGE_CLASS@", page_class(page_title))
     return rendered.replace("@BUILDINFO@", build_info_html)
 
 
diff --git a/tools/buildinfo.py b/tools/buildinfo.py
index f4fca87..66a599b 100755
--- a/tools/buildinfo.py
+++ b/tools/buildinfo.py
@@ -4,12 +4,10 @@
 from __future__ import annotations
 
 import argparse
-import getpass
 import html
 import os
-import socket
 import subprocess
-from datetime import datetime
+from datetime import datetime, timezone
 from pathlib import Path
 from typing import Optional
 
@@ -60,10 +58,22 @@ def resolve_commit(repo_root: Path, target: Optional[str]) -> str:
     return run_git(["rev-parse", "HEAD"], cwd=repo_root)
 
 
+def resolve_build_datetime(repo_root: Path, site_commit: str) -> datetime:
+    raw_epoch = os.environ.get("SOURCE_DATE_EPOCH")
+    if raw_epoch is None:
+        raw_epoch = run_git(["show", "-s", "--format=%ct", site_commit], cwd=repo_root)
+    try:
+        epoch = int(raw_epoch)
+    except ValueError as exc:
+        raise RuntimeError(f"Invalid SOURCE_DATE_EPOCH: {raw_epoch!r}") from exc
+    return datetime.fromtimestamp(epoch, tz=timezone.utc)
+
+
 def build_info(target: Optional[str] = None) -> str:
     script_dir = Path(__file__).resolve().parent
     repo_root = resolve_repo_root(script_dir)
 
+    site_commit = resolve_commit(repo_root, None)
     commit_sha = resolve_commit(repo_root, target)
     if not commit_sha:
         raise RuntimeError("Failed to resolve commit")
@@ -75,11 +85,10 @@ def build_info(target: Optional[str] = None) -> str:
     commit_dt = datetime.fromisoformat(commit_iso)
     commit_date_str = format_month_day_year(commit_dt)
 
-    user = getpass.getuser()
-    host = socket.gethostname()
-    build_dt = datetime.now().astimezone()
+    build_dt = resolve_build_datetime(repo_root, site_commit)
     build_date_str = format_month_day_year(build_dt)
-    build_time_str = build_dt.strftime("%I:%M %p %Z").lstrip("0")
+    build_time_str = build_dt.strftime("%H:%M UTC")
+    site_short_sha = run_git(["rev-parse", "--short", site_commit], cwd=repo_root)
 
     commit_url = f"https://tailrecursion.com/git-arr/r/homepage.git/c/{commit_sha}/"
     commit_msg_html = html.escape(commit_subject)
@@ -89,8 +98,10 @@ def build_info(target: Optional[str] = None) -> str:
         f"<a href=\"{commit_url}\">{short_sha}</a> — &ldquo;{commit_msg_html}&rdquo;</p>"
     )
 
+    site_commit_url = f"https://tailrecursion.com/git-arr/r/homepage.git/c/{site_commit}/"
     built_line = (
-        f"<p class=\"foot-built\">Built {build_date_str} at {build_time_str} by {user}@{host}</p>"
+        f"<p class=\"foot-built\">Built {build_date_str} at {build_time_str} "
+        f"from <a href=\"{site_commit_url}\">{site_short_sha}</a></p>"
     )
 
     indent = "      "
diff --git a/tools/check_links.py b/tools/check_links.py
index 17c8661..80ff898 100755
--- a/tools/check_links.py
+++ b/tools/check_links.py
@@ -3,7 +3,9 @@
 from __future__ import annotations
 
 import argparse
+import os
 import pathlib
+import shutil
 import subprocess
 import sys
 import xml.etree.ElementTree as ET
@@ -13,22 +15,32 @@ from typing import Iterable
 ROOT = pathlib.Path(__file__).resolve().parent.parent
 MD_DIR = ROOT / "md"
 CMARK_CANDIDATES: Iterable[pathlib.Path | str] = (
+    ROOT / "bin" / "cmark-gfm",
     ROOT / "bin" / "cmark",
-    pathlib.Path("/usr/bin/cmark-gfm"),
-    pathlib.Path("/usr/bin/cmark"),
     "cmark-gfm",
     "cmark",
 )
 
 
 def _cmark_cmd() -> list[str]:
-    for candidate in CMARK_CANDIDATES:
+    configured = os.environ.get("MD2HTML")
+    candidates: Iterable[pathlib.Path | str]
+    if configured:
+        candidates = (configured, *CMARK_CANDIDATES)
+    else:
+        candidates = CMARK_CANDIDATES
+
+    for candidate in candidates:
         if isinstance(candidate, pathlib.Path):
-            if candidate.is_file():
+            if candidate.is_file() and os.access(candidate, os.X_OK):
                 return [str(candidate)]
         else:
-            return [candidate]
-    raise RuntimeError("Unable to locate cmark-gfm executable")
+            resolved = shutil.which(candidate)
+            if resolved:
+                return [resolved]
+    raise RuntimeError(
+        "Unable to locate cmark-gfm; use make container-check-links or set MD2HTML"
+    )
 
 
 def collect_links(md_path: pathlib.Path) -> set[str]:
@@ -152,14 +164,18 @@ def main(argv: list[str] | None = None) -> int:
 
     broken: dict[str, list[tuple[str, str]]] = defaultdict(list)
 
-    for md_file in md_files:
-        links = collect_links(md_file)
-        for url in sorted(links):
-            ok, code, detail = check_url(url)
-            print(f"{code}\t{md_file}\t{url}")
-            sys.stdout.flush()
-            if not ok:
-                broken[str(md_file)].append((url, detail))
+    try:
+        for md_file in md_files:
+            links = collect_links(md_file)
+            for url in sorted(links):
+                ok, code, detail = check_url(url)
+                print(f"{code}\t{md_file}\t{url}")
+                sys.stdout.flush()
+                if not ok:
+                    broken[str(md_file)].append((url, detail))
+    except RuntimeError as exc:
+        print(str(exc), file=sys.stderr)
+        return 2
 
     if broken:
         print("Broken links found:")
diff --git a/tools/gen_index.py b/tools/gen_index.py
index 9dc5959..1e53781 100755
--- a/tools/gen_index.py
+++ b/tools/gen_index.py
@@ -105,6 +105,7 @@ def render_template(template_path: Path, build_info_html: str) -> str:
     template = template_path.read_text()
     template = template.replace("@ROOT@", "")
     template = template.replace("@TITLE@", "Index")
+    template = template.replace("@PAGE_CLASS@", "page-index")
     return template.replace("@BUILDINFO@", build_info_html)
 
 
@@ -122,10 +123,7 @@ def main() -> None:
     head_html = render_template(root_dir / "tpl" / "head.html", build_info_html)
     foot_html = render_template(root_dir / "tpl" / "foot.html", build_info_html)
 
-    body_lines = [
-        "<h1>Index of Alan's Homepage</h1>",
-        "<p>Browse every page in this wiki-style site:</p>",
-    ]
+    body_lines = ["<h1>Index of Alan's Homepage</h1>"]
 
     markdown_paths = list(collect_markdown(src_dir))
     tree = build_tree(markdown_paths)
diff --git a/tools/normalize_output.py b/tools/normalize_output.py
new file mode 100644
index 0000000..af3e525
--- /dev/null
+++ b/tools/normalize_output.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+"""Normalize generated-site metadata for reproducible builds."""
+
+from __future__ import annotations
+
+import argparse
+import os
+from pathlib import Path
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("directory", type=Path)
+    parser.add_argument("epoch", type=int)
+    return parser.parse_args()
+
+
+def normalize(root: Path, epoch: int) -> None:
+    if not root.is_dir():
+        raise SystemExit(f"Generated output does not exist: {root}")
+
+    paths = sorted(root.rglob("*"), key=lambda path: (len(path.parts), path.as_posix()), reverse=True)
+    for path in paths:
+        if path.is_symlink():
+            os.utime(path, (epoch, epoch), follow_symlinks=False)
+        elif path.is_dir():
+            path.chmod(0o755)
+            os.utime(path, (epoch, epoch))
+        else:
+            path.chmod(0o644)
+            os.utime(path, (epoch, epoch))
+
+    root.chmod(0o755)
+    os.utime(root, (epoch, epoch))
+
+
+def main() -> None:
+    args = parse_args()
+    normalize(args.directory, args.epoch)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/verify_reproducible.py b/tools/verify_reproducible.py
new file mode 100644
index 0000000..cfcc633
--- /dev/null
+++ b/tools/verify_reproducible.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""Compare two generated-site trees, including normalized metadata."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import stat
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass(frozen=True)
+class Entry:
+    kind: str
+    mode: int
+    mtime_ns: int
+    digest: str
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("first", type=Path)
+    parser.add_argument("second", type=Path)
+    return parser.parse_args()
+
+
+def digest(path: Path) -> str:
+    hasher = hashlib.sha256()
+    with path.open("rb") as stream:
+        for block in iter(lambda: stream.read(1024 * 1024), b""):
+            hasher.update(block)
+    return hasher.hexdigest()
+
+
+def manifest(root: Path) -> dict[str, Entry]:
+    entries: dict[str, Entry] = {}
+    for path in sorted(root.rglob("*")):
+        relative = path.relative_to(root).as_posix()
+        metadata = path.lstat()
+        mode = stat.S_IMODE(metadata.st_mode)
+        if path.is_symlink():
+            entries[relative] = Entry("link", mode, metadata.st_mtime_ns, path.readlink().as_posix())
+        elif path.is_dir():
+            entries[relative] = Entry("directory", mode, metadata.st_mtime_ns, "")
+        else:
+            entries[relative] = Entry("file", mode, metadata.st_mtime_ns, digest(path))
+    return entries
+
+
+def main() -> None:
+    args = parse_args()
+    first = manifest(args.first)
+    second = manifest(args.second)
+    if first != second:
+        for path in sorted(set(first) | set(second)):
+            if first.get(path) != second.get(path):
+                print(f"{path}: {first.get(path)!r} != {second.get(path)!r}")
+        raise SystemExit("Generated outputs differ")
+    print(f"Reproducible output verified: {len(first)} entries")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tpl/head.html b/tpl/head.html
index ec5b3ca..c4063fd 100644
--- a/tpl/head.html
+++ b/tpl/head.html
@@ -14,12 +14,17 @@
     gtag('config', 'G-XCMVL5K44X');
   </script>
 </head>
-<body>
+<body class="@PAGE_CLASS@">
   <div id="main">
     <header class="site-head">
-      <nav class="site-nav">
+      <a class="site-brand" href="@ROOT@Home.html">
+        <span class="site-brand-name">Alan Dipert</span>
+        <span class="site-brand-tagline">The Homepage™</span>
+      </a>
+      <nav class="site-nav" aria-label="Primary navigation">
         <a href="@ROOT@Home.html">Home</a>
         <a href="@ROOT@Index.html">Index</a>
+        <a href="@ROOT@updates.xml">RSS</a>
       </nav>
     </header>
     <div class="pages">
diff --git a/tpl/style.css b/tpl/style.css
index 468e3f6..d6bd08c 100644
--- a/tpl/style.css
+++ b/tpl/style.css
@@ -1,190 +1,460 @@
-:root{--fg:#2e3436;--bg:#ffffff;--muted:#6c757d;--link:#1a73e8;--shadow:0 4px 8px rgba(0,0,0,.2),0 6px 20px rgba(0,0,0,.19)}
+:root{
+  --ink:#29312f;
+  --paper:#fffefa;
+  --canvas:#f4f1e8;
+  --muted:#66706c;
+  --accent:#b33f1d;
+  --accent-dark:#873019;
+  --link:#155f78;
+  --link-hover:#0d4356;
+  --line:#ddd8cc;
+  --soft:#f5f2eb;
+  --focus:#f0a23a;
+  --shadow:0 20px 55px rgba(42,48,44,.12),0 2px 8px rgba(42,48,44,.08);
+  --radius:18px;
+}
+
+*{box-sizing:border-box;}
+
 html{
-  --s:82px;
-  --c1:#b2b2b2;
-  --c2:#ffffff;
-  --c3:#d9d9d9;
+  --s:104px;
+  --c1:#e2ded4;
+  --c2:#f8f6f0;
+  --c3:#ece8de;
   --_g:var(--c3) 0 120deg,#0000 0;
-  background:
+  min-height:100%;
+  background-color:var(--canvas);
+  background-image:
+    linear-gradient(rgba(244,241,232,.48),rgba(244,241,232,.48)),
     conic-gradient(from -60deg at 50% calc(100%/3),var(--_g)),
     conic-gradient(from 120deg at 50% calc(200%/3),var(--_g)),
-    conic-gradient(from  60deg at calc(200%/3),var(--c3) 60deg,var(--c2) 0 120deg,#0000 0),
+    conic-gradient(from 60deg at calc(200%/3),var(--c3) 60deg,var(--c2) 0 120deg,#0000 0),
     conic-gradient(from 180deg at calc(100%/3),var(--c1) 60deg,var(--_g)),
-    linear-gradient(90deg,var(--c1)   calc(100%/6),var(--c2) 0 50%,
-                    var(--c1) 0 calc(500%/6),var(--c2) 0);
-  background-size:calc(1.732*var(--s)) var(--s);
+    linear-gradient(90deg,var(--c1) calc(100%/6),var(--c2) 0 50%,var(--c1) 0 calc(500%/6),var(--c2) 0);
+  background-size:auto,calc(1.732*var(--s)) var(--s),calc(1.732*var(--s)) var(--s),calc(1.732*var(--s)) var(--s),calc(1.732*var(--s)) var(--s),calc(1.732*var(--s)) var(--s);
 }
+
 body{
   margin:0;
   padding:0;
-  font:16px/1.6 "Segoe UI",Roboto,Helvetica,Arial,sans-serif;
-  color:var(--fg);
+  color:var(--ink);
+  font:16.5px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+  text-rendering:optimizeLegibility;
 }
+
 #main{
-  margin:40px auto;
-  max-width:800px;
-  padding:1.5em;
-  background:var(--bg);
-  border-radius:.75em;
+  width:min(900px,calc(100% - 36px));
+  margin:22px auto;
+  padding:0 clamp(20px,3.5vw,34px) clamp(22px,3.5vw,34px);
+  overflow:hidden;
+  border:1px solid rgba(116,109,94,.24);
+  border-radius:var(--radius);
+  background:var(--paper);
   box-shadow:var(--shadow);
 }
+
 .site-head{
-  margin-bottom:1.5rem;
-  border-bottom:1px solid rgba(0,0,0,.08);
-  padding-bottom:.75rem;
+  display:flex;
+  justify-content:space-between;
+  align-items:center;
+  gap:1rem;
+  margin:0 calc(clamp(20px,3.5vw,34px)*-1) clamp(20px,3vw,28px);
+  padding:13px clamp(20px,3.5vw,34px);
+  border-bottom:1px solid var(--line);
+  background:rgba(245,242,235,.58);
+}
+
+.site-brand{
+  display:flex;
+  flex-direction:column;
+  color:var(--ink);
+  line-height:1.15;
+  text-decoration:none;
 }
+
+.site-brand-name{
+  font-family:ui-serif,Charter,"Bitstream Charter",Georgia,serif;
+  font-size:1.1rem;
+  font-weight:750;
+  letter-spacing:.01em;
+}
+
+.site-brand-tagline{
+  margin-top:.18rem;
+  color:var(--muted);
+  font-size:.72rem;
+  font-weight:650;
+  letter-spacing:.08em;
+  text-transform:uppercase;
+}
+
 .site-nav{
   display:flex;
-  gap:1rem;
+  flex:0 0 auto;
+  flex-wrap:wrap;
+  gap:.3rem;
   align-items:center;
-  font-size:1.125rem;
-  font-weight:600;
 }
+
 .site-nav a{
+  padding:.3rem .55rem;
+  border-radius:999px;
   color:var(--link);
-  text-decoration:underline;
+  font-size:.9rem;
+  font-weight:700;
+  text-decoration:none;
 }
+
 .site-nav a:hover{
-  text-decoration:none;
+  color:var(--link-hover);
+  background:#e9eee9;
+}
+
+a{
+  color:var(--link);
+  text-decoration-thickness:.08em;
+  text-underline-offset:.14em;
+  text-decoration-skip-ink:auto;
 }
+
+a:hover{color:var(--link-hover);}
+
+a:focus-visible{
+  outline:3px solid var(--focus);
+  outline-offset:3px;
+  border-radius:3px;
+}
+
 .content h1,
 .content h2,
 .content h3,
 .content h4,
 .content h5{
-  color:#cc3b12;
-  line-height:1.25;
+  color:var(--accent);
+  font-family:ui-serif,Charter,"Bitstream Charter",Georgia,serif;
+  font-weight:750;
+  line-height:1.16;
+  text-wrap:balance;
+}
+
+.content h1{
+  margin:.05em 0 .45em;
+  font-size:clamp(2rem,5vw,2.75rem);
+  letter-spacing:-.035em;
+}
+
+.content h2{
+  margin:1.3em 0 .45em;
+  font-size:clamp(1.4rem,3vw,1.65rem);
+  letter-spacing:-.018em;
+}
+
+.content h3{margin:1.25em 0 .4em;font-size:1.2rem;}
+.content h4,.content h5{margin:1.15em 0 .4em;}
+.content p{margin:0 0 .8em;}
+.content ul,.content ol{padding-left:1.35em;}
+.content li{margin:.28em 0;}
+
+.content>h1:first-child+ul{
+  margin:-.25rem 0 .9rem;
+  padding-left:1.2rem;
+  color:var(--muted);
+  font-size:.9em;
 }
-.content p{margin-top:0;}
+
+.content>h1:first-child+ul+h2{margin-top:1.05em;}
+
 .content blockquote{
-  margin:1.25rem 0;
+  margin:1rem 0;
   padding:.75rem 1rem;
-  border-left:4px solid rgba(0,0,0,.1);
-  background:rgba(0,0,0,.025);
+  border-left:4px solid #cf9a80;
+  border-radius:0 10px 10px 0;
+  background:var(--soft);
+  color:#4f5955;
+  font-family:ui-serif,Charter,"Bitstream Charter",Georgia,serif;
+  font-size:1.04em;
   font-style:italic;
-  color:#4a4f52;
 }
-.content blockquote p{margin:.5rem 0;}
+
+.content blockquote p{margin:.55rem 0;}
 .content blockquote p:first-child{margin-top:0;}
 .content blockquote p:last-child{margin-bottom:0;}
-.content pre,
-.content code{background:#f7f7f7;}
-.content pre{padding:.75rem;overflow:auto;}
-a{color:var(--link);}
-.content img{max-width:100%;height:auto;}
-img[src*="float_right"]{float:right;border-radius:.75em;}
+
+.content code{
+  padding:.12em .32em;
+  border:1px solid #e4e0d7;
+  border-radius:4px;
+  background:#f5f3ee;
+  font-size:.9em;
+}
+
+.content pre{
+  margin:1rem 0;
+  padding:.75rem .9rem;
+  overflow:auto;
+  border:1px solid var(--line);
+  border-radius:10px;
+  background:#f5f3ee;
+  line-height:1.5;
+}
+
+.content pre code{padding:0;border:0;background:transparent;font-size:.92em;}
+
+.content img{
+  display:block;
+  max-width:100%;
+  height:auto;
+  border-radius:10px;
+}
+
+.content p>a>img,
+.content p>img{
+  box-shadow:0 8px 24px rgba(42,48,44,.12);
+}
+
+.content p>a:has(>img){
+  display:block;
+  width:100%;
+}
+
+.content p>a>img{width:100%;}
+
+img[src*="float_right"]{
+  float:right;
+  margin:.2rem 0 1.25rem 2rem;
+}
+
 img[src*="200px"]{width:200px;}
-.level-one{background:#e6f7ff;color:#0b5394;padding:0 .25em;border-radius:.25em;}
-.level-two{background:#fff4e6;color:#a45f02;padding:0 .25em;border-radius:.25em;}
-.level-three{background:#fde7eb;color:#9c0d38;padding:0 .25em;border-radius:.25em;}
-.content table{width:100%;border-collapse:collapse;margin:1.5rem 0;box-shadow:inset 0 -1px 0 rgba(0,0,0,.05);}
+
+.page-home img[src*="float_right"]{
+  float:none;
+  width:138px;
+  aspect-ratio:1;
+  object-fit:cover;
+  margin:0;
+  border:5px solid #fff;
+  border-radius:16px;
+  box-shadow:0 0 0 1px var(--line),0 12px 30px rgba(42,48,44,.17);
+}
+
+.level-one{background:#e5f1f3;color:#154f62;padding:0 .25em;border-radius:.25em;}
+.level-two{background:#f8eddb;color:#83500d;padding:0 .25em;border-radius:.25em;}
+.level-three{background:#f6e4e8;color:#8b1739;padding:0 .25em;border-radius:.25em;}
+
+.content table{
+  width:100%;
+  margin:1rem 0;
+  overflow:hidden;
+  border:1px solid var(--line);
+  border-radius:10px;
+  border-collapse:separate;
+  border-spacing:0;
+  font-size:.96em;
+}
+
 .content th,
-.content td{padding:.5rem .75rem;border-bottom:1px solid rgba(0,0,0,.08);}
-.content th:first-child,
-.content td:first-child{white-space:nowrap;}
-.content table.definition-table{
-  font-size:.95em;
-  box-shadow:none;
-  margin:1.25rem 0;
-}
-.content table.definition-table th,
-.content table.definition-table td{
-  padding:.35rem .45rem;
-}
-.content table.definition-table td{
+.content td{
+  padding:.45rem .65rem;
+  border-bottom:1px solid var(--line);
   vertical-align:top;
 }
-.content table.definition-table th:first-child,
-.content table.definition-table td:first-child{
-  white-space:normal;
-}
+
+.content tr:last-child td{border-bottom:0;}
+.content th:first-child,.content td:first-child{white-space:nowrap;}
+.content thead th{background:var(--soft);font-weight:750;text-align:left;}
+.content tbody tr:nth-child(even){background:rgba(245,242,235,.35);}
+.content td[align="right"],.content th[align="right"]{text-align:right;}
+.content td[align="center"],.content th[align="center"]{text-align:center;}
+
+.content table.definition-table{font-size:.93em;box-shadow:none;}
+.content table.definition-table th,.content table.definition-table td{padding:.4rem .5rem;}
+.content table.definition-table td{vertical-align:top;}
+.content table.definition-table th:first-child,.content table.definition-table td:first-child{white-space:normal;}
 .content table.definition-table tr.name-row th{
-  background:rgba(0,0,0,.06);
+  padding:.48rem .55rem;
+  border-top:1px solid var(--line);
+  border-bottom:1px solid var(--line);
+  background:#ebe7dd;
   text-align:left;
+  font-size:1.04em;
+}
+
+.page-home .content{
+  display:grid;
+  grid-template-columns:repeat(2,minmax(0,1fr));
+  column-gap:2rem;
+  align-items:start;
+}
+
+.page-home .content>*{grid-column:1/-1;}
+
+.page-home .content>h1{
+  grid-column:1;
+  grid-row:1;
+  margin-bottom:.35em;
+}
+
+.page-home .content>p:first-of-type{
+  grid-column:2;
+  grid-row:1/span 2;
+  justify-self:end;
+  margin:0;
+}
+
+.page-home .content>p:nth-of-type(2){
+  grid-column:1;
+  grid-row:2;
+  align-self:start;
+  margin:0;
   font-size:1.05em;
-  padding:.4rem .45rem;
-  border-top:1px solid rgba(0,0,0,.12);
-  border-bottom:1px solid rgba(0,0,0,.12);
-}
-.content thead th{background:rgba(0,0,0,.04);font-weight:600;text-align:left;}
-.content td[align="right"],
-.content th[align="right"]{text-align:right;}
-.content td[align="center"],
-.content th[align="center"]{text-align:center;}
-.site-index{list-style:none;margin:1.5rem 0;padding:0;}
-.site-index>li{margin:.35rem 0;}
-.site-index ul{list-style:none;margin:.35rem 0;padding-left:1.25rem;}
-.site-index a{font-weight:600;}
-.content .backlinks{
-  margin:2rem 0 1.5rem;
-  padding:1rem;
-  border:1px solid rgba(0,0,0,.08);
-  border-radius:.5rem;
-  background:rgba(0,0,0,.02);
 }
-.content .backlinks h3{
-  margin:0 0 .5rem;
-  color:#cc3b12;
+
+.page-home .content>h2:nth-of-type(1){grid-column:1;grid-row:3;}
+.page-home .content>ul:nth-of-type(1){grid-column:1;grid-row:4;}
+.page-home .content>h2:nth-of-type(2){grid-column:2;grid-row:3;}
+.page-home .content>ul:nth-of-type(2){grid-column:2;grid-row:4;}
+.page-home .content>h2:nth-of-type(3){grid-column:1/-1;grid-row:5;}
+.page-home .content>p:nth-of-type(3){grid-column:1/-1;grid-row:6;}
+.page-home .content>table{grid-column:1/-1;grid-row:7;}
+
+.page-home .content>h2:nth-of-type(-n+2){
+  margin-top:1em;
+}
+
+.page-home .content>ul{
+  margin:.1rem 0 .65rem;
+  padding:0;
+  list-style:none;
+  border-top:1px solid var(--line);
+}
+
+.page-home .content>ul>li{
+  margin:0;
+  padding:.3rem .1rem;
+  border-bottom:1px solid var(--line);
+}
+
+.page-home .content>ul>li>a:first-child{font-weight:750;}
+.page-home .content>table td:first-child{color:var(--muted);font-variant-numeric:tabular-nums;}
+
+.site-index{
+  display:grid;
+  grid-template-columns:repeat(2,minmax(0,1fr));
+  gap:.4rem .55rem;
+  margin:.8rem 0;
+  padding:0;
+  list-style:none;
+}
+
+.site-index>li{
+  margin:0;
+  padding:.48rem .65rem;
+  border:1px solid var(--line);
+  border-radius:10px;
+  background:linear-gradient(145deg,#fffefa,#f7f4ed);
 }
-.content .backlinks ul{
-  margin:.25rem 0 0;
-  padding-left:1.25rem;
+
+.site-index ul{margin:.3rem 0 0;padding-left:1rem;}
+.site-index a{font-weight:700;}
+
+.content .backlinks{
+  clear:both;
+  margin:1.7rem 0 1rem;
+  padding:.75rem .9rem;
+  border:1px solid var(--line);
+  border-radius:10px;
+  background:var(--soft);
 }
+
+.content .backlinks h3{margin:0 0 .5rem;color:var(--accent);}
+.content .backlinks ul{margin:.25rem 0 0;padding-left:1.25rem;}
 .content .backlinks li{margin:.2rem 0;}
+
 .site-foot{
-  margin-top:2rem;
-  border-top:1px solid rgba(0,0,0,.08);
-  padding-top:1rem;
+  clear:both;
+  margin-top:1.8rem;
+  padding-top:.8rem;
+  border-top:1px solid var(--line);
   color:var(--muted);
-  font-size:.875rem;
-}
-.site-foot .foot-meta{
-  display:flex;
-  flex-direction:column;
-  gap:.35rem;
+  font-size:.78rem;
 }
+
+.site-foot .foot-meta{display:flex;flex-direction:column;gap:.3rem;}
 .site-foot p{margin:0;}
-.site-foot a{
-  color:var(--link);
-  text-decoration:underline;
-}
-.site-foot a:hover{
-  text-decoration:underline;
-}
-@media (max-width:768px){
-  html{background:#f4f4f4;}
-  body{font-size:15px;}
+.site-foot a{color:inherit;}
+
+@media (max-width:720px){
+  html{background:var(--canvas);}
+  body{font-size:16px;}
   #main{
-    margin:0;
+    width:100%;
     min-height:100vh;
+    margin:0;
+    border:0;
     border-radius:0;
     box-shadow:none;
-    padding:1.25em;
   }
-  .site-nav{
-    flex-direction:column;
-    align-items:flex-start;
-    gap:.35rem;
+  .site-index{grid-template-columns:1fr;}
+  .page-home .content{display:block;}
+  .page-home .content>p:first-of-type{
+    float:right;
+    width:118px;
+    margin:0 0 .65rem 1rem;
   }
+  .page-home .content>p:nth-of-type(2){margin-bottom:.8rem;}
+  .page-home .content>h2:nth-of-type(1){clear:both;}
 }
-@media (max-width:520px){
-  #main{padding:1em;}
-  .site-head{text-align:center;}
-  .site-nav{align-items:center;}
+
+@media (max-width:560px){
+  #main{padding-right:14px;padding-left:14px;}
+  .site-head{
+    align-items:flex-start;
+    margin-right:-14px;
+    margin-left:-14px;
+    padding:10px 14px;
+  }
+  .site-brand-tagline{display:none;}
+  .site-nav{justify-content:flex-end;}
+  .site-nav a{padding:.35rem .48rem;font-size:.84rem;}
+  .content h1{font-size:2rem;}
   img[src*="float_right"]{
     float:none;
-    display:block;
-    margin:0 auto 1rem;
-    max-width:70%;
+    max-width:100%;
+    margin:0;
   }
-  img[src*="200px"]{
-    width:auto;
-    max-width:180px;
+  .page-home img[src*="float_right"]{
+    width:118px;
+    max-width:100%;
+    margin:0;
   }
-  table,
-  .content table{
-    display:block;
-    overflow-x:auto;
-    width:100%;
+  .page-home .content>table,
+  .page-home .content>table tbody,
+  .page-home .content>table tr,
+  .page-home .content>table td{display:block;width:100%;}
+  .page-home .content>table{border:0;background:transparent;}
+  .page-home .content>table thead{
+    position:absolute;
+    width:1px;
+    height:1px;
+    padding:0;
+    overflow:hidden;
+    clip:rect(0,0,0,0);
+    white-space:nowrap;
+    border:0;
+  }
+  .page-home .content>table tr{
+    margin-bottom:.5rem;
+    padding:.6rem .7rem;
+    border:1px solid var(--line);
+    border-radius:10px;
+    background:linear-gradient(145deg,#fffefa,#f7f4ed);
+  }
+  .page-home .content>table td{padding:0;border:0;white-space:normal;}
+  .page-home .content>table td:first-child{
+    margin-bottom:.28rem;
+    font-size:.78rem;
+    font-weight:750;
+    letter-spacing:.04em;
   }
+  .content table:not(.definition-table){overflow-x:auto;}
 }