Skip to content

fix(site): rebuild the page layout on one grid, and close the owner's feedback pass - #145

Open
tnunamak wants to merge 21 commits into
mainfrom
fix/site-feedback-0807
Open

fix(site): rebuild the page layout on one grid, and close the owner's feedback pass#145
tnunamak wants to merge 21 commits into
mainfrom
fix/site-feedback-0807

Conversation

@tnunamak

@tnunamak tnunamak commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Closes the owner's feedback pass on the public site, and rebuilds the page
layout on measured evidence rather than taste.

The layout work started from a real defect: the article's horizontal edges were
set per element, so at 1920 the table began at 525, the prose at 595 and the
hero at 703 — three ragged edges down one page. That is now one named grid.
Prose holds a measure track, figures take a bounded outset, and nothing carries
its own centring, so the reading column stays optically centred between the
rails at every viewport (gap skew is 0px at 1280/1440/1920/2560; it was 286 and
766). Root cause was #nd-page shipping padding-inline-start: 48px against
padding-inline-end: 0.

Every page now uses one layout: wordmark, rail and content open on the same x,
the left column carries whatever that page needs, and the right column carries
a section TOC only where there is a document to index.

Review notes

Callum — one thing to check first. An earlier commit on this branch
replaced five of your dark footer tokens while calling itself a dark-mode
repair. It was not one: the repair was the .dark class in
theme-provider.tsx, which is what made your inverted footer (light panel,
dark text) finally render as designed. The token change was reverted in
bd69724 and the file is now byte-identical to 849ced2. Your values also
measured better — 9.05/6.67/3.78 against the replacement's 6.05/4.62/3.10.

Widths are measured, not chosen. 24 protocol/standards and docs sites were
measured live at 1920 (W3C, IETF, MCP, Matrix, OpenAPI, OAuth.net,
ActivityPub, OpenTelemetry, CNCF, Kubernetes, Rust, Deno, Astro, SQLite,
Let's Encrypt, Node.js, Cloudflare, Stripe, GitHub, MDN, Better Stack, Django,
PostgreSQL, Terraform). Only 2 of the 16 that cap at all cap at or below our
old 1080; median is 1312. Figures do not outset: table width is a median
1.00x of prose, code 1.00x, images 0.76x, and nothing anywhere exceeded 1.50x —
so the concept figure bound came down from 900 to 720. Full data is in the
dotfiles research corpus.

Two alternatives were built, measured and reverted, and the reasons are in
the CSS so they don't get retried blind: moving the concept contents rail to
the right (balanced the outer edges but stranded the gap mid-page and detached
the list from its heading), and a concept-only container (balanced the air but
moved the masthead 190px between pages). The reference class keeps one
container and lets the two-track page carry the extra air — Astro serves a
2-track and a 3-track page from one container with the wordmark identical on
both.

One container drives the whole site. The final shape is a single
--container-page with no per-surface override, because that is what holds
three properties at once that narrower schemes kept trading against each
other: the masthead sits at the same x on every page, the rail's links line up
with the wordmark, and the measure stays centred. Two narrower schemes were
built and reverted first (a railed-only 1560 override, then pinning the chrome
back to the landing width) — each fixed one property and broke another. The
deciding measurement was landing-vs-docs pairs, not docs-vs-docs: of eleven,
every same-origin site holds one masthead position, and the only four that
move it serve docs from a separate subdomain, which is not a shift a reader
experiences inside one site. Cost: a 640px measure at 1920 rather than 720,
inside the class (Better Stack 616 on a comparable three-track page, GitHub
720, Cloudflare 784, OpenTelemetry 976).

The landing width came from the class, not from production. 1080 was the
status quo, not a reference: of 16 protocol and infra landing pages, only two
cap at or below it and the median is 1280, which is what this uses.

The front door needed scoping, and I missed it on the first pass. The
wider container and the figure bound were written for railed pages but scoped
to the whole concept surface, so the home page — which has no rail — got them
too: at 1440 the container exceeded the viewport and the page started at x=0,
the hero was squeezed into the left 720px of a 1080px container, the data
columns clipped mid-word, and the CTA row wrapped. Both rules now require a
rail. The front door is back to production's geometry exactly, verified by
measuring both origins side by side.

