Long Range Scan: the 1971 galaxy, on the terminal it was written for

An independent rebuild of the 1971 starship patrol game. Eight by eight
quadrants of eight by eight sectors, three digits a quadrant on the chart,
courses round a nine-point compass, and a stardate clock that is the real
opponent.

The name and the nouns are ours and NOTICE.md says why: the mechanics are
free to rebuild, the trademarks are not. Raiders rather than the enemy from
the television programme, beams rather than the energy weapon, and every
sentence the machine prints written for this project rather than borrowed
from a listing.

This is the 1971 skin only -- paper, ink and a print head. The remaster and
the synth come next, and the tokens and the structured transcript are already
shaped for them.
This commit is contained in:
2026-09-08 23:26:22 -07:00
commit 593bbd888f
31 changed files with 7262 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
/* =====================================================================
One skin so far, and it is the machine as it was.
1971 is a printing terminal: black ink on fanfold paper, one weight, one
family, no colour at all. Anything that wants to be emphatic has to do it
with capitals or spacing, which is exactly the constraint the original was
written under.
Everything the skin owns is a token, so that the remaster can swap the lot
under `html[data-skin='modern']` without touching a rule below.
===================================================================== */
:root {
--ink: #241f1a;
--ink-dim: #6b6157;
--ink-bright: #0d0b09;
--accent: #241f1a;
--warn: #241f1a;
--outline: #b6a992;
--screen-bg: #e8e0cc;
--screen-radius: 2px;
--paper-edge: #d6cbb2;
--glow: none;
--btn-radius: 0;
--btn-shadow: none;
--btn-bg: #f3ecd9;
--btn-fg: #241f1a;
--field-bg: #f7f1e0;
--field-fg: #241f1a;
--body-font: ui-monospace, "DejaVu Sans Mono", "Cascadia Mono", "Courier New", monospace;
--line-height: 1.42;
--letter-spacing: 0.02em;
color-scheme: light;
}
/* =====================================================================
Layout
===================================================================== */
.app {
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.cabinet {
width: min(980px, 100%);
display: flex;
flex-direction: column;
gap: 12px;
}
.bezel {
position: relative;
padding: clamp(10px, 2vw, 20px);
border-radius: calc(var(--screen-radius) + 10px);
background: #cfc4ab;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
/* The platen: the roller the paper comes over, with the feed knob at the end. */
.platen {
height: 16px;
margin: -4px 0 10px;
border-radius: 8px;
background: linear-gradient(180deg, #4a4038 0%, #2b241e 60%, #1a1512 100%);
box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.5);
position: relative;
}
.platen::after {
content: '';
position: absolute;
right: -10px;
top: -5px;
width: 26px;
height: 26px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #6b5c4d, #241f1a 70%);
}
.screen {
position: relative;
overflow: hidden;
border-radius: var(--screen-radius);
background: var(--screen-bg);
aspect-ratio: 4 / 3;
box-shadow: var(--glow);
}
@media (max-width: 640px) {
/* A phone is taller than it is wide; a fixed 4:3 wastes most of it. */
.screen {
aspect-ratio: 3 / 4;
}
}
.screen-inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
padding: clamp(12px, 2.6vw, 26px) 34px;
color: var(--ink);
font-family: var(--body-font);
font-size: clamp(10px, 1.45vw, 14px);
line-height: var(--line-height);
letter-spacing: var(--letter-spacing);
}
.screen-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* Fit shrinks rather than clipping; see Frame.tsx. */
.screen-body > * {
transform: scale(var(--fit, 1));
transform-origin: top center;
width: 100%;
}
.stack {
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
max-width: 72ch;
margin: 0 auto;
}
.line {
white-space: pre-wrap;
min-height: 1.2em;
}
.dim {
color: var(--ink-dim);
}
/* On paper there is no colour, so emphasis is weight and spacing only. */
.accent,
.warn {
color: var(--accent);
font-weight: 700;
letter-spacing: 0.14em;
}
/* --- paper chrome ---------------------------------------------------- */
.sprockets {
position: absolute;
top: 0;
bottom: 0;
width: 22px;
background-image: radial-gradient(circle at 50% 10px, #b6a992 3.5px, transparent 4px);
background-size: 22px 26px;
background-color: var(--paper-edge);
pointer-events: none;
}
.sprockets-left {
left: 0;
border-right: 1px dashed #c3b79e;
}
.sprockets-right {
right: 0;
border-left: 1px dashed #c3b79e;
}
/* Paper is not flat and not one colour; a faint fibre keeps it from reading
as a beige rectangle. */
.paper-grain {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.35;
background-image:
repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.025) 0 1px, transparent 1px 3px),
repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.02) 0 1px, transparent 1px 4px);
}
/* =====================================================================
The transcript
===================================================================== */
.console {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.paper {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
display: flex;
flex-direction: column;
gap: 1px;
}
.paper::-webkit-scrollbar {
display: none;
}
.printed {
white-space: pre;
min-height: 1.2em;
}
/* The print head: a block where the next character is about to land. */
.head {
display: inline-block;
width: 0.62em;
height: 1em;
margin-left: 1px;
vertical-align: text-bottom;
background: currentColor;
opacity: 0.5;
}
/* =====================================================================
The prompt
===================================================================== */
.prompt {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.prompt-q {
font-weight: 700;
}
.prompt-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.keys {
display: flex;
flex-wrap: wrap;
gap: 2px 4px;
}
.key {
padding: 3px 8px;
font-size: 0.9em;
}
.statusbar {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--outline);
color: var(--ink-dim);
font-size: 0.9em;
}
/* Condition red. On paper it can only be said louder, not redder. */
.statusbar .senses {
color: var(--ink-bright);
font-weight: 700;
letter-spacing: 0.14em;
}
.banner {
margin: 0;
font-family: var(--body-font);
font-weight: 900;
font-size: clamp(1.3rem, 4.4vw, 2.4rem);
letter-spacing: 0.08em;
line-height: 1.1;
color: var(--ink-bright);
}
.field {
display: flex;
align-items: center;
gap: 10px;
margin: 3px 0;
}
.field label {
min-width: 11ch;
color: var(--ink-dim);
}
.field-input {
flex: 1;
max-width: 22ch;
padding: 5px 8px;
border: 2px solid var(--outline);
border-radius: var(--btn-radius);
background: var(--field-bg);
color: var(--field-fg);
font: inherit;
text-transform: uppercase;
}
/* =====================================================================
Buttons
===================================================================== */
.btn {
padding: 6px 14px;
border: 2px solid var(--outline);
border-radius: var(--btn-radius);
background: var(--btn-bg);
color: var(--btn-fg);
font: inherit;
font-weight: 700;
letter-spacing: 0.04em;
cursor: pointer;
box-shadow: var(--btn-shadow);
text-decoration: none;
display: inline-block;
}
.btn:disabled {
opacity: 0.45;
cursor: default;
}
.btn-primary {
background: #241f1a;
color: #f3ecd9;
}
.btn-ghost {
background: transparent;
color: var(--ink-dim);
border-color: transparent;
box-shadow: none;
font-weight: 600;
}
.btn-ghost:hover:not(:disabled) {
color: var(--ink);
}
/* The stack is a column, so a button in it stretches to the full width unless
it is told not to. A button that wide reads as a banner rather than a
control. */
.stack > .btn {
align-self: flex-start;
}
/* =====================================================================
The footer
It sits outside the frame, on the page rather than on the paper -- so it
cannot use the ink tokens, which are chosen to be dark on a light sheet.
===================================================================== */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
color: #9a9186;
font-family: var(--body-font);
font-size: 12px;
}
.controls .btn-ghost {
color: #9a9186;
}
.controls .btn-ghost:hover:not(:disabled) {
color: #f0e8d4;
}
/*
* The way back. The arrow is a pseudo-element rather than part of the label so
* that it can be the right arrow for the machine you are looking at: the two
* ASCII characters a printing terminal could actually strike.
*/
.controls .back::before {
content: '<-\00a0';
}
.seed {
margin-left: auto;
opacity: 0.7;
}