diff --git a/README.md b/README.md
index befffba..82f28fe 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
**What the Raspberry Pi time-server guides get wrong, and the numbers to prove it.**
-Docs: **[escapement.warehack.ing](https://escapement.warehack.ing)**
+Docs: **[cuckoo.warehack.ing](https://cuckoo.warehack.ing)**
We built a GPS-disciplined Stratum 1 NTP server on a Raspberry Pi 4, followed the
published advice, and measured everything. Most of that advice is wrong on this
diff --git a/dashboard/src/gpsntp_dashboard/static/style.css b/dashboard/src/gpsntp_dashboard/static/style.css
index 0e3071b..b7b918c 100644
--- a/dashboard/src/gpsntp_dashboard/static/style.css
+++ b/dashboard/src/gpsntp_dashboard/static/style.css
@@ -100,22 +100,8 @@ svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-wi
@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); } 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); } }
/* ---------- layout ---------- */
-/* `grid-template-columns: minmax(0, 1fr)` is load-bearing, not decoration.
- *
- * A grid column defaults to `auto`, whose MINIMUM is min-content — and grid items
- * refuse to shrink below it. The sources table has seven columns, giving it a
- * ~700px min-content, and that number propagates up and pins `main` to 771px
- * inside a 375px phone. Every panel then overflows and the document gets a
- * horizontal scrollbar. `.table-hold { overflow-x: auto }` cannot rescue it,
- * because the wrapper's own ancestors still won't shrink.
- *
- * The 0 in minmax(0, 1fr) overrides that automatic min-content floor. Same reason
- * .panel needs min-width: 0. Remove either and mobile breaks again.
- */
-main { max-width: 1180px; margin: 0 auto; padding: 20px;
- display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
+main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18px; }
.panel {
- min-width: 0;
background: linear-gradient(var(--panel), var(--bg-2));
border: 1px solid var(--border-soft); border-radius: 16px; padding: 18px;
box-shadow: var(--shadow);
@@ -268,77 +254,10 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px;
.ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; }
.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); }
-/* ---------- mobile ----------
- *
- * This is a status page. On a phone you are almost always asking ONE question:
- * "is the clock still good?" So the hero answer — time, stratum, lock — must be
- * readable without scrolling, and everything else can wait below the fold.
- *
- * The tables and the signal bars scroll sideways WITHIN their panels rather than
- * being crushed into unreadability. That's deliberate: a squeezed IPv6 address
- * wrapped over four lines is worse than one you swipe.
- */
-@media (max-width: 700px) {
- main { padding: 14px; gap: 14px; }
- .panel { padding: 14px; border-radius: 14px; }
-
- /* A panel head is a title plus its readout. Side by side they fight for a
- narrow line and the title wraps mid-phrase; stacked, both stay whole. */
- .panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
- .spark-meta { font-size: 11px; }
-
- /* Stack the clock above the stratum badge, and left-align the badge so the
- eye travels straight down instead of ping-ponging left-right. */
- .hero { flex-direction: column; align-items: flex-start; gap: 14px; }
- .verdict { text-align: left; width: 100%; }
- .ref-line { margin-top: 6px; }
-
- /* clamp() already scales this, but 11vw on a narrow phone leaves the seconds
- hard against the edge. Give it a slightly bigger floor and tighter tracking. */
- .clock { font-size: clamp(38px, 13vw, 60px); letter-spacing: 0; }
-
- /* Two columns, not one: six stat cards single-file is a lot of thumb. */
- .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
- .card { padding: 12px; }
- .card-value { font-size: 21px; }
-
- .spark-big { height: 90px; }
- .snr { min-height: 150px; }
-
- /* Swipe the source table rather than wrap it. The shadow hint tells the reader
- there IS more to the right — otherwise a cut-off column just looks broken. */
- .table-hold {
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- background:
- linear-gradient(to right, var(--panel) 30%, transparent) left / 24px 100% no-repeat,
- linear-gradient(to left, var(--panel) 30%, transparent) right / 24px 100% no-repeat,
- radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.35), transparent) left / 12px 100% no-repeat,
- radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.35), transparent) right / 12px 100% no-repeat;
- background-attachment: local, local, scroll, scroll;
- }
- .tbl { min-width: 560px; }
-
- .topbar { padding: 12px 14px; }
- .brand h1 { font-size: 17px; }
- .brand-sub { font-size: 11.5px; }
- .brand-icon { width: 26px; height: 26px; }
-
- .foot { padding: 10px 14px 24px; }
- /* Three items on one line is unreadable at 375px — let them stack. */
- .foot-meta { flex-direction: column; align-items: flex-start; gap: 6px; }
-}
-
@media (max-width: 560px) {
.ss-plate__link { flex-direction: column; align-items: flex-start; }
}
-/* Comfortable thumb targets. The WCAG 2.2 minimum is 24px; 44px is the iOS HIG
- figure and the one that actually feels right when you're standing up. */
-@media (pointer: coarse) {
- .foot-docs, .ss-plate__link { min-height: 44px; }
-}
-
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
/* "approx" marker when GPSNTP_POSITION=coarse */
diff --git a/docs-site/.env.example b/docs-site/.env.example
index 38f021f..e77a21f 100644
--- a/docs-site/.env.example
+++ b/docs-site/.env.example
@@ -4,6 +4,6 @@
COMPOSE_PROJECT_NAME=cuckoo-escapement-docs
-# Production: escapement.warehack.ing
-# Local dev: escapement.l.warehack.ing (internal only — never reference publicly)
-DOMAIN=escapement.warehack.ing
+# Production: cuckoo.warehack.ing
+# Local dev: cuckoo.l.warehack.ing (internal only — never reference publicly)
+DOMAIN=cuckoo.warehack.ing
diff --git a/docs-site/Makefile b/docs-site/Makefile
index af563f6..fbc9515 100644
--- a/docs-site/Makefile
+++ b/docs-site/Makefile
@@ -39,21 +39,21 @@ shell: ## Open a shell in the running dev container (debugging)
# ---- Production deploy --------------------------------------------------
#
-# `make deploy` SSHes to dell01 (where the docs run, behind the shared
-# caddy-docker-proxy), has it pull origin/main from Gitea via the forwarded
-# agent key, and rebuilds the container. Nothing persistent is provisioned.
+# `make deploy` pulls origin/main on the warehack.ing prod host and rebuilds
+# the docs container. Agent-forwarding (`-A`) lets the remote `git pull` use
+# the operator's local SSH key for Gitea — nothing persistent is provisioned
+# on the deploy host.
#
-# `git fetch origin` (bare) — NOT `git fetch origin main`, which only writes
-# FETCH_HEAD, leaving origin/main unresolved so the reset fails on a fresh
-# checkout. Override DEPLOY_HOST / DEPLOY_PATH for a different target.
+# Override DEPLOY_HOST / DEPLOY_PATH for a different deployment without
+# editing this file. The defaults are the warehack.ing cookie-cutter shape
# (see ~/.claude/references/warehacking.md).
-DEPLOY_HOST ?= dell01
-DEPLOY_PATH ?= warehack-ing/cuckoo-escapement
+DEPLOY_HOST ?= warehack-ing@warehack.ing
+DEPLOY_PATH ?= ~/cuckoo-escapement
.PHONY: deploy
-deploy: ## Pull origin/main on dell01 + rebuild the docs container
- @echo "==> deploying on $(DEPLOY_HOST):$(DEPLOY_PATH)"
- ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch --quiet origin && git reset --hard origin/main && cd docs-site && make prod"
- @echo "==> sanity check (title of a real page; bogus path should 404)"
- @curl -sS "https://escapement.warehack.ing/explanation/preempt-rt-made-it-worse/" | grep -oE "
[^<]*" || echo " title not found"
+deploy: ## Pull main + rebuild the docs container on the prod host
+ @echo "==> deploying $(DEPLOY_HOST):$(DEPLOY_PATH)"
+ ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch origin main && git reset --hard origin/main && cd docs-site && make prod"
+ @echo "==> sanity check"
+ @curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/preempt-rt-made-it-worse/"
diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs
index 5732dc2..435948b 100644
--- a/docs-site/astro.config.mjs
+++ b/docs-site/astro.config.mjs
@@ -10,17 +10,16 @@
// when the dev server runs behind Caddy (TLS-terminating proxy) — without an
// explicit host/protocol/clientPort, Vite's WebSocket drops every ~10s.
//
-// Site URL comes from DOMAIN so one image serves both escapement.warehack.ing (prod)
-// and escapement.l.warehack.ing (local dev).
+// Site URL comes from DOMAIN so one image serves both cuckoo.warehack.ing (prod)
+// and cuckoo.l.warehack.ing (local dev).
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import starlight from "@astrojs/starlight";
import { defineConfig } from "astro/config";
-import remarkGfm from "remark-gfm";
import starlightLinksValidator from "starlight-links-validator";
-const domain = process.env.DOMAIN ?? "escapement.warehack.ing";
+const domain = process.env.DOMAIN ?? "cuckoo.warehack.ing";
export default defineConfig({
site: `https://${domain}`,
@@ -40,20 +39,12 @@ export default defineConfig({
description:
"What the Raspberry Pi time-server guides get wrong, and the numbers to prove it. " +
"GPS Stratum 1 on a Pi 4: PREEMPT_RT makes PPS jitter worse, the PPS interrupt " +
- "cannot be pinned, PTP needs a CM4 not a 4B, and your dashboard is taxing your clock.",
+ "cannot be pinned, PTP is impossible, and your dashboard is taxing your clock.",
// The mark IS the word "cuckoo" — a rebus. replacesTitle stops Starlight
// rendering the title text beside it (which would read "…escapement The
// Cuckoo Escapement"). The SVG's aria-label carries the full name.
- //
- // Two variants, because Starlight renders the logo as an and CSS
- // cannot reach inside it. The single cream-on-black logo was invisible in
- // the light theme — cream wordmark on a cream page.
- logo: {
- light: "./src/assets/logo-light.svg",
- dark: "./src/assets/logo.svg",
- replacesTitle: true,
- },
+ logo: { src: "./src/assets/logo.svg", replacesTitle: true },
favicon: "/favicon.svg",
customCss: ["./src/styles/brass.css"],
@@ -108,11 +99,7 @@ export default defineConfig({
pagination: true,
lastUpdated: true,
}),
- // GFM tables do NOT render in .mdx without this, even though they work fine
- // in plain .md — Astro's remark-gfm doesn't reach the MDX pipeline here. The
- // failure is silent and ugly: the table falls through as a paragraph of raw
- // pipe characters. It shipped that way on the landing page. Don't remove.
- mdx({ remarkPlugins: [remarkGfm] }),
+ mdx(),
sitemap(),
],
});
diff --git a/docs-site/docker-compose.yml b/docs-site/docker-compose.yml
index ad03415..cb28879 100644
--- a/docs-site/docker-compose.yml
+++ b/docs-site/docker-compose.yml
@@ -2,7 +2,7 @@
#
# Default (no --profile flag):
# prod-style — Caddy serves the built dist/. Use for production-like
-# deploys (the public site at escapement.warehack.ing runs this).
+# deploys (the public site at cuckoo.warehack.ing runs this).
#
# --profile dev:
# Astro dev server with HMR. Volume mounts on src/ so edits hot-reload.
@@ -13,7 +13,7 @@
#
# Both services attach to the external `caddy` network and expose
# themselves to caddy-docker-proxy via labels. Edit DOMAIN in .env to
-# switch between escapement.warehack.ing (prod) and escapement.l.warehack.ing
+# switch between cuckoo.warehack.ing (prod) and cuckoo.l.warehack.ing
# (local-dev tier).
services:
@@ -28,16 +28,10 @@ services:
networks:
- caddy
labels:
- caddy: ${DOMAIN:-escapement.warehack.ing}
+ caddy: ${DOMAIN:-cuckoo.warehack.ing}
caddy.reverse_proxy: "{{upstreams 80}}"
# encode + gzip already in the container; let caddy pass through.
- # The site was briefly live at cuckoo.warehack.ing before the rename. Keep
- # the old host as a permanent redirect rather than letting any link that
- # escaped rot. {uri} preserves the path, so deep links survive too.
- caddy_1: cuckoo.warehack.ing
- caddy_1.redir: https://escapement.warehack.ing{uri} permanent
-
docs-dev:
profiles: ["dev"]
build:
@@ -47,7 +41,7 @@ services:
container_name: cuckoo-escapement-docs-dev
restart: unless-stopped
environment:
- - DOMAIN=${DOMAIN:-escapement.l.warehack.ing}
+ - DOMAIN=${DOMAIN:-cuckoo.l.warehack.ing}
- ASTRO_TELEMETRY_DISABLED=1
volumes:
# Hot-reload bind mounts. node_modules stays inside the container
@@ -59,7 +53,7 @@ services:
networks:
- caddy
labels:
- caddy: ${DOMAIN:-escapement.l.warehack.ing}
+ caddy: ${DOMAIN:-cuckoo.l.warehack.ing}
caddy.reverse_proxy: "{{upstreams 4321}}"
# Vite HMR over WebSocket. Caddy's defaults close "idle" WS
# connections after ~10-15s; HMR doesn't send app-level pings, so
diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json
index 7ba2fd5..3966f5a 100644
--- a/docs-site/package-lock.json
+++ b/docs-site/package-lock.json
@@ -12,7 +12,6 @@
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"astro": "^6.3.1",
- "remark-gfm": "^4.0.1",
"sharp": "^0.34.0",
"starlight-links-validator": "^0.24.0"
}
diff --git a/docs-site/package.json b/docs-site/package.json
index 96dc08d..c74adc2 100644
--- a/docs-site/package.json
+++ b/docs-site/package.json
@@ -14,7 +14,6 @@
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"astro": "^6.3.1",
- "remark-gfm": "^4.0.1",
"sharp": "^0.34.0",
"starlight-links-validator": "^0.24.0"
}
diff --git a/docs-site/src/assets/logo-light.svg b/docs-site/src/assets/logo-light.svg
deleted file mode 100644
index 1586853..0000000
--- a/docs-site/src/assets/logo-light.svg
+++ /dev/null
@@ -1,43 +0,0 @@
-
diff --git a/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md
index 04a3390..14fe93c 100644
--- a/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md
+++ b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md
@@ -1,33 +1,14 @@
---
-title: No PTP on a Pi 4 Model B (but yes on a CM4)
-description: The CM4 does hardware PTP. The Pi 4 Model B does not — and both report the identical PHY ID, so the kernel tells them apart by MDIO address and declines ours by name.
+title: Why PTP is off the table on a Pi 4
+description: PTP's entire value is hardware timestamping. The Pi 4's NIC has no PTP hardware clock. Software PTP is a worse NTP.
sidebar:
order: 3
---
-:::caution[Corrected — 2026-07-14]
-This page used to be called *"Why PTP is off the table on a Pi 4"* and blamed the
-`bcmgenet` MAC driver for not exposing a PHC. **The headline overclaimed and the
-mechanism was wrong.** A reader sent us
-[Jeff Geerling's CM4 PTP write-up](https://www.jeffgeerling.com/blog/2022/ptp-and-ieee-1588-hardware-timestamping-on-raspberry-pi-cm4/),
-which shows the **Compute Module 4 doing hardware PTP** — two of them synced to
-within 10–15 nanoseconds.
+The reference builds all reach for **PTP** (IEEE 1588), and they're right to: on
+the right hardware it's dramatically better than NTP.
-Our measurement was real, but it is specific to the **Pi 4 Model B**, and the
-actual reason is far better than "unsupported." Corrected version below. This is
-the site working as intended.
-:::
-
-PTP's entire value is **hardware timestamping**: the network silicon stamps the
-packet as it crosses the wire, outside the operating system. That's what takes
-scheduling, driver latency and queueing out of the measurement, and it's why PTP
-reaches nanoseconds where NTP reaches microseconds.
-
-With no PHC (PTP Hardware Clock), PTP is just a protocol — packets stamped by the
-*kernel*, on the *CPU*, subject to precisely the jitter you were trying to escape.
-A more complicated NTP with worse tooling.
-
-On our board:
+The Pi 4 is not the right hardware. One command settles it:
```console
$ ethtool -T eth0
@@ -36,106 +17,36 @@ Capabilities:
software-receive
software-system-clock
PTP Hardware Clock: none
-
-$ ls /dev/ptp*
-ls: cannot access '/dev/ptp*': No such file or directory
+Hardware Transmit Timestamp Modes: none
+Hardware Receive Filter Modes: none
```
-On a CM4 that same command reports `PTP Hardware Clock: 0`, and
-`/sys/class/ptp/ptp0/clock_name` says `bcm_phy_ptp`. Same family, same era,
-different answer.
+**`PTP Hardware Clock: none`.** There isn't one. There's no `/dev/ptp0` to open.
-## Two different chips — that report the same ID
+## Why that's fatal rather than inconvenient
-| board | PHY | hardware PTP |
-|---|---|---|
-| Compute Module 4 | BCM54210PE | **yes** |
-| Compute Module 5 | — | **yes**, out of the box |
-| Pi 4 Model B | BCM54213PE | no |
+PTP's whole advantage is **hardware timestamping**: the network card itself
+stamps the packet as it crosses the wire, in silicon, outside the operating
+system. That's what removes kernel scheduling, driver latency, and queueing from
+the measurement, and it's why PTP reaches nanoseconds where NTP reaches
+microseconds.
-Here's the part that makes this worth a page. **Both PHYs report an identical ID:**
+Take the hardware clock away and PTP is just... a protocol. Software-timestamped
+PTP has the packets stamped by the *kernel*, on the *CPU*, subject to exactly the
+scheduling jitter you were trying to escape. It is a more complicated NTP with
+worse tooling.
-```console
-$ cat /sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:01/phy_id
-0x600d84a2
-```
+## But the guides say the Pi 4's PHY supports PTP
-That value is `PHY_ID_BCM54213PE` in `include/linux/brcmphy.h` — and a CM4
-reports it too. The silicon genuinely differs; the identifier does not. So the
-kernel *cannot* tell these boards apart by asking the chip who it is.
+They do, and the *chip* does — the BCM54213PE PHY has PTP capability on paper.
+It doesn't matter. The Pi 4's `bcmgenet` MAC driver doesn't expose a PHC, so
+Linux has nothing to give you. And the reference builds that make PTP work feed
+the PPS into the NIC through a **SYNC pin that only the CM4/CM5 break out** — a
+regular Pi 4 board doesn't route it anywhere you can reach.
-## So it tells them apart by where they're plugged in
+## So don't chase it
-From the Raspberry Pi kernel, `drivers/net/phy/bcm-phy-ptp.c`, comments and all:
-
-```c
-struct bcm_ptp_private *bcm_ptp_probe(struct phy_device *phydev)
-{
- switch (BRCM_PHY_MODEL(phydev)) {
- case PHY_ID_BCM54210E:
- break;
-#ifdef PHY_ID_BCM54213PE
- case PHY_ID_BCM54213PE:
- switch (phydev->mdio.addr) {
- case 0: // CM4 - this is a BCM54210PE which supports PTP
- break;
- case 1: // 4B - this is a BCM54213PE which doesn't
- return NULL;
- default: // Unknown - assume it's BCM54210PE
- break;
- }
- break;
-```
-
-The disambiguator is the **MDIO bus address**. A CM4's PHY sits at address 0; a
-Pi 4 Model B's sits at address 1. Ours:
-
-```console
-$ ls -d /sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:*
-/sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:01
- ^^ address 1 — the 4B branch
-```
-
-Address 1 → `return NULL` → no PHC is ever registered. That is the whole story.
-
-## It is not your kernel, and there is no option to flip
-
-This is the part worth internalising, because it's a trap that looks exactly like
-a misconfiguration. All the support is *present*:
-
-```console
-$ grep -E "CONFIG_NETWORK_PHY_TIMESTAMPING|CONFIG_BCM_NET_PHYPTP" .config
-CONFIG_NETWORK_PHY_TIMESTAMPING=y
-CONFIG_BCM_NET_PHYPTP=y
-```
-
-The driver is compiled in. The PHY ID is a recognised constant. The code path
-exists, and it runs. **It runs, looks at your board, and declines.** No kernel
-option changes that, no patch, no `rpi-update` — the refusal is a deliberate and
-correct statement about the silicon in front of it.
-
-It also means our [`IRQF_NO_THREAD` kernel](/reference/the-patch/) is not the
-culprit. That was the obvious suspicion when we re-opened this, so we checked it
-first. It isn't.
-
-## What to actually do
-
-**On a Pi 4 Model B:** stop. Spend the effort on the PPS path, which is where the
-nanoseconds actually live — and
+We spent real time on this before running `ethtool -T`, which we should have run
+first. If your board reports `PTP Hardware Clock: none`, close the tab. Put the
+effort into the PPS path instead — that's where the nanoseconds actually are, and
[it needs the help](/explanation/preempt-rt-made-it-worse/).
-
-**If you want PTP on a Pi:** buy a **CM4 or CM5**, not a 4B. You get `/dev/ptp0`,
-`ethtool -T` reports hardware transmit and receive timestamping, and on the CM4 IO
-board you can drive a PPS out of pin 9. Geerling's post is the guide; we're not
-going to rewrite it.
-
-**Either way, run `ethtool -T` first.** We didn't, and burned real time on a
-question that one command answers.
-
-:::note[Credit]
-The correction, and every CM4 figure here, comes from
-[Jeff Geerling's write-up](https://www.jeffgeerling.com/blog/2022/ptp-and-ieee-1588-hardware-timestamping-on-raspberry-pi-cm4/)
-and the kernel patches it links to. We had the right conclusion for the wrong
-reason, and stated it too broadly. Checking beats believing — including when the
-thing being checked is us.
-:::
diff --git a/docs-site/src/content/docs/findings.md b/docs-site/src/content/docs/findings.md
index a46b007..ee7cb67 100644
--- a/docs-site/src/content/docs/findings.md
+++ b/docs-site/src/content/docs/findings.md
@@ -12,18 +12,11 @@ A GPS-disciplined Stratum 1 NTP server: **Raspberry Pi 4** + **BerryGPS-IMU v4**
199 ns**, root delay ~1 ns, survives a cold power cut unattended. About $130 of
parts, replacing an appliance that costs $1,500–$10,000.
-## What the guides get wrong on a Pi 4 Model B
-
-:::note[Model B, specifically]
-These are properties of the **Pi 4 Model B**, not the whole Pi 4 family. The
-**CM4 and CM5 do hardware PTP** — [we got that one wrong at first, and corrected
-it](/explanation/no-ptp-on-a-pi-4/). If a claim here matters to you, check it on
-*your* board. That's the entire point of this site.
-:::
+## What the guides get wrong on a Pi 4
| Claim | Reality |
|---|---|
-| "Use PTP for real precision" | **Not on a 4B — but yes on a CM4.** Both report the *same* PHY ID, so the kernel disambiguates by MDIO address and hands the 4B a `return NULL`. `ethtool -T` → `PTP Hardware Clock: none`. |
+| "Use PTP for real precision" | **Impossible.** `ethtool -T eth0` → `PTP Hardware Clock: none`. No hardware timestamping exists on this NIC. |
| "Isolate the PPS IRQ on a dedicated core" | **Not permitted.** GPIO IRQs demux through `pinctrl-bcm2835` and reject `smp_affinity`. |
| "Install PREEMPT_RT" | **Made jitter 3× worse** until patched — it threads the handler that takes the timestamp. |
| "Raise the GPS baud rate" | **Irrelevant.** PPS offset measured −1 ns at 9600 vs 115200. Identical. NMEA only *labels* the second. |
diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx
index 8ab6809..07cd3bb 100644
--- a/docs-site/src/content/docs/index.mdx
+++ b/docs-site/src/content/docs/index.mdx
@@ -76,10 +76,9 @@ board rather than take our word for it. That's the whole point.
costs you the accuracy.
[→ Why](/explanation/the-interrupt-you-cannot-move/)
-
- The CM4 and the 4B report the **same PHY ID**, so the kernel tells them apart
- by MDIO address and hands the 4B a `return NULL`. The driver is compiled in;
- it looks at your board and declines. Buy a CM4 if you want PTP.
+
+ `ethtool -T eth0` → `PTP Hardware Clock: none`. There is no hardware
+ timestamping. Software PTP is just a worse NTP. Don't chase it.
[→ Why](/explanation/no-ptp-on-a-pi-4/)