A correction worth stating. The full-bleed spec grid was justified in a
comment as "Cloudflare pins both rails to the viewport." That was a measurement
error — it read their rail container, not the link text inside it, which sits
at x=16 against a wordmark at x=24. Of 11 docs sites with a rail, 9 align rail
text to the wordmark and none pins it to the viewport edge.

Owner feedback, all verified against the running build: favicon on a deep
blue plate; dark mode fixed; grant-line copy; dates wired to spec-core.md and
MAINTAINERS.md through prebuild (verified live by editing MAINTAINERS.md and
watching the generated file follow); "Implement PDPP" retired; Fly.io and
Railway panels legible with corrected copy; Docker line breaks; "Collection
Profile"; 90-entry section nav on desktop and a merged control row on mobile;
the four reference documents moved off the rail to a noindex page that is also
absent from the sitemap and disallowed in robots.txt — that last one was a real
leak, since only the index had been hidden while all four documents stayed
crawlable.

Known and unfixed: search failures are intermittent and I could not
reproduce them, so nothing was changed there.

Verified: 13/13 CI checks green, 196/196 site tests, spec:check and
spec:dates pass, no overflow/HTTP/console errors across 40
page/viewport/mode combinations, and every page reviewed as a screenshot at
375/1440/2560 rather than only swept for errors.

Assisted-by: AI

Seven things, all verified against rendered pixels rather than source.

ONE TEXT MEASURE. Body prose was 544px on the concept pages, 612px on the
specification, and one-offs at 952px and 680px, which is why line wrapping
read as inconsistent from page to page. --container-measure is now a single
600px rebound under [data-surface="concept"], inside the 60-75 character
convention for both surfaces' fonts. The container, the rail and the grid are
untouched.

FLY.IO AND RAILWAY WERE ILLEGIBLE. Those two tabs carry prose rather than a
command, and it rendered on the dark code ground at 1.39:1 against a 4.5
requirement. Prose that is not a command now sits on the page background at
8.1:1. The Docker tabs are unchanged at 7.31:1.

ONE WEIGHT IN THE CODE BLOCK. The image line rendered at 600 while every
other line was 400, which read as a font bug rather than emphasis. The block
is one weight now.

DESKTOP HAD NO WAY TO MOVE AROUND THE SPECIFICATION. tableOfContent was
disabled on the strength of a comment claiming the per-page contents lived in
the left rail; the rail lists documents, not sections, so there was no section
navigation at all on a 113,000px page. The fumadocs TOC column is live at
1280px and up.

Finding that uncovered a real cascade bug. The spec route pulls two
independent Tailwind builds, and the concept build's unconditional .flex
utility loads after fumadocs' chunk, so fumadocs' own max-xl:hidden lost at
every width. The TOC was never actually hidden below 1280px, only collapsed to
a 16px sliver with leaking text. Fixed with an unlayered rule that cannot race
the same way.

MOBILE HEADER ROW. The sidebar drawer icon floated alone at the right edge
above the section dropdown. The two rows now match height and share one seam,
and the icon sits above the dropdown's own leading icon.

THE DOCUMENT TREE. "Profile" now reads "Collection Profile". Reference
Topology, Reference Implementation Notes, Auth Design and Change Tracking come
off the specification rail onto /specification/reference-materials, which
carries noindex, is disallowed in robots.txt, is absent from the sitemap and
is linked from nowhere. Every one of those documents keeps its URL.

DATES CANNOT SILENTLY ROT AGAIN. The site already derived them from the
spec-*.md headers; the headers themselves were stale, spec-core by 130 days
and all ten by some margin. `pnpm spec:dates` fails when a declared Date is
older than the last substantive commit to that file, where substantive
excludes header-only and whitespace-only diffs so stamping cannot retrigger
itself. `--write` stamps them in one command. Wired into lefthook and the
spec-check workflow, whose checkout needed fetch-depth 0 or the check would
have silently passed on a shallow clone.

