Remove everything that can change after first paint

Firefox users saw a flash on the first navigation after an idle spell, and
Chrome occasionally flashed white. Nothing in the network path explained it
(assets are immutable, the stylesheet is render-blocking, a cold Firefox
load paints fully styled), so this removes every remaining way a page could
look different between its first paint and its final state:

- Both Newsreader faces are embedded in the served stylesheet as data: URIs
  (CSP gains `font-src 'self' data:`; the preloads go away). A font fetched
  by URL is applied after first paint whenever the browser has to bring it
  back from disk, which is exactly the "first click after a while" case.
- The cross-document view transition is gone; the operator wants snappy.
- A color-scheme meta, kept in step with the saved theme, so the canvas the
  browser paints before the stylesheet is the right shade.
- The theme toggle's icon and label are chosen by CSS from html[data-theme]
  (set pre-paint by theme.js) instead of being rewritten by app.js.
- Dashboard table filters are rendered by the templates (shown under
  `.has-js`) rather than inserted by app.js, so tables no longer jump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
This commit is contained in:
thalladaandClaude Fable 5.1 committed 2026-09-04 18:37:44 +00:00
1 parent 9acd1b0800
commit 1047cf4633
13 files changed
+91 -82

No files matched your search

+7 -35
View File
@@ -82,40 +82,6 @@
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior:auto !important; transition-duration:0.01ms !important; } }
}
@view-transition {
navigation: auto;
}
.site-header {
view-transition-name: site-header;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 140ms;
animation-timing-function: ease-out;
}
::view-transition-group(site-header),
::view-transition-new(site-header) {
animation: none;
}
::view-transition-old(site-header) {
display: none;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(site-header),
::view-transition-old(root),
::view-transition-new(root),
::view-transition-old(site-header),
::view-transition-new(site-header) {
animation: none;
}
}
@layer components {
/* ---------- shared controls ---------- */
.btn, .button { @apply inline-flex min-h-10 items-center justify-center rounded-sm border border-rule-strong bg-transparent px-3 py-1.5 font-sans text-sm font-medium text-ink no-underline duration-150 hover:bg-ink hover:text-paper focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-paper; transition-property:transform, background-color, color, border-color; }
@@ -231,7 +197,13 @@
.filters .filter-actions { @apply flex items-center gap-3; }
.form-inline { @apply flex flex-wrap items-center gap-3 font-sans text-sm; }
.form-inline label { @apply flex items-center gap-2 text-sm; }
.table-filter { @apply my-2 w-80 max-w-full font-sans text-sm; }
/* Rendered by the templates but only useful with the script; `has-js` is set pre-paint. */
.table-filter { @apply my-2 hidden w-80 max-w-full font-sans text-sm; }
.has-js .table-filter { @apply block; }
/* Theme toggle: the current theme's icon and label, chosen without script. */
[data-theme-icon], [data-theme-label] { display:none; }
html:not([data-theme]) [data-theme-icon="system"], html[data-theme="light"] [data-theme-icon="light"], html[data-theme="dark"] [data-theme-icon="dark"] { display:block; }
html:not([data-theme]) [data-theme-label="system"], html[data-theme="light"] [data-theme-label="light"], html[data-theme="dark"] [data-theme-label="dark"] { display:inline; }
.pager { @apply flex flex-wrap items-center justify-between gap-x-6 gap-y-2 py-1 font-sans text-sm tabular-nums text-muted; }
.pager a { @apply text-ink no-underline hover:text-accent; }