Splux Linux

source-based Linux using SPS

splux-site · log · files · refs · tags · releases · people · languages · GitHub · commit

a878fa9


Add handbook docs for setup, manual tty install, and the slim ISO.

Date
Commita878fa93d16cbb66b08d27e2485a7694f4953656
GitHubmirror

Parents

Stat

 README.md                       |   7 +
 site/404.html                   |   2 +-
 site/assets/docs.js             | 131 +++++++++++++
 site/assets/style.css           | 226 +++++++++++++++++++++
 site/docs/first-boot/index.html |  76 ++++++++
 site/docs/images/index.html     |  78 ++++++++
 site/docs/index.html            |  56 +++++-
 site/docs/sps/index.html        | 106 ++++++++++
 site/download/index.html        |   7 +-
 site/index.html                 |   9 +-
 site/install/index.html         | 421 +++++++++++++++++++++++++++++++++++++---
 tools/build-site.sh             |  41 +++-
 tools/subst.awk                 |   5 +
 13 files changed, 1123 insertions(+), 42 deletions(-)

Diff

diff --git a/README.md b/README.md
index e62a4b9..76673c8 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,13 @@ ISO names are stable:
 The visible tag name is filled at build time from the GitHub Releases API
 and refreshed from GitHub when you open the page.
 
+## Handbook
+
+`docs/` and `install/` are a static handbook (sidebar plus tabs). The
+install page covers guided `setup`, a full manual tty install without
+`setup`, and the slim ISO. New handbook HTML under `site/docs/` must be
+registered in `tools/build-site.sh` with the correct `../../../../` depth.
+
 ## News
 
 `news/` is generated at build time from GitHub, not edited by hand:
diff --git a/site/404.html b/site/404.html
index addf7da..eb7a9c1 100644
--- a/site/404.html
+++ b/site/404.html
@@ -14,7 +14,7 @@
 <h2>404</h2>
 <hr class="rule">
 <p>The requested page does not exist.</p>
-<p><a href="../../../../">home</a></p>
+<p><a href="../../../../">home</a> · <a href="../../../../docs/">handbook</a> · <a href="../../../../install/">install</a></p>
 </main>
 @@FOOTER@@
 </div>
diff --git a/site/assets/docs.js b/site/assets/docs.js
new file mode 100644
index 0000000..bf62e13
--- /dev/null
+++ b/site/assets/docs.js
@@ -0,0 +1,131 @@
+// Handbook tabs. No framework. Hash URLs stay shareable.
+(function () {
+	function ready() {
+		var sets = document.querySelectorAll("[data-tabs]");
+		if (!sets.length)
+			return;
+		document.documentElement.classList.add("js-tabs");
+		for (var i = 0; i < sets.length; i++)
+			initSet(sets[i]);
+		applyHash();
+		window.addEventListener("hashchange", applyHash);
+	}
+
+	function tabsOf(set) {
+		var n = set.firstElementChild;
+		var list = null;
+		while (n) {
+			if (n.getAttribute && n.classList.contains("tablist")) {
+				list = n;
+				break;
+			}
+			n = n.nextElementSibling;
+		}
+		if (!list)
+			return [];
+		return list.querySelectorAll('[role="tab"]');
+	}
+
+	function initSet(set) {
+		var tabs = tabsOf(set);
+		var selected = null;
+		for (var i = 0; i < tabs.length; i++) {
+			bindTab(set, tabs, tabs[i], i);
+			if (!selected && tabs[i].getAttribute("aria-selected") === "true")
+				selected = tabs[i];
+		}
+		if (!selected && tabs.length)
+			selected = tabs[0];
+		if (selected)
+			selectTab(set, selected, false);
+	}
+
+	function bindTab(set, tabs, tab, index) {
+		tab.addEventListener("click", function () {
+			selectTab(set, tab, true);
+		});
+		tab.addEventListener("keydown", function (e) {
+			var next = -1;
+			if (e.key === "ArrowRight" || e.key === "ArrowDown")
+				next = (index + 1) % tabs.length;
+			else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
+				next = (index - 1 + tabs.length) % tabs.length;
+			else if (e.key === "Home")
+				next = 0;
+			else if (e.key === "End")
+				next = tabs.length - 1;
+			else
+				return;
+			e.preventDefault();
+			tabs[next].focus();
+			selectTab(set, tabs[next], true);
+		});
+	}
+
+	function selectTab(set, tab, updateHash) {
+		var tabs = tabsOf(set);
+		for (var i = 0; i < tabs.length; i++) {
+			var on = tabs[i] === tab;
+			tabs[i].setAttribute("aria-selected", on ? "true" : "false");
+			tabs[i].tabIndex = on ? 0 : -1;
+			var id = tabs[i].getAttribute("aria-controls");
+			var panel = id ? document.getElementById(id) : null;
+			if (!panel)
+				continue;
+			if (on)
+				panel.removeAttribute("hidden");
+			else
+				panel.setAttribute("hidden", "");
+		}
+		if (updateHash) {
+			var hash = tab.getAttribute("data-hash");
+			if (hash && history.replaceState)
+				history.replaceState(null, "", "#" + hash);
+			else if (hash)
+				location.hash = hash;
+		}
+	}
+
+	function reveal(tab) {
+		var t = tab;
+		while (t) {
+			var set = t.closest("[data-tabs]");
+			if (!set)
+				break;
+			selectTab(set, t, false);
+			var panel = set.closest(".tabpanel");
+			if (!panel)
+				break;
+			t = document.querySelector(
+				'[role="tab"][aria-controls="' + panel.id + '"]'
+			);
+		}
+	}
+
+	function applyHash() {
+		var hash = (location.hash || "").replace(/^#/, "");
+		if (!hash)
+			return;
+		var tab = document.querySelector(
+			'[role="tab"][data-hash="' + hash + '"]'
+		);
+		if (!tab) {
+			var el = document.getElementById(hash);
+			if (el) {
+				var panel = el.closest(".tabpanel") ||
+					(el.classList.contains("tabpanel") ? el : null);
+				if (panel)
+					tab = document.querySelector(
+						'[role="tab"][aria-controls="' + panel.id + '"]'
+					);
+			}
+		}
+		if (tab)
+			reveal(tab);
+	}
+
+	if (document.readyState === "loading")
+		document.addEventListener("DOMContentLoaded", ready);
+	else
+		ready();
+})();
diff --git a/site/assets/style.css b/site/assets/style.css
index 8fee3fa..e67a4e5 100644
--- a/site/assets/style.css
+++ b/site/assets/style.css
@@ -400,3 +400,229 @@ footer p {
 		overflow-x: auto;
 	}
 }