The ten stale dates are reported, not rewritten: restamping a normative
document is the owner's call.

Verified: 196/196 tests, typecheck clean, spec:check and generated-artifacts
clean, no horizontal overflow at 320/390/768/1024/1280/1440/1920 across five
routes.

Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pdpp Ready Ready Preview Aug 15, 2026 3:37pm

Request Review

All ten root specs carried a Date older than their last substantive commit,
spec-core by 130 days. Stamped to 2026-08-14.

Stamping surfaced two defects in the check itself, both found by running it
rather than by reading it:

The site renders each spec through a committed header sidecar that repeats the
root's Date inside a Callout, and spec-check.ts compares the two. Stamping only
the root traded a red spec:dates for a red spec:check. --write now mirrors into
the sidecar.

And the stamp replaced the whole Date line, which destroyed the editorial tails
several of these carry: "(revised from 2026-03-30)", "(original); superseded
2026-04-12". That is real provenance, and spec-check compares the full string,
so dropping it both lost information and failed the comparison. Only the ISO
date is replaced now, on both sides.

Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Five things, each checked against a screenshot rather than a number. The last
round passed a contrast measurement while shipping a visibly broken panel,
which is the failure this commit is mostly correcting.

THE FAVICON was still teal while the rest of the site had moved to blue. It
was the first thing asked for and it was missed entirely. It now carries the
same two-tone mark the masthead, apple icon and OG card already ship.

No background plate. Research found the shops split two ways: Stripe and Linear
fill the canvas with an opaque brand plate, while GitHub, Vercel, Anthropic and
Notion go bare but drop to near-black or near-white to survive tab chrome. A
flat saturated hue cannot clear 3:1 on both light and dark chrome — brightening
the blue for Chrome dark breaks it on Chrome light every time. But a two-tone
mark only needs one tone to carry: copper holds the light chromes at 4.17 to
5.20, blue holds the dark ones at 2.62 to 3.71. Rendered at 16, 32 and 48px on
real tab-strip colours and looked at: the counter stays open at 16px and the
glyph reads on both.

THE PROSE PANEL WAS NESTED INSIDE THE CODE PANEL. Fixing the Fly.io and
Railway contrast by giving the paragraph its own light background left a
600px light box inside a 726px dark one, so a thick dark frame showed around
it. The panel itself is now light when a tab carries prose instead of a
command; Docker and Docker Compose keep the dark code ground.

ENABLING THE TABLE OF CONTENTS SQUEEZED THE SPECIFICATION. Rail plus TOC plus
prose no longer fit the container, and body prose fell to 488px against the
600px site measure. Prose is back to 600px.

THE TOC WAS BIGGER THAN THE RAIL IT SITS OPPOSITE. Measured 15px on 24px
against the rail's 13px on 17.55px. An earlier reading of "15px on both" came
from sampling the rail's first link, which is its GitHub header rather than a
nav item. The TOC now matches the rail exactly.

THE DOCKER COMMAND keeps its line breaks: the single-line form measures 713px
against 594px of available width, so collapsing it would force a horizontal
scrollbar. The real defect was that the image line was not indented to match
the other continuations. It is now.

Verified: 196/196 tests, typecheck clean, spec:check and spec:dates pass, no
horizontal overflow across seven widths and five routes.

Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
…yout

Four things, each looked at rather than measured and assumed.

THE FAVICON is the production single-p mark on a deep blue plate. Teal was the
old colour and is gone; the two-tone split-p is gone with it.

The plate is the answer to a question worth recording. No flat colour clears
3:1 on both light and dark tab chrome: the old teal managed 2.84 on Chrome
light, and every candidate blue, grey or near-black failed at least one real
chrome. A plate makes glyph-vs-plate contrast fixed and chrome-independent, at
8.18:1 here. That is also what the shops do. Fetching and decoding their actual
favicon files shows Stripe and Linear ship an opaque brand plate with a neutral
glyph, while GitHub, Vercel, Anthropic and Notion go bare but drop to
near-black or near-white and give up brand colour. Nobody ships a bare
saturated brand hue, which is what we were doing.

