relmio 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,48 @@ checks the registry separately after publication.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.2.7] - 2026-08-02
11
+
12
+ ### Changed
13
+
14
+ - Restore the original Relmio hosted layout and local browser wizard flow, with
15
+ the horizontal five-step Signal Spine and GitHub star/version control kept
16
+ visible.
17
+ - Add Astryx's built theme and accessible segmented appearance control to the
18
+ hosted app, plus lightweight System/Light/Dark preference support to the
19
+ local wizard.
20
+ - Add responsive dark-mode logo treatment, phone-sized controls, and matching
21
+ browser-wizard guidance to the GitHub and npm README variants.
22
+
23
+ ### Fixed
24
+
25
+ - Keep the checksum-verified temporary Node.js 22 runtime on the child process
26
+ path so Git Bash and other Node-free systems can launch the Relmio package
27
+ shim without falling back to a missing or outdated system `node` command.
28
+
29
+ ## [0.2.6] - 2026-08-02
30
+
31
+ ### Added
32
+
33
+ - Add a curl-based wizard bootstrap for macOS, Linux, WSL, and Git Bash that
34
+ reuses Node.js 22+ or downloads and checksum-verifies a temporary official
35
+ runtime when Node.js is not installed.
36
+ - Add a native Windows PowerShell bootstrap for PowerShell and Command Prompt
37
+ that works without Git Bash or a preinstalled Node.js runtime and verifies
38
+ the temporary official Windows archive before execution.
39
+ - Expand the hosted installer into an accessible macOS/Linux, PowerShell,
40
+ Command Prompt, and NPX terminal switcher.
41
+
42
+ ### Changed
43
+
44
+ - Revamp the hosted Vercel experience and local browser wizard around the
45
+ Signal Spine composition and Patchbay Ledger design language.
46
+ - Integrate the Astryx component system, neutral theme, CLI, and AI-readable
47
+ setup guidance in the hosted React application while keeping the published
48
+ wizard dependency-light.
49
+ - Add persistent route context, sanitized preview status, responsive layouts,
50
+ reduced-motion behavior, and synchronized GitHub/npm setup documentation.
51
+
10
52
  ## [0.2.5] - 2026-07-31
11
53
 
12
54
  ### Added
package/README.md CHANGED
@@ -72,20 +72,64 @@ installing the private n8n sidecar.
72
72
  </figure>
73
73
 
74
74
  Use the site's [Install wizard](https://relmio.vercel.app/install) page for a
75
- copyable command tailored to the current self-hosted n8n and Hostinger VPS
76
- setup path.
75
+ clickable macOS/Linux, PowerShell, Command Prompt, and NPX command switcher
76
+ tailored to the current self-hosted n8n and Hostinger VPS setup path.
77
+
78
+ ### Browser interface and theme modes
79
+
80
+ The hosted site and local wizard keep the original Relmio relay layout while
81
+ adding a compact **System / Light / Dark** appearance control. System follows
82
+ the computer's preference; Light and Dark are remembered on that browser only.
83
+ The local wizard keeps its horizontal five-step Signal Spine at the top of the
84
+ flow, and the hosted site keeps the live GitHub star/version control visible.
85
+ Both surfaces collapse their controls for narrow phone screens without
86
+ turning the setup flow into a side rail or requiring a separate mobile app.
77
87
 
78
88
  ## Quick start
79
89
 
