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.
87 lines
3.0 KiB
HTML
87 lines
3.0 KiB
HTML
<!DOCTYPE html>
|
|
<!-- Served by nginx (error_page) and Apache (ErrorDocument) for any address that is
|
|
not a file, the root, or a locale directory. Deliberately self-contained: no
|
|
stylesheet, no script, no font — a 404 must render even when the thing that is
|
|
broken is the asset pipeline itself. -->
|
|
<html lang="en" dir="ltr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="robots" content="noindex, follow">
|
|
<title>Page not found - SecureBit.chat</title>
|
|
<style>
|
|
html { color-scheme: dark; }
|
|
/* No script on this page either, so the theme is whatever the system says. */
|
|
:root {
|
|
color-scheme: dark;
|
|
--e-bg: #0f0f11;
|
|
--e-text: #e8e8eb;
|
|
--e-heading: #f4f4f6;
|
|
--e-muted: #8a8a92;
|
|
--e-accent: #f0892a;
|
|
--e-accent-rgb: 240, 137, 42;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
color-scheme: light;
|
|
--e-bg: #f6f7f8;
|
|
--e-text: #1b1b20;
|
|
--e-heading: #0e0e12;
|
|
--e-muted: #63636c;
|
|
--e-accent: #b05c08;
|
|
--e-accent-rgb: 176, 92, 8;
|
|
}
|
|
}
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 32px 24px;
|
|
background: var(--e-bg);
|
|
color: var(--e-text);
|
|
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
line-height: 1.6;
|
|
}
|
|
main { max-width: 34rem; }
|
|
.code {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: .16em;
|
|
color: var(--e-accent);
|
|
margin: 0 0 14px;
|
|
}
|
|
h1 {
|
|
font-size: clamp(26px, 5vw, 34px);
|
|
font-weight: 800;
|
|
letter-spacing: -1px;
|
|
line-height: 1.15;
|
|
color: var(--e-heading);
|
|
margin: 0 0 12px;
|
|
}
|
|
p { color: var(--e-muted); font-size: 15.5px; margin: 0 0 22px; }
|
|
a {
|
|
display: inline-block;
|
|
color: var(--e-accent);
|
|
border: 1px solid rgba(var(--e-accent-rgb), .35);
|
|
border-radius: 8px;
|
|
padding: 9px 18px;
|
|
text-decoration: none;
|
|
font-size: 14.5px;
|
|
font-weight: 600;
|
|
}
|
|
a:hover, a:focus-visible { background: rgba(var(--e-accent-rgb), .08); }
|
|
a:focus-visible { outline: 2px solid var(--e-accent); outline-offset: 2px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<p class="code">404</p>
|
|
<h1>There is no page at this address.</h1>
|
|
<p>The link may be mistyped, or it pointed at something that has since moved. Nothing was lost on our side — the app keeps no server-side state to lose.</p>
|
|
<a href="/">Open SecureBit.chat</a>
|
|
</main>
|
|
</body>
|
|
</html>
|