DARK MODE WAS BROKEN ON THE SPECIFICATION and the body never left light mode
while every child switched. The provider set only `data-theme`, and fumadocs
reads a plain `.dark` class, so its own body and sidebar colours stayed light
under dark content. The provider now sets both.

THE FLY.IO AND RAILWAY COPY described settings that are not on screen. Reading
the deploy files, both sentences were also wrong on the facts. Railway's
template prompts for exactly one value, a password, and generates the database,
the encryption key and the public address itself. Fly is not "more setup than
fits in one command" at all: it is a single fly launch, and the real blocker is
that Fly requires a payment method on the account before it will finish
creating the app. Both now say the true thing.

THE SPECIFICATION WAS SQUEEZED because the rails and the prose competed for one
1080px container. The rails now sit near the page edges, which is the pattern
Cloudflare uses on a comparably long technical reference: it pins both rails to
the viewport and measures only the prose, so adding a table of contents costs
the article nothing. Better Stack and Dub keep a container, but at 1130px and
1152px, both wider than ours was. Prose holds the 600px site measure at every
width from 1280 up.

Verified: 196/196 tests, typecheck clean, spec:check, spec:dates and
generated-artifacts all pass, and no horizontal overflow across seven widths,
five routes and both colour schemes.

Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
The article's right edge moved as the reader scrolled. Prose held its measure
while other blocks did not: at 1440 diagrams ran 838px against 600, and at 2560
they ran 1958px against 720. Mermaid renders into a bare centring div with no
width of its own, and the measure was applied to a list of prose tags that
never included it.

Every direct child of the doc body now shares the measure, and an SVG scales
down inside it rather than the box growing to fit. Measured from 360 to 2560:
blocks and prose are the same width at every step from 768 up.

The measure is also centred in its column now. It was pinned left, so the gap
either side ran 296px apart at 1440 and 776px at 1920, and the whole document
hugged the rail with a void beside it. Cloudflare's docs, which this layout
follows, stay within 12px of centre at both widths.

Content types, decided against that same reference rather than by taste:
prose and code share one edge there (648 and 646 at 1440, 784 and 782 at 2560),
and their measure grows with the viewport before capping around 780 rather than
pinning to one value or growing without limit. Ours now grows to 720px above
1600.

Code blocks keep wrapping instead of scrolling sideways, which is where we
depart from them: they set white-space: pre and scroll. A normative spec is
read rather than copied line by line, and a horizontal scrollbar hides half of
the line a reader needs. It costs height - our tallest block is 4662px - but
that block is 197 lines with a longest line of 152 characters, so most of the
height is content rather than wrapping, and it would still be around 2900px
unwrapped. No height cap either: a max-height on a 197-line sample puts a
scrollbar inside a scrollbar.

The favicon glyph also sat at 73% of its plate, above the 55-70% a plated mark
normally occupies, and read as cramped against the edges. It is 66% now, on the
site favicon, the console favicon and the app icon.

Verified: 196/196 tests, typecheck clean, and no horizontal overflow across 100
combinations of ten widths from 320 to 2560, five routes, and both colour
schemes.

Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
The article's horizontal edges were set per element - prose here, tables
there, the hero somewhere else - and had drifted apart: measured at 1920,
the table started at 525, the prose at 595 and the hero at 703.

Replace that with a single named grid on the document. Prose occupies a
`measure` track; tables take a small symmetric outset from it. Nothing
carries its own centring, so the reading column stays optically centred
between the two rails at every viewport (gap skew is now 0px at 1280,
1440, 1920 and 2560; it was 286px and 766px before).

The root cause of the skew was #nd-page's padding-inline-start: 48px
against padding-inline-end: 0. Every centred descendant inherited a
column 48px narrower on one side.

Table width is measured against shipped docs sites rather than assumed:
Stripe holds tables at the prose width exactly (797 vs 799 at 1440),
GitHub likewise (720/720), and MDN - the only one of four that outsets -
goes 77px wider. An earlier revision here gave tables a 140px break,
wider than any of them; this uses 80px total and goes flush below 1024.

