[M3-3c] The 404 as an open goal - #109
Conversation
overrides/404.html shadows Material's boilerplate not-found page through the same custom_dir mechanism as partials/source.html: an Agda goal that cannot be filled, in JuliaMono with the site's own Pygments colouring, over the line the issue asked for and three ways out -- index, projects, and search, the last through the __search checkbox Material's header already toggles, so no script. The sidebars are gone (a wrong URL is not a docs page); header and footer stay. An empty .goal-404__sky element is the mount point for the constellation background (#94); until that lands the page is text-first and inert. Styles live in the template's extrahead block, tokens only -- extra.css is deliberately untouched while #93-#95 work there. MkDocs renders error templates with base_url taken from site_url's path, so every href survives however deep the missing URL is; verified against a nested bogus path under mkdocs serve, in both schemes, at 1280px and 375px. The hole's ink leans 20% toward the fg token: the bare accent measured 4.63:1 on the wash in light -- passing, but the thinnest ratio on the site. The mix clears 5.88:1 light / 6.48:1 dark. That change exposed a parser bug in audit_contrast.mjs, fixed here: Chromium serialises a color-mix() result as `color(srgb r g b)` with 0..1 channels, which the audit read as 0..255 -- near-black, a false fail on dark and a false pass on light. This was the first text colour on the site to hit that path; #93-#95 will produce more. make check clean; make design-audit green with site/404.html crawled like any other page. Closes #98. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on #109, two changes. The goal now names the URL the visitor actually asked for: _ : ∃ cv/index.html.bak _ = {! !} The path cannot exist at build time -- GitHub Pages serves this one static file for every wrong URL -- so six lines of inline script write location.pathname into the goal at runtime. textContent only: the path is attacker-chosen text and must never be parsed as markup. Encoded whitespace is flattened so a crafted URL cannot re-shape the block, a long path keeps its tail (the part that names the page), and the static `ThisPage` stands when scripts are off. This is the page's only script; nothing moves or fetches. `∃ cv/index.html.bak` even parses as Agda -- `/` is legal in a name -- so the statement survives a literate reader. The block now sits in a small terminal window: sunken title bar, three monochrome dots, `404.agda` as the filename. Tokens only -- the palette has no traffic-light colours, and depth on this site is a hairline, not ornament. The window takes over the border and corners that extra.css draws on .highlight; the inner block gives them up. make check clean; make design-audit green -- 16,388 text elements per theme, 0 below AA, no fallback glyph (∃ ships in the JuliaMono symbols subset), no cross-origin request. Verified end-to-end under mkdocs serve: /cv/index.html.bak renders the goal above, both schemes, 1280px and 375px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 2, from review: the goal now names the missing page itself ( |
Review feedback on #109, second round: the terminal-window chrome (title bar, dots, 404.agda) reads as ornament next to the rest of the site, and heading-size type reads as display, not code. Both revert. The goal keeps the dynamic path -- that part stays -- and now sits in the same hairline-and-radius .highlight box every real Agda block wears, at the same --type-code size. The block is supposed to read as authentic Agda, and authentic Agda on this site looks exactly like this. Only the padding is more generous than a block in running prose, so the box holds the centre of an otherwise empty page. make check clean; make design-audit green in both themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 3, from review: terminal chrome and heading-size type reverted — the goal (still naming the visitor's path) now sits in the same hairline |
Review feedback on #109, third round: the hint sentence moves into the box as an Agda comment -- `--`, not `#` -- reworded "The hole", and it types itself out, in editor green, a beat after the page paints. The typing is CSS only. The full sentence is in the DOM from the first byte; the animation is a steps(56) reveal over a clip-path (56 steps, 56 characters), so the box never changes size, screen readers never wait, and prefers-reduced-motion shows the line standing whole -- verified by emulation, zero animation objects. Below 45em, where the line has to wrap and a horizontal reveal over two lines would be a curtain rather than typing, the comment simply appears, wrapped. Verified against the animation timeline over a cold load: the document timeline holds the animation at zero until first paint, then it plays exactly once -- first paint + 0.8s + 2.2s of typing -- with no restart. The greens are pinned per scheme like the syntax colours in extra.css: dark carries the palette's own string green, 12.5:1 on the raised surface; in light that token measures 4.59:1 there, thinner than anything else on the site, so light wears a deeper leaf of the same green, 6.0:1. make check clean; make design-audit green in both themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 4, from review: the hint sentence moved into the box as an Agda comment ( |
There was a problem hiding this comment.
Pull request overview
This PR implements a custom MkDocs Material 404 page override that presents missing URLs as an “unfillable Agda goal” (including runtime substitution of the requested path), and updates the contrast audit script to correctly parse Chromium’s color(srgb …) serialization produced by color-mix().
Changes:
- Add
overrides/404.htmlto replace Material’s default not-found page with the new goal-themed design and navigation exits (index/projects/search). - Fix
scripts/js/audit_contrast.mjscolor parsing to correctly handle CSS Color 4color(srgb …)output (including scientific notation). - Update
mkdocs.ymlcomment to reflect the presence of two template overrides.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| scripts/js/audit_contrast.mjs | Corrects contrast-audit parsing for color(srgb …) values emitted by Chromium. |
| overrides/404.html | Adds a full custom 404 template with scoped styling and runtime path injection. |
| mkdocs.yml | Updates theme override commentary to mention the new 404 override. |
Suppressed comments (1)
overrides/404.html:197
- This hard-codes a raw hex color for the typed comment. Since the dark-scheme string highlight is already defined as --md-code-hl-string-color (see docs/stylesheets/extra.css), you can reference that variable directly and avoid duplicating palette literals here.
[data-md-color-scheme="slate"] .goal-404__typed {
color: #86efac;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| error template base_url *is* the site root's path. -#} | ||
| <a href="{{ base_url }}">index</a> | ||
| <a href="{{ 'projects/' | url }}">projects</a> | ||
| <label for="__search">search</label> |
There was a problem hiding this comment.
Fair catch — fixed in 8282f9b. The affordance is now a real <button type="button"> dressed in the same link clothes (baseline-identical to its link siblings — measured, 0px delta), wired to click() the same __search checkbox the header's magnifier toggles and then focus the query field explicitly. That last part matters: Material only auto-focuses the field from its own s shortcut, not from a toggle change — measured on both layouts. Verified with trusted CDP input: Tab reaches the button, and both mouse click and Enter open the overlay with the caret in the query field. Two deliberate corners: with scripts off the button is inert, which gives up nothing because Material search is a JS feature end to end (index fetch, worker, results); and if the search plugin is ever removed, the script hides the dead control rather than leaving it.
| [data-md-color-scheme="default"] .goal-404__typed { | ||
| color: #136b34; | ||
| } |
There was a problem hiding this comment.
Half agreed, and the half that stands is fixed in 8282f9b: the extrahead comment's "tokens only" claim did overstate, and it now names its exceptions (two structural values, one per-scheme colour pair) instead. But deriving these from --md-code-hl-string-color is the one thing this file must not do. That token measures 4.59:1 on the raised code surface in light — below everything else on the site (the current floor is 4.83:1) — and adopting a value that fails the margin is exactly what ADR-005 exists to refuse; it adjusted three upstream agda-algebras values for the same reason rather than shipping them broken. Dark's #86efac does coincide with the dark string token today, but the coincidence is not the meaning: this green is the editor-comment green the page speaks in, pinned per scheme the same way extra.css pins its syntax colours, so it stays green — and stays at 6.0:1 / 12.5:1 — even if the string token is ever retuned. The comment above the pair carries the measured ratios. (Same answer for the suppressed duplicate on the slate value.)
Copilot's review on #109 flagged the search <label>: not naturally focusable, so keyboard-only users could not Tab to the visible affordance. Its keyboard story leaned on the header's search field and Material's s/f// bindings, which is a path around the control rather than through it. It is now a <button type=button> stripped to the site's link clothes -- same colour, same 35%-accent underline, baseline-identical to its link siblings (measured, delta 0px) -- wired to click() the same __search checkbox the header's magnifier toggles, then focus the query field. The focus is explicit and synchronous: Material only auto-focuses the field from its own `s` shortcut, not from a toggle change, and a next-frame callback can land hundreds of milliseconds late in a busy renderer. Verified with trusted CDP input on both layouts: mouse click and Enter both open the overlay with the caret in the query field. With scripts off the button is inert, which gives up nothing: Material search is a JS feature end to end. If the search plugin is ever removed, the script hides the dead control. Also from the review: the extrahead comment claimed "tokens only", which the typed comment's per-scheme greens had quietly outgrown. The claim now names its exceptions instead of overstating. The greens themselves stay pinned: the suggested --md-code-hl-string-color measures 4.59:1 on the raised surface in light, and adopting a value below the site's floor is what ADR-005 refuses. make check clean; make design-audit green in both themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #98 (M3-3c, from the 2026-08-03 design review).
What
overrides/404.htmlshadows Material's boilerplate not-found page — the samecustom_dirmechanism asoverrides/partials/source.html— and turns the onepage every typo'd URL reaches into an unfillable Agda goal that names the
page the visitor asked for. Requesting, say,
/cv/index.html.bakshows:set in JuliaMono, the hole wearing the accent tokens the way agda2-mode
paints an interactive hole green — and the comment line types itself
out below the goal, in editor green, a beat after the page paints.
Below the box, three ways out: index, projects, search. Search
is a real
<button>dressed in the site's link clothes — Copilot's reviewrightly flagged the earlier
<label>as unreachable by Tab — and click,Enter, and Space all open Material's overlay through the
__searchcheckbox and put the caret in the query field (Material only auto-focuses
it from its own
sshortcut, so the button does that part explicitly;verified with trusted CDP input on both layouts). (
∃ cv/index.html.bakis even parseable Agda —
/is legal in a name, so the path reads as aqualified identifier.)
Details that took deciding:
sentence is in the DOM from the first byte; the animation is a
steps(56)reveal over aclip-path(56 steps, 56 characters), so thebox never changes size, screen readers never wait,
prefers-reduced-motionshows the line standing whole (verified by emulation: zero animation
objects), and below 45em — where the line must wrap and a horizontal
reveal over two lines would be a curtain, not typing — the comment
simply appears, wrapped. Verified against the animation timeline: it
plays exactly once, starting at first paint + 0.8s, no restart.
--, not#(it is an Agda comment), and it isgreen per scheme like the syntax colours in extra.css: dark carries the
palette's own string green (12.5:1 on the raised surface); in light that
token measures 4.59:1 there — thinner than anything else on the site —
so light wears a deeper leaf of the same green at 6.0:1.
time: GitHub Pages serves this one static file for every missing URL.
Six lines of inline script write
location.pathnameinto the goal —through
textContentonly (the path is attacker-chosen text and mustnever be parsed as markup), with encoded whitespace flattened so a crafted
URL cannot re-shape the goal, long paths keeping their tail, and the
static
ThisPagestanding when scripts are off. This is the page's onlyscript; nothing moves or fetches.
.highlightbox every real Agda block wears, with the code at the same--type-codesize — the block is supposed to read as authentic Agda, andauthentic Agda on this site looks exactly like this. Only the padding is
more generous, so the box holds the centre of an otherwise empty page.
(A terminal-window chrome — title bar, dots,
404.agda— was tried andreverted on review; heading-size type likewise.)
site_navblock emptied): the navsidebar is the docs frame, and a wrong URL is not a docs page. Header and
footer stay, so search and the archive link remain one interaction away.
extraheadblock, notextra.css—scoped to this one page, written entirely in
tokens.cssvocabulary, anddeliberately out of the file [M3-2a] Rebuild the landing page as a stage: hide nav/TOC on home, hero region #93–[M3-2c] Evidence strip: computed counts, each linking to its proof #95 are working in.
the spans follow the
AgdaLexervocabulary, and the path span wearsnfdeliberately — it is the name the goal is about.
base_urlhandling: MkDocs renders error templates withbase_urltaken from
site_url's path (/), so every href and stylesheet URLsurvives however deep the missing path is. (
nav.homepage.url | urlrenders as
/.on this page; the template says whybase_urlis usedinstead.)
.goal-404__skyelement sits behind the composition as the mount point, shipped
text-first as the issue allows.
mkdocs.yml("One partial only") updated to nameboth overrides.
Verification
make check(strict build): clean.make design-audit: all three audits green withsite/404.htmlcrawledlike any other page — no font fallback (
∃comes from the shippedJuliaMono symbols subset), no cross-origin request, 0 elements below WCAG
AA in either theme.
passing, but the thinnest ratio on the entire site, which is the kind of
margin ADR-005 exists to refuse. The hole's text now leans 20% toward
the ink token (
color-mix), which widens both themes (fg opposes bgin each): light 4.63 → 5.88:1, dark 5.76 → 6.48:1 (computed from the
rendered colours), hue intact.
Chromium serialises a
color-mix()result in the css-color-4 formcolor(srgb r g b), whose channels are 0..1; the audit's parser readthem as 0..255, i.e. near-black — a false fail at 1.22:1 in dark and,
worse, a false pass at ~17.9:1 in light, on the same wrong colour.
audit_contrast.mjsnow scalescolor(…)channels (and survivesChromium's
6e-05-style exponents). This was the first text colour onthe site produced by
color-mix(), which is why the gap never showed;[M3-2a] Rebuild the landing page as a stage: hide nav/TOC on home, hero region #93–[M3-2c] Evidence strip: computed counts, each linking to its proof #95 are likely to produce more of them.
mkdocs servereturns HTTP 404 with this page for a nested bogus path,and the goal names it (
_ : ∃ cv/index.html.bakverified end-to-end);screenshotted in both schemes at 1280px and at 375px (no horizontal
scroll; the box fits).
The issue's last checkbox — GitHub Pages actually serving it for a missing
path on the deployed site, in both themes — can only be ticked after this
merges and deploys; verify with any bogus URL on the live domain.
🤖 Generated with Claude Code