+
+/* Handbook: sidebar, tabs, callouts. Same black / gray / yellow. */
+
+.wrap.wide {
+	max-width: 1100px;
+}
+
+.handbook {
+	display: grid;
+	grid-template-columns: 13.5rem minmax(0, 1fr);
+	gap: 2rem;
+	align-items: start;
+}
+
+.docs-side {
+	position: sticky;
+	top: 1rem;
+	padding-top: 0.15rem;
+}
+
+.docs-kicker {
+	margin: 0 0 0.45rem;
+	color: var(--muted);
+	font-size: 0.8rem;
+}
+
+.docs-toc {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	border-top: 1px solid var(--rule);
+}
+
+.docs-toc li {
+	margin: 0;
+	max-width: none;
+}
+
+.docs-toc a {
+	display: block;
+	padding: 0.35rem 0;
+	border-bottom: 1px solid var(--rule);
+	color: var(--muted);
+}
+
+.docs-toc a:hover {
+	color: var(--accent);
+}
+
+.docs-toc a[aria-current="page"] {
+	color: var(--fg);
+	text-decoration: none;
+	border-bottom-color: var(--accent);
+}
+
+.handbook main h2:first-child {
+	margin-top: 0;
+}
+
+.handbook h3 {
+	margin: 1.5rem 0 0.35rem;
+	font-size: 0.95rem;
+	font-weight: 700;
+	color: var(--fg);
+}
+
+.handbook h4 {
+	margin: 1.15rem 0 0.3rem;
+	font-size: 0.9rem;
+	font-weight: 700;
+	color: var(--muted);
+}
+
+.local-toc {
+	list-style: none;
+	padding: 0;
+	margin: 0.5rem 0 1rem;
+}
+
+.local-toc li {
+	margin: 0.15rem 0;
+}
+
+.tabs {
+	margin: 0.85rem 0 1.25rem;
+}
+
+.tablist {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 0;
+	border-bottom: 1px solid var(--rule);
+	margin: 0 0 1rem;
+}
+
+.tablist [role="tab"] {
+	font: inherit;
+	background: transparent;
+	color: var(--muted);
+	border: 1px solid transparent;
+	border-bottom: 0;
+	padding: 0.45rem 0.85rem;
+	cursor: pointer;
+	margin-bottom: -1px;
+}
+
+.tablist [role="tab"]:hover {
+	color: var(--fg);
+}
+
+.tablist [role="tab"][aria-selected="true"] {
+	color: var(--accent);
+	background: #111;
+	border-color: var(--rule);
+	border-bottom-color: var(--bg);
+}
+
+.tabpanel {
+	margin: 0;
+}
+
+html.js-tabs .tabpanel[hidden] {
+	display: none;
+}
+
+.callout {
+	border: 1px solid var(--rule);
+	border-left: 3px solid var(--accent);
+	padding: 0.65rem 0.85rem;
+	margin: 0.85rem 0 1rem;
+	color: var(--muted);
+}
+
+.callout strong {
+	color: var(--fg);
+	font-weight: 700;
+}
+
+.callout p {
+	margin: 0.35rem 0;
+}
+
+.callout p:first-child {
+	margin-top: 0;
+}
+
+.callout p:last-child {
+	margin-bottom: 0;
+}
+
+table.docs {
+	width: 100%;
+	border-collapse: collapse;
+	font-size: 0.9rem;
+	margin: 0.75rem 0 1rem;
+}
+
+table.docs th,
+table.docs td {
+	text-align: left;
+	vertical-align: top;
+	padding: 0.35rem 0.7rem 0.35rem 0;
+	border-bottom: 1px solid #1a1a1a;
+}
+
+table.docs th {
+	color: var(--muted);
+	font-weight: 400;
+	border-bottom: 1px solid var(--rule);
+}
+
+.steps {
+	list-style: none;
+	padding: 0;
+	margin: 0.75rem 0 1rem;
+	counter-reset: step;
+}
+
+.steps > li {
+	counter-increment: step;
+	position: relative;
+	padding-left: 2.4rem;
+	margin: 0.9rem 0;
+	max-width: none;
+}
+
+.steps > li::before {
+	content: counter(step, decimal-leading-zero);
+	position: absolute;
+	left: 0;
+	top: 0;
+	color: var(--accent);
+	font-weight: 700;
+}
+
+@media (max-width: 860px) {
+	.handbook {
+		grid-template-columns: 1fr;
+		gap: 1.25rem;
+	}
+
+	.docs-side {
+		position: static;
+	}
+
+	.docs-toc {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 0 1rem;
+		border-top: 0;
+	}
+
+	.docs-toc li {
+		border: 0;
+	}
+
+	.docs-toc a {
+		border: 0;
+		padding: 0.15rem 0;
+	}
+
+	table.docs {
+		display: block;
+		overflow-x: auto;
+	}
+}
diff --git a/site/docs/first-boot/index.html b/site/docs/first-boot/index.html
new file mode 100644
index 0000000..0ba11a0
--- /dev/null
+++ b/site/docs/first-boot/index.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>After first boot - Splux Linux</title>
+<meta name="description" content="What to do after a Splux install: passwords, network, services, packages.">
+<link rel="icon" href="../../../../assets/favicon.png" type="image/png">
+<link rel="stylesheet" href="../../../../assets/style.css">
+</head>
+<body>
+<a class="skip" href="#main">Skip to content</a>
+<div class="wrap wide">
+@@HEADER@@
+<div class="handbook">
+@@DOCSNAV@@
+<main id="main">
+<h2>After first boot</h2>
+<hr class="rule">
+<p>This chapter assumes the machine booted the kernel you installed, with GRUB, and reached a getty. If it did not, go back to the <a href="../../../../install/">install chapter</a> and check the kernel package, <code>/etc/fstab</code>, and <code>grub-install</code>.</p>
+
+<h3>Passwords</h3>
+<p>If you did not set hashes during <code>setup</code>, root and the first user are locked. From a live root you can <code>chroot</code> and run <code>passwd</code>. After a successful boot:</p>
+<pre class="block">passwd
+passwd alex</pre>
+<p>Busybox <code>passwd</code> is on the live images. The installed <code>shadow</code> package provides it on the new system once <code>base-system</code> is in place.</p>
+
+<h3>Read the notes</h3>
+<p><code>setup</code> writes text files under <code>/root/</code>:</p>
+<ul>
+<li><code>sps-bootloader.txt</code> if GRUB was not installed during setup</li>
+<li><code>sps-user.txt</code> for the first account</li>
+<li><code>sps-init.txt</code> for the chosen PID 1</li>
+<li><code>sps-network.txt</code> (setup does not write DHCP or static network config)</li>
+<li><code>sps-fstab.txt</code> and <code>/etc/fstab</code></li>
+<li><code>sps-services.txt</code> when services were enabled</li>
+</ul>
+<p>A manual install does not create those files unless you write them yourself. The same facts still apply.</p>
+
+<h3>Network</h3>
+<p>On a console install, bring an interface up by hand. <code>dhcpcd</code> is in <code>base-system</code>:</p>
+<pre class="block">ip link
+dhcpcd eth0</pre>
+<p>On Plasma, use plasma-nm after graphical login. OpenSSH is present when you used the server profile or installed <code>openssh</code>.</p>
+
+<h3>Services</h3>
+<p>The file <code>/etc/sps/init</code> records which PID 1 was chosen. Known services (sshd, dbus, SDDM, NetworkManager, and others mapped by SPS) are enabled during <code>setup</code> unless you passed <code>--services no</code>. Later <code>sget install</code> of a mapped package enables it for that init too.</p>
+<table class="docs">
+<thead>
+<tr><th>Init</th><th>Enable a service</th></tr>
+</thead>
+<tbody>
+<tr><td>systemd</td><td><code>systemctl enable --now sshd</code> (unit names follow the package)</td></tr>
+<tr><td>OpenRC</td><td><code>rc-update add sshd default</code></td></tr>
+<tr><td>s6</td><td>symlink under <code>/etc/s6/enabled</code></td></tr>
+<tr><td>runit</td><td>symlink under <code>/etc/runit/runsvdir/default</code></td></tr>
+<tr><td>dinit</td><td>symlink under <code>/etc/dinit.d/boot.d</code></td></tr>
+<tr><td>Shepherd</td><td>Scheme files under <code>/etc/shepherd.d</code></td></tr>
+</tbody>
+</table>
+
+<h3>Next packages</h3>
+<pre class="block">src update
+sget install nano
+pkstat</pre>
+<p>Search the <a href="../../../../packages/">package browser</a> for names. Extra is where desktop, init, and most applications live. Core is the base system, toolchain pieces that exist as recipes, boot, filesystems, and networking.</p>
+
+<h3>Configuration files</h3>
+<p>Files under <code>etc</code> are preserved when you change them. On upgrade, SPS keeps the local file. If the new package also changed its copy, the new one is written beside it as <code>.sps-new</code>. SPS does not merge the two.</p>
+</main>
+</div>
+@@FOOTER@@
+</div>
+<script src="../../../../assets/docs.js?v=4c4f12c-20261019130111"></script>
+</body>
+</html>
diff --git a/site/docs/images/index.html b/site/docs/images/index.html
new file mode 100644
index 0000000..f702d12
--- /dev/null
+++ b/site/docs/images/index.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Live images - Splux Linux</title>
+<meta name="description" content="What is on sps-live-tty, sps-live-tty-slim, and sps-live-plasma.">
+<link rel="icon" href="../../../../assets/favicon.png" type="image/png">
+<link rel="stylesheet" href="../../../../assets/style.css">
+</head>
+<body>
+<a class="skip" href="#main">Skip to content</a>
+<div class="wrap wide">
+@@HEADER@@
+<div class="handbook">
+@@DOCSNAV@@
+<main id="main">
+<h2>Live images</h2>
+<hr class="rule">
+<p>Images are hybrid ISOs from <a href="https://github.com/RobertFlexx/SPS">RobertFlexx/SPS</a>. Filenames stay the same; the current tag is <a href="https://github.com/RobertFlexx/SPS/releases/latest">live-2026-09-06-1</a> (2026-09-06). Verify against <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a>.</p>
+<p>Every image includes SPS (<code>src</code>, <code>sget</code>, <code>mkpkg</code>, <code>pkin</code>, <code>setup</code>, <code>mkiso</code>, and the rest), <code>dialog</code>, disk tools, <code>dhcpcd</code>, <code>grub-install</code>, and <code>grub-mkconfig</code>. Live PID 1 is busybox init on published snapshots. <code>/bin/sh</code> is busybox. Login is <code>root</code> with an empty password.</p>
+
+<table class="docs">
+<thead>
+<tr><th></th><th>tty</th><th>slim</th><th>Plasma</th></tr>
+</thead>
+<tbody>
+<tr><td>File</td><td><code>sps-live-tty.iso</code></td><td><code>sps-live-tty-slim.iso</code></td><td><code>sps-live-plasma.iso</code></td></tr>
+<tr><td>Recipes</td><td>core and extra on the disc</td><td>none (git URLs in repos.conf)</td><td>core and extra on the disc</td></tr>
+<tr><td>Compiler</td><td>seed gcc, headers, startfiles</td><td>no</td><td>same as tty</td></tr>
+<tr><td>git / make / python / meson</td><td>yes</td><td>no</td><td>yes</td></tr>
+<tr><td>Kernel modules for the live kernel</td><td>yes</td><td>no</td><td>yes</td></tr>
+<tr><td>Source install with <code>setup</code> or <code>sget</code></td><td>yes</td><td>no</td><td>yes</td></tr>
+<tr><td>Session</td><td>console</td><td>console</td><td>Plasma / Wayland</td></tr>
+<tr><td>Use it for</td><td>install Splux</td><td>rescue, partition, network debug</td><td>install Splux from a graphical live session</td></tr>
+</tbody>
+</table>
+
+<div class="tabs" data-tabs>
+<div class="tablist" role="tablist" aria-label="Live image">
+<button type="button" role="tab" id="tab-img-tty" aria-controls="panel-img-tty" aria-selected="true" tabindex="0" data-hash="tty">tty</button>
+<button type="button" role="tab" id="tab-img-slim" aria-controls="panel-img-slim" aria-selected="false" tabindex="-1" data-hash="slim">slim</button>
+<button type="button" role="tab" id="tab-img-plasma" aria-controls="panel-img-plasma" aria-selected="false" tabindex="-1" data-hash="plasma">Plasma</button>
+</div>
+
+<section class="tabpanel" role="tabpanel" id="panel-img-tty" aria-labelledby="tab-img-tty">
+<h3>sps-live-tty.iso</h3>
+<p>This is the install disc. <code>make iso</code> copies sps-core and sps-extra into <code>/usr/src/sps/{core,extra}</code> and writes <code>dir</code> entries in <code>/etc/sps/repos.conf</code>. A host seed compiler, headers, glibc startfiles, and kernel modules come along so packages can be built offline.</p>
+<p>Seed tools include gcc, g++, make, cmake, ninja, meson, python3, perl, git, curl, wget, tar, xz, gzip, bzip2, zstd, nano, vim, pkg-config, and the binutils used to link. Partition tools, <code>dhcpcd</code>, <code>openssl</code>, <code>grub-install</code>, and <code>efibootmgr</code> (when the builder had it) are on this image too.</p>
+<p>Manual install: <a href="../../../../install/#manual">Install, Manual tab</a>. Guided: <a href="../../../../install/#setup">Install, setup tab</a>.</p>
+</section>
+
+<section class="tabpanel" role="tabpanel" id="panel-img-slim" aria-labelledby="tab-img-slim">
+<h3>sps-live-tty-slim.iso</h3>
+<p><code>make iso-slim</code> passes <code>--no-seed --no-modules</code> and does not copy core or extra. <code>repos.conf</code> falls back to git URLs. There is no gcc, no git, no make, no python, no meson, no recipes, and no live kernel modules.</p>
+<p>Busybox still provides <code>sh</code>, <code>vi</code>, <code>wget</code>, <code>udhcpc</code>, <code>ip</code>, <code>mount</code>, <code>chroot</code>, <code>tar</code>, <code>gzip</code>, <code>xz</code>, <code>passwd</code>, <code>sha256sum</code>, <code>ping</code>, and <code>modprobe</code>. Host copies of <code>dialog</code>, <code>lsblk</code>, <code>sfdisk</code>, <code>cfdisk</code>, <code>mkfs.ext4</code>, <code>mkfs.vfat</code>, <code>mkswap</code>, <code>dhcpcd</code>, <code>grub-install</code>, and <code>openssl</code> are still on the disc. There is no busybox <code>git</code> applet and no busybox <code>zstd</code> applet.</p>
+<p><code>setup</code> is installed (SPS is on every image) but it cannot complete a source install from slim. Full slim handbook: <a href="../../../../install/#slim">Install, slim tab</a>.</p>
+</section>
+
+<section class="tabpanel" role="tabpanel" id="panel-img-plasma" aria-labelledby="tab-img-plasma">
+<h3>sps-live-plasma.iso</h3>
+<p>Same installer, recipes, and seed compiler as tty, plus a host Plasma / Wayland stack. It needs a KMS device: virtio-gpu in QEMU, or a real GPU. NVIDIA on the live session is nouveau.</p>
+<p>Open Konsole and run <code>setup</code>, or follow the <a href="../../../../install/#manual">manual tty chapter</a>. The live graphical session is not the installed desktop. Installing Plasma still compiles Qt and KDE from source on the target.</p>
+</section>
+</div>
+
+<h3>Write a USB</h3>
+<pre class="block">dd if=sps-live-tty.iso of=/dev/sdX bs=4M status=progress conv=fsync</pre>
+<p>Replace <code>/dev/sdX</code> with the USB device, not a partition. That command erases the destination. Boot the "SPS Linux live (Splux)" entry.</p>
+<p>For QEMU, attach a writable second disk. The live image never runs <code>grub-install</code> on its own.</p>
+</main>
+</div>
+@@FOOTER@@
+</div>
+<script src="../../../../assets/live.js?v=4c4f12c-20261019130111" data-root="../../../../"></script>
+<script src="../../../../assets/docs.js?v=4c4f12c-20261019130111"></script>
+</body>
+</html>
diff --git a/site/docs/index.html b/site/docs/index.html
index 4dce92c..8bf2a3a 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -3,33 +3,73 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Documentation - Splux Linux</title>
-<meta name="description" content="Splux and SPS documentation.">
+<title>Handbook - Splux Linux</title>
+<meta name="description" content="Splux Linux handbook: live images, installation, and SPS.">
 <link rel="icon" href="../../../../assets/favicon.png" type="image/png">
 <link rel="stylesheet" href="../../../../assets/style.css">
 </head>
 <body>
 <a class="skip" href="#main">Skip to content</a>
