sites/randogoth.com/bbs/static/bbs.css
randogoth f761709c68 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

230 lines
12 KiB
CSS

/* randogoth BBS
*
* An ANSI bulletin board, not a DOS application. The distinction is the whole
* point: Turbo Vision is grey chrome, dialogs and a menu bar, which is what the
* previous theme gave. A BBS is 80 columns of CP437 on black, the 16 colour
* EGA palette, numbered areas with hotkeys, and a status line at the bottom.
*
* Everything is sized in `ch` so the screen stays exactly 80 columns wide at
* any font size, the way a terminal does.
*/
@font-face {
font-family: 'Classic Console';
src: url('clacon2.woff2') format('woff2');
font-display: block;
}
/* the IBM EGA/VGA text palette, unaltered */
:root {
/* A board is black by definition; tell the browser so the canvas between
documents and the scrollbar match instead of flashing light. */
color-scheme: dark;
--black: #000000; --blue: #0000aa; --green: #00aa00; --cyan: #00aaaa;
--red: #aa0000; --magenta: #aa00aa; --brown: #aa5500; --grey: #aaaaaa;
--dgrey: #555555; --lblue: #5555ff; --lgreen: #55ff55; --lcyan: #55ffff;
--lred: #ff5555; --lmagenta: #ff55ff; --yellow: #ffff55; --white: #ffffff;
}
/* ── paper ──────────────────────────────────────────────────────────────
* Light mode is a remap of the sixteen slots above, not a second stylesheet.
* Every rule in this file goes through those variables, so the whole board
* inverts by redefining them and nothing below needs to know.
*
* Warm greys on sepia, monochrome: this is not an EGA palette and does not
* pretend to be one. It is the amber-on-paper end of the same machine. The
* 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 in the
* dark theme stays recessive here: --dgrey is quiet, --blue is chrome,
* --yellow carries the hotkeys, --lred is the loudest thing on the screen.
*
* Unlike the dark palette these are allowed to depart from EGA fidelity, so
* --dgrey is lifted to 4.5:1 rather than left at the 2.7:1 its counterpart
* sits at on black.
*/
[data-theme="light"] {
color-scheme: light;
--black: #f4ecdc; /* 1.0 the paper */
--blue: #a0917c; /* 2.6 frames and rules */
--brown: #887a64; /* 3.6 section marks */
--lblue: #a0917c; /* 2.6 */
--dgrey: #756b5b; /* 4.5 dates, numbers, the sysline */
--green: #6a6053; /* 5.2 quote rule */
--cyan: #655c50; /* 5.6 list bullets */
--magenta: #655c50; /* 5.6 */
--red: #534d43; /* 7.1 */
--lgreen: #534d43; /* 7.1 code, counts, quoted text */
--grey: #47423a; /* 8.5 body */
--lcyan: #38342e; /* 10.5 links and the lightbar */
--lmagenta:#38342e; /* 10.5 */
--yellow: #2b2824; /* 12.5 headings, hotkeys, the address */
--white: #1e1c19; /* 14.5 emphasis */
--lred: #121210; /* 16.0 new */
}
html { background: var(--black); }
body {
margin: 0; padding: 0;
background: var(--black);
color: var(--grey);
font-family: 'Classic Console', ui-monospace, monospace;
/* clamp so 80ch always fits the viewport, phone to desktop */
font-size: clamp(7px, 2.32vw, 17px);
/* Exactly one cell per row. Each box row is its own <pre>, so any leading at
all opens a gap between them and the vertical box-drawing characters stop
meeting. Prose gets its leading back on .msgbody below. */
line-height: 1;
-webkit-font-smoothing: none;
font-smooth: never;
}
#screen { width: 80ch; margin: 0 auto; padding: 1.2em 0 2.4em; }
pre, .row { margin: 0; font: inherit; white-space: pre; }
/* ── the masthead ──────────────────────────────────────────────────────── */
/* The masthead is TheDraw's "Syndicate", an ANSI font from the board era, baked
in as static text. It uses exactly three colour levels, so the ramp is three
custom properties and recolouring costs nothing. See bbs.js: the ramp is drawn
at random once per session, which is the one piece of decoration on this site
that is also its subject. */
.logo { margin: 0 0 .2em; }
/* Both planes carry tone. ANSI paints solid areas as a space with a background,
so mapping only the foreground drops most of the mass: the first attempt at
this lost 247 of 355 inked cells and the letters came out hollow. */
.logo .f1 { color: var(--l1, #0000aa); }
.logo .f2 { color: var(--l2, #00aa00); }
.logo .f3 { color: var(--l3, #ffff55); }
.logo .b1 { background: var(--l1, #0000aa); }
.logo .b2 { background: var(--l2, #00aa00); }
.logo .b3 { background: var(--l3, #ffff55); }
.rule { color: var(--blue); }
.sysline { color: var(--dgrey); }
.sysline b { color: var(--grey); font-weight: normal; }
/* ── links behave like a lightbar: inverse on hover, as a BBS selection ─── */
a { color: var(--lcyan); text-decoration: none; }
a:hover, a:focus { background: var(--lcyan); color: var(--black); outline: none; }
/* A lightbar inverts the whole cell. Spans inside a link carry their own colour
(.area is cyan, .key is yellow) and would otherwise keep it against the cyan
highlight, which left the area names cyan on cyan and invisible. */
a:hover *, a:focus * { color: inherit; background: transparent; }
a:visited { color: var(--lcyan); }
/* ── hotkeys ───────────────────────────────────────────────────────────── */
.key { color: var(--yellow); }
.key b { color: var(--white); font-weight: normal; }
/* ── area / message listings ───────────────────────────────────────────── */
.box { color: var(--blue); }
.boxtitle { color: var(--yellow); }
.area { color: var(--lcyan); }
.desc { color: var(--grey); }
.count { color: var(--lgreen); }
.date { color: var(--dgrey); }
.sect { color: var(--brown); }
.num { color: var(--dgrey); }
.new { color: var(--lred); }
/* ── a message ─────────────────────────────────────────────────────────── */
.msghdr { color: var(--dgrey); }
.msghdr b { color: var(--lcyan); font-weight: normal; }
.msghdr .subj { color: var(--yellow); }
.msgbody { color: var(--grey); margin: 1em 0 0; line-height: 1.4; }
.msgbody p { margin: 0 0 1em; }
.msgbody h1, .msgbody h2, .msgbody h3 {
color: var(--yellow); font-size: 1em; font-weight: normal;
text-transform: uppercase; margin: 1.6em 0 .6em; }
.msgbody h1::before, .msgbody h2::before, .msgbody h3::before { content: "── "; color: var(--blue); }
.msgbody ul, .msgbody ol { margin: 0 0 1em; padding-left: 3ch; }
.msgbody li { margin: 0; }
.msgbody ul { list-style: none; }
.msgbody ul > li::before { content: "\00B7 "; color: var(--cyan); }
/* Pictures keep the scanlines the page gave up: on a board, an image was a
monitor showing it. See plugins/crtimages.py for the wrapper. */
.msgbody .crt { position: relative; display: block; margin: 1em 0; line-height: 0;
border: 1px solid var(--blue); }
/* Everything fills the column, including images smaller than it. Upscaling is
nearest-neighbour, so a 320 wide DOS screenshot comes out as bigger pixels
rather than a blurred one, which is the right answer for this material. */
.msgbody .crt img { display: block; width: 100%; height: auto; margin: 0;
border: 0; image-rendering: pixelated; }
/* The scanlines are the monitor it is being shown on. Look straight at it and
they go, which is also the only way to see the picture clean. */
.msgbody .crt::after { content: ""; position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(to bottom,
rgba(0,0,0,0) 0 2px, rgba(0,0,0,.38) 2px 3px);
opacity: 1; transition: opacity .12s linear; }
.msgbody .crt:hover::after,
.msgbody .crt:focus-within::after { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.msgbody .crt::after { transition: none; }
}
/* Embeds fill the column like everything else. The ratio is injected per
element by plugins/crtimages.py from each embed's own attributes. */
.msgbody iframe { display: block; width: 100%; height: auto; margin: 1em 0;
border: 1px solid var(--blue); }
.msgbody blockquote { margin: 0 0 1em; padding-left: 2ch; color: var(--lgreen);
border-left: 1px solid var(--green); }
/* Code blocks only. A bare :not() would have been enough until a page put box
drawing in the body: the frames are <pre> too, and were being wrapped, boxed
and turned green as if they were source. */
.msgbody code { color: var(--lgreen); font: inherit; }
.msgbody pre:not([class]), .msgbody pre.highlight {
color: var(--lgreen); font: inherit; white-space: pre-wrap;
border: 1px solid var(--blue); padding: .4em 1ch; margin: 0 0 1em;
overflow-x: auto; }
.msgbody strong { color: var(--white); font-weight: normal; }
.msgbody em { color: var(--lcyan); font-style: normal; }
.msgbody hr { border: 0; border-top: 1px solid var(--blue); margin: 1.4em 0; }
.msgbody a { text-decoration: underline; }
/* the Fidonet tagline that closed every message */
.tagline { color: var(--dgrey); margin-top: 1.6em; }
.tagline .origin { color: var(--green); }
/* ── the prompt. A BBS always leaves you at one. ───────────────────────── */
.prompt { color: var(--grey); margin-top: 1.4em; }
.prompt .cursor {
background: var(--grey); color: var(--grey);
animation: blink 1.06s steps(1) infinite; }
@keyframes blink { 50% { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .prompt .cursor { animation: none; } }
/* the keyword cloud is the one thing that cannot hold an 80 column frame */
.cloud { color: var(--grey); padding: .4em 2ch .6em; line-height: 1.6; }
.cloud a { margin-right: .4ch; }
.cloud .num { margin-right: 1.4ch; }
/* pages */
.bigmail { margin: 0 0 1.4em; }
.bigmail a, .mail { color: var(--yellow); }
.bigmail a:hover { background: var(--yellow); color: var(--black); }
.prompt .hint { color: var(--dgrey); }
/* guestbook */
.gb-rule { margin: 1.5em 0 0; }
/* Box drawing is on the grid wherever it appears, including inside a page body,
which otherwise hands it the 1.4 leading that prose wants. */
.msgbody .box, .msgbody .gb-rule, .msgbody .rule { line-height: 1; }
.gb-body { color: var(--grey); line-height: 1.4; padding: .5em 0 0 4ch; }
/* The form does not try to close each line with a vertical: a text input is not
a character cell and will not line up with one. Top and bottom rules only. */
.gb-form { margin: 0; padding: .8em 0 .4em; }
.gb-row { margin: 0 0 .7em; padding-left: 2ch; line-height: 1.6; }
.gb-row label { display: inline-block; width: 10ch; color: var(--dgrey); }
.gb-row label.second { width: auto; padding: 0 1ch 0 3ch; }
.gb-form input, .gb-form textarea {
font: inherit; background: var(--black); color: var(--white);
border: 1px solid var(--blue); outline: 0; padding: 0 1ch; }
.gb-form input { width: 22ch; }
.gb-form textarea { width: 58ch; vertical-align: top; resize: vertical; line-height: 1.4; }
.gb-form ::placeholder { color: var(--dgrey); }
.gb-form input:focus, .gb-form textarea:focus { border-color: var(--lcyan); color: var(--lcyan); }
.gb-form button { font: inherit; background: none; border: 0; padding: 0;
color: var(--yellow); cursor: pointer; }
.gb-form button:hover { background: var(--yellow); color: var(--black); }