Also fix both panes lurching 22px on first scroll: --fd-banner-height
was 3rem while the real masthead measures 70px, so the rail and TOC sat
below the header at rest and snapped to the declared offset once
scrolled. The masthead now publishes its measured height.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…rail

The four reference documents - implementation notes, change tracking,
auth design and reference topology - were moved off the specification
rail onto a noindex index page a reader has to navigate to by hand.
Only that index was actually hidden. All four documents it links were
fully indexable and listed in the sitemap, so a crawler that never saw
the index still found, crawled and ranked every one of them.

Give the documents the same treatment as their index: noindex/nofollow
in generateMetadata, a robots.txt disallow, and no sitemap entry. All
three are derived from REFERENCE_MATERIALS_SLUGS rather than repeated,
so the rail and the crawl policy cannot drift apart. The pages stay live
at their URLs, which is the point - they are reachable, just not
competing with the specification.

The existing suite passed throughout because the fixtures contained only
the index page, never the four documents. Adding them to the fixture
makes the leak reproducible: reverting either fix now fails the suite
(verified - 2 tests red before, 5 green after).

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
A 1px border on the sidebar's trailing edge was added to make the wide
gutters read as structure. Surveying shipped docs UIs shows two coherent
patterns and this was neither: a rail on a FILLED panel whose edge a line
terminates (Exa, Base44, GitBook, Cloudflare), or a transparent rail
separated by whitespace and type weight alone (Mintlify, OpenAI
Platform). This rail is transparent, so the border was a line with
nothing behind it.

It also made /specification inconsistent with /self-host and
/participate, which carry the same rail with no line.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Table rules now use one selector shape (.pdpp-docs-body > .pdpp-docs-table-scroll)
across the grid rule, the scrollport rule and the phone override, so the later
ones stay genuine overrides rather than lower-specificity rules that happen to
come after a higher one — a real ordering hazard, not only a lint error.

Also drops a dead null-guard on the masthead ref (the <header> is rendered
unconditionally, so the ref is always populated by the time the effect runs)
and renames a shadowed loop variable.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
The dark-mode fix earlier in this branch made the shared provider write
both `data-theme` and a plain `class`, because fumadocs' own components
key their dark styling off `.dark` while the brand tokens read the data
attribute.

Console and site each keep a copy of the assertion against that one
shared provider file, and only the site's copy was updated - so the
console's still pinned the single-attribute form and failed CI (1 of
1772 tests). Match it to the provider as shipped.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Option B, for comparison against the current single-column layout.

/self-host and /participate capped at a fixed 1080px with no upper
relief, so past ~1600px they stopped using the screen: at 2560 the shell
held 1080 of 2560px while /specification spanned 2486, and clicking
between them moved the whole page including the masthead.

Measured on shipped sites 2026-08-14: none caps this way at 1920 —
Tailscale's community page and Proton's both run the full 1920, Vercel's
docs 1905, Tailscale's docs 1680. Community pages that carry
label-plus-sentence entries (Proton, Vercel, Tailscale) put them in
multi-column grids rather than a single stack.

So: the container gains relief above 1600px, and .pdpp-features and the
plain .pdpp-ruled-list go to two columns above 900px. The numbered
ruled-list variant stays single-column — columns would break the reading
order of ordered steps. Last-row rules are suppressed per row, not per
last child, which no longer means the same thing in a grid.

Wide blocks are bounded on the shared document track: capping tables
individually does nothing, since they carry max-width:100% of a section
that is already narrower. Right-edge spread at 1920 is 300px (prose 600
against figures 900 - the intended prose/figure relationship) rather
than the 448px an unbounded version produced.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
/self-host and /participate capped at a fixed 1080px with no upper
relief, so past ~1600px they stopped using the screen: at 2560 the shell
held 1080 of 2560px while /specification spanned 2486, and clicking
between them moved the whole page including the masthead.