-<div class="wrap">
+<div class="wrap wide">
 @@HEADER@@
+<div class="handbook">
+@@DOCSNAV@@
 <main id="main">
-<h2>Documentation</h2>
+<h2>Handbook</h2>
 <hr class="rule">
+<p>This is the Splux Linux handbook. Splux is a source-based distribution. Packages are built on the machine that installs them, from recipes in <a href="https://github.com/RobertFlexx/sps-core">sps-core</a> and <a href="https://github.com/RobertFlexx/sps-extra">sps-extra</a>, using SPS.</p>
+<p>Live images are development snapshots, not a numbered stable release. Read the chapter that matches the ISO you booted.</p>
+
+<h3>Start here</h3>
+<table class="docs">
+<thead>
+<tr><th>Chapter</th><th>What it covers</th></tr>
+</thead>
+<tbody>
+<tr>
+<td><a href="../../../../docs/images/">Live images</a></td>
+<td>tty, slim, and Plasma: what is on each disc, and which one to download.</td>
+</tr>
+<tr>
+<td><a href="../../../../install/">Install</a></td>
+<td>Three paths in tabs: <code>setup</code>, a full manual install from the tty ISO without <code>setup</code>, and the slim image.</td>
+</tr>
+<tr>
+<td><a href="../../../../docs/sps/">SPS tools</a></td>
+<td><code>src</code>, <code>sget</code>, <code>mkpkg</code>, <code>pkin</code>, and the rest of the package system.</td>
+</tr>
+<tr>
+<td><a href="../../../../docs/first-boot/">After first boot</a></td>
+<td>Passwords, network, services, and the next packages.</td>
+</tr>
+</tbody>
+</table>
+
+<h3>Install paths</h3>
+<p>Use <a href="../../../../install/#setup">guided setup</a> on <code>sps-live-tty.iso</code> or <code>sps-live-plasma.iso</code> if you want dialog screens. Use the <a href="../../../../install/#manual">manual chapter</a> if you want the same work as shell commands, with only tools that exist on the tty disc. The <a href="../../../../install/#slim">slim chapter</a> is rescue, partitioning, and network debugging. Slim is not a source-install disc.</p>
+
+<h3>Architecture and status</h3>
+<p>Published images are x86_64 hybrid ISOs. Live PID 1 is busybox init with gettys on tty1 and ttyS0 (or systemd when the machine that built the ISO had it). The login is <code>root</code> with an empty password.</p>
+<p>SPS 1.0 is usable. Official recipe trees are still being built out. A console install compiles for a long time. A Plasma install compiles Qt and KDE and takes much longer.</p>
+
+<h3>Reference trees</h3>
 <ul class="plain">
-<li><a href="../../../../install/">Installation</a></li>
-<li><a href="../../../../packages/">Package browser</a></li>
+<li><a href="../../../../packages/">Package browser</a> (1474 packages)</li>
 <li><a href="https://github.com/RobertFlexx/SPS/blob/main/README.md">SPS README</a></li>
 <li><a href="https://github.com/RobertFlexx/SPS/blob/main/DESIGN.md">SPS 1.0 formats and behavior</a></li>
+<li><a href="https://github.com/RobertFlexx/SPS/blob/main/man/setup.1">setup(1)</a></li>
 <li><a href="https://github.com/RobertFlexx/SPS/blob/main/PACKAGE-MAINTAINER.md">Package maintainer guide</a></li>
-<li><a href="https://github.com/RobertFlexx/SPS/blob/main/MANIFESTO.md">SPS manifesto</a></li>
 <li><a href="https://github.com/RobertFlexx/sps-core/blob/main/README.md">core collection</a></li>
 <li><a href="https://github.com/RobertFlexx/sps-extra/blob/main/README.md">extra collection</a></li>
 <li><a href="../../../../source/">Source repositories</a></li>
 </ul>
-<p>SPS stores package definitions in ordinary Git repositories. The core tree contains the base system and toolchain. Extra contains packages outside the base installation.</p>
+
+<h3>Fastfetch</h3>
 <p>Fastfetch is a native SPS package. On a Splux install it counts SPS packages in its Packages module and still reports Flatpak, Homebrew, Nix, and other managers when they are present. The system default is gray labels and a yellow P in the SPS logo. A user config in <code>~/.config/fastfetch/</code> overrides that default. Fastfetch is not started from shell profiles.</p>
 </main>
+</div>
 @@FOOTER@@
 </div>
+<script src="../../../../assets/docs.js?v=4c4f12c-20261019130111"></script>
 </body>
 </html>
