The palette lived as ~620 hex literals in inline styles plus a few hundred more in
the stylesheets, so there was no single thing to change. It is now 113 custom
properties in src/styles/theme.css, in two blocks.
src/scripts/theme-boot.js decides the theme before first paint — blocking, in <head>,
above the stylesheet, because a deferred script paints dark first and corrects itself.
It stores the mode ('system' | 'light' | 'dark'), never the colour it resolved to, and
stamps data-theme so an explicit choice can beat the media query. The switcher in the
header is a view onto it.
A filled accent stays the brand colour in both themes — the ink on it is near-black
either way — while an accent used as text darkens to clear 4.5:1 on white. A colour
reaches a fill by four routes (a style property, a constant, a helper argument, an SVG
source string), and tests/theme-switching.test.mjs covers all four.
The dark theme is unchanged: every colour declaration the previous build produced comes
out of this one identically once the properties are resolved.
Also: the roadmap drops its status chips on mobile, and Roadmap.jsx no longer splits a
colour with parseInt at runtime, which a var() reference cannot survive.
The bundles carried all thirteen translations at once and a page fetched them a
third time as raw source; each page now loads only its own language. Alongside
that: JavaScript is minified, the eight stylesheets are served as one file, the
QR scanner is fetched after the app is up instead of on every visit, Inter ships
once rather than five copies of the same file, and Font Awesome is subset to the
82 icons this app draws instead of all 2468.
1.85 MB across 43 requests becomes under 700 KB across 33. On mobile the page
starts drawing in 1.6 s instead of 6.3 s and is usable in 4.5 s instead of 11 s.
Pages also carry their text in the HTML now. Everything was drawn by JavaScript
into an empty div, so crawlers saw correct metadata around nothing, and twelve of
the thirteen language pages had never been shown to anyone. The documentation is
published under /docs/ with a new FAQ, and unknown addresses return a real 404.
Separately: the localized shells were served with the year-long immutable cache
header meant for static assets, which pinned anyone who opened /de/ or /ru/ to
that build. The header is fixed and the service worker refreshes what it cached.
Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2
Four right-to-left languages at /ar/, /he/, /fa/ and /ur/ — thirteen in all.
The layout no longer has a left and a right, it has a start and an end: the
margins, insets and corners are CSS logical properties now, so they follow
dir on <html>. Directional glyphs flip; keys, safety codes and session
descriptors are pinned left-to-right so bidi cannot reorder what two people
compare against each other's screens.
Also fixed: the Service Worker was registered as './sw.js', which 404s from
every locale subdirectory, so twelve of the thirteen pages had no worker at
all. And the bundler ran before the dictionaries were generated, so a newly
added language could reach the page ahead of the app that renders it.
The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese,
Korean and Hindi alongside English — 623 strings per language, 5,607
translations, covering the landing page, the key exchange, the chat, group
calls and every error along the way.
Each locale is a real page at a real URL (/de/, /fr/, …), generated at build
time from locales/*.json. That is the whole point: the app is client-rendered,
so a language that only swaps strings at runtime has no address for a crawler
to index and no link anyone can share. Each page carries its own canonical, a
reciprocal hreflang cluster and translated schema.org.
The URL decides the language, always. A stored preference only applies where
the address does not say, and nothing ever redirects on Accept-Language —
that is how sites become invisible to search engines outside one country.
robots.txt and sitemap.xml did not exist before; both are generated now.
Bugs found on the way, each with a test that would have caught it:
- partner logos used page-relative paths and 404'd from any /xx/ page
- post-build stamped ?v= only into the root shell, leaving locales behind
- "Back online" appeared on every page load, not just after being offline
- update timestamps were hard-coded to US format for every reader
- t() threw on a partial window, taking whole components down with it
Each conversation now runs its own WebRTC session with separate keys and SAS verification, so chats never mix. Adds a side panel to switch between open chats with unread badges, a New chat action that leaves existing chats connected, per-chat local labels stored only on this device, and an availability status (Available, Away, Busy, Invisible) shared end-to-end with connected peers. Also includes vendored Prism syntax highlighting, more reliable PWA update handling, and offline send queueing fixes. Version 4.10.0.
- Add UpdateManager and UpdateChecker for automatic version detection
- Add post-build script for meta.json generation and version injection
- Enhance Service Worker with version-aware caching
- Add .htaccess configuration for proper cache control
This ensures all users receive the latest version after deployment
without manual cache clearing.