Surveyed 24 protocol/standards and open-source infrastructure pages at
1920 (W3C, IETF, MCP, Matrix, OpenAPI, OAuth.net, ActivityPub,
OpenTelemetry, CNCF, Kubernetes, Rust, Deno, Astro, SQLite, Let's
Encrypt, Node.js and others). Only 2 of the 16 that cap at all cap at or
below 1080; median 1312, and eight run the full viewport. 1400 sits
inside that band, and buys margin rather than line length - the reading
measure is unchanged.

The same survey settled a question this commit deliberately does NOT
act on. An earlier draft put the heading-plus-sentence lists into two
columns. Multi-column blocks in this class are overwhelmingly link cards
(17 of 24 found), and the standards bodies keep comparable prose lists
in a single column even with room for two: IETF participate runs 4 items
at 17 words in a 1300px block, Kubernetes contribute 7 items at 28 words
in 1204px, OpenTelemetry a 19-item list at 1220px. Our lists are 13-17
words per item with one link between them; .pdpp-channels, which IS all
links, is already a row. So the lists stay stacked.

Wide blocks are bounded on the shared document track, because capping
tables individually does nothing - they carry max-width:100% of a
section that is already narrower.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
The figure bound was 900px against a 600px measure, putting tables at
exactly 1.50x prose and code at 1.28x. Surveyed 24 protocol/standards and
docs sites at 1920 (W3C, IETF, ActivityPub, Matrix, MCP, OpenAPI,
OAuth.net, OpenTelemetry, Kubernetes, Rust, Deno, Astro, SQLite, Node.js,
Cloudflare, Stripe, GitHub, MDN, Better Stack, Django, PostgreSQL,
Terraform): table width is a median 1.00x of prose (n=6, max 1.50x), code
1.00x (n=11, max 1.25x), images 0.76x (n=7, never wider than prose).
Nothing exceeded 1.50x anywhere.

So the old value sat at the ceiling of the observed range, matched only by
Terraform, while flush is the modal answer - GitHub holds 135 code blocks
at exactly prose width, PostgreSQL 37, and Deno, Node.js and
OpenTelemetry likewise.

720 puts tables at 1.20x and code at 0.98x, inside the band at every
viewport. Right-edge spread on these pages drops from 300px to 120px, and
that residual is now the intended prose/figure relationship rather than
an unexamined value.

/specification was already inside the band (tables 1.11x, code 0.92x,
diagrams 0.62x) and is unchanged.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
The specification surface went full-bleed while the marketing pages sat in
a container, so the wordmark moved 575px between them at 2560 and the spec
rail floated at x=32 against its own wordmark at x=607.

The full-bleed override was justified in a comment as "Cloudflare Docs'
pattern — both rails pinned to the viewport." That was a measurement
error: it read Cloudflare's rail CONTAINER, which is full-bleed, not the
link text inside it, which sits at x=16 against a wordmark at x=24.
Re-measured across 11 docs sites that have a rail, 9 align the rail's link
text to the wordmark within 16px (Cloudflare 24/16, Stripe 4/12, Deno
18/18, Astro 24/16, MCP 272/256, GitHub 16/24, Terraform 24/16,
OpenTelemetry 12/21, Better Stack 405/391) and NONE pins rail text to the
viewport edge.

So every page now uses one grid: the left column carries whatever that
page needs (spec front-matter and document list, or a Contents list), the
right column carries a TOC only where there is a document to index, and
the wordmark, rail and content all open on the same x everywhere.

Aligning the rail means the rails cost the reading column real width
(centre track = container - 480px), which squeezed the measure to 440px at
1440 - under the 60-75ch convention and narrower than any of the 24 sites
surveyed. That is paid for by raising the SHARED container to 1560 rather
than a docs-only override, because a docs-only widening reintroduces the
moving masthead: Better Stack, Tailscale, OpenTelemetry, MCP and
Kubernetes all keep one container across surfaces and hold the wordmark at
an identical x on both.

Measure is back to 600/720, gap skew stays 0 at every viewport, and every
text element still shares one edge.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
An earlier commit on this branch replaced five dark-mode tokens while
describing itself as a dark-mode repair. It was not one. The repair was
in theme-provider.tsx, which now sets the `.dark` class fumadocs reads
alongside data-theme; that alone made dark mode engage.

