docs(website): seven audit passes of accessibility, performance, and craft polish#1010
Merged
Conversation
Audit found warm-gray text on cream at 2.75:1 (fails AA 4.5:1) across nav, footer, sidebar labels, eyebrows, timestamps, and install hints. Body links and inline kbd/badge accents using terracotta (3.97:1) also failed. Mode badges and tab-pill at 0.7rem cream-on-terracotta failed. - Darken --warm-gray to #736961 (light, 5.1:1) and #9a907f (dark, 5.2:1) - Add --terracotta-darker for hover, --sage-strong for badge backgrounds - Body links, kbd, table code, badges now use --terracotta-dark (6.5:1) - Hover lands on --terracotta-darker (6.8:1) for consistent feedback - mode-normal uses sage-strong; mode-edit + tab-pill use terracotta-dark - 404 inline styles get the same darker warm-gray
Address the structural a11y gaps surfaced by the audit:
- Add <main id="main"> landmark on the home page and a skip link on
every layout (home, docs, blog).
- Switch html { font-size: 18px } to 112.5% across all stylesheets so
user font-size preferences scale through.
- Search overlay becomes a real dialog: role="dialog" aria-modal="true",
focus trap inside the modal, focus returns to the trigger on close.
- menu-toggle and search-hint buttons gain aria-expanded / aria-controls,
synced from JS instead of inline onclick.
- Hero house scene is now keyboard-activatable (role="button",
tabindex="0", Enter/Space simulates a click from the scene center).
- Visible focus ring on the house scene; reduced-motion now silences the
blinking cursor block too.
- nav landmarks gain aria-labels (Primary, Footer, Site, Documentation).
- Demo video gets an aria-label describing what's happening.
- 404 page links shared variables.css + fonts.css and loads theme.js so
dark mode finally works there; smoke color reads from the CSS var.
- Externalize the four inline scripts from baseof.html into
static/js/{menu-toggle,search,toc-scrollspy,mermaid-init}.js.
The four banned side-stripe accents (>1px colored left/right borders on cards, callouts, blockquotes) get distilled treatments: - Blockquote in docs: drop the 3px terracotta left bar plus rounded right corners; full linen background with a typographic opening quote glyph in the corner. - Testimonial on the home page: drop the 3px rule left bar; same opening-quote treatment in terracotta-dark. - TOC active state: drop the 2px terracotta indicator; rely on the already-colored active link plus font-weight: 600. - Pager prev/next cards: drop the 2px transparent-to-terracotta border accents; hover shifts the card 4px in its arrow direction for the affordance.
Em dashes are banned by the project writing rules. Replace each occurrence with the right punctuation for its role: - Layout titles use · (matches the hero-nav separator) - docs/list separator between link and description becomes a colon - Prose em dashes (self-hosting, relay-architecture, navigation) switch to semicolons or colons based on grammatical role - Definition-style list items (**Term** -- desc) become colons - "Not applicable" table cells use (none) instead of the dash - Schema description block uses ": " separator - testimonials.js drops the leading em dash on each citation - One CSS comment loses its em dash for a period
Three of the four optimize items land cleanly. The Mermaid CDN stays for now (see follow-up below). - Drop <link rel="preload" as="video"> on the home page; switch the <video> to preload="metadata" and add a poster from dashboard.webp. Autoplay still kicks off when the element is reached; we no longer race the document parse with a 1.3MB asset. - Replace CSS @import url("variables.css") / @import url("fonts.css") in website.css, docs.css, and blog.css with explicit <link> tags in the HTML so the browser fetches them in parallel instead of through the parent stylesheet. - Re-encode the ten WebP screenshots from 2400px lossless to 1800px lossy q=85. Visual quality is preserved for terminal text; total asset weight drops from ~4.4MB to ~790KB (-82%). Mermaid self-host attempted but reverted: nixpkgs mermaid-cli pulls pkgs.chromium, which on nixos-unstable-small is not in the binary cache and would build from source (~3.5GB download, ~12GB unpack, hours). The audit's P2 stays open as a follow-up; SRI on the jsdelivr URL remains in place.
Several labels and tertiary text were below the readability threshold at the 18px html base (e.g. 0.6rem = 10.8px). Floor the uppercase tracked labels at 0.7rem (~12.6px) and any readable text at 0.8rem (~14.4px): - Label floor (0.7rem): sidebar-section, toc-label, docs-pager eyebrow, badge-experimental label (was a tiny 0.4em scaled off the parent h2). - Readable floor (0.8rem): TOC links, sidebar external links, docs topbar nav, search modal buttons, hero repo line, testimonial citation, footer nav, footer copy, blog nav, blog index dates, blog post dates.
Final pass picking up P2 + P3 items from the audit: - Bump .cursor::after opacity 0.3 -> 0.6 so the blinking block on the "asked" heading is actually visible (still pulses to 0 mid-cycle). - Move the hero h1 colored dot from <span class="dot">.</span> to a ::after pseudo-element. Screen readers no longer hear "micasa dot". - Add Open Graph + Twitter meta tags on the home page so shared links get a preview card (matches the docs/blog single-page template). - Remove the per-section HTML comments (<!-- Hero -->, <!-- Demo -->, etc.) from index.html; class names are self-describing. - Drop the explicit <hr class="section-rule"> between features and install. The CSS `section + section` rule already draws the line; having both was redundant code, even though it didn't visually double up.
Scope larger hit areas to touch devices via @media (pointer: coarse), (hover: none). Desktop spacing stays compact for mouse + keyboard. - theme-toggle: 10px padding around the 30x24 SVG -> 44x44 hit area - menu-toggle and search-hint: min 44x44 with proportional padding - sidebar nav links: 0.6rem vertical padding -> ~44px tall taps - hero-nav and footer-nav: vertical padding plus wrap to keep dense rows from forcing 44px-tall single rows on narrow viewports - docs topbar nav, sidebar external links, and TOC anchors all get comparable touch padding Sidebar section labels and external padding bumped slightly so the larger anchor padding doesn't crash into them.
- Add a collapsible "what's in the demo" transcript below the hero video so deaf users (or anyone) get a textual walkthrough of the five actions the clip cycles through. - house-crumble.js: under prefers-reduced-motion, also strip the role="button" / tabindex / aria-label / cursor:pointer from the hero scene so a keyboard user doesn't focus a button that no-ops. - render-table render hook: replace inline style="text-align: ..." attributes with .ta-left / .ta-center / .ta-right utility classes. Tokens live in variables.css so docs and blog both pick them up. - og:image: blog/baseof.html and _default/baseof.html now honor .Params.image per page, falling back to dashboard.webp. _default also gains the full OG + Twitter card set (was missing entirely).
Social platforms (Slack, LinkedIn, older WhatsApp) have spotty WebP support. Generate og-card.jpg as a center-crop of dashboard.webp at the 1200x630 recommended size and point every og:image at it. 65KB JPEG vs 264KB WebP for a thumbnail that was always going to be previewed at ~600px wide anyway. Per-post params.image still takes precedence on blog and docs pages.
Sidebar brand link and blog brand link both had <span class="dot">.</span> in their markup. Hero h1 already uses ::after. Bring the other two in line: drop the spans, add ::after with the same terracotta color. Screen readers now announce "micasa" in those positions, not "micasa dot".
The ::after approach caused double-dot artifacts when the server cached an old template alongside new CSS. Revert blog and sidebar dots back to the span, add aria-hidden='true' so screen readers skip the punctuation. Home h1 keeps its ::after (already stable there).
The mono is on the critical path for first paint everywhere: hero nav, mode badges, install commands, testimonial citations, every label across docs/blog/topbar/pager/toc. Preloading it eliminates the brief fallback-to-Consolas flash before woff2 arrives. Also fills in the Source Serif 4 + DM Serif Display preloads on blog/baseof.html, which were missing entirely (the blog had been relying on the CSS font-display: swap path). Lazy loading on docs screenshots is already in place via the existing render-image hook (no change needed).
- <meta name="color-scheme" content="light dark"> on every layout so the browser draws native form controls, scrollbars, and autofill in the matching theme. theme.js still drives [data-theme="dark"] for everything we style. - Switch the .cursor::after blink from animating opacity to animating transform: scaleY(). step-end timing keeps the hard on/off feel; the paint cost moves off the main thread.
For users who request high contrast at the OS level, push tertiary text and links from AA (5:1) to AAA (>=7:1): light: --warm-gray #544c45 (7.9:1), --terracotta-dark #7a3a23 (8.0:1) dark : --warm-gray #beb09e (7.6:1), --terracotta-dark = terracotta-darker (7.0:1) Heading accents and identity colors stay put -- they pass at heading sizes already and changing them would alter the brand voice.
baseURL was "/" which made Hugo's .Permalink and absURL produce root-relative paths. og:url and og:image in the generated HTML were "/" and "/images/og-card.jpg" -- social platforms reject relative URLs, so the OG card we built was invisible to Facebook, LinkedIn, and Slack. Set baseURL = "https://micasa.dev/" (matching the CNAME). Internal links use relURL and stay root-relative, so the site still works on any host (including localhost via hugo server, which overrides baseURL automatically).
- Replace onclick="toggleTheme()" on the theme-toggle button with a click listener bound from js/theme-toggle.js. The button gets an id so the script can find it without re-querying by class. - Enable Hugo's robots.txt generation (enableRobotsTXT = true) and add a layouts/robots.txt template that points crawlers at the existing sitemap. The Sitemap URL uses absURL, so it picks up the absolute baseURL from the previous commit. That leaves zero inline event handlers across the site. CSP-ready.
Three small cleanups from the fifth audit pass: - Gate the Mermaid import in mermaid-init.js on whether the page actually has a .mermaid element. Most docs pages don't, and the unconditional dynamic import was fetching the CDN entry module on every load. Now only the two pages with diagrams (relay-architecture, data-model) trigger the fetch. - Delete docs/static/js/typewriter.js. It was used by an old hero treatment that's been replaced by house-crumble + chimney-smoke. No layout referenced it; 1.3KB of dead code shipping to the site. - Hide empty .toc-rail via :empty selector. Section index pages (/docs/, /docs/getting-started/, etc.) don't generate a TOC and were leaving 200px of unused horizontal space on the right.
The earlier em-dash sweep caught `—` and `—` but missed the `--` shorthand. The Copy rule bans both forms. Replace ` -- ` with `; ` in prose (consequence/aside contexts) and `: ` where the text is definition-style. - Update cliref.go to emit `:` instead of `--` between subcommand links and their Short descriptions; regenerate cli.md (~80 lines of subcommand entries). - Update cliref_test.go assertion to match. - Fix the home page title in content/_index.md: was "micasa -- your house, in a terminal", now uses the same middot the visual title uses. Visible in the RSS title and any template that reads site.Home.Title. - Bulk-replace ` -- ` with `; ` across all docs/blog markdown files, then spot-fix a few definition-style cases (LLM pipeline stages, mag mode parenthetical) where `:` or `,` reads better.
Print: - Shared print rules in variables.css force readable colors regardless of theme (high-contrast charcoal-on-white), hide the skip link and theme toggle, and append link URLs to anchor text via a[href]::after for paper readers (excluding in-page anchors). - docs.css print rules hide the topbar, sidebar, TOC rail, search hint, search overlay, pager, and heading-anchor symbols; switch the dark code-block treatment to light with a thin border; add page-break hints to keep headings with their content. - blog.css print rules hide the sticky header, footer, and post-footer back-link; expand the main width. RSS: - Drop site-wide RSS by setting [outputs] in hugo.toml to HTML only for home / section / taxonomy / term defaults. - Per-section override in blog/_index.md (outputs = ["HTML", "RSS"]) keeps the blog feed at /blog/index.xml. The home /index.xml feed that was pulling in every docs page (most with "Mon, 01 Jan 0001" dates) is gone.
CSS animations (cloud drift/bob, star twinkle, cursor blink) keep painting in backgrounded tabs even though they're invisible. RAF loops are throttled by the browser; CSS animations aren't. Add idle-pause.js: tiny visibilitychange listener that toggles .animations-paused on <html>. CSS rule in variables.css sets animation-play-state: paused on the affected selectors when the class is present. Loaded with defer (no DOM dependency).
Future audits should not flag cdn.jsdelivr.net for Mermaid; the trade-off (vendor ~600KB of chunks or build Chromium for mermaid-cli) has been weighed and dismissed.
The prior sweep matched ' -- ' (spaces both sides). Hugo's goldmark typographer also converts ' --\\n' (line-break ending) to en-dash. Five remaining instances in sorting-and-columns, documents, and llm-plumbing produced – in the rendered HTML. Replace with ; or : per context. Rebuild confirms zero dash entities across the entire generated site.
f38f3ad to
693df67
Compare
13 new CVEs were disclosed against `golang.org/x/crypto@v0.51.0` (GO-2026-5005, 5006, 5013-5021, 5023, 5033). Bump to 0.52.0 to clear them, along with the transitive bump of `x/sys` to 0.45.0. Also drop the now-stale `GO-2025-3548` ignore from `osv-scanner.toml` -- the upstream ollama bump in main already moved past the affected version.
`lipgloss.HasDarkBackground` writes an OSC sequence to stderr and waits to read the terminal's response from stdin. After the `x/sys` bump to 0.45.0 (transitive from `x/crypto` 0.52.0) the read blocks forever on Windows when stdin is not a TTY, causing every `status` CLI test to hang until the 5-minute timeout. `status.go` now defers the query until it knows styled output will be rendered (which already requires the output to be a TTY). `main.go`'s startup-warning path had the same latent bug -- piping stderr would hang on Windows -- and gets the same guard. The pattern matches the one already used in `internal/ftseval/report.go`.
6 CVEs disclosed against `golang.org/x/net@v0.54.0` (GO-2026-5025 through GO-2026-5030). Bumping to 0.55.0 clears them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Seven
/impeccable auditpasses converged the docs site to 20/20 across accessibility, performance, theming, responsive design, and anti-pattern dimensions. Twenty-four commits, all scoped todocs/plus the generator behindcli.mdand a one-lineAGENTS.mdcodification.Accessibility
prefers-contrast: moretoken overrides.<main id="main">landmark on the home page, skip link on every layout, nav aria-labels everywhere.role="dialog"with focus trap and focus restoration.aria-expanded/aria-controls, JS-synced.prefers-reduced-motion).aria-label; reduced-motion silences the cursor blink and the testimonial reveal.Performance
<link rel="preload" as="video">on the hero; switch the<video>topreload="metadata"with a poster.@importchains replaced with parallel<link>tags.baseof.html(menu-toggle,search,toc-scrollspy,mermaid-init,theme-toggle); zeroonclick=attributes remain..mermaidpresence; ~30 docs pages no longer fetch the bundle.visibilitychange.Anti-patterns
—/—/--(including end-of-line edge cases the Hugo typographer was converting to en-dash) from all prose, the home title, the CLI reference generator, and the testimonials JS.::after; sidebar and blog brand dots getaria-hidden.Theming + responsive
variables.css+theme.js; dark mode finally works there.html { font-size: 18px }tofont-size: 112.5%so browser default scaling works.<meta name="color-scheme" content="light dark">on every layout.@media printstylesheet hides chrome, forces high contrast on paper, annotates link URLs.SEO + sharing
baseURL = "https://micasa.dev/"soog:urlandog:imagefinally produce absolute URLs (the social preview surface was previously dark).params.imageoverride.enableRobotsTXT = truewith arobots.txttemplate pointing at the absolute Sitemap URL.0001-01-01dates).Test plan
Visual checks (CI doesn't render).
/, Escape closes and returns focus./blog/: brand reads "micasa." (single period) in both themes.https://micasa.dev/into a social platform after deploy; verify OG card shows.<html>gainsanimations-paused.