diff --git a/site/docs/sps/index.html b/site/docs/sps/index.html
new file mode 100644
index 0000000..b746272
--- /dev/null
+++ b/site/docs/sps/index.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>SPS tools - Splux Linux</title>
+<meta name="description" content="SPS package tools used on Splux: src, sget, mkpkg, pkin, and setup.">
+<link rel="icon" href="../../../../assets/favicon.png" type="image/png">
+<link rel="stylesheet" href="../../../../assets/style.css">
+</head>
+<body>
+<a class="skip" href="#main">Skip to content</a>
+<div class="wrap wide">
+@@HEADER@@
+<div class="handbook">
+@@DOCSNAV@@
+<main id="main">
+<h2>SPS tools</h2>
+<hr class="rule">
+<p>SPS is a set of small POSIX shell and AWK programs, not one large front end. Splux uses them for the installer, live images, and official collections. The same tools can manage a tree that is not Splux.</p>
+<p>On a live image they live on <code>PATH</code> under <code>/usr</code>. Package database, cache, and build directories are under <code>SPS_ROOT</code> (default <code>/</code>).</p>
+
+<table class="docs">
+<thead>
+<tr><th>Tool</th><th>Role</th></tr>
+</thead>
+<tbody>
+<tr><td><code>src</code></td><td>Repository handling. <code>src update</code> clones or indexes trees from <code>/etc/sps/repos.conf</code>.</td></tr>
+<tr><td><code>sget</code></td><td>Dependency-aware frontend. Resolves a graph, then calls <code>mkpkg</code> and <code>pkin</code>.</td></tr>
+<tr><td><code>mkpkg</code></td><td>Builds one recipe into a <code>.pkg.tar.zst</code> (or gz/xz when compression says so).</td></tr>
+<tr><td><code>pkin</code></td><td>Installs a built package into <code>SPS_ROOT</code>.</td></tr>
+<tr><td><code>pkdel</code></td><td>Removes an installed package.</td></tr>
+<tr><td><code>pkstat</code></td><td>Queries the installed database.</td></tr>
+<tr><td><code>pkcheck</code></td><td>Verifies files against recorded checksums.</td></tr>
+<tr><td><code>pkmark</code></td><td>Marks why a package is installed (explicit / as-needed).</td></tr>
+<tr><td><code>setup</code></td><td>Splux installer. Dialog screens, or the same choices from flags. Not required to use SPS.</td></tr>
+<tr><td><code>mkiso</code></td><td>Builds Splux live images. Maintainer tool.</td></tr>
+</tbody>
+</table>
+
+<h3>Configuration</h3>
+<p><code>/etc/sps/sps.conf</code> must exist before the first <code>sget</code>. Live images already have one. Default keys:</p>
+<pre class="block">db /var/lib/sps
+cache /var/cache/sps
+build /var/tmp/sps
+repo_root /usr/src/sps
+makejobs auto
+cflags -O2 -pipe
+cxxflags -O2 -pipe
+march native
+compression auto
+preserve etc</pre>
+<p>Paths in that file are relative to <code>SPS_ROOT</code> unless you override them with environment variables. For an install into <code>/mnt</code>:</p>
+<pre class="block">export SPS_ROOT=/mnt
+unset SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG SPS_REPO_ROOT</pre>
+<p>Leaving those unset is required so they resolve under the target, not under the live root.</p>
+
+<h3>Repositories</h3>
+<p><code>/etc/sps/repos.conf</code> lists trees. Higher priority wins.</p>
+<pre class="block">dir core /usr/src/sps/core 100
+dir extra /usr/src/sps/extra 80
+git core https://github.com/RobertFlexx/sps-core.git 100
+git extra https://github.com/RobertFlexx/sps-extra.git 80</pre>
+<p>On tty and Plasma images the live <code>repos.conf</code> uses <code>dir</code> entries pointing at the bundled trees. Slim images have no recipes, so they use the git lines. <code>src update</code> for a git repo needs <code>git</code>, which slim does not ship.</p>
+<p>During a target install, <code>setup</code> writes <code>dir</code> paths prefixed with the target (<code>/mnt/usr/src/sps/core</code>) so <code>src update</code> can index them from the live system, then rewrites them to <code>/usr/src/sps/...</code> for the booted machine. The <a href="../../../../install/#manual">manual install</a> does the same.</p>
+
+<h3>Install a package</h3>
+<pre class="block">src update
+sget install curl</pre>
+<p>That resolves dependencies, builds each recipe with the live compiler, and installs into <code>SPS_ROOT</code>. Builds take as long as compiling that software usually takes. Plasma graphs compile Qt and KDE.</p>
+
+<h3>Init packages</h3>
+<p>These conflict. <code>sget</code> will not install more than one. Write the choice to <code>$SPS_ROOT/etc/sps/init</code> before the first package so service hooks see it.</p>
+<table class="docs">
+<thead>
+<tr><th>File</th><th>Package</th><th>PID 1</th></tr>
+</thead>
+<tbody>
+<tr><td><code>systemd</code></td><td><code>init-systemd</code></td><td>systemd (udev, logind)</td></tr>
+<tr><td><code>openrc</code></td><td><code>init-openrc</code></td><td>OpenRC, eudev, elogind</td></tr>
+<tr><td><code>s6</code></td><td><code>init-s6</code></td><td>s6, eudev, elogind</td></tr>
+<tr><td><code>runit</code></td><td><code>init-runit</code></td><td>runit, eudev, elogind</td></tr>
+<tr><td><code>dinit</code></td><td><code>init-dinit</code></td><td>dinit, eudev, elogind</td></tr>
+<tr><td><code>shepherd</code></td><td><code>init-shepherd</code></td><td>GNU Shepherd, eudev, elogind, Guile</td></tr>
+<tr><td><code>none</code></td><td>(no init package)</td><td>the installed tree has no PID 1 from SPS</td></tr>
+</tbody>
+</table>
+<p>Init packages live in extra. A bootable console needs extra recipes on the disc (tty or Plasma), not slim.</p>
+
+<h3>Kernels</h3>
+<p><code>linux-desktop</code> is the Splux daily-driver config (DRM, NVMe, virtio, filesystems). It installs <code>/boot/vmlinuz-desktop-7.2.2</code>. <code>linux</code> is upstream defconfig and installs <code>/boot/vmlinuz-7.2.2</code>. Neither package runs <code>grub-install</code>. <code>none</code> will not boot a new disk.</p>
+
+<h3>Further reading</h3>
+<ul class="plain">
+<li><a href="https://github.com/RobertFlexx/SPS/blob/main/README.md">SPS README</a></li>
+<li><a href="https://github.com/RobertFlexx/SPS/blob/main/DESIGN.md">DESIGN.md</a></li>
+<li><a href="https://github.com/RobertFlexx/SPS/blob/main/PACKAGE-MAINTAINER.md">Recipe format</a></li>
+<li><a href="../../../../packages/">Package browser</a></li>
+</ul>
+</main>
+</div>
+@@FOOTER@@
+</div>
+<script src="../../../../assets/docs.js?v=4c4f12c-20261019130111"></script>
+</body>
+</html>
diff --git a/site/download/index.html b/site/download/index.html
index 2c45c8c..806be2d 100644
--- a/site/download/index.html
+++ b/site/download/index.html
@@ -22,11 +22,12 @@
 <h2>Images</h2>
 <hr class="rule">
 <p><a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-tty.iso">sps-live-tty.iso</a><br>
-Console live image with sps-core, sps-extra, and a seed compiler so <code>setup</code> can build packages.</p>
+Console live image with sps-core, sps-extra, and a seed compiler so <code>setup</code> or a manual source install can build packages. This is the install disc.</p>
 <p><a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-tty-slim.iso">sps-live-tty-slim.iso</a><br>
-Small console image without the seed compiler. <code>setup</code> needs the network to fetch recipes and a toolchain.</p>
+Small console image without recipes, git, or a compiler. Rescue, partitioning, and network debugging. Not a source-install disc. See the <a href="../../../../install/#slim">slim handbook</a>.</p>
 <p><a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-plasma.iso">sps-live-plasma.iso</a><br>
-Plasma/Wayland live session plus the same installer path. Needs a KMS device (virtio-gpu or a real GPU).</p>
+Plasma/Wayland live session plus the same installer path as tty. Needs a KMS device (virtio-gpu or a real GPU).</p>
+<p><a href="../../../../docs/images/">What is on each image</a> · <a href="../../../../install/">Install handbook</a></p>
 <h2>Checksums</h2>
 <hr class="rule">
 <p><a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a> is produced from the uploaded ISOs. Verify on the machine that downloads the image:</p>
diff --git a/site/index.html b/site/index.html
index 55b2761..df23376 100644
--- a/site/index.html
+++ b/site/index.html
@@ -34,13 +34,13 @@
 <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-tty.iso">sps-live-tty.iso</a>
 (console image with recipes and a compiler)<br>
 <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-tty-slim.iso">sps-live-tty-slim.iso</a>
-(small console image; setup needs the network)<br>
+(rescue console; no compiler, no recipes)<br>
 <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/sps-live-plasma.iso">sps-live-plasma.iso</a>
 (Plasma/Wayland live session)<br>
 <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a><br>
 <a href="https://github.com/RobertFlexx/SPS/releases/latest">release notes</a>
 </p>