The token change was separate and wrong. Callum's dark footer (PR #85) is
a designed inversion — a LIGHT panel (#98bde1) carrying dark text
(#17181a / #2c3138 / #495765), the reverse of light mode. It only looked
broken because dark mode never engaged, so the panel rendered
light-on-light. Replacing it with a conventional dark panel was not a fix,
it overwrote the designer's intent, and it measured worse: his values give
9.05 / 6.67 / 3.78 against the replacement's 6.05 / 4.62 / 3.10.

Every token in this file is now byte-identical to Callum's, verified by
diff against 849ced2. Local light and dark footers now match production
exactly; the only remaining difference is the body background going dark
under dark mode, which is the class fix doing its job.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…t rail

No behaviour change from the previous commit — the container is 1560 for
every surface as before. This records the two alternatives that were built,
measured and reverted, so neither gets retried blind.

The problem: /specification runs three tracks and needs 1560 to hold a
600px measure past 480px of rails, while the concept pages run two, so the
surplus lands as ~705px of one-sided air at 1920 — wider than any of the
twelve reference pages measured (322-615).

Tried and reverted (1): move the concept contents rail to the right, as
nine of ten short docs pages do. Outer edges balanced to 10px, but the gap
moved to the middle of the page and the contents list detached from the
heading it indexes, floating 200px above the title. Worse, not better.

Tried and reverted (2): a narrower concept-only container. Balanced the
air to 118px, but moved the masthead 190px between /specification and
/self-host — the exact defect an earlier commit on this branch fixed.

What the reference class actually does is keep ONE container and let the
two-track page carry the extra air. Astro is the decisive case: a 2-track
and a 3-track page from the same container, wordmark at an identical x on
both (24/24), the 2-track page simply wider on the right (232 vs 315).
Better Stack, MCP, OpenTelemetry and Kubernetes hold one wordmark position
across surfaces too.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak requested a review from callumflack August 15, 2026 02:45
@tnunamak tnunamak changed the title fix(site): close the owner's feedback pass on the public site fix(site): rebuild the page layout on one grid, and close the owner's feedback pass Aug 15, 2026
… door

The wider container and the figure bound were both written for pages that
carry a rail, but they were scoped to the whole concept surface, so the
front door got them too. It has neither a rail nor a TOC — it is a single
centred hero — and the result was bad: the container went to 1560 and at
1440 exceeded the viewport, so the page started at x=0; the figure bound
capped the hero at 720px inside a 1080px container, stranding it in the
left half with ~690px of dead space; the data columns clipped mid-word
("artist Grou", "title Weath"); and the three CTAs wrapped onto two rows.

Both rules now require a rail to be present. The front door is back to
production's geometry exactly — container 175-1255 at 1440 and 415-1495 at
1920, document track 207-1223 at max-width 100%, three CTAs on one row —
verified by measuring both origins side by side.

Railed pages are unaffected: they still take 1560 above 1600px, and their
32px inset at 1440 sits mid-range against nine reference docs sites
(median 21, min 16 — Cloudflare, Astro, Deno and Terraform all at 16).

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…tainer

The previous commit gated the wider container on
`.container:has([data-slot=pdpp-concept-rail])`. That matches the page but
not the masthead, which is its sibling — so on /self-host the document
track widened while the nav stayed put, splitting the wordmark from the
rail by 175px and breaking the single-masthead-position property an
earlier commit established.

Moving the :has() onto [data-surface="concept"], which contains both,
makes them move together. Wordmark and rail are now aligned (delta 0) and
identical across /specification, /self-host and /participate at 1440,
1920 and 2560, while the front door — which has no rail — keeps
production's 1080 container.

The two container rules are split and ordered low-specificity first:
grouped, or written the other way round, they put a (0,3,0) selector after
a (0,4,0) one, which Biome rejects and which is a real ordering hazard.
They select disjoint surfaces, so neither overrides the other.

Verified: 196/196 tests, typecheck clean, biome clean on every file this
branch touches, and no overflow/HTTP/console errors across 40
page/viewport/mode combinations.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…ction

The previous commit justified the front door's 1080px container as
"production's" width. That is the status quo, not a reference, so it got
measured: of 16 protocol and infrastructure landing pages at 1920 on
2026-08-15, only OAuth.net (960) and Matrix (1000) cap at or below 1080.
The median is 1280, hit exactly by Deno, Astro and Let's Encrypt, with
Kubernetes 1200, Rust 1152, W3C 1312, IETF 1300, OpenTelemetry 1320,
Tailscale 1440, MCP 1472, Terraform 1600 and SQLite uncapped. Our hero
lede was correspondingly narrow at 564px against a 704px median.

--container-page moves to 1280, the median. Raised on the token rather
than per-page: a first attempt scoped it to the page container alone,
which widened the hero but not the masthead and footer that share the
class, leaving the hero hanging 100px OUTSIDE the chrome framing it.
Masthead, hero and footer now hold one left edge on the front door
(107/107/107 at 1440, 347/347/347 at 1920).

The rationale lives in the file header because that file's own contract
forbids comments inside @theme, and site-surface-ownership.test.ts pins
the value to keep it declared there rather than as a runtime primitive —
that pin is updated with a note that it guards the location, not the
number.

Railed surfaces are unaffected: they override to 1560 in components.css.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
The railed-surface override at 1560 was justified by a measurement of
docs-vs-docs pairs. The real question was landing-vs-docs, and measured
across eleven such pairs on 2026-08-15 the answer is different: every
same-origin site holds the masthead at an identical x (Kubernetes,
Tailscale, OpenTelemetry, Let's Encrypt, MCP, Better Stack; Matrix
differs by 8px), and the only four that move it — Deno, Astro,
Cloudflare, Stripe — serve docs from a separate subdomain, so it is not a
shift any reader experiences inside one site. pdpp.dev is one origin with
one masthead component, so ours should not move. It was moving 75px.

Two narrower fixes were built and reverted first. Keeping the railed
override aligned the rail to the wordmark but left the masthead moving.
Pinning the chrome back to the landing width fixed the masthead but
pushed the rail 75px outside it. Both are false trades: the three
same-origin sites that hold one masthead position also align rail to
wordmark within 16px, because a single container drives both.

Dropping the override entirely holds all three properties at once — one
masthead position, rail aligned to the wordmark, gap skew 0 — at the cost
of a 640px measure at 1920 instead of 720. That is inside the class
(Better Stack 616 on a comparable three-track page, GitHub 720,
Cloudflare 784, OpenTelemetry 976), and the rail-to-TOC span of 1216px
sits between Better Stack's 1146 and MCP's 1408.

Verified: 196/196 tests, typecheck clean, biome clean on every file this
branch touches, no overflow/HTTP/console errors across 40
page/viewport/mode combinations.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Three defects in the front door's ambient record columns, all visible in
the running build.

MOTION was 41s / 33s / 22s assigned by column index, so the columns sped
up left to right and the eye read one mechanism with a speed dial rather
than three independent streams — the outer columns differed by nearly 2x.
Durations are now 31s / 43s / 37s: a narrower band, prime-ish so the
columns drift in and out of phase instead of settling into a repeating
pattern, and deliberately not ordered by index, with the slowest in the
middle. Each column also starts part-way through its own cycle via a
negative animation-delay; without that all three begin perfectly aligned
on load, which is the most mechanical moment of all.

DUPLICATES: the component renders 17 rows per column and cycles with
`rowIndex % stream.length`, but every stream held only 8 rows, so rows
1-8 repeated as 9-16 and the reader saw "title Weather" twice a few lines
apart. Each stream is now 17 rows of real fields from the specification's
worked examples.

OVERFLOW: at 600px "artist Stars of the Lid" (23 characters) painted over
the neighbouring column. The column clipped its own box but the row was
an unbounded nowrap line. Rows now clip with an ellipsis, and the data
keeps pairs at or under 19 characters so that path is a backstop rather
than the norm.

Verified across 375/600/1024/1440/1920: zero adjacent duplicates and zero
rows painting past their column, where before both were present.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant