Skip to content

fix(theme): stop the dark scheme reading as a stock generated landing page - #164

Merged
sanity merged 4 commits into
mainfrom
worktree-dark-theme-polish
Sep 2, 2026
Merged

fix(theme): stop the dark scheme reading as a stock generated landing page#164
sanity merged 4 commits into
mainfrom
worktree-dark-theme-polish

Conversation

@sanity

@sanity sanity commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Ian flagged that freenet.org looked like cliché AI-generated design ("the glowing colors"). In light mode that turned out to be mostly wrong; in dark mode it was right, and this fixes it.

Root cause

The dark palette was a mechanical lightness inversion of the light one. Hue and saturation were preserved and only lightness flipped:

token light dark (before)
--text #404654 #abb1bf
--surface #ffffff #14161a
heading accent #0066CC #4da6ff

That lands on a near-black ground (#121212) carrying a fully saturated #4da6ff on every card heading. Add a gradient-filled headline and a blue glow under the primary button and you have the recognisable generated-template look. A designed dark theme desaturates its accents and lifts the ground off pure black; a flipped one does neither.

Changes

  • Accent #4da6ff#79a7d6, chosen for the same relative luminance as the old value, so every contrast pair involving it is unchanged (~7.1:1 on the ground) while chroma is roughly halved. The neon read came from saturation, not brightness, so this is a chroma cut rather than a dimming.
  • Ground #121212#15171b — off pure near-black, slight cool cast.
  • Card headings use --text-strong instead of the accent, in both schemes. Blue is now reserved for things you can click.
  • Cards get --surface (#1c1f26) and a stronger border. #1a1a1a on #121212 was a 1.03:1 difference and essentially invisible.
  • Headline is solid ink in both schemes; gradient text fill removed.
  • Primary CTA is a flat fill with a neutral shadow rather than a gradient with a coloured glow.
  • Ring visualisation: teal halo band removed, peers and event hues desaturated (the five originals were Tailwind's default 400-level ramp), chord alpha raised (0.05 → 0.13 light, it was washing out entirely against the page), and the canvas ground now matches the page so there is no disc edge.

Two accessibility defects fixed on the way

Corrected after review. An earlier version of this description said the wordmark measured "1:1 on every page" and that this PR fixed "15 contrast failures". Both overstated. See the review-response comment.

  • The "Freenet" wordmark was gradient-clipped text with color: transparent. It rendered fine in production, because background-clip: text painted the gradient into the glyphs — an automated checker reports 1:1 for it, but a human sees the word. The real defect is that transparent-fill text is fragile: it breaks under forced-colors, and it broke in light mode mid-way through this branch when the gradient was removed but the color: transparent was left behind. Now a plain solid colour.
  • "Live network dashboard" was #888 at 0.6 opacity, compositing to 1.95:1 light / 2.67:1 dark. Now a real link at 5.32:1 / 7.1:1. This is the one unambiguous contrast fix, and it appears twice.

Verification

Playwright across home, quickstart, river, faq, news, donate, ghostkey in both schemes plus mobile: no page errors, no transparent-text, no horizontal overflow.

A contrast sweep was run against production as a control, so pre-existing failures could be separated from new ones. After the review round: 7 failures remain, all pre-existing on /river/, and none are introduced by this PR.

The first commit did introduce a regression (/quickstart/ showing two different accent blues, because the find-and-replace only covered freenet.css) and a contrast bug on the donate CTA hover at 2.53:1. Both are fixed in f4e0fed; details in the review-response comment.

Known gap

The light-mode hero screenshot (river-hero-light.webp) is not addressed. Its legible content shows users reporting that the tutorial is outdated and that there is no search engine. It needs a fresh capture from a real River session, which cannot be produced authentically here. See the review comment for detail.

[AI-assisted - Claude]

https://claude.ai/code/session_01Cwq3ZRYFqSW9rMtGJYqfo2

… page

The dark palette was a mechanical lightness inversion of the light one:
hue and saturation were kept and only lightness flipped, which landed on a
near-black ground (#121212) carrying a fully saturated #4da6ff on every
heading. That combination, plus a gradient-filled headline and a coloured
glow under the primary button, is the recognisable generated-template look.

Changes, all of which keep or improve measured contrast:

- Accent #4da6ff -> #79a7d6. Chosen for the SAME relative luminance as the
  old value, so every contrast pair involving it is unchanged (~7.1:1 on the
  ground) while the chroma is roughly halved. The neon read came from
  saturation, not brightness.
- Ground #121212 -> #15171b, off pure near-black with a slight cool cast.
- Card headings use --text-strong instead of the accent, in both schemes.
  Blue is now reserved for things that are clickable.
- Cards get --surface (#1c1f26) and a stronger border so they are visible
  against the ground; previously #1a1a1a on #121212 essentially vanished.
- Headline is solid ink in both schemes; the gradient text fill is removed.
- Primary CTA is a flat fill with a neutral shadow rather than a gradient
  with a blue glow.
- Ring visualisation: the teal halo band is removed, peers and event hues
  are desaturated (the five originals were Tailwind's default 400 ramp), and
  the canvas ground now matches the page so there is no disc edge.

Two accessibility defects fixed on the way, both confirmed against
production as pre-existing:

- The "Freenet" wordmark was gradient-clipped text with color: transparent,
  measuring 1:1 on every page in both schemes. It is now solid.
- "Live network dashboard" was #888 at 0.6 opacity, compositing to 1.95:1
  light / 2.67:1 dark. It is now a real link at 5.32:1 / 7.1:1.

Verified with Playwright across home, river, faq, news, donate and ghostkey
in both schemes plus mobile: no page errors, no horizontal overflow, and an
automated contrast sweep run against production as a control shows 15
failures fixed and none introduced. The 7 remaining are pre-existing on
/river/ and live outside these files.

Claude-Session: https://claude.ai/code/session_01Cwq3ZRYFqSW9rMtGJYqfo2
bgColor() had no call site: the canvas is cleared with clearRect and
composites over the page, so nothing ever painted a ground. The comment
added in the previous commit claimed it prevented a disc edge, which it
cannot do. Removing it rather than leaving a comment that misdescribes it.

origResize captured window.onresize and was never read.

Verified in a browser: resize still repaints (non-blank pixel fraction
0.121 -> 0.120 -> 0.120 across shrink and restore), and a live OS theme
flip still recolours the ring (mean canvas colour 106,148,190 dark ->
59,104,157 light -> 106,147,190 back), which is handled by the
isDark() !== lastDark check in frame().

Claude-Session: https://claude.ai/code/session_01Cwq3ZRYFqSW9rMtGJYqfo2
Adversarial review found the first commit's find-and-replace stopped at the
stylesheet boundary, leaving the old neon accent live in four other files.
The result was worse than either state alone: /quickstart/ rendered BOTH
blues in one viewport, the muted #79a7d6 on "Try River in your browser" a
few hundred px above the old #4da6ff on the active OS tab.

- os-install.html: 7x #4da6ff, 1x #7cc0ff (drives /quickstart/)
- river/baseof.html: --rv-brand, --rv-brand-bright
- donation-success.html, river-invite-button.html: #1a1a1a card surfaces,
  which the ground lift had made LESS visible (1.10 -> 1.06), the exact
  defect the first commit set out to fix. Now on the same #1c1f26 surface
  as every other card, at 1.09.

Also from review:

- .funding-donate-button:hover had a dark override setting a bare accent
  fill under white text: 2.53:1, and it LIGHTENED on hover while every
  other primary CTA darkens. The base rule already uses the variables and
  resolves correctly in dark, so the override was both redundant and wrong.
  Deleting it gives 8.42:1 in both schemes (verified by hovering).
- Reduced-motion drew one static frame and returned, so the scheme poll in
  frame() never ran and a reduced-motion visitor whose OS switched theme
  kept the old colours for the life of the page. Now redraws on the media
  query. Verified: mean canvas colour 105,148,190 -> 58,104,157 on flip,
  where before it did not move at all.
- .network-hero-link lost its hover colour change in the first commit
  (only the underline moved). Restored, plus a dead text-decoration: none
  that was overridden two lines later.

Correcting an overstatement in the first commit message: it said the
wordmark measured "1:1 on every page in both schemes". That is what an
automated checker reports for color: transparent, but background-clip: text
was painting the gradient into the glyphs, so the wordmark was legible in
production. The real problem was that transparent-fill text is fragile, not
that it was invisible. The PR body has been corrected too.

Claude-Session: https://claude.ai/code/session_01Cwq3ZRYFqSW9rMtGJYqfo2
@sanity

sanity commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1 — findings addressed

An adversarial review pass found a regression I introduced plus several real defects. All fixed in f4e0fed, each verified in a browser rather than by inspection.

Correcting two claims I made in the PR description

The wordmark was not invisible. I wrote that it "measured 1:1 on every page in both schemes". That is what an automated checker reports for color: transparent, but background-clip: text was painting the gradient into the glyphs, so it rendered fine in production. The genuine issue is that transparent-fill text is fragile (it breaks under forced-colors, and it broke in light mode mid-way through this branch when the gradient was removed but the color: transparent was left behind). Real defect, but a much milder one than I described.

"15 contrast failures fixed" was inflated. 12 of those were the wordmark, and 1 more (Install Freenet) was a measurement artifact of walking up to a transparent background-color over a gradient. The honest count of real contrast fixes is 2 — both instances of the "Live network dashboard" link, which was #888 at 0.6 opacity compositing to 1.95:1 light / 2.67:1 dark.

The "0 introduced" half of the claim was also wrong; see below.

Regression I introduced

/quickstart/ rendered both accents at once. The find-and-replace only covered freenet.css; layouts/shortcodes/os-install.html kept 7 instances of #4da6ff. So the muted accent on "Try River in your browser" sat a few hundred pixels above the old neon on the active OS tab. Worse than either state alone. Also missed: river/baseof.html (--rv-brand), and donation-success.html + river-invite-button.html, whose #1a1a1a card surfaces the ground lift had made less visible (1.10 → 1.06) — the exact defect this PR set out to fix. All now on the shared #1c1f26 surface at 1.09.

Other fixes

  • .funding-donate-button:hover set a bare accent fill under white text: 2.53:1, and lightened on hover while every other primary CTA darkens. The base rule already resolves correctly in dark, so the override was redundant and wrong. Deleted → 8.42:1 in both schemes, confirmed by actually hovering. A non-hover contrast sweep cannot catch this, which is why mine did not.
  • Reduced-motion canvas never repainted on a scheme change. That path draws one frame and returns, so the per-frame scheme poll never runs. A reduced-motion visitor whose OS switches theme with the tab open kept the old scheme's ring permanently. Now redraws on the media query: mean canvas colour 105,148,19058,104,157 on flip, where before it did not move.
  • Hero link hover had been reduced to an underline-only change; colour shift restored. Plus a dead text-decoration: none overridden two lines later.

Verified after the fixes

/quickstart/ dark now reports a single blue accent (rgb(121,167,214)). Full sweep across home, river, faq, news, donate, ghostkey in both schemes plus mobile: no page errors, no transparent text, no horizontal overflow, and the contrast sweep shows the same 7 pre-existing /river/ failures and nothing new.

Knowingly not fixed here

  • The 7 /river/ contrast failures (#6b7a8d small text at 4.19–4.38:1, and white-on-image hero text that may be a checker false positive). Pre-existing, outside these files.
  • --accent-text is a half-applied abstraction: ~20 raw #79a7d6 literals remain in dark media blocks. That is precisely the fragility that produced the /quickstart/ miss, but finishing it is a mechanical refactor better done on its own.
  • The light-mode hero screenshot, which needs a fresh capture from a live River session.

[AI-assisted - Claude]

Accessibility review at f4e0fed found the surface lift had broken the
focus indicator, which is a defect this branch caused.

base.css drew the ring in --color-primary (#0066CC). That is a FILL colour,
chosen to carry white text, and it was never checked against the surfaces it
lands on. Against the old #1a1a1a card it scraped 3.13:1; raising --surface
to #1c1f26 dropped it to 2.96:1, under the 3:1 that WCAG SC 1.4.11 requires
of a focus indicator. Six homepage card links were affected.

Rather than move --color-primary (which would change every button fill),
the ring now has its own --focus-ring token, defaulting to --color-primary
so nothing outside this theme changes. Dark resolves to the accent:
2.96 -> 6.53:1 on cards, verified by focusing a real link.

Also from that review, all pre-existing but all in scope here:

- --navbar-item-hover was declared only in the light scheme, so base.css
  painted a near-white #f1f2f4 under near-white text on a keyboard-focused
  navbar item. The wordmark disappeared on Tab: 1.05:1, measured. Declared
  for dark -> 12.26:1.
- os-install.html kept the coloured glow this branch strips from .cta-button,
  on a visually identical button, so Windows and macOS visitors still got it.
  Now the same flat fill and neutral shadow. Its :focus-visible ring stays
  coloured on purpose (indicator, not decoration) and uses the new accent.
- .home-browser was the last dark card left on #1a1a1a; its separation from
  the ground had fallen to 1.031:1. Now --surface, 1.09:1 like the others.
- Four remaining rgba(77,166,255) tints migrated.
- color-scheme: light dark declared. base.css forces overflow-y: scroll, so
  a scrollbar is always present and was rendering light against a dark page.

Corrected the --accent-text comment: it claimed contrast was "unchanged",
but the ground moved too, so accent-on-ground is 7.33 -> 7.10:1. The drop
comes from the lifted ground, not from the new accent.

Verified: full sweep over home, quickstart, river, faq, news, donate,
ghostkey in both schemes plus mobile, with production as control. Still 7
pre-existing /river/ failures and nothing new; every earlier fix re-checked
and still holding.

Claude-Session: https://claude.ai/code/session_01Cwq3ZRYFqSW9rMtGJYqfo2
@sanity

sanity commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Review round 2 — accessibility / cross-browser findings addressed

All fixed in 2d4568f, each verified by measuring the real state in a browser.

Blocking regression this branch caused

The focus ring stopped meeting SC 1.4.11. base.css drew it in --color-primary (#0066CC) — a fill colour chosen to carry white text, never checked against the surfaces it lands on. Against the old #1a1a1a card it scraped 3.13:1; raising --surface to #1c1f26 dropped it to 2.96:1, under the 3:1 required of a focus indicator. Six homepage card links affected.

Fixed by giving the ring its own --focus-ring token (defaulting to --color-primary, so nothing outside this theme changes) and resolving it to the accent in dark. Measured by focusing a real link: 2.96 → 6.53:1.

Pre-existing, but in scope and fixed here

  • --navbar-item-hover was declared only in the light scheme. base.css therefore painted a near-white #f1f2f4 under near-white text on a keyboard-focused navbar item — the wordmark disappeared on Tab at 1.05:1. Now declared for dark: 12.26:1. Worth noting this directly contradicted this PR's own claim that the wordmark is "now solid"; it wasn't, in the focused state.
  • os-install.html kept the coloured glow this branch strips from .cta-button, on a visually identical button — so Windows and macOS visitors still got it. Now flat fill + neutral shadow. Its :focus-visible ring stays coloured deliberately (indicator, not decoration) and uses the new accent.
  • .home-browser was the last dark card on #1a1a1a; separation from the ground had fallen to 1.031:1. Now --surface, 1.09:1 like the rest.
  • Four remaining rgba(77,166,255) tints migrated.
  • color-scheme: light dark declared. base.css forces overflow-y: scroll, so a scrollbar is always present and was rendering light against the dark page.

Independent confirmation of the core claim

The reviewer recomputed the luminance argument: L(#4da6ff) = 0.360702, L(#79a7d6) = 0.365574 — the new accent is 1.35% lighter, so every pair where it is the foreground improves slightly. It also flagged my wording: I said contrast was "unchanged", but the ground moved too, so accent-on-ground is 7.33 → 7.10:1. The small drop comes from the lifted ground, not the new accent. Comment in the code corrected.

It also independently reproduced the honest accounting: of the "15 failures fixed", 13 were checker artifacts and 2 were real. Pixel-sampling showed the wordmark rendered at 7.3–20:1 in Chromium, Firefox and WebKit, and was fine under forced-colors too. Already corrected in the PR body.

Verified clean

  • Chromium 145 / Firefox 146 / WebKit 26: no engine divergence, no page errors, canvas renders identically in all three.
  • prefers-reduced-motion honoured (0 changed pixels over 1.8s), and the redraw fix confirmed against production, which kept stale dark-scheme colours on a white page indefinitely.
  • forced-colors: no regression.
  • Full sweep, both schemes, 10 pages, production as control: 0 new text-contrast failures.

Still knowingly not fixed

The pre-existing /river/ contrast failures, the half-applied --accent-text abstraction (~20 raw literals; mechanical, better on its own), and the light-mode hero screenshot, which needs a fresh capture from a live River session.

[AI-assisted - Claude]

@sanity
sanity merged commit f5d73d7 into main Sep 2, 2026
3 checks passed
@sanity
sanity deleted the worktree-dark-theme-polish branch September 2, 2026 21:07
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