-<p class="note">These are development live snapshots, not a numbered stable distro release. Installation still compiles packages from source. A Plasma install takes a long time. See <a href="../../../../install/">install</a>.</p>
+<p class="note">These are development live snapshots, not a numbered stable distro release. Installation still compiles packages from source. A Plasma install takes a long time. See the <a href="../../../../install/">install handbook</a>.</p>
 
 <h2 id="packages"><span class="num">[003]</span> Package system</h2>
 <hr class="rule">
@@ -77,8 +77,9 @@ Desktop software, graphics, multimedia, development tools, and other packages. @
 
 <h2 id="docs"><span class="num">[005]</span> Documentation</h2>
 <hr class="rule">
-<p><a href="../../../../install/">Installation</a><br>
-<a href="../../../../docs/">Documentation index</a><br>
+<p><a href="../../../../docs/">Handbook</a><br>
+<a href="../../../../install/">Install</a> (setup, manual tty, slim)<br>
+<a href="../../../../docs/images/">Live images</a><br>
 <a href="https://github.com/RobertFlexx/SPS/blob/main/README.md">SPS README</a><br>
 <a href="https://github.com/RobertFlexx/SPS/blob/main/DESIGN.md">SPS 1.0 formats</a></p>
 
diff --git a/site/install/index.html b/site/install/index.html
index 8a8b0d0..0c19273 100644
--- a/site/install/index.html
+++ b/site/install/index.html
@@ -4,42 +4,419 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Install - Splux Linux</title>
-<meta name="description" content="How to boot a Splux live image and run setup.">
+<meta name="description" content="Install Splux: guided setup, a full manual tty install without setup, and the slim image.">
 <link rel="icon" href="../../../../assets/favicon.png" type="image/png">
 <link rel="stylesheet" href="../../../../assets/style.css">
 </head>
 <body>
 <a class="skip" href="#main">Skip to content</a>
-<div class="wrap">
+<div class="wrap wide">
 @@HEADER@@
+<div class="handbook">
+@@DOCSNAV@@
 <main id="main">
 <h2>Install</h2>
 <hr class="rule">
-<p>The installation procedure is still being finalized. The path that exists today is: boot a live ISO, mount a target filesystem, and run <code>setup</code>.</p>
-<p><code>setup</code> is the Splux installer. It is not a generic SPS or LFS installer. It uses dialog screens, or the same choices from flags for scripts.</p>
-<h2>1. Download and verify</h2>
-<hr class="rule">
-<p>Get an image from <a href="../../../../download/">download</a>. For a source install use <code>sps-live-tty.iso</code>, not the slim image. Check it against <a href="https://github.com/RobertFlexx/SPS/releases/latest/download/SHA256SUMS">SHA256SUMS</a>.</p>
-<h2>2. Write the image</h2>
-<hr class="rule">
+<p>Splux is installed from a live ISO. Packages are compiled on that live system into a target root. Pick a tab. Hash URLs stay on the method: <code>#setup</code>, <code>#manual</code>, <code>#slim</code>.</p>
+<ul class="local-toc">
+<li><a href="#setup">Guided setup</a> on tty or Plasma</li>
+<li><a href="#manual">Manual install</a> from the tty ISO, without <code>setup</code></li>
+<li><a href="#slim">Slim image</a> (rescue, not a source install)</li>
+</ul>
+<div class="callout">
+<p><strong>Which ISO.</strong> Source install needs <code>sps-live-tty.iso</code> or <code>sps-live-plasma.iso</code>. Those discs carry sps-core, sps-extra, and a seed compiler. <code>sps-live-tty-slim.iso</code> does not. Compare images in the <a href="../../../../docs/images/">live images</a> chapter.</p>
+</div>
+
+<div class="tabs" data-tabs>
+<div class="tablist" role="tablist" aria-label="Install method">
+<button type="button" role="tab" id="tab-setup" aria-controls="panel-setup" aria-selected="true" tabindex="0" data-hash="setup">setup</button>
+<button type="button" role="tab" id="tab-manual" aria-controls="panel-manual" aria-selected="false" tabindex="-1" data-hash="manual">Manual (tty)</button>
+<button type="button" role="tab" id="tab-slim" aria-controls="panel-slim" aria-selected="false" tabindex="-1" data-hash="slim">Slim ISO</button>
+</div>
+
+<section class="tabpanel" role="tabpanel" id="panel-setup" aria-labelledby="tab-setup">
+<h3>Guided setup</h3>
+<p><code>setup</code> is the Splux installer. It is not a generic SPS or LFS installer. Dialog screens collect the same choices you can pass as flags. It never guesses a disk for <code>grub-install</code>.</p>
+
+<h4>1. Download and verify</h4>
+<p>Get <a href="../../../../download/">sps-live-tty.iso</a> (or the Plasma image). Check it on the machine that downloaded it:</p>
+<pre class="block">sha256sum -c SHA256SUMS</pre>
+<p>Current published tag: <a href="https://github.com/RobertFlexx/SPS/releases/latest">live-2026-09-06-1</a> (2026-09-06). Signatures are not published yet.</p>
+
+<h4>2. Write the image</h4>
 <pre class="block">dd if=sps-live-tty.iso of=/dev/sdX bs=4M status=progress conv=fsync</pre>
 <p>Replace <code>/dev/sdX</code> with the USB device. That command erases the destination.</p>
