aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 21:51:23 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 21:51:23 -0500
commitb7374fe09306c495131cf3673addb76a1dcf017d (patch)
tree2a6b87ee9d2cdb918a0e8b321167e0b7b97f8ed5
parent556eaa133e9cfe7f4c8e542708119fdfd311357e (diff)
Match private chat to the site's ASCII style
Reuse the historical Message Me form's ASCII frames, red logo, green prompts, bracketed actions, and the homepage light/dark palette. Preserve readable controls on small screens, align the license page and footer, and run browser fixtures beside the active local preview.
-rw-r--r--crates/chat-server/tests/browser.mjs4
-rw-r--r--crates/chat-server/web/index.html59
-rw-r--r--crates/chat-server/web/javascript.html6
-rw-r--r--crates/chat-server/web/style.css123
-rw-r--r--docs/CHAT-APPLICATION.txt16
5 files changed, 152 insertions, 56 deletions
diff --git a/crates/chat-server/tests/browser.mjs b/crates/chat-server/tests/browser.mjs
index a56c7cc..08c5c88 100644
--- a/crates/chat-server/tests/browser.mjs
+++ b/crates/chat-server/tests/browser.mjs
@@ -24,7 +24,7 @@ import { chromium, firefox } from '../../../.tools/browser/node_modules/playwrig
const root = await mkdtemp(join(tmpdir(), 'stack-chat-e2e-'));
const token = randomBytes(32).toString('hex');
-const origin = 'http://127.0.0.1:9080';
+const origin = `http://127.0.0.1:${process.env.CHAT_TEST_PORT || '9081'}`;
const password = 'browser-test-only-unique-passphrase';
await writeFile(join(root, '.env'), `CHAT_ORIGIN=${origin}\nCHAT_SETUP_TOKEN=${token}\n`, {mode:0o600});
for (const path of ['crates','target','docs','web','LICENSE']) await symlink(resolve(path), join(root,path));
@@ -32,7 +32,7 @@ await mkdir(join(root,'var/build'), {recursive:true});
let server;
let serverErrors = '';
async function launchServer() {
- server = spawn(resolve('target/debug/stack-chat-server'), ['--root',root], {stdio:['ignore','pipe','pipe']});
+ server = spawn(resolve('target/debug/stack-chat-server'), ['--root',root,'--bind',new URL(origin).host], {stdio:['ignore','pipe','pipe']});
server.stderr.on('data', bytes => { serverErrors += bytes.toString(); });
await Promise.race([
new Promise((ready, reject) => { server.stdout.once('data', ready); server.once('exit', () => reject(new Error(serverErrors || 'Server exited'))); }),
diff --git a/crates/chat-server/web/index.html b/crates/chat-server/web/index.html
index 9783789..c240b85 100644
--- a/crates/chat-server/web/index.html
+++ b/crates/chat-server/web/index.html
@@ -25,32 +25,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</head>
<body>
<main>
-<header><a href="https://doan.luuduct.ai/">~doan</a> / message me <span class="status">EXPERIMENTAL / LOCAL TEST</span></header>
-<h1>[ private chat ]</h1>
-<p>Text only. A dedicated identity file. Encrypted for you and Doan.</p>
-<p class="notice">This is an experimental application, not an audited messenger. <a href="/LIMITATIONS.txt">Read its limitations</a> before use. Keep your file and password safe: there is no account recovery.</p>
+<div class="libre-badge">[ <a href="https://www.gnu.org/software/librejs/" aria-describedby="js-warning">GNU LibreJS-compliant</a><span id="js-warning" role="tooltip">LibreJS compliance concerns software freedom, not a guarantee of privacy or security. Review the source code, dependencies, and scripts delivered to your browser. Server-delivered code can change between visits; free software can still track users or behave maliciously.</span> ]</div>
+<header>
+<pre class="logo-art"><a href="https://doan.luuduct.ai/" class="logo" aria-label="Doan Luu Duc Tai home">
+ ____ ___ _ _ _
+| _ \ / _ \ / \ | \ | |
+| | | | | | |/ _ \ | \| |
+| |_| | |_| / ___ \| |\ |
+|____/ \___/_/ \_\_| \_| LUU DUC TAI
+</a></pre>
+<nav class="site-nav" aria-label="Site navigation"><a href="https://doan.luuduct.ai/">Home</a><a href="https://doan.luuduct.ai/#services">Services</a><a href="/" aria-current="page">Message Me</a></nav>
+</header>
+<div class="ascii-panel introduction">
+<div class="ascii-bar" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div>
+<div class="panel-body">
+<h1>MESSAGE ME</h1>
+<p>Private text conversations between you and Doan.</p>
+<p class="build-status">EXPERIMENTAL / LOCAL TEST</p>
+<p>Keep your file and password. There is no account recovery.<br><a href="/LIMITATIONS.txt">Read the limitations</a> before using this experimental messenger.</p>
+</div>
+<div class="ascii-bar" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div>
+</div>
<p id="status" role="status" aria-live="polite">Loading cryptographic module...</p>
-<section id="entry" hidden>
+<section id="entry" class="actions" hidden>
<button id="create-choice">CREATE NEW CHAT</button>
<button id="open-choice">OPEN CHAT FILE</button>
</section>
<section id="create" hidden>
<h2 id="create-title">Create a new chat</h2>
<form id="create-form" autocomplete="off">
-<label>Display name <input id="name" maxlength="80" required autocomplete="off"></label>
-<label>Password <input id="new-password" type="password" minlength="12" maxlength="1024" required autocomplete="off"></label>
-<label>Confirm password <input id="confirm-password" type="password" minlength="12" required autocomplete="off"></label>
-<label id="setup-label" hidden>Owner setup token from the root .env <input id="setup-token" type="password" autocomplete="off"></label>
+<div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="name">Display name</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="name" maxlength="80" required autocomplete="off"></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
+<div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="new-password">Password</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="new-password" type="password" minlength="12" maxlength="1024" required autocomplete="off"></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
+<div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="confirm-password">Confirm password</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="confirm-password" type="password" minlength="12" required autocomplete="off"></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
+<div class="ascii-field" id="setup-label" hidden><p id="setup-help">Read CHAT_SETUP_TOKEN from your root .env.</p><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="setup-token">Owner setup token</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input aria-describedby="setup-help" id="setup-token" type="password" autocomplete="off"></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
<p>Use a strong, unique passphrase. This identity is solely for this chat; do not reuse it elsewhere.</p>
-<button type="submit">GENERATE AND DOWNLOAD IDENTITY FILE</button>
+<button type="submit">DOWNLOAD IDENTITY FILE</button>
</form>
</section>
<section id="unlock" hidden>
<h2>Open your saved identity file</h2>
<p id="reopen-note">Your file is opened locally. The file and password are never uploaded.</p>
<form id="unlock-form" autocomplete="off">
-<label>Identity file <input id="file" type="file" required></label>
-<label>Password <input id="password" type="password" required autocomplete="off"></label>
+<div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="file">Identity file</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="file" type="file" required></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
+<div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="password">Password</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="password" type="password" required autocomplete="off"></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div>
<button type="submit">UNLOCK CHAT</button>
</form>
</section>
@@ -62,20 +79,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h3 id="thread-name"></h3>
<p id="conversation-fingerprint"></p>
<p id="counts"></p>
-<form id="history-form"><label>Latest messages to load (0–50) <input id="history-count" type="number" min="0" max="50" step="1" value="0" required></label><button type="submit">LOAD HISTORY</button></form>
+<form id="history-form"><div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="history-count">Latest messages (0-50)</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><input id="history-count" type="number" min="0" max="50" step="1" value="0" required></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div><button type="submit">LOAD HISTORY</button></form>
<button id="older" hidden>LOAD OLDER</button>
<ol id="messages" aria-label="Messages"></ol>
-<form id="send-form" autocomplete="off"><label>Plain text message (up to 8192 UTF-8 bytes) <textarea id="text" rows="5" required maxlength="8192" autocomplete="off"></textarea></label><button type="submit">ENCRYPT AND SEND</button></form>
-<button id="retry" hidden>RETRY THE SAME ENCRYPTED MESSAGE</button>
+<p id="message-limit">Plain text only. Up to 8192 UTF-8 bytes per message.</p>
+<form id="send-form" autocomplete="off"><div class="ascii-field"><div class="ascii-bar field-header"><span aria-hidden="true">+--[ </span><label class="field-heading" for="text">Message body</label><span aria-hidden="true"> ]</span><span class="ascii-rule" aria-hidden="true">--------------------------------------------------------------------------------</span><span aria-hidden="true">+</span></div><div class="field-row"><textarea aria-describedby="message-limit" id="text" rows="5" required maxlength="8192" autocomplete="off"></textarea></div><div class="ascii-bar field-end" aria-hidden="true"><span>+</span><span class="ascii-rule">--------------------------------------------------------------------------------</span><span>+</span></div></div><button type="submit">ENCRYPT AND SEND</button></form>
+<button id="retry" hidden>RETRY MESSAGE</button>
</section>
</section>
<noscript>This chat requires free JavaScript and WebAssembly for local cryptography. The main website remains JavaScript-free.</noscript>
-<footer>
-<p><a class="badge" href="https://www.gnu.org/software/librejs/" aria-describedby="js-warning">GNU LibreJS-compliant</a><span id="js-warning" role="tooltip">LibreJS compliance concerns software freedom, not a guarantee of privacy or security. Review the source code, dependencies, and scripts delivered to your browser. Server-delivered code can change between visits; free software can still track users or behave maliciously.</span></p>
-<a href="https://www.gnu.org/licenses/agpl-3.0.html">LICENSE</a>
-<a href="/THIRD-PARTY-LICENSES.html">THIRD PARTY LICENSES</a>
-<a href="/SOURCE.tar.gz">CORRESPONDING SOURCE CODE</a>
-<a href="/javascript.html" rel="jslicense">JAVASCRIPT LICENSES / RAW SOURCE</a>
+<footer class="source-footer" aria-label="Licensing and source">
+<a class="project-license" href="https://www.gnu.org/licenses/agpl-3.0.html">[ LICENSE ]</a>
+<a class="dependency-licenses" href="/THIRD-PARTY-LICENSES.html">[ THIRD PARTY LICENSES ]</a>
+<a href="/SOURCE.tar.gz">[ SOURCE CODE ]</a>
+<a href="/javascript.html" rel="jslicense">[ JAVASCRIPT LICENSES ]</a>
</footer>
</main>
</body>
diff --git a/crates/chat-server/web/javascript.html b/crates/chat-server/web/javascript.html
index 927a678..205a103 100644
--- a/crates/chat-server/web/javascript.html
+++ b/crates/chat-server/web/javascript.html
@@ -15,14 +15,14 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!doctype html>
-<html lang="en"><head><meta charset="utf-8"><title>JavaScript licenses</title><link rel="stylesheet" href="/style.css"></head><body><main>
+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>JavaScript licenses</title><link rel="stylesheet" href="/style.css"></head><body><main class="license-page">
<h1>JavaScript licenses and source</h1>
<p>All first-party scripts are available under the GNU Affero General Public License, version 3 or, at your option, any later version. The table uses the license identifiers recognized by GNU LibreJS.</p>
-<table id="jslicense-labels1"><thead><tr><th>Script / raw file</th><th>License</th><th>Corresponding source</th></tr></thead><tbody>
+<div class="license-table"><table id="jslicense-labels1"><thead><tr><th>Script / raw file</th><th>License</th><th>Corresponding source</th></tr></thead><tbody>
<tr><td><a href="/app.js">app.js</a></td><td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0</a></td><td><a href="/SOURCE.tar.gz">Complete corresponding source</a></td></tr>
<tr><td><a href="/worker.js">worker.js</a></td><td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0</a></td><td><a href="/SOURCE.tar.gz">Complete corresponding source</a></td></tr>
<tr><td><a href="/wasi.mjs">wasi.mjs</a></td><td><a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU-AGPL-3.0</a></td><td><a href="/SOURCE.tar.gz">Complete corresponding source</a></td></tr>
-</tbody></table>
+</tbody></table></div>
<p>The WebAssembly module is compiled from the Rust workspace and unmodified OpenSSL 3.6.5, under their respective free software licenses. The source archive includes first-party sources, Cargo dependencies, the OpenSSL source archive, build scripts, and notices. General-purpose Rust and WASI SDK tools are documented with verified download locations in CHAT-OPENSSL-MIGRATION.txt.</p>
<p><a href="/THIRD-PARTY-LICENSES.html">Third-party licenses</a> / <a href="/SOURCE.tar.gz">Source archive</a> / <a href="/">Return</a></p>
</main></body></html>
diff --git a/crates/chat-server/web/style.css b/crates/chat-server/web/style.css
index 0983b2e..80b07a6 100644
--- a/crates/chat-server/web/style.css
+++ b/crates/chat-server/web/style.css
@@ -14,36 +14,101 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+
+:root {
+ color-scheme: light dark;
+ --background: #fff;
+ --foreground: #000;
+ --link: #06c;
+ --logo: #a00;
+ --good: #090;
+ --warning: #806000;
+ --license: #923900;
+ --third-party: #355b3c;
+ --muted: #555;
+}
+
* { box-sizing: border-box; }
-:root { color-scheme: dark; font-family: "Courier New", monospace; background: #131715; color: #d4d8cd; line-height: 1.6; }
-body { margin: 0; }
-main { max-width: 58rem; margin: 2rem auto; padding: 0 1.25rem; }
-header, footer { border-top: 1px solid #667367; padding-top: 1rem; }
-header { display: flex; flex-wrap: wrap; gap: .6rem; }
-h1 { font-size: 1.8rem; color: #e3e3ce; }
-h2, h3 { font-size: 1.05rem; }
-a { color: #aec2a5; text-underline-offset: .2rem; }
-.status, .badge { color: #e0c46d; }
-.status { margin-left: auto; }
-.notice { border-left: 2px solid #c5ad68; padding-left: 1rem; }
-button, input, textarea { font: inherit; color: inherit; background: #1c231e; border: 1px solid #667367; border-radius: 0; padding: .55rem .7rem; }
-button { cursor: pointer; margin: .4rem .6rem .4rem 0; }
-button:hover, button:focus-visible { border-color: #d7c884; color: #f0e5b0; }
+body { overflow-y: scroll; font: 16px/1.5 monospace, monospace; margin: 0; background: var(--background); color: var(--foreground); }
+main { width: 80ch; max-width: calc(100% - 32px); margin: 16px auto; position: relative; }
+a { color: var(--link); text-decoration: none; }
+a:hover { text-decoration: underline; }
+a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--link); outline-offset: 4px; }
+.logo-art { font: inherit; line-height: 1.2; margin: 0 0 2em; overflow-x: auto; }
+.logo { color: var(--logo); display: inline-block; }
+.logo:hover { text-decoration: none; }
+.site-nav { display: flex; flex-wrap: wrap; gap: 2ch; margin: 0 0 2em; }
+.site-nav [aria-current] { color: inherit; }
+h1, h2, h3 { font: inherit; font-weight: bold; }
+h1 { margin: 0; }
+h2, h3 { margin: 0 0 1em; }
+p { margin: 0 0 1em; }
+.introduction { text-align: center; }
+.introduction p:last-child { margin-bottom: 0; }
+.build-status, .notice { color: var(--warning); }
+.ascii-bar { display: flex; white-space: pre; line-height: 1.5; user-select: none; }
+.ascii-rule { flex: 1; min-width: 0; overflow: hidden; }
+.panel-body { padding: 1em 2ch; background-image: repeating-linear-gradient(to bottom, currentColor 0 1em, transparent 1em 1.5em), repeating-linear-gradient(to bottom, currentColor 0 1em, transparent 1em 1.5em); background-size: 1px 100%; background-position: .5ch 0, calc(100% - .5ch) 0; background-repeat: no-repeat; }
+.ascii-panel { margin: 1.5em 0; }
+.ascii-field { margin: 1.5em 0; }
+.field-heading { white-space: nowrap; font-weight: normal; margin: 0; }
+.field-row { background-image: repeating-linear-gradient(to bottom, currentColor 0 1em, transparent 1em 1.5em), repeating-linear-gradient(to bottom, currentColor 0 1em, transparent 1em 1.5em); background-size: 1px 100%; background-position: .5ch 0, calc(100% - .5ch) 0; background-repeat: no-repeat; padding: .25em 2ch; display: flex; gap: 1ch; align-items: baseline; }
+.field-row::before { content: '>'; color: var(--good); font-weight: bold; }
+.field-end { height: 1.5em; }
+button, input, textarea { font: inherit; color: inherit; background: transparent; border-radius: 0; }
+button { appearance: none; border: 0; padding: 0; margin: .5em 2ch .5em 0; color: var(--link); font-weight: bold; cursor: pointer; }
+button::before { content: '[ < '; }
+button::after { content: ' > ]'; }
+button:hover { text-decoration: underline; }
button:disabled { opacity: .5; cursor: wait; }
-input, textarea { display: block; width: 100%; margin: .35rem 0 1rem; }
-input[type=number] { width: 8rem; }
+#create-choice, #create-form button, #unlock-form button, #send-form button { color: var(--good); }
+#lock { color: var(--logo); }
+#retry { color: var(--warning); }
+input, textarea { appearance: none; border: 0; padding: 0; margin: 0; min-width: 0; width: 100%; line-height: 1.5; }
+input[type=file] { font-size: .875em; }
+input[type=file]::file-selector-button { font: inherit; color: var(--link); background: transparent; border: 1px dashed currentColor; margin-right: 1ch; padding: .2em 1ch; cursor: pointer; }
+input[type=number] { width: 7ch; }
+textarea { resize: vertical; min-height: 7.5em; }
label { display: block; }
-section { margin: 1.5rem 0; }
+section { margin: 2em 0; }
[hidden] { display: none !important; }
-.chat-heading { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; }
-#fingerprint, #conversation-fingerprint { overflow-wrap: anywhere; font-size: .8rem; }
-#messages { padding: 0; list-style: none; }
-#messages li { border-top: 1px dashed #48564a; padding: 1rem 0; }
-#messages p { white-space: pre-wrap; overflow-wrap: anywhere; margin: .4rem 0; }
-#messages small { color: #a8b39e; }
-#status { min-height: 1.6rem; color: #e0c46d; }
-footer { margin-top: 3rem; padding-bottom: 2rem; }
-footer > a { display: block; width: fit-content; }
-footer p { position: relative; }
-#js-warning { display: none; max-width: 48rem; border: 1px solid #c5ad68; background: #1c231e; padding: 1rem; margin: .8rem 0; }
-.badge:hover + #js-warning, .badge:focus-visible + #js-warning { display: block; }
+.actions { display: flex; flex-wrap: wrap; gap: 0 2ch; }
+.actions button { margin-right: 0; }
+.chat-heading { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 1ch; }
+.chat-heading h2 { margin-bottom: 0; }
+#fingerprint, #conversation-fingerprint { overflow-wrap: anywhere; font-size: .875em; color: var(--muted); margin-top: 1em; }
+#thread { margin-top: 3em; }
+#counts { color: var(--good); }
+#history-form { display: flex; flex-wrap: wrap; align-items: baseline; gap: 1ch 2ch; }
+#history-form .ascii-field { flex: 0 1 31ch; min-width: 24ch; margin: .5em 0; }
+#messages { padding: 0; list-style: none; margin: 2em 0; }
+#messages li::before { content: '--------------------------------------------------------------------------------'; display: block; overflow: hidden; white-space: pre; margin: 1em 0; }
+#messages p { white-space: pre-wrap; overflow-wrap: anywhere; margin: .5em 0 1em; }
+#messages small { color: var(--link); font: inherit; }
+#status { min-height: 1.5em; color: var(--link); margin: 2em 0 1em; padding-left: 2ch; position: relative; }
+#status::before { content: '>'; color: var(--good); position: absolute; left: 0; font-weight: bold; }
+.libre-badge { position: absolute; top: 0; right: 0; color: var(--warning); font-weight: bold; }
+.libre-badge > a { color: inherit; text-decoration: underline wavy; text-underline-offset: .2em; }
+#js-warning { display: none; position: absolute; top: 2em; right: 0; width: 48ch; max-width: calc(100vw - 32px); padding: 1em 2ch; background: var(--background); color: var(--foreground); border: 1px dashed var(--warning); z-index: 1; font-weight: normal; }
+.libre-badge > a:hover + #js-warning, .libre-badge > a:focus-visible + #js-warning { display: block; }
+.source-footer { display: flex; flex-direction: column; align-items: flex-end; gap: .5em; text-align: right; margin: 3em 0 1em; font-weight: bold; }
+.source-footer::before { content: '________________________________________________________________________________'; display: block; width: 100%; overflow: hidden; white-space: pre; margin-bottom: 1em; font-weight: normal; }
+.project-license { color: var(--license); }
+.dependency-licenses { color: var(--third-party); }
+.license-page h1 { margin: 2em 0 1em; }
+.license-table { overflow-x: auto; }
+table { border-collapse: collapse; width: 100%; font: inherit; }
+th, td { padding: .75em 1ch; text-align: left; border-bottom: 1px dashed currentColor; vertical-align: top; }
+
+@media (prefers-color-scheme: dark) {
+ :root { --background: #000; --foreground: #fff; --link: #6cf; --logo: #f33; --good: #0f0; --warning: #fc3; --license: #efb366; --third-party: #a3c7a8; --muted: #aaa; }
+}
+
+@media (max-width: 600px) {
+ .libre-badge { position: relative; width: fit-content; margin: 0 0 2em auto; }
+ .logo-art { font-size: clamp(10px, 2.4vw, 16px); }
+ .field-heading { font-size: .875em; }
+ .panel-body { padding-inline: 2ch; }
+ .source-footer { align-items: flex-start; text-align: left; }
+ .actions { flex-direction: column; align-items: flex-start; }
+}
diff --git a/docs/CHAT-APPLICATION.txt b/docs/CHAT-APPLICATION.txt
index 2a06649..9d281ed 100644
--- a/docs/CHAT-APPLICATION.txt
+++ b/docs/CHAT-APPLICATION.txt
@@ -263,6 +263,18 @@ BACKUP, EXPORT, AND RESTORE
archive. Deleting a message locally or losing a file does not delete stored
ciphertext, old exports, or other participants' copies.
+INTERFACE AND SITE STYLE
+
+ The interface follows web/styles.css and the previous www/message.html and
+ www/form.css from the parent of e373f15, the commit that removed the original
+ Message Me form. It reuses the red ASCII logo, 80-column desktop width,
+ black/white light and dark themes, blue links, green prompts/actions, ASCII
+ form frames, and bracketed controls. Licensing links retain the main site's
+ amber and muted green accents. The LibreJS badge stays yellow with its
+ keyboard-accessible warning. Small screens keep readable form controls and
+ fit the ASCII rules to the available width instead of shrinking all text.
+ No remote fonts, images, or additional scripts are required.
+
LICENSES, SOURCE, AND VERIFICATION
Portable root CLI builds now also need a musl C compiler because consistent
@@ -305,7 +317,9 @@ LICENSES, SOURCE, AND VERIFICATION
and its browser downloads under .tools/browsers. These are development tools
excluded from releases. The fixtures use temporary isolated installation roots
with one temporary .env each, and remove their private data on completion.
- They do not initialize your real owner identity or touch production.
+ They listen on loopback port 9081 by default (override with CHAT_TEST_PORT),
+ leaving the normal local preview on 9080 available. They do not initialize
+ your real owner identity or touch production.
LibreJS verification uses the Mozilla-distributed GNU LibreJS 7.21.1 in
Playwright Firefox 157.0, without whitelisting the application. The full chat