80
- Run this on your own macOS, Windows, or Linux computer—not on the VPS:
90
+ Choose the terminal already on your own computer. Do not run these commands on
91
+ the VPS.
92
+
93
+ ### macOS, Linux, WSL, or Git Bash
94
+
95
+ ```bash
96
+ curl -fsSL https://relmio.vercel.app/install.sh | sh
97
+ ```
98
+
99
+ ### Windows PowerShell
100
+
101
+ ```powershell
102
+ irm https://relmio.vercel.app/install.ps1 | iex
103
+ ```
104
+
105
+ ### Windows Command Prompt
106
+
107
+ ```bat
108
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
109
+ ```
110
+
111
+ No Node.js or Git Bash installation is required first. The
112
+ [POSIX](https://github.com/Demonbane18/relmio/blob/main/web/public/install.sh)
113
+ and [Windows PowerShell](https://github.com/Demonbane18/relmio/blob/main/web/public/install.ps1)
114
+ bootstraps reuse Node.js 22 or newer when available. Otherwise they download
115
+ the matching current official Node.js 22 runtime to a private temporary
116
+ directory, verify its SHA-256 checksum, run Relmio with npm lifecycle scripts
117
+ disabled, and remove the temporary runtime when the wizard closes.
118
+
119
+ Users who already have Node.js 22 or newer can run the npm package directly:
120
+
121
+ ### NPX (requires Node.js 22+)
81
122
 
82
123
  ```bash
83
124
  npx --yes --ignore-scripts relmio@latest
84
125
  ```
85
126
 
86
- Requirements:
127
+ ### Requirements
87
128
 
88
- - Node.js 22 or newer
129
+ - On macOS/Linux/WSL/Git Bash: `curl`, `awk`, `tar`, and either `sha256sum` or
130
+ `shasum`; Git Bash also needs `unzip`
131
+ - On native Windows: Windows PowerShell 5.1 or PowerShell 7; Command Prompt can
132
+ launch the included PowerShell bootstrap
89
133
  - A browser and an eligible ChatGPT/Codex account
90
134
  - A self-hosted n8n Docker deployment on a VPS
91
135
  - Docker Compose v2, SSH access, and a Docker network shared with n8n
@@ -5,7 +5,19 @@ Every command on this page targets the separate
5
5
 
6
6
  ## Refresh an expired ChatGPT login
7
7
 
8
- The easiest method:
8
+ The easiest method is to open the
9
+ [hosted install page](https://relmio.vercel.app/install) and choose the local
10
+ terminal you already have. For macOS, Linux, WSL, or Git Bash:
11
+
12
+ ```bash
13
+ curl -fsSL https://relmio.vercel.app/install.sh | sh
14
+ ```
15
+
16
+ For Windows PowerShell, with no Git Bash or preinstalled Node.js required:
17
+
18
+ ```powershell
19
+ irm https://relmio.vercel.app/install.ps1 | iex
20
+ ```
9
21
 
10
22
  1. Start the local wizard again.
11
23
  2. Select **Refresh ChatGPT sign-in**.
package/docs/security.md CHANGED
@@ -92,8 +92,14 @@ The current release pins:
92
92
  - `ssh2` `1.17.0`
93
93
  - `openai-oauth` `2.0.0`
94
94
 
95
- Install scripts are disabled for local dependencies. The generated sidecar
96
- also installs `openai-oauth` with `--ignore-scripts`.
95
+ The POSIX and native Windows PowerShell bootstraps reuse a compatible local
96
+ Node.js runtime or download the matching current official Node.js 22 archive
97
+ to a private temporary directory. Each validates the archive against Node.js's
98
+ SHA-256 manifest before execution and removes it when the wizard closes. The
99
+ PowerShell bootstrap accepts only strict Windows x64 or ARM64 archive names,
100
+ uses HTTPS without redirects, and enables TLS 1.2 for Windows PowerShell 5.1.
101
+ npm lifecycle scripts are disabled when either bootstrap starts Relmio. The
102
+ generated sidecar also installs `openai-oauth` with `--ignore-scripts`.
97
103
 
98
104
  Do not replace pinned versions with `latest` in production. Follow the upgrade
99
105
  checklist in [maintenance.md](maintenance.md).
@@ -30,17 +30,39 @@ it after the wizard saves the credential.
30
30
 
31
31
  ## Confirm the local package first
32
32
 
33
- Run these commands on your own computer, not on the VPS:
33
+ Close every old wizard terminal and browser tab, then run the newest published
34
+ build on your own computer, not on the VPS. Choose the command for the terminal
35
+ you already have.
36
+
37
+ macOS, Linux, WSL, or Git Bash:
34
38
 
35
39
  ```bash
36
- node --version
37
- npm view relmio version
40
+ curl -fsSL https://relmio.vercel.app/install.sh | sh
41
+ ```
42
+
43
+ Windows PowerShell:
44
+
45
+ ```powershell
46
+ irm https://relmio.vercel.app/install.ps1 | iex
38
47
  ```
39
48
 
40
- Node must be version 22 or newer. Then close every old wizard terminal and
41
- browser tab and start the newest published build:
49
+ Windows Command Prompt:
50
+
51
+ ```bat
52
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
53
+ ```
54
+
55
+ These commands do not require Node.js to be installed. The Windows options do
56
+ not require Git Bash. Each bootstrap reuses Node.js 22 or newer when available,
57
+ or downloads an official temporary runtime and verifies its SHA-256 checksum
58
+ before execution.
59
+
60
+ If you choose the existing-Node fallback, confirm Node is version 22 or newer
61
+ and check the published package version first:
42
62
 
43
63
  ```bash
64
+ node --version
65
+ npm view relmio version
44
66
  npx --yes --ignore-scripts relmio@latest
45
67
  ```
46
68
 
@@ -49,7 +71,7 @@ newest printed `http://127.0.0.1:...` URL into the browser. That URL contains a
49
71
  temporary setup token: do not post it in an issue or screenshot.
50
72
 
51
73
  You do not need to sign in to npm, configure npm 2FA, or own this package to
52
- run its public `npx` command. npm authentication is required only for the
74
+ run any public command. npm authentication is required only for the
53
75
  maintainer who publishes a release.
54
76
 
55
77
  ## Quick VPS checks
@@ -88,7 +110,9 @@ bypassed.
88
110
 
89
111
  | Symptom | Meaning | Fix |
90
112
  |---|---|---|
91
- | `node: command not found`, `node is not recognized`, or Node is older than 22 | The local runtime is missing or unsupported. | Install Node.js 22 or newer on the local computer, open a new terminal, and rerun the `@latest` command. Do not install it on the VPS for the wizard. |
113
+ | `node: command not found`, `node is not recognized`, or Node is older than 22 | The NPX fallback cannot use the local runtime. | Use the macOS/Linux curl command or the native Windows PowerShell/Command Prompt command above. Either can run with a verified temporary runtime. Do not install Node.js on the VPS for the wizard. |
114
+ | `curl` or `sh` is not recognized on Windows | The macOS/Linux command was pasted into a native Windows terminal. | Use the PowerShell command in PowerShell, or the longer `powershell -NoProfile ...` command in Command Prompt. Git Bash is not required. |
115
+ | A bootstrap reports a checksum mismatch | The Node.js download did not match the official SHA-256 manifest, so it was not executed. | Retry on a trusted connection. Do not bypass the check. If it repeats, use an existing Node.js 22+ installation and report the sanitized error. |
92
116
  | The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
93
117
  | An old wizard page reports an invalid or expired setup session | The local server was closed or a newer wizard run created a different one-time session token. | Close the old page and use only the URL printed by the currently running terminal. |
94
118
  | `npx` appears to run an older wizard | An old terminal or tab is still active, or the package was run without an explicit tag. | Close old runs, check `npm view relmio version`, then run `npx --yes --ignore-scripts relmio@latest`. |
@@ -41,17 +41,26 @@ Use this simple explanation:
41
41
 
42
42
  On screen: the Mermaid flowchart in the README.
43
43
 
44
- ### 2:20 — Start the npm package
44
+ ### 2:20 — Start the wizard
45
45
 
46
- Run locally:
46
+ Open the hosted install page and choose the terminal already on the local
47
+ computer. Show both the macOS/Linux and native Windows choices:
47
48
 
48
49
  ```bash
49
- npx --yes --ignore-scripts relmio@latest
50
+ curl -fsSL https://relmio.vercel.app/install.sh | sh
51
+ ```
52
+
53
+ ```powershell
54
+ irm https://relmio.vercel.app/install.ps1 | iex
50
55
  ```
51
56
 
52
57
  Explain that:
53
58
 
54
- - `npx` downloads and runs the published version for this session;
59
+ - no preinstalled Node.js runtime is required;
60
+ - native Windows does not require Git Bash and also has a Command Prompt option;
61
+ - the bootstrap reuses Node.js 22+ or downloads and checksum-verifies a
62
+ temporary official Node.js 22 runtime;
63
+ - npm lifecycle scripts stay disabled when it starts the published package;
55
64
  - the wizard binds to `127.0.0.1`;
56
65
  - the printed setup URL is private and temporary;
57
66
  - the terminal must remain open until the wizard finishes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relmio",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Turn a supported ChatGPT/Codex OAuth sign-in into a private OpenAI-compatible endpoint, starting with self-hosted n8n.",
5
5
  "keywords": [
6
6
  "relmio",
package/src/ui/app.js CHANGED
@@ -294,7 +294,7 @@ async function discover() {
294
294
  element("container-select"),
295
295
  result.containers.map((container) => ({
296
296
  value: container.name,
297
- label: `${container.name} ${container.image}`,
297
+ label: `${container.name} - ${container.image}`,
298
298
  })),
299
299
  result.containers[0].name,
300
300
  );
package/src/ui/index.html CHANGED
@@ -7,12 +7,13 @@
7
7
  name="description"
8
8
  content="Relmio connects compatible AI clients to a private ChatGPT/Codex OAuth sidecar, starting with self-hosted n8n."
9
9
  />
10
- <title>Relmio n8n Setup</title>
10
+ <title>Relmio | n8n Setup</title>
11
11
  <link
12
12
  rel="icon"
13
13
  href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='%23137c74' d='M7 8h19l31 24H41L26 20v10L7 17Z'/%3E%3Cpath fill='%2312211f' d='M7 47l19-13v10l15-12h16L26 56H7Z'/%3E%3C/svg%3E"
14
14
  />
15
15
  <link rel="stylesheet" href="/styles.css" />
16
+ <script src="/theme.js" type="module"></script>
16
17
  <script src="/app.js" type="module"></script>
17
18
  </head>
18
19
  <body>
@@ -31,7 +32,24 @@
31
32
  </svg>
32
33
  <span>Relmio</span>
33
34
  </div>
34
- <span class="local-badge">Runs only on this computer</span>
35
+ <div class="header-actions">
36
+ <fieldset class="theme-picker">
37
+ <legend class="visually-hidden">Color theme</legend>
38
+ <label>
39
+ <input type="radio" name="color-theme" value="system" checked />
40
+ <span>System</span>
41
+ </label>
42
+ <label>
43
+ <input type="radio" name="color-theme" value="light" />
44
+ <span>Light</span>
45
+ </label>
46
+ <label>
47
+ <input type="radio" name="color-theme" value="dark" />
48
+ <span>Dark</span>
49
+ </label>
50
+ </fieldset>
51
+ <span class="local-badge">Runs only on this computer</span>
52
+ </div>
35
53
  </header>
36
54
 
37
55
  <main id="main-content" class="shell">
@@ -141,7 +159,7 @@
141
159
  placeholder="your-vps-ip-or-hostname"
142
160
  required
143
161
  />
144
- <small>Use the full IP address—do not add <code>ssh root@</code>.</small>
162
+ <small>Use the full IP address. Do not add <code>ssh root@</code>.</small>
145
163
  </label>
146
164
 
147
165
  <label class="field compact">
@@ -226,8 +244,8 @@
226
244
  </div>
227
245
 
228
246
  <div class="version-strip" aria-label="Detected Docker versions">
229
- <span>Docker <strong id="docker-version">—</strong></span>
230
- <span>Compose <strong id="compose-version">—</strong></span>
247
+ <span>Docker <strong id="docker-version">Not detected</strong></span>
248
+ <span>Compose <strong id="compose-version">Not detected</strong></span>
231
249
  </div>
232
250
 
233
251
  <div class="field-grid single">
@@ -266,7 +284,7 @@
266
284
  <ul class="check-list">
267
285
  <li>Create or update <code>/docker/n8n-openai-oauth</code></li>
268
286
  <li>Build and start only the <code>openai-oauth</code> sidecar</li>
269
- <li>Attach the sidecar to <strong id="review-network">—</strong></li>
287
+ <li>Attach the sidecar to <strong id="review-network">Not selected</strong></li>
270
288
  <li>
271
289
  Use the private hostname
272
290
  <code id="review-endpoint">n8n-openai-oauth</code>
@@ -361,12 +379,12 @@
361
379
  </button>
362
380
 
363
381
  <div class="model-box">
364
- <h3>2. OpenAI Chat Model AI Agent or Basic LLM Chain</h3>
382
+ <h3>2. OpenAI Chat Model for AI Agent or Basic LLM Chain</h3>
365
383
  <dl class="result-list">
366
384
  <div>
367
385
  <dt>Model ID</dt>
368
386
  <dd class="result-value">
369
- <code id="result-model">—</code>
387
+ <code id="result-model">Not detected</code>
370
388
  <button
371
389
  class="button secondary copy-value"
372
390
  type="button"
@@ -383,7 +401,7 @@
383
401
  <dd><strong>On</strong> for Chat Model node version 1.3</dd>
384
402
  </div>
385
403
  </dl>
386
- <p>All models detected: <span id="result-models">—</span></p>
404
+ <p>All models detected: <span id="result-models">Not detected</span></p>
387
405
  <p>
388
406
  If the Responses API switch is absent, keep the node's default
389
407
  Chat Completions behavior; the bridge supports that route too.
package/src/ui/styles.css CHANGED
@@ -11,11 +11,15 @@
11
11
  --accent-deep: #0c5f59;
12
12
  --accent-bright: #1ba397;
13
13
  --accent-soft: #e9f4ef;
14
+ --accent-border: #b9d9d2;
15
+ --accent-icon: #d3eae0;
16
+ --accent-copy: #355a54;
14
17
  --warning: #8a4b0f;
15
18
  --warning-soft: #fff4e2;
16
19
  --warning-border: #e3c491;
17
20
  --danger: #a22b2b;
18
21
  --danger-soft: #fff0f0;
22
+ --danger-border: #e4abab;
19
23
  --success: #17653f;
20
24
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
21
25
  --shadow-sm: 0 1px 2px rgb(13 27 24 / 5%);
@@ -27,6 +31,62 @@
27
31
  --radius-lg: 1.25rem;
28
32
  }
29
33
 
34
+ :root[data-theme="dark"] {
35
+ color-scheme: dark;
36
+ --background: #101513;
37
+ --surface: #1d2421;
38
+ --surface-muted: #171d1b;
39
+ --text: #edf3f0;
40
+ --text-muted: #a9b8b3;
41
+ --border: #303a36;
42
+ --border-strong: #52615b;
43
+ --accent-bright: #52c7bb;
44
+ --accent-soft: #1c2925;
45
+ --accent-border: #35564f;
46
+ --accent-icon: #20342f;
47
+ --accent-copy: #b8d8d1;
48
+ --warning: #f0b37f;
49
+ --warning-soft: #2a2118;
50
+ --warning-border: #725237;
51
+ --danger: #f09a9a;
52
+ --danger-soft: #2b1b1b;
53
+ --danger-border: #744141;
54
+ --success: #61c897;
55
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 24%);
56
+ --shadow-md:
57
+ 0 1px 2px rgb(0 0 0 / 22%),
58
+ 0 12px 36px -14px rgb(0 0 0 / 58%);
59
+ }
60
+
61
+ @media (prefers-color-scheme: dark) {
62
+ :root:not([data-theme]) {
63
+ color-scheme: dark;
64
+ --background: #101513;
65
+ --surface: #1d2421;
66
+ --surface-muted: #171d1b;
67
+ --text: #edf3f0;
68
+ --text-muted: #a9b8b3;
69
+ --border: #303a36;
70
+ --border-strong: #52615b;
71
+ --accent-bright: #52c7bb;
72
+ --accent-soft: #1c2925;
73
+ --accent-border: #35564f;
74
+ --accent-icon: #20342f;
75
+ --accent-copy: #b8d8d1;
76
+ --warning: #f0b37f;
77
+ --warning-soft: #2a2118;
78
+ --warning-border: #725237;
79
+ --danger: #f09a9a;
80
+ --danger-soft: #2b1b1b;
81
+ --danger-border: #744141;
82
+ --success: #61c897;
83
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 24%);
84
+ --shadow-md:
85
+ 0 1px 2px rgb(0 0 0 / 22%),
86
+ 0 12px 36px -14px rgb(0 0 0 / 58%);
87
+ }
88
+ }
89
+
30
90
  * {
31
91
  box-sizing: border-box;
32
92
  }
@@ -103,7 +163,7 @@ a:focus-visible {
103
163
  min-height: 4.25rem;
104
164
  padding: 0.75rem clamp(1rem, 4vw, 3rem);
105
165
  border-bottom: 1px solid rgb(221 227 223 / 75%);
106
- background: rgb(244 242 236 / 82%);
166
+ background: color-mix(in srgb, var(--background) 82%, transparent);
107
167
  backdrop-filter: blur(14px) saturate(1.4);
108
168
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
109
169
  }
@@ -122,6 +182,78 @@ a:focus-visible {
122
182
  width: 2rem;
123
183
  height: 2rem;
124
184
  flex: none;
185
+ border-radius: 0.4375rem;
186
+ transition:
187
+ background 180ms var(--ease),
188
+ box-shadow 180ms var(--ease);
189
+ }
190
+
191
+ .header-actions {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 0.625rem;
195
+ }
196
+
197
+ .theme-picker {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ gap: 0.125rem;
201
+ min-width: 0;
202
+ margin: 0;
203
+ padding: 0.1875rem;
204
+ border: 1px solid var(--border);
205
+ border-radius: 0.625rem;
206
+ background: var(--surface-muted);
207
+ }
208
+
209
+ .theme-picker label {
210
+ position: relative;
211
+ cursor: pointer;
212
+ }
213
+
214
+ .theme-picker input {
215
+ position: absolute;
216
+ opacity: 0;
217
+ pointer-events: none;
218
+ }
219
+
220
+ .theme-picker span {
221
+ min-height: 2rem;
222
+ display: inline-flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ padding: 0 0.625rem;
226
+ border-radius: 0.4375rem;
227
+ color: var(--text-muted);
228
+ font-size: 0.6875rem;
229
+ font-weight: 700;
230
+ transition:
231
+ color 160ms var(--ease),
232
+ background 160ms var(--ease),
233
+ box-shadow 160ms var(--ease);
234
+ }
235
+
236
+ .theme-picker input:checked + span {
237
+ color: var(--text);
238
+ background: var(--surface);
239
+ box-shadow: var(--shadow-sm);
240
+ }
241
+
242
+ .theme-picker input:focus-visible + span {
243
+ outline: 3px solid #f2a94a;
244
+ outline-offset: 2px;
245
+ }
246
+
247
+ .visually-hidden {
248
+ position: absolute;
249
+ width: 1px;
250
+ height: 1px;
251
+ padding: 0;
252
+ margin: -1px;
253
+ overflow: hidden;
254
+ clip: rect(0 0 0 0);
255
+ white-space: nowrap;
256
+ border: 0;
125
257
  }
126
258
 
127
259
  .local-badge {
@@ -215,7 +347,7 @@ h3 {
215
347
  align-items: start;
216
348
  margin: 1.25rem 0 0;
217
349
  padding: 1rem 1.125rem;
218
- border: 1px solid #b9d9d2;
350
+ border: 1px solid var(--accent-border);
219
351
  border-radius: var(--radius);
220
352
  background: var(--accent-soft);
221
353
  box-shadow: var(--shadow-sm);
@@ -229,7 +361,7 @@ h3 {
229
361
  place-items: center;
230
362
  border-radius: 0.625rem;
231
363
  color: var(--accent-deep);
232
- background: #d3eae0;
364
+ background: var(--accent-icon);
233
365
  font-size: 1rem;
234
366
  font-weight: 800;
235
367
  content: "✓";
@@ -240,7 +372,7 @@ h3 {
240
372
  }
241
373
 
242
374
  .safety-note span {
243
- color: #355a54;
375
+ color: var(--accent-copy);
244
376
  }
245
377
 
246
378
  .backup-note {
@@ -250,7 +382,7 @@ h3 {
250
382
 
251
383
  .backup-note::before {
252
384
  color: var(--warning);
253
- background: #f6e3c3;
385
+ background: color-mix(in srgb, var(--warning) 18%, var(--surface));
254
386
  content: "!";
255
387
  }
256
388
 
@@ -383,7 +515,7 @@ h3 {
383
515
  }
384
516
 
385
517
  .error {
386
- border: 1px solid #e4abab;
518
+ border: 1px solid var(--danger-border);
387
519
  color: var(--danger);
388
520
  background: var(--danger-soft);
389
521
  font-weight: 650;
@@ -478,7 +610,7 @@ h3 {
478
610
  border: 1px solid var(--border-strong);
479
611
  border-radius: var(--radius-sm);
480
612
  color: var(--text);
481
- background: #fff;
613
+ background: var(--surface);
482
614
  transition:
483
615
  border-color 200ms var(--ease),
484
616
  box-shadow 200ms var(--ease),
@@ -527,7 +659,7 @@ h3 {
527
659
  code {
528
660
  padding: 0.1rem 0.3rem;
529
661
  border-radius: 0.25rem;
530
- background: #edf0ef;
662
+ background: var(--surface-muted);
531
663
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
532
664
  font-size: 0.875em;
533
665
  overflow-wrap: anywhere;
@@ -626,7 +758,7 @@ code {
626
758
  .button.copied {
627
759
  border-color: var(--success);
628
760
  color: var(--success);
629
- background: #e9f5ee;
761
+ background: color-mix(in srgb, var(--success) 12%, var(--surface));
630
762
  }
631
763
 
632
764
  /* ---------- Fingerprint & approvals ---------- */
@@ -655,7 +787,7 @@ code {
655
787
  padding: 0.625rem 0.75rem;
656
788
  border: 1px solid var(--warning-border);
657
789
  border-radius: var(--radius-sm);
658
- background: #fff;
790
+ background: var(--surface);
659
791
  }
660
792
 
661
793
  .check-row {
@@ -709,12 +841,12 @@ code {
709
841
  }
710
842
 
711
843
  .review-grid section:first-child {
712
- border-color: #b9d9d2;
844
+ border-color: var(--accent-border);
713
845
  background: var(--accent-soft);
714
846
  }
715
847
 
716
848
  .review-grid section:last-child {
717
- border-color: #e4abab;
849
+ border-color: var(--danger-border);
718
850
  background: var(--danger-soft);
719
851
  }
720
852
 
@@ -831,6 +963,32 @@ footer p {
831
963
  margin: 0;
832
964
  }
833
965
 
966
+ /* ---------- Dark mode refinement ---------- */
967
+
968
+ :root[data-theme="dark"] .brand-mark {
969
+ padding: 0.125rem;
970
+ background: #f7f8f5;
971
+ box-shadow: 0 0 0 1px rgb(255 255 255 / 14%);
972
+ }
973
+
974
+ :root[data-theme="dark"] .eyebrow,
975
+ :root[data-theme="dark"] .step-kicker {
976
+ color: var(--accent-bright);
977
+ }
978
+
979
+ @media (prefers-color-scheme: dark) {
980
+ :root:not([data-theme]) .brand-mark {
981
+ padding: 0.125rem;
982
+ background: #f7f8f5;
983
+ box-shadow: 0 0 0 1px rgb(255 255 255 / 14%);
984
+ }
985
+
986
+ :root:not([data-theme]) .eyebrow,
987
+ :root:not([data-theme]) .step-kicker {
988
+ color: var(--accent-bright);
989
+ }
990
+ }
991
+
834
992
  /* ---------- Animations ---------- */
835
993
 
836
994
  @keyframes panel-in {
@@ -884,6 +1042,18 @@ footer p {
884
1042
  display: none;
885
1043
  }
886
1044
 
1045
+ .site-header {
1046
+ padding-inline: 1rem;
1047
+ }
1048
+
1049
+ .header-actions {
1050
+ gap: 0.375rem;
1051
+ }
1052
+
1053
+ .theme-picker span {
1054
+ padding-inline: 0.5rem;
1055
+ }
1056
+
887
1057
  .shell {
888
1058
  width: min(100% - 1rem, 54rem);
889
1059
  padding-top: 2rem;
@@ -0,0 +1,50 @@
1
+ const colorModes = new Set(["system", "light", "dark"]);
2
+ const storageKey = "relmio-color-mode";
3
+
4
+ function applyMode(mode) {
5
+ if (mode === "light" || mode === "dark") {
6
+ document.documentElement.dataset.theme = mode;
7
+ } else {
8
+ delete document.documentElement.dataset.theme;
9
+ }
10
+
11
+ document
12
+ .querySelectorAll('input[name="color-theme"]')
13
+ .forEach((input) => {
14
+ input.checked = input.value === mode;
15
+ });
16
+ }
17
+
18
+ let initialMode = "system";
19
+
20
+ try {
21
+ const savedMode = window.localStorage.getItem(storageKey);
22
+ if (colorModes.has(savedMode)) {
23
+ initialMode = savedMode;
24
+ }
25
+ } catch {
26
+ // System mode remains available when browser storage is unavailable.
27
+ }
28
+
29
+ applyMode(initialMode);
30
+
31
+ document
32
+ .querySelectorAll('input[name="color-theme"]')
33
+ .forEach((input) => {
34
+ input.addEventListener("change", () => {
35
+ if (!input.checked || !colorModes.has(input.value)) return;
36
+
37
+ applyMode(input.value);
38
+ try {
39
+ window.localStorage.setItem(storageKey, input.value);
40
+ } catch {
41
+ // The selection still applies for this page view.
42
+ }
43
+ });
44
+ });
45
+
46
+ window.addEventListener("storage", (event) => {
47
+ if (event.key === storageKey) {
48
+ applyMode(colorModes.has(event.newValue) ? event.newValue : "system");
49
+ }
50
+ });
package/src/web/server.js CHANGED
@@ -179,6 +179,7 @@ async function loadDefaultUiFiles() {
179
179
  const files = await Promise.all([
180
180
  readFile(new URL("../ui/index.html", import.meta.url), "utf8"),
181
181
  readFile(new URL("../ui/app.js", import.meta.url), "utf8"),
182
+ readFile(new URL("../ui/theme.js", import.meta.url), "utf8"),
182
183
  readFile(new URL("../ui/time.js", import.meta.url), "utf8"),
183
184
  readFile(new URL("../ui/styles.css", import.meta.url), "utf8"),
184
185
  ]);
@@ -186,8 +187,9 @@ async function loadDefaultUiFiles() {
186
187
  return {
187
188
  "/": files[0],
188
189
  "/app.js": files[1],
189
- "/time.js": files[2],
190
- "/styles.css": files[3],
190
+ "/theme.js": files[2],
191
+ "/time.js": files[3],
192
+ "/styles.css": files[4],
191
193
  };
192
194
  }
193
195