-<h2>3. Boot and target disk</h2>
-<hr class="rule">
-<p>Boot the "SPS Linux live (Splux)" entry. The live system comes up on the console (or Plasma on the Plasma image). For QEMU, give the guest a writable second disk in addition to the CD. The live image never runs <code>grub-install</code> on its own.</p>
-<h2>4. Run setup</h2>
-<hr class="rule">
-<pre class="block">setup
-setup --plan --profile plasma-desktop --target /mnt --user alex
-setup --non-interactive --profile minimal --target /mnt --hostname darkstar</pre>
-<p>Profiles are <code>minimal</code>, <code>server</code>, <code>plasma-desktop</code>, and <code>plasma-full</code>. Init is a choice: systemd (default) or OpenRC. Those packages conflict. Guided partitioning is optional. <code>grub-install</code> still needs <code>--install-bootloader</code> and <code>--disk</code>.</p>
-<p>The installer writes hostname, timezone, keymap, and a first user, copies bundled recipe trees when present, runs <code>src update</code>, and <code>sget install</code>. Packages are built from source on the target. A Plasma install compiles Qt and KDE and takes a long time.</p>
-<h2>More</h2>
-<hr class="rule">
-<p><a href="https://github.com/RobertFlexx/SPS/blob/main/README.md">SPS README</a> and <a href="https://github.com/RobertFlexx/SPS/blob/main/man/setup.1">setup(1)</a> are the current command references.</p>
+
+<h4>3. Boot</h4>
+<p>Boot the "SPS Linux live (Splux)" entry. Login is <code>root</code> with an empty password. On Plasma, open Konsole. Live PATH often omits <code>/sbin</code>:</p>
+<pre class="block">export PATH=/sbin:/usr/sbin:/usr/bin:/bin:$PATH</pre>
+<p>For QEMU, give the guest a writable second disk. The live image never runs <code>grub-install</code> on its own.</p>
+
+<h4>4. Run setup</h4>
+<pre class="block">setup</pre>
+<p>Profiles: <code>minimal</code>, <code>server</code>, <code>plasma-desktop</code>, <code>plasma-full</code>. Init: systemd, OpenRC, s6, runit, dinit, or Shepherd. Those packages conflict. Guided partitioning waits until the last confirmation. Kernel <code>none</code> is the interactive default for a console profile; a disk that must boot itself needs <code>linux-desktop</code> or <code>linux</code>.</p>
+<p>Non-interactive examples:</p>
+<pre class="block">setup --plan --profile plasma-desktop --target /mnt --user alex
+
+setup --non-interactive \
+  --profile minimal \
+  --target /mnt \
+  --hostname darkstar \
+  --timezone UTC \
+  --keymap us \
+  --locale C.UTF-8 \
+  --init systemd \
+  --kernel linux-desktop \
+  --partition efi-swap-root \
+  --disk /dev/sda \
+  --format yes \
+  --bootloader grub \
+  --install-bootloader \
+  --user alex</pre>
+<p><code>--plan</code> and <code>--non-interactive</code> never touch a disk unless you pass <code>--disk</code> and <code>--format yes</code> together. <code>grub-install</code> still needs <code>--install-bootloader</code> and <code>--disk</code>.</p>
+
+<h4>Profiles and kernels</h4>
+<table class="docs">
+<thead>
+<tr><th>Profile</th><th>Packages (plus init and kernel you select)</th></tr>
+</thead>
+<tbody>
+<tr><td><code>minimal</code></td><td><code>base-system dialog splux-base fastfetch</code></td></tr>
+<tr><td><code>server</code></td><td>minimal, plus <code>openssh gnupg rsync sudo</code></td></tr>
+<tr><td><code>plasma-desktop</code></td><td>minimal, plus Plasma, SDDM, Mesa, PipeWire, portals</td></tr>
+<tr><td><code>plasma-full</code></td><td>plasma-desktop, plus <code>kde-apps firefox flatpak</code></td></tr>
+</tbody>
+</table>
+<p><code>setup --list-profiles</code> and <code>setup --list-sets</code> print the catalog. Optional sets cover SSH, printing, Flatpak, NVIDIA, firmware, and more. <code>--kernel</code> is <code>none</code>, <code>linux</code>, <code>linux-desktop</code>, or <code>custom</code>.</p>
+
+<h4>Partition layouts</h4>
+<table class="docs">
+<thead>
+<tr><th>Layout</th><th>GPT map</th></tr>
+</thead>
+<tbody>
+<tr><td><code>efi-root</code></td><td>512MiB ESP, rest root</td></tr>
+<tr><td><code>efi-swap-root</code></td><td>ESP, swap (default 8GiB), rest root</td></tr>
+<tr><td><code>efi-swap-root-home</code></td><td>ESP, swap, 48GiB root, rest home</td></tr>
+<tr><td><code>bios-root</code></td><td>1MiB BIOS boot, rest root</td></tr>
+<tr><td><code>existing</code> / <code>manage</code></td><td>you mount or name partitions</td></tr>
+</tbody>
+</table>
+<p>Formats match the installer: ESP is FAT32 labeled <code>SPS_EFI</code>, root is ext4 labeled <code>sps-root</code>, swap is labeled <code>sps-swap</code>. <code>/etc/fstab</code> is written from <code>blkid</code> UUIDs when setup partitioned the disk.</p>
+
+<h4>What setup does</h4>
+<p>It writes hostname, timezone, keymap, locale, and <code>/etc/sps/init</code>, copies bundled recipe trees when they exist, writes <code>sps.conf</code>, runs <code>src update</code>, then <code>sget install</code>. Users land after packages so <code>filesystem</code>'s passwd file is already there. If you passed <code>--install-bootloader</code>, it runs live <code>grub-install</code> (EFI removable <code>EFI/BOOT/BOOTX64.EFI</code>, then NVRAM via <code>efibootmgr</code> when that binary exists; BIOS uses <code>i386-pc</code>). It does not run <code>grub-mkconfig</code>. See <code>/root/sps-bootloader.txt</code> on the target.</p>
+<p>A Plasma install compiles Qt and KDE and takes a long time. See <a href="https://github.com/RobertFlexx/SPS/blob/main/man/setup.1">setup(1)</a> for every flag.</p>
+</section>
+
+<section class="tabpanel" role="tabpanel" id="panel-manual" aria-labelledby="tab-manual">
+<h3>Manual install (no setup)</h3>
+<p>This is the same work <code>setup</code> does, written as shell commands. It uses tools that exist on <code>sps-live-tty.iso</code> (the Plasma ISO has the same installer stack). Do not follow this chapter on slim.</p>
+<div class="callout">
+<p><strong>Tools on the tty disc.</strong> SPS (<code>src</code>, <code>sget</code>, <code>mkpkg</code>, <code>pkin</code>, and the rest), gcc/g++, make, cmake, ninja, meson, python3, perl, git, curl, wget, tar, xz, gzip, zstd, nano, vim, <code>dialog</code>, <code>openssl</code>, <code>lsblk</code>, <code>blkid</code>, <code>sfdisk</code>, <code>cfdisk</code>, <code>fdisk</code>, <code>wipefs</code>, <code>mkfs.ext4</code>, <code>mkfs.vfat</code>, <code>mkswap</code>, <code>partprobe</code>, <code>dhcpcd</code>, <code>grub-install</code>, <code>grub-mkconfig</code>, <code>efibootmgr</code> (when the builder had it), plus busybox <code>sh</code>, <code>mount</code>, <code>chroot</code>, <code>passwd</code>. Recipes are at <code>/usr/src/sps/core</code> and <code>/usr/src/sps/extra</code>.</p>
+</div>
+
+<ol class="steps">
+<li>
+<p><strong>Boot tty and fix PATH.</strong> Login is <code>root</code> with an empty password.</p>
+<pre class="block">export PATH=/sbin:/usr/sbin:/usr/bin:/bin:$PATH
+lsblk -o NAME,SIZE,TYPE,TRAN,MODEL</pre>
+<p>Pick a writable disk that is not the live USB. On QEMU that is the second disk. Optional DHCP: <code>dhcpcd</code>.</p>
+</li>
+<li>
+<p><strong>Partition and format.</strong> Use the firmware tabs. Names: <code>/dev/sda1</code> versus <code>/dev/nvme0n1p1</code>. These layouts match <code>setup</code>.</p>
+<div class="tabs" data-tabs>
+<div class="tablist" role="tablist" aria-label="Firmware">
+<button type="button" role="tab" id="tab-uefi" aria-controls="panel-uefi" aria-selected="true" tabindex="0" data-hash="uefi">UEFI (GPT)</button>
+<button type="button" role="tab" id="tab-bios" aria-controls="panel-bios" aria-selected="false" tabindex="-1" data-hash="bios">BIOS (GPT)</button>
+</div>
+<section class="tabpanel" role="tabpanel" id="panel-uefi" aria-labelledby="tab-uefi">
+<p>ESP (type EFI System), swap, then Linux root. Default swap size is 8GiB, same as setup.</p>
+<pre class="block">DISK=/dev/sda
+wipefs -a "$DISK"
+sfdisk --wipe always --wipe-partitions always "$DISK" &lt;&lt;'EOF'
+label: gpt
+size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name=EFI
+size=8GiB, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, name=swap
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+EOF
+partprobe "$DISK"
+
+EFI=${DISK}1
+SWAP=${DISK}2
+ROOT=${DISK}3
+# nvme: EFI=${DISK}p1 and so on
+
+mkfs.vfat -F 32 -n SPS_EFI "$EFI"
+mkswap -L sps-swap "$SWAP"
+mkfs.ext4 -F -L sps-root "$ROOT"
+
+mount "$ROOT" /mnt
+mount -o remount,rw,exec,suid,dev /mnt
+mkdir -p /mnt/boot/efi
+mount "$EFI" /mnt/boot/efi
+swapon "$SWAP"</pre>
+<p>No swap: omit the swap line in the sfdisk script (setup layout <code>efi-root</code>) and skip <code>mkswap</code> / <code>swapon</code>. Separate <code>/home</code>: setup layout <code>efi-swap-root-home</code> uses a 48GiB root then the rest as home; format that partition ext4 labeled <code>sps-home</code> and mount it on <code>/mnt/home</code>.</p>
+<p><code>cfdisk "$DISK"</code> is on the disc if you want a TUI instead of a script. Types: EFI System, Linux swap, Linux filesystem.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-bios" aria-labelledby="tab-bios">
+<p>1MiB BIOS boot partition (GRUB GPT), then Linux root. No ESP.</p>
+<pre class="block">DISK=/dev/sda
+wipefs -a "$DISK"
+sfdisk --wipe always --wipe-partitions always "$DISK" &lt;&lt;'EOF'
+label: gpt
+size=1MiB, type=21686148-6449-6E6F-744E-656564454649, name=BIOS
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+EOF
+partprobe "$DISK"
+
+ROOT=${DISK}2
+mkfs.ext4 -F -L sps-root "$ROOT"
+mount "$ROOT" /mnt
+mount -o remount,rw,exec,suid,dev /mnt</pre>
+<p>The remount matters. Autoconf runs conftests on this mount; a <code>noexec</code> or <code>nodev</code> root fails those builds.</p>
+</section>
+</div>
+</li>
+<li>
+<p><strong>Target directories and sps.conf.</strong> <code>sget</code> will not run without a config.</p>
+<pre class="block">mkdir -p /mnt/etc/sps /mnt/usr/src/sps /mnt/root
+if [ -f /etc/sps/live ] &amp;&amp; [ -f /etc/sps/sps.conf ]; then
+  cp /etc/sps/sps.conf /mnt/etc/sps/sps.conf
+else
+  cat &gt; /mnt/etc/sps/sps.conf &lt;&lt;'EOF'
+db /var/lib/sps
+cache /var/cache/sps
+build /var/tmp/sps
+repo_root /usr/src/sps
+makejobs auto
+cflags -O2 -pipe
+cxxflags -O2 -pipe
+march native
+compression auto
+preserve etc
+EOF
+fi</pre>
+</li>
+<li>
+<p><strong>Copy bundled recipes.</strong> tty and Plasma already have them. Copying avoids a git clone during <code>src update</code>.</p>
+<pre class="block">cp -a /usr/src/sps/core/. /mnt/usr/src/sps/core/
+cp -a /usr/src/sps/extra/. /mnt/usr/src/sps/extra/
+rm -rf /mnt/usr/src/sps/core/.git /mnt/usr/src/sps/extra/.git
+
+cat &gt; /mnt/etc/sps/repos.conf &lt;&lt;'EOF'
+dir core /mnt/usr/src/sps/core 100
+dir extra /mnt/usr/src/sps/extra 80
+EOF</pre>
+<p>Those paths are for indexing from the live system. After packages you rewrite them to <code>/usr/src/sps/...</code> for the booted machine.</p>
+</li>
+<li>
+<p><strong>Point SPS at /mnt and record init.</strong> Write <code>/etc/sps/init</code> before the first package so service hooks see it. The init packages conflict.</p>
+<pre class="block">export SPS_ROOT=/mnt
+unset SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG SPS_REPO_ROOT</pre>
+<div class="tabs" data-tabs>
+<div class="tablist" role="tablist" aria-label="Init system">
+<button type="button" role="tab" id="tab-systemd" aria-controls="panel-init-systemd" aria-selected="true" tabindex="0" data-hash="systemd">systemd</button>
+<button type="button" role="tab" id="tab-openrc" aria-controls="panel-init-openrc" aria-selected="false" tabindex="-1" data-hash="openrc">OpenRC</button>
+<button type="button" role="tab" id="tab-s6" aria-controls="panel-init-s6" aria-selected="false" tabindex="-1" data-hash="s6">s6</button>
+<button type="button" role="tab" id="tab-runit" aria-controls="panel-init-runit" aria-selected="false" tabindex="-1" data-hash="runit">runit</button>
+<button type="button" role="tab" id="tab-dinit" aria-controls="panel-init-dinit" aria-selected="false" tabindex="-1" data-hash="dinit">dinit</button>
+<button type="button" role="tab" id="tab-shepherd" aria-controls="panel-init-shepherd" aria-selected="false" tabindex="-1" data-hash="shepherd">Shepherd</button>
+</div>
+<section class="tabpanel" role="tabpanel" id="panel-init-systemd" aria-labelledby="tab-systemd">
+<pre class="block">printf '%s\n' systemd &gt; /mnt/etc/sps/init
+# later: sget install init-systemd</pre>
+<p>PID 1 is systemd, with udev and logind. Interactive <code>setup</code> defaults to systemd when the live disc recorded that init.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-init-openrc" aria-labelledby="tab-openrc">
+<pre class="block">printf '%s\n' openrc &gt; /mnt/etc/sps/init
+# later: sget install init-openrc</pre>
+<p>OpenRC plus eudev and elogind. Enable services with <code>rc-update</code> after boot.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-init-s6" aria-labelledby="tab-s6">
+<pre class="block">printf '%s\n' s6 &gt; /mnt/etc/sps/init
+# later: sget install init-s6</pre>
+<p>s6 plus eudev and elogind. Enabled longruns live in <code>/etc/s6/enabled</code>.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-init-runit" aria-labelledby="tab-runit">
+<pre class="block">printf '%s\n' runit &gt; /mnt/etc/sps/init
+# later: sget install init-runit</pre>
+<p>runit plus eudev and elogind. Enable with a symlink in <code>/etc/runit/runsvdir/default</code>.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-init-dinit" aria-labelledby="tab-dinit">
+<pre class="block">printf '%s\n' dinit &gt; /mnt/etc/sps/init
+# later: sget install init-dinit</pre>
+<p>dinit plus eudev and elogind. Enable by linking into <code>/etc/dinit.d/boot.d</code>.</p>
+</section>
+<section class="tabpanel" role="tabpanel" id="panel-init-shepherd" aria-labelledby="tab-shepherd">
+<pre class="block">printf '%s\n' shepherd &gt; /mnt/etc/sps/init
+# later: sget install init-shepherd</pre>
+<p>GNU Shepherd plus eudev, elogind, and Guile. Services are Scheme files under <code>/etc/shepherd.d</code>.</p>
+</section>
+</div>
+</li>
+<li>
+<p><strong>Probe the live compiler, then install packages.</strong> Fail here rather than inside the first autoconf package.</p>
+<pre class="block">printf '%s\n' 'int main(void){return 0;}' | gcc -x c - -o /tmp/sps-cc-probe
+rm -f /tmp/sps-cc-probe
+meson --version
+
+src update
+
+sget install base-system dialog splux-base fastfetch
+sget install init-systemd
+sget install linux-desktop
+sget install grub</pre>
+<p>Replace <code>init-systemd</code> with the package from the init tab. <code>base-system</code> is a metapackage (shell, coreutils, curl, git, e2fsprogs, busybox, shadow, tzdata, and the rest of the essential userland). <code>splux-base</code> is branding and os-release. <code>linux-desktop</code> is the Splux daily-driver kernel and installs <code>/boot/vmlinuz-desktop-7.2.2</code>. <code>linux</code> is upstream defconfig (<code>/boot/vmlinuz-7.2.2</code>). Skipping a kernel package will not boot a new disk.</p>
+<p>Server add-ons: <code>sget install openssh gnupg rsync sudo</code>. Plasma add-ons are the plasma-desktop profile set (Plasma, SDDM, Mesa, PipeWire, portals) and compile for a long time.</p>
+</li>
+<li>
+<p><strong>Rewrite repos.conf for the booted system.</strong></p>
+<pre class="block">cat &gt; /mnt/etc/sps/repos.conf &lt;&lt;'EOF'
+dir core /usr/src/sps/core 100
+dir extra /usr/src/sps/extra 80
+EOF</pre>
+</li>
+<li>
+<p><strong>Hostname, locale, keymap, timezone, fstab.</strong> Do this after packages so <code>filesystem</code> has already laid down passwd and a stub fstab. <code>tzdata</code> is in <code>base-system</code>; the live disc also has <code>/usr/share/zoneinfo</code> from the seed.</p>
+<pre class="block">printf '%s\n' darkstar &gt; /mnt/etc/hostname
+printf '%s\n' UTC &gt; /mnt/etc/timezone
+printf 'KEYMAP=%s\n' us &gt; /mnt/etc/vconsole.conf
+printf 'keymap %s\n' us &gt; /mnt/etc/sps/keymap
+printf 'LANG=%s\n' C.UTF-8 &gt; /mnt/etc/locale.conf
+cp /usr/share/zoneinfo/UTC /mnt/etc/localtime
+
+ROOT_UUID=$(blkid -s UUID -o value "$ROOT")
+{
+  printf '%s\n' '# /etc/fstab'
+  printf 'UUID=%s / ext4 defaults 1 1\n' "$ROOT_UUID"
+  if [ -n "$EFI" ]; then
+    printf 'UUID=%s /boot/efi vfat umask=0077 0 2\n' \
+      "$(blkid -s UUID -o value "$EFI")"
+  fi
+  if [ -n "$SWAP" ]; then
+    printf 'UUID=%s none swap defaults 0 0\n' \
+      "$(blkid -s UUID -o value "$SWAP")"
+  fi
+} &gt; /mnt/etc/fstab</pre>
+</li>
+<li>
+<p><strong>Users.</strong> After <code>shadow</code> / <code>filesystem</code> exist. setup uses uid 1000 and groups wheel, audio, video, input, kvm, users. Busybox <code>passwd</code> can set a hash in the live tree; <code>openssl passwd -6</code> is on the disc too.</p>
+<pre class="block"># example: lock root until you set a password, add user alex
+# edit /mnt/etc/passwd /mnt/etc/group /mnt/etc/shadow, or:
+mkdir -p /mnt/home/alex
+printf '%s\n' 'alex:x:1000:1000:alex:/home/alex:/bin/sh' &gt;&gt; /mnt/etc/passwd
+printf '%s\n' 'alex:x:1000:' &gt;&gt; /mnt/etc/group
+printf '%s\n' 'alex:!:0:0:99999:7:::' &gt;&gt; /mnt/etc/shadow
+chown -R 1000:1000 /mnt/home/alex
+chmod 0750 /mnt/home/alex
+# add alex to wheel,audio,video,input,kvm,users in /mnt/etc/group
+chroot /mnt passwd root
+chroot /mnt passwd alex</pre>
+<p>A full chroot for <code>passwd</code> needs a working target userland (after <code>base-system</code>). If chroot cannot run the target <code>passwd</code>, set hashes from the live system with <code>openssl passwd -6</code> and put the hash in the shadow field.</p>
+</li>
+<li>
+<p><strong>Bootloader.</strong> Explicit, same as setup. Run it after <code>grub</code> and a kernel are installed. Use the live <code>grub-install</code> pointed at the mounted tree.</p>
+<p>UEFI (writes <code>\EFI\BOOT\BOOTX64.EFI</code>, no NVRAM required):</p>
+<pre class="block">grub-install --target=x86_64-efi \
+  --efi-directory=/mnt/boot/efi \
+  --boot-directory=/mnt/boot \
+  --removable --no-nvram \
+  "$DISK"
+if command -v efibootmgr &gt;/dev/null 2&gt;&amp;1; then
+  grub-install --target=x86_64-efi \
+    --efi-directory=/mnt/boot/efi \
+    --bootloader-id=SPS \
+    --boot-directory=/mnt/boot \
+    "$DISK" || printf '%s\n' 'NVRAM update failed; removable path is in place'
+fi</pre>
+<p>BIOS:</p>
+<pre class="block">grub-install --target=i386-pc --boot-directory=/mnt/boot "$DISK"</pre>
+<p><code>setup</code> does not run <code>grub-mkconfig</code>. After the kernel is in <code>/mnt/boot</code>:</p>
+<pre class="block">mount --bind /dev /mnt/dev
+mount --bind /proc /mnt/proc
+mount --bind /sys /mnt/sys
+chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg</pre>
+</li>
+<li>
+<p><strong>Unmount and reboot.</strong></p>
+<pre class="block">umount /mnt/dev /mnt/proc /mnt/sys 2&gt;/dev/null || :
+swapoff -a
+umount /mnt/boot/efi 2&gt;/dev/null || :
+umount /mnt
+reboot</pre>
+<p>Continue in <a href="../../../../docs/first-boot/">After first boot</a>.</p>
+</li>
+</ol>
+
+<h4>Time and limits</h4>
+<p>Source install compiles on the live compiler into <code>SPS_ROOT</code>. <code>linux-desktop</code> is a kernel build. Plasma is Qt and KDE. Leave it running.</p>
+<p>Packages are linked against the live C library. There is no glibc recipe in sps-core yet. That is the same for <code>setup</code> and this chapter. Splux is still under testing; a first boot needs the kernel, an init, GRUB, and a userland that can actually start.</p>
+</section>
+
+<section class="tabpanel" role="tabpanel" id="panel-slim" aria-labelledby="tab-slim">
+<h3>Slim ISO</h3>
+<p><code>sps-live-tty-slim.iso</code> is the small console image from <code>make iso-slim</code> (<code>--no-seed --no-modules</code>, no <code>--core</code>, no <code>--extra</code>). It is a rescue and disk tool. It is not a Splux source-install disc.</p>
+<div class="callout">
+<p><strong>What slim cannot do.</strong> No gcc, no make, no git, no python, no meson, no recipes, no live kernel modules. <code>repos.conf</code> lists git URLs, but <code>git</code> is not on the image, so <code>src update</code> cannot clone sps-core. <code>setup</code> is present because SPS is installed on every live root; it still cannot build packages here.</p>
+</div>
+
+<h4>What is on the disc</h4>
+<table class="docs">
+<thead>
+<tr><th>Class</th><th>Commands</th></tr>
+</thead>
+<tbody>
+<tr><td>SPS</td><td><code>src sget mkpkg pkin pkdel pkstat pkcheck pkmark setup mkiso</code></td></tr>
+<tr><td>Disk</td><td><code>lsblk blkid sfdisk cfdisk fdisk wipefs mkfs.ext4 mke2fs mkfs.vfat mkswap partprobe</code></td></tr>
+<tr><td>Boot</td><td><code>grub-install grub-mkconfig</code>, and <code>efibootmgr</code> when the builder had it</td></tr>
+<tr><td>Network</td><td>host <code>dhcpcd</code>; busybox <code>udhcpc</code>, <code>wget</code>, <code>ip</code>, <code>ifconfig</code>, <code>ping</code></td></tr>
+<tr><td>Crypto / linker</td><td><code>openssl ldd ldconfig</code></td></tr>
+<tr><td>Busybox</td><td><code>sh vi mount umount chroot tar gzip xz passwd sha256sum modprobe fdisk mkswap</code> and the rest of the applet list. No <code>git</code>. No <code>zstd</code> applet. No <code>nano</code> / <code>vim</code>.</td></tr>
+</tbody>
+</table>
+
+<h4>Boot and PATH</h4>
+<pre class="block">export PATH=/sbin:/usr/sbin:/usr/bin:/bin:$PATH
+lsblk -o NAME,SIZE,TYPE,TRAN,MODEL</pre>
+<p>Login is <code>root</code> with an empty password. Edit files with busybox <code>vi</code>.</p>
+
+<h4>Network</h4>
+<pre class="block">ip link
+dhcpcd
+# if dhcpcd is missing or the interface needs busybox:
+udhcpc -i eth0
+ping -c 1 1.1.1.1
+wget -O - https://splux.robertflexx.dev/ &gt; /dev/null</pre>
+<p><code>wget</code> can fetch SHA256SUMS or another ISO. It cannot replace git for <code>src update</code>.</p>
+
+<h4>Partition and filesystems</h4>
+<p>The same <code>sfdisk</code> / <code>mkfs.ext4</code> / <code>mkfs.vfat</code> / <code>mkswap</code> commands as the <a href="#manual">manual tty chapter</a> work here. That only prepares disks. It does not install Splux. Typical rescue:</p>
+<pre class="block">lsblk
+# mount an existing Splux or other Linux root
+mount /dev/sda2 /mnt
+mount /dev/sda1 /mnt/boot/efi
+mount --bind /dev /mnt/dev
+mount --bind /proc /mnt/proc
+mount --bind /sys /mnt/sys
+chroot /mnt /bin/sh</pre>
+<p>Reinstall GRUB from slim onto a disk that already has a <code>/boot</code> tree, using live <code>grub-install</code> the same way the tty chapter does. You still need a kernel and userland on that disk already.</p>
+
+<h4>Why setup fails here</h4>
+<p><code>setup</code> copies recipes from <code>/usr/src/sps/{core,extra}</code> when those trees exist. On slim they do not, so it would try git clones. <code>git</code> is missing. Even with recipes, the live gcc probe fails because there is no compiler. Use tty or Plasma for install.</p>
+<p>Download the install disc from another machine, or from slim if you have network and a place to write it, then reboot into tty:</p>
+<pre class="block"># on a machine that can download:
+# sps-live-tty.iso from https://github.com/RobertFlexx/SPS/releases/latest</pre>
+<p>Full install: <a href="#setup">setup tab</a> or <a href="#manual">manual tab</a>.</p>
+</section>
+</div>
+
+<p class="note">Splux is under active development. These snapshots track installer and live-session work. They are not a numbered stable release.</p>
 </main>
