feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
var BBS = {};
|
|
|
|
|
|
|
|
|
|
/* One ANSI ramp per session, and the dark/paper switch.
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
*
|
|
|
|
|
* Boards rotated their welcome screens; this draws a palette instead. It is
|
|
|
|
|
* held in sessionStorage so it stays put while you read and changes when you
|
|
|
|
|
* come back, rather than flickering on every page load.
|
|
|
|
|
*
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
* Every dark ramp is three of the sixteen EGA indices, ordered dark to bright,
|
|
|
|
|
* and none of them bottom out on the frame blue, which would dissolve the
|
|
|
|
|
* logo's feet into the chrome. Without JS the CSS defaults apply and nothing
|
|
|
|
|
* is lost.
|
|
|
|
|
*
|
|
|
|
|
* On paper the ordering reverses: presence is ink rather than light, so the
|
|
|
|
|
* feet are the palest step and the crown the darkest, which keeps the weight
|
|
|
|
|
* where the dark version puts it. None of them start at the frame tone, for
|
|
|
|
|
* the same reason as above.
|
|
|
|
|
*
|
|
|
|
|
* This runs from <head>, before first paint, so neither the ramp nor the
|
|
|
|
|
* theme flashes. Dark is the default and the no-JS result; paper is opt in
|
|
|
|
|
* and remembered in localStorage rather than per session, because it is a
|
|
|
|
|
* preference and not a welcome screen.
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
*/
|
|
|
|
|
(function () {
|
|
|
|
|
var RAMPS = [
|
|
|
|
|
['#0000aa', '#00aaaa', '#55ffff'], // ice
|
|
|
|
|
['#0000aa', '#00aaaa', '#ffffff'], // ice/white
|
|
|
|
|
['#00aaaa', '#55ffff', '#ffffff'], // shallow
|
|
|
|
|
['#555555', '#00aaaa', '#55ffff'], // slate cyan
|
|
|
|
|
['#00aa00', '#55ff55', '#ffffff'], // moss
|
|
|
|
|
['#555555', '#00aa00', '#55ff55'], // slate green
|
|
|
|
|
['#00aa00', '#00aaaa', '#55ffff'], // verdigris
|
|
|
|
|
['#aa0000', '#aa5500', '#ffff55'], // ember
|
|
|
|
|
['#aa0000', '#ff5555', '#ffff55'], // fire
|
|
|
|
|
['#aa5500', '#ff5555', '#ffff55'], // rust
|
|
|
|
|
['#aa5500', '#ffff55', '#ffffff'], // amber
|
|
|
|
|
['#555555', '#aaaaaa', '#ffffff'], // mono
|
|
|
|
|
['#555555', '#aaaaaa', '#55ffff'], // cold mono
|
|
|
|
|
['#aa00aa', '#ff55ff', '#ffffff'], // violet
|
|
|
|
|
['#aa0000', '#aa00aa', '#ff55ff'] // sunset
|
|
|
|
|
];
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
/* The paper ramps, pale to dark, off the same warm grey scale as bbs.css. */
|
|
|
|
|
var PAPER = [
|
|
|
|
|
['#887a64', '#655c50', '#38342e'], // paper
|
|
|
|
|
['#887a64', '#534d43', '#1e1c19'], // ink
|
|
|
|
|
['#756b5b', '#534d43', '#2b2824'], // umber
|
|
|
|
|
['#756b5b', '#47423a', '#1e1c19'], // strong
|
|
|
|
|
['#655c50', '#47423a', '#2b2824'], // close
|
|
|
|
|
['#887a64', '#756b5b', '#534d43'], // soft
|
|
|
|
|
['#534d43', '#38342e', '#1e1c19'], // heavy
|
|
|
|
|
['#38342e', '#655c50', '#887a64'], // sunlit, the one that runs the other way
|
|
|
|
|
['#a0917c', '#756b5b', '#2b2824'] // wide
|
|
|
|
|
];
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
var i;
|
|
|
|
|
try {
|
|
|
|
|
i = sessionStorage.getItem('ramp');
|
|
|
|
|
if (i === null) {
|
|
|
|
|
i = Math.floor(Math.random() * RAMPS.length);
|
|
|
|
|
sessionStorage.setItem('ramp', i);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
i = Math.floor(Math.random() * RAMPS.length);
|
|
|
|
|
}
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
|
|
|
|
|
BBS.ramp = function (theme) {
|
|
|
|
|
var set = theme === 'light' ? PAPER : RAMPS;
|
|
|
|
|
var r = set[i % set.length];
|
|
|
|
|
var s = document.documentElement.style;
|
|
|
|
|
s.setProperty('--l1', r[0]);
|
|
|
|
|
s.setProperty('--l2', r[1]);
|
|
|
|
|
s.setProperty('--l3', r[2]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var t;
|
|
|
|
|
try { t = localStorage.getItem('theme'); } catch (e) { /* private mode */ }
|
|
|
|
|
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
|
|
|
BBS.ramp(t === 'light' ? 'light' : 'dark');
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* The prompt accepts commands.
|
|
|
|
|
*
|
|
|
|
|
* Numbers mean different things depending on the screen, which is how a board
|
|
|
|
|
* worked: at the main menu they select an area, inside an area they select a
|
|
|
|
|
* message. Nothing has to disambiguate because no two things on one screen
|
|
|
|
|
* claim the same key. Anything typeable carries data-key and the chrome that
|
|
|
|
|
* marks it, so the brackets are a promise rather than decoration.
|
|
|
|
|
*/
|
|
|
|
|
(function () {
|
|
|
|
|
// The script sits in <head> so the ramp applies before first paint; the
|
|
|
|
|
// command handler has to wait for a DOM to read its keys out of.
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function init() {
|
|
|
|
|
var buf = '', timer;
|
|
|
|
|
var out = document.getElementById('typed');
|
|
|
|
|
if (!out) return;
|
|
|
|
|
var hint = document.createElement('span');
|
|
|
|
|
hint.className = 'hint';
|
|
|
|
|
out.parentNode.insertBefore(hint, out.nextSibling.nextSibling);
|
|
|
|
|
|
|
|
|
|
function keys() {
|
|
|
|
|
var m = {};
|
|
|
|
|
[].forEach.call(document.querySelectorAll('[data-key]'), function (a) {
|
|
|
|
|
m[String(a.dataset.key).trim().toUpperCase()] = {
|
|
|
|
|
href: a.getAttribute('href'),
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
cmd: a.dataset.cmd,
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
label: a.textContent.trim().replace(/\s+/g, ' ').slice(0, 44)
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
var MAP = keys();
|
|
|
|
|
|
|
|
|
|
// Echo what the number is pointing at, so a pause is informative rather than
|
|
|
|
|
// just a delay: you can see it is right and wait, keep typing, or hit Enter.
|
|
|
|
|
function show() {
|
|
|
|
|
out.textContent = buf;
|
|
|
|
|
var hit = MAP[buf];
|
|
|
|
|
hint.textContent = hit ? ' ' + hit.label : '';
|
|
|
|
|
}
|
|
|
|
|
function go(k) {
|
|
|
|
|
var hit = MAP[k];
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
if (!hit) return false;
|
|
|
|
|
if (hit.cmd === 'theme') { flip(); return true; }
|
|
|
|
|
location.href = hit.href;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Dark or paper, as a command rather than a button: a board had no widgets,
|
|
|
|
|
it had a letter you typed. The entry names where you are going, so it is
|
|
|
|
|
[L]ight on a dark screen and [D]ark on a light one, and the key moves with
|
|
|
|
|
the label. Injected rather than written into the template, so a reader
|
|
|
|
|
without JS is not offered a switch that cannot work. */
|
|
|
|
|
var root = document.documentElement;
|
|
|
|
|
var slot = document.getElementById('themecmd');
|
|
|
|
|
|
|
|
|
|
function theme() {
|
|
|
|
|
return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
|
|
|
}
|
|
|
|
|
function draw() {
|
|
|
|
|
if (!slot) return;
|
|
|
|
|
var next = theme() === 'light' ? 'dark' : 'light';
|
|
|
|
|
var k = next.charAt(0).toUpperCase();
|
|
|
|
|
slot.innerHTML = '<a href="#" data-cmd="theme" data-key="' + k +
|
|
|
|
|
'"><span class="key">[<b>' + k + '</b>]</span>' + next.slice(1) + '</a>';
|
|
|
|
|
slot.firstChild.addEventListener('click', function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
flip();
|
|
|
|
|
});
|
|
|
|
|
MAP = keys();
|
|
|
|
|
}
|
|
|
|
|
function flip() {
|
|
|
|
|
var next = theme() === 'light' ? 'dark' : 'light';
|
|
|
|
|
if (next === 'light') root.setAttribute('data-theme', 'light');
|
|
|
|
|
else root.removeAttribute('data-theme');
|
|
|
|
|
try { localStorage.setItem('theme', next); } catch (e) { /* private mode */ }
|
|
|
|
|
BBS.ramp(next);
|
|
|
|
|
draw();
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
}
|
feat: a paper mode on randogoth.com, as a prompt command
Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every
rule in bbs.css already goes through those variables, so the whole board
inverts by redefining them and nothing below has to know.
Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be
one: the amber-on-paper end of the same machine. Tones are picked by contrast
ratio against the ground rather than by eye, and the ordering of the sixteen is
preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome,
--yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA
fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits
at on black.
The switch is a command, not a button, because a board had no widgets. It names
where it is going, so it reads [L]ight on a dark screen and [D]ark on a light
one and the key moves with the label. L and D were the only two letters free
across all 71 pages. It is injected by bbs.js rather than written into the
template, so a reader without JS is not offered a switch that cannot work.
The logo takes a second set of ramps: on paper presence is ink rather than
light, so they run pale to dark and the feet stay the lightest step, which
keeps the weight where the dark version puts it. The session's ramp index is
shared, so the two themes are the same draw rendered two ways.
Dark stays the default and the no-JS result. The preference is in localStorage
rather than sessionStorage, since it is a preference and not a welcome screen,
and it is applied from <head> before first paint.
Also fixes a hardcoded #000 on the guestbook form, which would have been a
black box on paper.
Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns,
the command flips both ways, the ramp follows, and the choice survives a
navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:09:47 +03:00
|
|
|
draw();
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
|
|
|
|
|
addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
feat: replies on every article, in the board's own chrome
A message base is a thing people answer, and until now this one could only be
read. The site already calls its articles messages in areas, so replies are the
feature the metaphor was asking for rather than a comment box bolted onto it.
Talks to the comment server's API directly instead of loading its widget. The
widget is a React app in an iframe and would arrive with its own fonts, spacing
and avatars; an iframe cannot inherit the page's font in any case. Rules are
drawn the way the guestbook draws them, padded to exactly 80 cells, so a reply
is indistinguishable from any other row on the board. All five pre elements in
the section measure 80 in a real browser.
Two things this surfaced, both verified by driving a browser rather than curl:
The command handler took every letter key as a command, which was harmless when
there was nothing on the board to type into. With a message box, typing `i`
navigated to the index mid-sentence. It now ignores keys aimed at an input or
textarea.
bbs.js already says the bracket chrome is a promise rather than decoration, and
[P]ost was drawn without wiring P to anything. P now focuses the message box.
Handles are checked here as well as at the server, since remark42 wants three
or more characters and only letters, numbers, underscores and spaces; failing
in the page is better than a refusal with no explanation.
Without JS the section is simply absent, which is honest, and the capsule never
sees it since Gemini has no way to render it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 18:10:23 +03:00
|
|
|
// Until the reply form existed there was nothing on the board to type
|
|
|
|
|
// into, so every letter was a command. Now `i` in a message box would
|
|
|
|
|
// navigate to the index mid-sentence.
|
|
|
|
|
var t = e.target;
|
|
|
|
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until
October 2026, serving four audiences from a single dated list, which is the only
format that can hold unrelated things at once.
flux.vision is recovered from the Wayback Machine, where it had sat since being
deleted in 2021: 90 articles, closed and maintained rather than updated.
tobias.raayoni-last.com is the professional record, 97 articles back to 1999.
randogoth.com is the technical work, 41 articles across six areas, served as an
ANSI bulletin board.
History starts here deliberately. The development history, the 266-row inventory
that records why each article went where it did, the Wayback pipelines, the
pruned articles, the source archives and the full-size images are kept beside
the repo rather than in it, so a clone is the web presence and not the argument
that produced it. REFACTOR.md is gitignored for the same reason. See README.md
for where that material lives, and keep it: the sort is not reproducible from
these three directories alone.
Each site documents itself in its own README, including the traps that cost time
and look like nothing when you get them wrong.
2026-10-05 20:54:17 +03:00
|
|
|
var k = e.key;
|
|
|
|
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
if (k === 'Enter') {
|
|
|
|
|
if (buf && !go(buf)) { out.textContent = ''; }
|
|
|
|
|
buf = ''; show(); e.preventDefault(); return;
|
|
|
|
|
}
|
|
|
|
|
if (k === 'Backspace') { buf = buf.slice(0, -1); show(); e.preventDefault(); return; }
|
|
|
|
|
if (k === 'Escape') { buf = ''; show(); e.preventDefault(); return; }
|
|
|
|
|
|
|
|
|
|
if (/^[0-9]$/.test(k)) {
|
|
|
|
|
buf += k;
|
|
|
|
|
show();
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
var longer = Object.keys(MAP).some(function (x) {
|
|
|
|
|
return x.length > buf.length && x.indexOf(buf) === 0;
|
|
|
|
|
});
|
|
|
|
|
if (!longer) {
|
|
|
|
|
// Nothing longer can match, so settle it now: go, or drop what was
|
|
|
|
|
// typed so a mistyped number never sticks at the prompt.
|
|
|
|
|
if (MAP[buf]) { go(buf); } else { buf = ''; show(); }
|
|
|
|
|
} else if (MAP[buf]) {
|
|
|
|
|
// Ambiguous: "1" is an area and "12" is an entry further down the same
|
|
|
|
|
// screen. Terminal menus settled this on a short pause rather than
|
|
|
|
|
// making every selection need Enter, so this does too.
|
|
|
|
|
// 1200ms, because the message index runs past 40 and reaching for a
|
|
|
|
|
// second digit takes longer than a confirmation would. Enter commits
|
|
|
|
|
// at once for anyone who does not want to wait.
|
|
|
|
|
timer = setTimeout(function () { go(buf); }, 1200);
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (/^[a-zA-Z]$/.test(k)) {
|
|
|
|
|
if (go(k.toUpperCase())) e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/* The address is stored rot13 so it is not sitting in the HTML for a scraper to
|
|
|
|
|
* lift. Same trick as tobias.raayoni-last.com. Without JS the fallback text
|
|
|
|
|
* still reads, spelled out, so nothing is lost but the click.
|
|
|
|
|
*/
|
|
|
|
|
(function () {
|
|
|
|
|
function rot13(s) {
|
|
|
|
|
return s.replace(/[a-zA-Z]/g, function (c) {
|
|
|
|
|
return String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function init() {
|
|
|
|
|
[].forEach.call(document.querySelectorAll('.mail[data-r]'), function (el) {
|
|
|
|
|
var a = document.createElement('a');
|
|
|
|
|
a.href = 'mailto:' + rot13(el.dataset.r);
|
|
|
|
|
a.textContent = rot13(el.dataset.r);
|
|
|
|
|
el.replaceWith(a);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
else init();
|
|
|
|
|
})();
|
feat: replies on every article, in the board's own chrome
A message base is a thing people answer, and until now this one could only be
read. The site already calls its articles messages in areas, so replies are the
feature the metaphor was asking for rather than a comment box bolted onto it.
Talks to the comment server's API directly instead of loading its widget. The
widget is a React app in an iframe and would arrive with its own fonts, spacing
and avatars; an iframe cannot inherit the page's font in any case. Rules are
drawn the way the guestbook draws them, padded to exactly 80 cells, so a reply
is indistinguishable from any other row on the board. All five pre elements in
the section measure 80 in a real browser.
Two things this surfaced, both verified by driving a browser rather than curl:
The command handler took every letter key as a command, which was harmless when
there was nothing on the board to type into. With a message box, typing `i`
navigated to the index mid-sentence. It now ignores keys aimed at an input or
textarea.
bbs.js already says the bracket chrome is a promise rather than decoration, and
[P]ost was drawn without wiring P to anything. P now focuses the message box.
Handles are checked here as well as at the server, since remark42 wants three
or more characters and only letters, numbers, underscores and spaces; failing
in the page is better than a refusal with no explanation.
Without JS the section is simply absent, which is honest, and the capsule never
sees it since Gemini has no way to render it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 18:10:23 +03:00
|
|
|
|
|
|
|
|
/* Replies.
|
|
|
|
|
*
|
|
|
|
|
* A message base is a thing people answer, and until now this one could only be
|
|
|
|
|
* read. The comments come from a remark42 instance which serves three sites
|
|
|
|
|
* from one database; everything below talks to its API directly rather than
|
|
|
|
|
* loading its widget, because the widget is a React app in an iframe and would
|
|
|
|
|
* arrive with its own fonts, spacing and avatars. None of that can be made to
|
|
|
|
|
* look like 80 columns of CP437, and an iframe cannot inherit the page's font
|
|
|
|
|
* in any case.
|
|
|
|
|
*
|
|
|
|
|
* The rules are drawn the way the guestbook draws them, padded to exactly 80
|
|
|
|
|
* cells, so a reply is indistinguishable from any other row on the board.
|
|
|
|
|
*
|
|
|
|
|
* Auth is anonymous and the token comes back in a header rather than a cookie,
|
|
|
|
|
* which is what lets this work from another origin at all: a cross-site cookie
|
|
|
|
|
* would be blocked outright by Safari and Firefox.
|
|
|
|
|
*/
|
|
|
|
|
(function () {
|
|
|
|
|
var W = 80;
|
|
|
|
|
|
|
|
|
|
function init() {
|
|
|
|
|
var box = document.getElementById('replies');
|
|
|
|
|
if (!box) return;
|
|
|
|
|
var host = box.dataset.host, site = box.dataset.site, url = box.dataset.url;
|
|
|
|
|
var token = null;
|
|
|
|
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
return String(s).replace(/[&<>"]/g, function (c) {
|
|
|
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function fill(n) { return n > 0 ? new Array(n + 1).join('─') : ''; }
|
|
|
|
|
function span(cls, text) { return '<span class="' + cls + '">' + esc(text) + '</span>'; }
|
|
|
|
|
|
|
|
|
|
/* `── 06 oct 26 · 14:09 ──`, lower case, as the listings elsewhere do it. */
|
|
|
|
|
function stamp(iso) {
|
|
|
|
|
var d = new Date(iso);
|
|
|
|
|
if (isNaN(d)) return '';
|
|
|
|
|
var mon = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'][d.getMonth()];
|
|
|
|
|
function p(n) { return (n < 10 ? '0' : '') + n; }
|
|
|
|
|
return p(d.getDate()) + ' ' + mon + ' ' + String(d.getFullYear()).slice(2) +
|
|
|
|
|
' · ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* One rule, padded so the visible width is exactly W. */
|
|
|
|
|
function rule(n, who, when) {
|
|
|
|
|
var num = (' ' + n).slice(-3) + ' ';
|
|
|
|
|
var left = '──[' + num + ']── ' + who;
|
|
|
|
|
var right = ' ' + when + ' ──';
|
|
|
|
|
var gap = W - left.length - right.length;
|
|
|
|
|
if (gap < 1) { // a very long handle: trim it, never overflow
|
|
|
|
|
who = who.slice(0, Math.max(1, who.length + gap - 1)) + '…';
|
|
|
|
|
left = '──[' + num + ']── ' + who;
|
|
|
|
|
gap = W - left.length - right.length;
|
|
|
|
|
}
|
|
|
|
|
return '<pre class="gb-rule">' + span('box', '──[') + span('count', num) +
|
|
|
|
|
span('box', ']── ') + span('area', who) + span('box', fill(gap)) +
|
|
|
|
|
span('date', ' ' + when + ' ') + span('box', '──') + '</pre>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function header(count) {
|
|
|
|
|
var title = ' REPLIES ';
|
|
|
|
|
var tail = count === 1 ? '1 message ' : count + ' messages ';
|
|
|
|
|
var left = '┌─' + title;
|
|
|
|
|
var right = tail + '─┐';
|
|
|
|
|
return '<pre class="box">' + esc(left) + fill(W - left.length - right.length) + esc(right) + '</pre>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render(list) {
|
|
|
|
|
var html = header(list.length);
|
|
|
|
|
list.forEach(function (c, i) {
|
|
|
|
|
html += rule(i + 1, c.user.name, stamp(c.time));
|
|
|
|
|
html += '<div class="gb-body">' + c.text + '</div>';
|
|
|
|
|
});
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
html += '<div class="gb-body">No replies yet. Be the first.</div>';
|
|
|
|
|
}
|
|
|
|
|
box.innerHTML = html + form();
|
|
|
|
|
wire();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function form() {
|
|
|
|
|
return '<pre class="rule">' + fill(W) + '</pre>' +
|
|
|
|
|
'<form class="gb-form" id="replyform">' +
|
|
|
|
|
'<div class="gb-row"><label for="rh">Handle</label>' +
|
|
|
|
|
'<input id="rh" name="handle" maxlength="24" required placeholder="anonymous"></div>' +
|
|
|
|
|
'<div class="gb-row"><label for="rt">Message</label>' +
|
|
|
|
|
'<textarea id="rt" name="text" rows="4" required></textarea></div>' +
|
|
|
|
|
'<div class="gb-row"><label></label>' +
|
|
|
|
|
'<button type="submit">' + span('key', '[') + '<b>P</b>' + span('key', ']') + 'ost</button>' +
|
|
|
|
|
' <span class="hint" id="rs"></span></div></form>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function say(msg) { var s = document.getElementById('rs'); if (s) s.textContent = msg; }
|
|
|
|
|
|
|
|
|
|
function load() {
|
|
|
|
|
return fetch(host + '/api/v1/find?site=' + encodeURIComponent(site) +
|
|
|
|
|
'&url=' + encodeURIComponent(url) + '&format=tree')
|
|
|
|
|
.then(function (r) { return r.json(); })
|
|
|
|
|
.then(function (d) {
|
|
|
|
|
render((d.comments || []).map(function (n) { return n.comment; }));
|
|
|
|
|
})
|
|
|
|
|
.catch(function () { box.innerHTML = header(0) +
|
|
|
|
|
'<div class="gb-body">The message base is unreachable.</div>'; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Anonymous login hands the JWT back in a header; we keep it ourselves so
|
|
|
|
|
no cookie is involved and nothing is blocked as third-party. */
|
|
|
|
|
function login(name) {
|
|
|
|
|
return fetch(host + '/auth/anonymous/login?user=' + encodeURIComponent(name) +
|
|
|
|
|
'&aud=' + encodeURIComponent(site) + '&site=' + encodeURIComponent(site))
|
|
|
|
|
.then(function (r) {
|
|
|
|
|
token = r.headers.get('X-JWT');
|
|
|
|
|
if (!token) throw new Error('no token');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function post(name, text) {
|
|
|
|
|
// The site goes on the query string as well as in the body: a middleware
|
|
|
|
|
// reads it from there, and without it an authenticated request is
|
|
|
|
|
// refused outright.
|
|
|
|
|
return fetch(host + '/api/v1/comment?site=' + encodeURIComponent(site), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', 'X-JWT': token },
|
|
|
|
|
body: JSON.stringify({ text: text, locator: { site: site, url: url } })
|
|
|
|
|
}).then(function (r) {
|
|
|
|
|
if (!r.ok) throw new Error(r.status);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wire() {
|
|
|
|
|
var f = document.getElementById('replyform');
|
|
|
|
|
if (!f) return;
|
|
|
|
|
f.addEventListener('submit', function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
var name = (f.handle.value || 'anonymous').trim();
|
|
|
|
|
var text = f.text.value.trim();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
// remark42 wants at least three characters, letters, numbers,
|
|
|
|
|
// underscores and spaces only. Fail here rather than at the server.
|
|
|
|
|
if (name.length < 3 || /[^A-Za-z0-9_ ]/.test(name)) {
|
|
|
|
|
say('handle: 3+ characters, letters numbers _ and spaces'); return;
|
|
|
|
|
}
|
|
|
|
|
say('posting…');
|
|
|
|
|
login(name).then(function () { return post(name, text); })
|
|
|
|
|
.then(function () { say(''); return load(); })
|
|
|
|
|
.catch(function () { say('could not post, try again'); });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// P puts the cursor in the message box. The bracket chrome is a promise
|
|
|
|
|
// everywhere else on this board, so it has to be one here too.
|
|
|
|
|
addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
|
|
|
var t = e.target;
|
|
|
|
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
|
|
|
|
if (e.key === 'p' || e.key === 'P') {
|
|
|
|
|
var ta = document.getElementById('rt');
|
|
|
|
|
if (ta) { ta.focus(); e.preventDefault(); }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
else init();
|
|
|
|
|
})();
|