+</div>
 @@FOOTER@@
 </div>
 <script src="../../../../assets/live.js?v=4c4f12c-20261019130111" data-root="../../../../"></script>
+<script src="../../../../assets/docs.js?v=4c4f12c-20261019130111"></script>
 </body>
 </html>
diff --git a/tools/build-site.sh b/tools/build-site.sh
index d13be05..ea15d4c 100755
--- a/tools/build-site.sh
+++ b/tools/build-site.sh
@@ -44,7 +44,8 @@ fi
 
 rm -rf "$OUT"
 mkdir -p "$OUT/assets" "$OUT/data" "$OUT/download" "$OUT/packages" \
-	"$OUT/install" "$OUT/docs" "$OUT/source" "$OUT/news"
+	"$OUT/install" "$OUT/docs" "$OUT/docs/images" "$OUT/docs/sps" \
+	"$OUT/docs/first-boot" "$OUT/source" "$OUT/news"
 
 cp -a site/assets/. "$OUT/assets/"
 if [ -f CNAME ]; then
@@ -85,6 +86,32 @@ write_footer() {
 EOF
 }
 
+write_docs_nav() {
+	root=$1
+	cur=$2
+	printf '%s\n' '<nav class="docs-side" aria-label="Handbook">'
+	printf '%s\n' '<p class="docs-kicker">Handbook</p>'
+	printf '%s\n' '<ul class="docs-toc">'
+	set -- \
+		overview "${root}docs/" "Overview" \
+		images "${root}docs/images/" "Live images" \
+		install "${root}install/" "Install" \
+		sps "${root}docs/sps/" "SPS tools" \
+		firstboot "${root}docs/first-boot/" "After first boot"
+	while [ "$#" -ge 3 ]
+	do
+		if [ "$cur" = "$1" ]; then
+			printf '<li><a href="%s" aria-current="page">%s</a></li>\n' \
+				"$2" "$3"
+		else
+			printf '<li><a href="%s">%s</a></li>\n' "$2" "$3"
+		fi
+		shift 3
+	done
+	printf '%s\n' '</ul>'
+	printf '%s\n' '</nav>'
+}
+
 tmp=$(mktemp -d "${TMPDIR:-/tmp}/splux-site.XXXXXX") || exit 1
 trap 'rm -rf "$tmp"' 0 HUP INT TERM
 
@@ -196,7 +223,7 @@ if command -v gh >/dev/null 2>&1; then
 fi
 if [ "$tag" = unknown ] || [ -z "$tag" ]; then
 	tag=latest
-	date=see GitHub
+	date="see GitHub"
 	published=
 fi
 generated=$(date -u '+%Y-%m-%d %H:%M UTC' 2>/dev/null || date)
@@ -263,12 +290,15 @@ subst() {
 	src=$1
 	dest=$2
 	root=$3
+	docscurrent=${4-}
 	write_header "$root" >"$tmp/header.html"
 	write_footer "$root" >"$tmp/footer.html"
+	write_docs_nav "$root" "$docscurrent" >"$tmp/docsnav.html"
 	mkdir -p "$(dirname "$dest")"
 	awk -f tools/subst.awk \
 		-v headerfile="$tmp/header.html" \
 		-v footerfile="$tmp/footer.html" \
+		-v docsnavfile="$tmp/docsnav.html" \
 		-v rowsfile="$tmp/rows.html" \
 		-v newsfile="$tmp/news.html" \
 		-v brieffile="$tmp/news-brief.html" \
@@ -291,8 +321,11 @@ subst() {
 subst site/index.html "$OUT/index.html" "./"
 subst site/download/index.html "$OUT/download/index.html" "../"
 subst site/packages/index.html "$OUT/packages/index.html" "../"
-subst site/install/index.html "$OUT/install/index.html" "../"
-subst site/docs/index.html "$OUT/docs/index.html" "../"
+subst site/install/index.html "$OUT/install/index.html" "../" install
+subst site/docs/index.html "$OUT/docs/index.html" "../" overview
+subst site/docs/images/index.html "$OUT/docs/images/index.html" "../../" images
+subst site/docs/sps/index.html "$OUT/docs/sps/index.html" "../../" sps
+subst site/docs/first-boot/index.html "$OUT/docs/first-boot/index.html" "../../" firstboot
 subst site/source/index.html "$OUT/source/index.html" "../"
 
 awk -f tools/news.awk -v mode=html -v page=1 -v per="$news_per" \
diff --git a/tools/subst.awk b/tools/subst.awk
index 1507da0..9ab025f 100644
--- a/tools/subst.awk
+++ b/tools/subst.awk
@@ -63,6 +63,11 @@ $0 == "@@PAGER@@" {
 	next
 }
 
+$0 == "@@DOCSNAV@@" {
+	dump_file(docsnavfile)
+	next
+}
+
 {
 	gsub(/../../../..//, root)
 	gsub(/live-2026-09-06-1/, tag)