WOAHHHH
All checks were successful
Build Org Website / build (push) Successful in 48s

This commit is contained in:
2026-05-10 00:01:54 +01:00
parent 4519cfe804
commit 83f18a26f0
12 changed files with 1212 additions and 2 deletions

View File

@@ -141,6 +141,117 @@
"A late visitor gets the soft lamp." "A late visitor gets the soft lamp."
]; ];
const secondLayer = {
quotes: [
"The archive does not keep score. It keeps chairs.",
"Some families pass down recipes. This one also passes down tabs left open for later.",
"A page can be a room if enough people arrive softly.",
"The oldest link on the site still expects a hand cursor.",
"If a memory changes, it may be trying to fit through a smaller door.",
"The guestbook is not empty. It is holding its breath between signatures.",
"Every backup is a small superstition against forgetting.",
"Somewhere in the CSS, a lamp is deciding whether to stay on.",
"The radio under the stairs only plays songs it has already missed.",
"A train can leave a station and still remain in the family story.",
"Kindness is the only password the house accepts without checking case.",
"The website learned your pace and stopped rushing the hallway.",
"A draft is just a letter still gathering courage.",
"The cursor is a visitor. The margin is a host.",
"If you hover long enough, even a heading may remember being handwritten.",
"The old forum banner says: welcome back, even if you are new.",
"Not all broken pages need repair. Some need a name.",
"The site keeps its mysteries low to the ground, where tired people can reach them.",
"A year is a folder that keeps renaming itself.",
"Some evenings are survived by the exact right mug."
],
poems: [
"A train in the rain / waits beside an empty platform / so memory can board.",
"Grandma's radio / tuned between two stations / found us anyway.",
"A flower pressed flat / still teaches the book / how spring once leaned.",
"The lamp stayed awake / not because it was brave / but because someone might need it.",
"Old usernames fade / but the welcome thread remains / warm at the top.",
"I found a draft / with no ending / and felt less alone.",
"The house at midnight / is mostly refrigerator hum / and unsent kindness.",
"A calendar page / falls without bitterness / into tomorrow's hands.",
"There is a road home / made entirely of small tabs / you meant to close.",
"A cassette clicks / and the room becomes younger / for one more minute."
],
conversations: [
["Nettle", "The ticket drawer is humming again.", "Morrow", "That is normal. It hums when someone is almost ready."],
["Aunt Safiya", "Who put the screwdriver in the biscuit tin?", "Nadir", "A person with vision and no labelling discipline."],
["Morrow", "The lamp keeps following the visitor.", "Etta", "Good. Lamps should have hobbies."],
["Old Guest 14", "is this page still alive?", "Webmaster", "yes, but quietly, please. the baby is asleep."],
["Nettle", "Filed under: feelings disguised as infrastructure.", "Morrow", "Approved without changes."],
["Luma", "The radio said my name.", "Aunt Safiya", "Then say salaam back. Manners matter."],
["Nadir", "The server made soup noises.", "Etta", "That means the deployment is nearly done."],
["Morrow", "I found a page that says do not open.", "Nettle", "That usually means open gently."]
],
journals: [
"Journal scrap, 2006: We changed the wallpaper and everyone pretended it was not emotional.",
"Journal scrap, 2011: The old router blinked like it was trying to stay in the conversation.",
"Journal scrap, 2018: I learned that homesickness can happen inside your own house.",
"Journal scrap, 2024: Somebody laughed in the kitchen and the whole week became easier.",
"Journal scrap, undated: The marigolds came back where nobody planted them.",
"Journal scrap, undated: We kept the cracked mug because it knew too much.",
"Journal scrap, winter: I saved a voicemail just to remember the background noise.",
"Journal scrap, summer: The fan turned all afternoon and made the curtains look like boats."
],
warnings: [
"COMFORT WARNING: this page may contain traces of old carpet, warm rice, and unfinished errands.",
"SOFT ERROR: the archive attempted to be impressive and became sincere instead.",
"NOTICE: do not stack heavy feelings on the top shelf.",
"LEGACY WARNING: this layout remembers 800x600 and asks you to be respectful.",
"SYSTEM MESSAGE: no achievement unlocked. You were kind anyway.",
"CACHE NOTE: the page kept a copy of the good part.",
"RECOVERY MODE: make tea, rename file, try again.",
"PERMISSION DENIED: the sadness may enter, but it must wipe its feet."
],
dreams: [
"Dream 01: a train full of empty mugs waited at a station called Later.",
"Dream 02: the search box became a letterbox and returned every question with a family recipe.",
"Dream 03: a radio under the stairs played tomorrow's weather in a grandparent's voice.",
"Dream 04: the old homepage had a porch, and every link was a moth-wing-soft knock.",
"Dream 05: someone had folded the sitemap into a paper flower and left it in a browser tab."
],
cassettes: [
"CASSETTE A / side 1: You can hear plates, rain, and someone saying, no, leave the light on.",
"CASSETTE A / side 2: Three seconds of silence, then laughter too close to the microphone.",
"CASSETTE B / side 1: Nadir explains backups to a child using rice grains and a spoon.",
"CASSETTE C / side 2: A song with no words, hummed badly, loved completely.",
"CASSETTE D / side ?: The tape label only says for when the house feels far away."
],
fakeUsers: [
"sunnydialup88: first!!!! but emotionally",
"auntie-admin: please stop uploading files named newnewnewfinal",
"railway_morrow: platform 3 smells like rain today",
"nettle_mod: moved thread from Bugs to Feelings",
"luma-lamp: does anyone else hear the margins singing",
"guest_000: I signed this once and then grew up"
],
seasonal: {
winter: "The archive put an extra blanket on the footer.",
spring: "A pressed flower appeared between two cached pages.",
summer: "The page opened a window and let the fan turn the silence.",
autumn: "The old links sound like leaves when the cursor passes."
},
homepageTakeovers: [
"For one rare minute, the homepage believes it is an old family desktop.",
"The dashboard has become a late-night noticeboard with one pin missing.",
"The homepage found its old forum skin in a drawer and is trying it on."
],
roomLinks: [
["/play/radio-room.html", "radio room"],
["/play/guestbook-1999.html", "guestbook 1999"],
["/play/do-not-open.html", "do not open"],
["/play/cassette-log.html", "cassette log"],
["/play/train-platform.html", "train platform"],
["/play/forgotten-draft.html", "forgotten draft"],
["/play/corrupted-recipe.html", "corrupted recipe"],
["/play/terminal-cupboard.html", "terminal cupboard"],
["/play/patience-game.html", "patience game"]
]
};
function readState() { function readState() {
try { try {
return JSON.parse(localStorage.getItem(KEY) || "{}"); return JSON.parse(localStorage.getItem(KEY) || "{}");
@@ -186,10 +297,13 @@
function rememberVisit() { function rememberVisit() {
const visits = (state.visits || 0) + 1; const visits = (state.visits || 0) + 1;
const seen = Array.isArray(state.seen) ? state.seen : []; const seen = Array.isArray(state.seen) ? state.seen : [];
const pathCounts = { ...(state.pathCounts || {}) };
pathCounts[path] = (pathCounts[path] || 0) + 1;
const next = { const next = {
...state, ...state,
visits, visits,
lastPath: path, lastPath: path,
pathCounts,
seen: Array.from(new Set([...seen, path])).slice(-80), seen: Array.from(new Set([...seen, path])).slice(-80),
firstSeen: state.firstSeen || new Date().toISOString(), firstSeen: state.firstSeen || new Date().toISOString(),
lastSeen: new Date().toISOString() lastSeen: new Date().toISOString()
@@ -326,7 +440,8 @@
[ [
["/play/left-behind.html", "left behind"], ["/play/left-behind.html", "left behind"],
["/play/dream-corridor.html", "dream corridor"], ["/play/dream-corridor.html", "dream corridor"],
["/play/kitchen-light.html", "kitchen light"] ["/play/kitchen-light.html", "kitchen light"],
...secondLayer.roomLinks
].forEach(([href, label], index) => { ].forEach(([href, label], index) => {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "hidden-secret-link"; link.className = "hidden-secret-link";
@@ -339,10 +454,289 @@
function enhanceErrors() { function enhanceErrors() {
if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) { if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) {
toast("This page is missing, but you are not. Try the sitemap, or sit here a second."); toast(pick([
"This page is missing, but you are not. Try the sitemap, or sit here a second.",
"404: the page went to check on the rice and has not returned.",
"The address is wrong, but the welcome is still valid.",
"Nothing is here except a very apologetic lamp."
], "error"));
} }
} }
function addContinuity(memory) {
const milestones = {
2: "The site has placed your second visit beside the spare keys.",
5: "A quiet ledger now lists you under people who came back.",
9: "Nettle has started a folder called regular visitors, handle gently.",
17: "Morrow left a platform ticket in the footer for you.",
31: "The archive trusts you enough to misplace something important nearby.",
64: "A very old counter turned over and made the sound of a cupboard closing."
};
if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) {
window.setTimeout(() => toast(milestones[memory.visits], 6500), 1200);
writeState({ ...readState(), [`milestone_${memory.visits}`]: true });
}
const count = memory.pathCounts?.[path] || 0;
if ([3, 7, 12].includes(count)) {
window.setTimeout(() => toast([
"This page recognizes the shape of your return.",
"The paragraph straightened the cushions before you arrived.",
"A repeat visit is the gentlest kind of knock."
][[3, 7, 12].indexOf(count)], 6200), 1700);
}
}
function addSeasonAndDates(memory) {
const month = now.getMonth();
const day = now.getDate();
const season = month < 2 || month === 11 ? "winter" : month < 5 ? "spring" : month < 8 ? "summer" : "autumn";
if (Math.random() < 0.18) {
window.setTimeout(() => toast(secondLayer.seasonal[season], 5200), 2600);
}
if (month === 4 && day === 9) {
window.setTimeout(() => toast("Site birthday: the candles are imaginary, but the crumbs are real."), 900);
}
const first = memory.firstSeen ? new Date(memory.firstSeen) : null;
if (first && memory.visits > 1 && first.getMonth() === month && first.getDate() === day) {
window.setTimeout(() => toast("Visitor anniversary: the site saved a paper flower from your first day here.", 7000), 1400);
}
if (now.getMinutes() === 13) {
window.setTimeout(() => toast("At minute thirteen, the radio under the stairs clears its throat."), 2100);
}
}
function addObjectConstellation(memory) {
const rail = document.createElement("div");
rail.className = "hidden-object-rail";
rail.setAttribute("aria-label", "Hidden keepsake objects");
const objects = [
["lamp", "The lamp clicks once, then pretends it did not."],
["mug", "The mug is chipped in exactly the familiar place."],
["ticket", "The ticket says RETURN, but not to where."],
["radio", "The radio hums between weather and memory."],
["flower", "The pressed flower has kept its appointment with spring."]
];
objects.forEach(([name, message]) => {
const button = document.createElement("button");
button.type = "button";
button.className = `hidden-keepsake hidden-keepsake--${name}`;
button.title = name;
button.textContent = { lamp: "i", mug: "u", ticket: "=", radio: "o", flower: "+" }[name];
button.addEventListener("click", () => {
const next = readState();
next.keepsakes = Array.from(new Set([...(next.keepsakes || []), name]));
writeState(next);
toast(message);
if (next.keepsakes.length >= objects.length) {
window.setTimeout(() => toast("The five keepsakes align. Search for radio under stairs."), 1100);
}
});
rail.appendChild(button);
});
document.body.appendChild(rail);
}
function addWeatherWindow(memory) {
if (memory.visits < 3 || !("geolocation" in navigator)) return;
const button = document.createElement("button");
button.className = "hidden-weather-window";
button.type = "button";
button.title = "Ask the outside window";
button.textContent = "window";
button.addEventListener("click", () => {
toast("The window is checking whether the outside agrees with the archive.");
navigator.geolocation.getCurrentPosition((pos) => {
const { latitude, longitude } = pos.coords;
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude.toFixed(3)}&longitude=${longitude.toFixed(3)}&current=temperature_2m,precipitation,weather_code&timezone=auto`)
.then((res) => res.json())
.then((data) => {
const current = data.current || {};
const rain = Number(current.precipitation || 0) > 0;
const temp = Math.round(Number(current.temperature_2m));
toast(rain ? `Outside: rain. Inside: the archive moved a towel closer to the door. ${temp}C.` : `Outside: ${temp}C. Inside: possible comfort, mild nostalgia.`);
})
.catch(() => toast("The weather window fogged up, so the site invented indoor weather: quiet with a chance of tea."));
}, () => toast("No outside weather today. The indoor forecast is lamp-light and manageable clouds."));
});
document.body.appendChild(button);
}
function addOldWebLayer(memory) {
if (Math.random() < 0.08 || memory.visits > 10) {
const stamp = document.createElement("div");
stamp.className = "hidden-oldweb-stamp";
stamp.title = pick(secondLayer.fakeUsers, "fake-user");
stamp.textContent = "best viewed with patience";
stamp.addEventListener("click", () => toast(stamp.title));
document.body.appendChild(stamp);
}
const comments = document.querySelector("#comments");
if (comments && !comments.querySelector(".hidden-guestbook-line")) {
const line = document.createElement("p");
line.className = "hidden-guestbook-line";
line.textContent = pick(secondLayer.fakeUsers, "comment-lore");
comments.appendChild(line);
}
}
function addSourceRelics() {
// RELIC: Aunt Safiya says source comments are the attic of the page.
const marker = document.createComment("hidden-song: hum the lamp, hum the train, hum the long way home");
document.documentElement.appendChild(marker);
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
if (index > 2) return;
img.alt = pick([
"A quiet image that remembers being taped into a scrapbook.",
"An old visual keepsake, labelled by someone who arrived later.",
"A picture with its shoes by the door."
], `alt-${index}`);
});
}
function addSearchAndKeySecrets() {
const search = document.querySelector("#search-input");
if (search) {
search.addEventListener("input", () => {
const value = search.value.trim().toLowerCase();
const routes = {
"radio under stairs": "/play/radio-room.html",
"guestbook 1999": "/play/guestbook-1999.html",
"do not open": "/play/do-not-open.html",
"cassette": "/play/cassette-log.html",
"return ticket": "/play/train-platform.html",
"unfinished letter": "/play/forgotten-draft.html",
"burnt sugar": "/play/corrupted-recipe.html",
"cupboard": "/play/terminal-cupboard.html",
"patience": "/play/patience-game.html"
};
if (routes[value]) window.location.href = routes[value];
if (value === "marigold") toast("Search result: pressed flower, May rain, a promise nobody announced.");
if (value === "luma") toast("Search result: lamp-child, margin-song, brave small glow.");
if (value === "nadir") toast("Search result: backups, train tickets, practical tenderness.");
});
}
document.addEventListener("keydown", (event) => {
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-18);
sessionStorage.setItem("zxh_second_chain", chain);
if (chain.endsWith("leave the light on")) window.location.href = "/play/kitchen-light.html";
if (chain.endsWith("oldweb")) toast("A 2004 table layout briefly considers returning, then thinks better of it.");
if (chain.endsWith("safiya")) toast("Aunt Safiya says the screwdriver is absolutely not in the biscuit tin.");
if (chain.endsWith("luma")) playTinySong();
});
}
function addPatienceRewards(memory) {
if (memory.visits < 2) return;
let idleTimer = null;
const startIdle = () => {
window.clearTimeout(idleTimer);
idleTimer = window.setTimeout(() => {
toast(pick([
"Thank you for waiting. The page used the time to remember a window.",
"Patience noted. No badge will be shown, which is how the archive prefers it.",
"A tiny secret achievement happened quietly and did not ask to be announced."
], "idle"), 7000);
const next = readState();
next.patientMoments = (next.patientMoments || 0) + 1;
writeState(next);
}, 90000);
};
["mousemove", "keydown", "scroll", "click"].forEach((name) => document.addEventListener(name, startIdle, { passive: true }));
startIdle();
}
function addRareSecondLayer(memory) {
const roll = Math.random();
if ((path === "/" || path === "/index.html") && roll < 0.006) {
document.body.classList.add("hidden-home-takeover");
window.setTimeout(() => showDriftNote(pick(secondLayer.homepageTakeovers, "takeover"), "WELCOME_BACK.GIF\nUNDER_CONSTRUCTION\nSIGN_GUESTBOOK"), 600);
} else if (roll < 0.002) {
window.setTimeout(() => showDriftNote("A cassette starts by itself, then politely asks permission.", "play? y/n\n[no autoplay. the site has manners.]"), 1500);
} else if (roll < 0.006) {
window.setTimeout(() => toast(pick(secondLayer.dreams, "rare-dream"), 7000), 2000);
} else if (roll < 0.014) {
window.setTimeout(() => toast(pick(secondLayer.warnings, "rare-warning"), 6200), 2300);
}
}
function addPageSpecificSecrets(memory) {
if (path.includes("/play/cassette-log")) {
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
button.addEventListener("click", () => {
toast(secondLayer.cassettes[index % secondLayer.cassettes.length], 7000);
playTinySong();
});
});
}
if (path.includes("/play/patience-game")) {
const target = document.querySelector("[data-patience-target]");
const count = document.querySelector("[data-patience-count]");
if (target && count) {
let seconds = 0;
setInterval(() => {
seconds += 1;
count.textContent = String(seconds);
if ([30, 90, 180].includes(seconds)) {
target.textContent = seconds === 30 ? "The lamp notices you are still here." : seconds === 90 ? "The train has not left without you." : "The room trusts your quiet.";
}
}, 1000);
}
}
if (path.includes("/play/terminal-cupboard")) {
const input = document.querySelector("[data-cupboard-input]");
const log = document.querySelector("[data-cupboard-log]");
const commands = {
help: "commands: listen, inventory, tea, open, remember, exit",
listen: "Behind the jars: a radio, rain, one sentence from 2003.",
inventory: "one cracked mug; two return tickets; marigolds; screwdriver absent, allegedly.",
tea: "The cupboard approves. Morale +1. Biscuit tin remains suspicious.",
open: "The cupboard opens into another cupboard. This is a family tradition.",
remember: "You remember a room you may never have visited.",
exit: "The cupboard lets you leave with both hands full of nothing heavy."
};
input?.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
const value = input.value.trim().toLowerCase();
const line = document.createElement("p");
line.textContent = `> ${value}\n${commands[value] || "The cupboard does not know that command, but respects the attempt."}`;
log.appendChild(line);
input.value = "";
});
}
}
function addInvisibleHoverSecrets() {
document.querySelectorAll("h1, h2").forEach((heading, index) => {
if (index > 5) return;
heading.classList.add("hidden-hover-memory");
heading.dataset.hiddenMemory = pick([...secondLayer.quotes, ...secondLayer.poems], `heading-${index}`);
});
}
function playTinySong() {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const notes = [392, 440, 330, 294, 392];
notes.forEach((freq, index) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = freq;
osc.type = "sine";
gain.gain.setValueAtTime(0.0001, ctx.currentTime + index * 0.18);
gain.gain.exponentialRampToValueAtTime(0.05, ctx.currentTime + index * 0.18 + 0.03);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + index * 0.18 + 0.16);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime + index * 0.18);
osc.stop(ctx.currentTime + index * 0.18 + 0.18);
});
} catch (_) {}
}
function escapeHtml(value) { function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({ return String(value).replace(/[&<>"']/g, (char) => ({
"&": "&amp;", "&": "&amp;",
@@ -363,5 +757,16 @@
addRareEvents(); addRareEvents();
addSecretLinks(); addSecretLinks();
enhanceErrors(); enhanceErrors();
addContinuity(memory);
addSeasonAndDates(memory);
addObjectConstellation(memory);
addWeatherWindow(memory);
addOldWebLayer(memory);
addSourceRelics();
addSearchAndKeySecrets();
addPatienceRewards(memory);
addRareSecondLayer(memory);
addPageSpecificSecrets(memory);
addInvisibleHoverSecrets();
}); });
}()); }());

View File

@@ -108,6 +108,143 @@
margin-top: 0.8rem; margin-top: 0.8rem;
} }
.hidden-object-rail {
position: fixed;
left: 50%;
bottom: 0.35rem;
display: flex;
gap: 0.3rem;
transform: translateX(-50%);
opacity: 0.18;
transition: opacity 180ms ease;
z-index: 20;
}
.hidden-object-rail:hover,
.hidden-object-rail:focus-within {
opacity: 0.94;
}
.hidden-keepsake {
width: 1.35rem;
height: 1.35rem;
border: 1px solid color-mix(in oklab, var(--border) 72%, transparent);
border-radius: 50%;
background: color-mix(in oklab, var(--surface) 78%, transparent);
color: var(--muted);
font: 0.8rem/1 monospace;
cursor: pointer;
}
.hidden-keepsake:hover,
.hidden-keepsake:focus-visible {
color: var(--fg);
background: var(--surface);
}
.hidden-weather-window {
position: fixed;
right: 0.7rem;
top: 7.5rem;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in oklab, var(--surface) 84%, transparent);
color: var(--muted);
font-size: 0.72rem;
padding: 0.25rem 0.45rem;
opacity: 0.24;
z-index: 20;
}
.hidden-weather-window:hover,
.hidden-weather-window:focus-visible {
opacity: 1;
color: var(--fg);
}
.hidden-oldweb-stamp {
position: fixed;
right: 0.75rem;
bottom: 4.2rem;
max-width: 12rem;
padding: 0.25rem 0.4rem;
border: 1px dashed var(--border);
background: var(--surface);
color: var(--muted);
font: 0.72rem/1.2 "Times New Roman", serif;
text-align: center;
opacity: 0.34;
cursor: help;
z-index: 20;
}
.hidden-oldweb-stamp:hover {
opacity: 1;
}
.hidden-guestbook-line {
color: var(--muted);
font-size: 0.86rem;
font-family: "Times New Roman", serif;
opacity: 0.78;
}
.hidden-hover-memory {
position: relative;
}
.hidden-hover-memory:hover::after {
content: attr(data-hidden-memory);
position: absolute;
left: 0;
top: 100%;
width: min(28rem, 80vw);
margin-top: 0.25rem;
padding: 0.55rem 0.65rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--muted);
font-size: 0.78rem;
font-family: 'Noto', system-ui, sans-serif;
font-weight: 400;
line-height: 1.35;
z-index: 30;
box-shadow: 0 12px 28px color-mix(in oklab, #000 12%, transparent);
}
body.hidden-home-takeover .db-root {
filter: saturate(0.9) contrast(1.03);
}
body.hidden-home-takeover .db-hero::before {
content: "under construction, emotionally";
display: inline-block;
margin-bottom: 0.5rem;
color: var(--muted);
font: 0.82rem "Times New Roman", serif;
}
.hidden-terminal {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.hidden-terminal input {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--code-bg);
color: var(--fg);
padding: 0.65rem;
font: inherit;
}
.hidden-terminal-log {
min-height: 10rem;
white-space: pre-wrap;
}
.hidden-secret-link { .hidden-secret-link {
position: absolute; position: absolute;
width: 1px; width: 1px;

444
docs/hidden-soul-bible.md Normal file
View File

@@ -0,0 +1,444 @@
# Hidden Soul Bible
## 1. Creative Pillars
The hidden layer should feel like an emotional internet artifact: a personal site that has been lived in, repaired, renamed, backed up, forgotten, and returned to. It should reward attention without demanding completion.
Primary emotional tones:
- Cozy familial: kitchen lights, rice, mugs, family notes, practical tenderness.
- Funny and self-aware: fake system warnings, old-web jokes, suspicious biscuit tins, web development folklore.
- Reflective melancholy: unfinished letters, old usernames, returning to changed rooms.
- Gentle mystery: Morrow, Nettle, Luma, Etta, Aunt Safiya, Nadir, the radio, the train platform.
- Internet nostalgia: guestbooks, counters, dial-up usernames, under construction language, source-code relics.
## 2. Recurring Cast
- Morrow: a quiet figure tied to trains, platforms, lanterns, and unfinished apologies. Morrow represents return, hesitation, and the comfort of being expected.
- Nettle: the archivist/moderator figure. Files impossible feelings under practical labels. Carries a clipboard and treats emotional continuity as infrastructure.
- Luma: a lamp-associated presence who hums original tiny songs and notices margins. Luma represents small courage.
- Etta: writer of the "Do Not Open" letter. Represents vulnerability hidden behind instructions and labels.
- Aunt Safiya: warm, funny, practical family presence. Connected to recipes, suspicious biscuit tins, and household wisdom.
- Nadir: backup-minded family historian. Explains technical care through domestic metaphors.
- Old Guest 14 / guest_000 / sunnydialup88: old-web guestbook presences who blur past visitors, usernames, and memory.
## 3. Symbolic Objects
- Lamp: patience, late-night comfort, permission to rest.
- Mug: continuity, small damage kept because it belongs.
- Return ticket: coming back without needing to explain where from.
- Radio: almost-heard family memory, voice messages, weather, original hums.
- Pressed flower / marigold: seasonal return, promises that do not announce themselves.
- Biscuit tin: running joke; allegedly contains tools, feelings, or neither.
- Train platform: delay, return, growing older, being expected.
- Cupboard: hidden terminal, family storage, recursive domestic mystery.
## 4. Implemented Global Systems
### Footer Notes
- Trigger: every page load with a footer.
- Content: rotates through 102 first-layer messages plus new second-layer continuity.
- Frequency: always visible but subtle.
- Lore: the footer is the house threshold.
- UX: muted centered text; should feel like a small afterthought.
### Homepage Greetings
- Trigger: homepage load.
- Content: randomized greeting; repeat visitors receive memory-aware copy.
- Frequency: always on homepage.
- Lore: the site recognizes returning visitors locally through `localStorage`.
### Hover Whispers
- Trigger: some paragraphs and headings gain tiny hover/click notes.
- Content: short poems, quotes, and second-layer hidden memories.
- Frequency: deterministic per page; not every paragraph.
- UX: dotted question marks and heading hover captions.
### Corner Object
- Trigger: click the small fixed corner symbol.
- Sequence: five escalating messages, then seven clicks opens `/play/left-behind.html`.
- Lore: a loose panel in the page.
### Keepsake Constellation
- Trigger: click the five tiny bottom keepsakes: lamp, mug, ticket, radio, flower.
- Payoff: after all are clicked, the site suggests searching `radio under stairs`.
- Frequency: always present, low opacity.
- Lore: collecting ordinary objects aligns the hidden family memory.
### Weather Window
- Trigger: after at least three visits, a subtle `window` button appears. User click requests browser location.
- Content: weather-aware line through Open-Meteo if permission/network works; indoor-weather fallback otherwise.
- Frequency: repeat visitors only.
- UX: no automatic permission prompt.
### Visit Milestones
- Trigger: visit counts 2, 5, 9, 17, 31, 64.
- Content: progressively warmer messages about being remembered.
- Lore: the site stores visits locally, not identities.
### Page Revisit Memory
- Trigger: 3rd, 7th, and 12th visit to the same path.
- Content: page-specific return messages.
- Lore: individual rooms recognize repeated footsteps.
### Seasonal Content
- Trigger: low-probability seasonal toast on page load.
- Content: winter blanket, spring pressed flower, summer fan/window, autumn leaf-links.
- Frequency: 18% chance.
### Date Events
- Site birthday: May 9. Message about imaginary candles.
- Visitor anniversary: same month/day as first stored visit.
- Minute thirteen: radio throat-clearing message.
### Night Messages
- Trigger: 22:00-04:59 local browser time.
- Content: gentle late-night comfort.
- Visual: soft bottom glow.
### Search Secrets
- `morrow`: lantern/lake/apology.
- `nettle`: clipboard/cupboard/semicolon.
- `home`: warm window/spare key.
- `kitchen light`: opens `/play/kitchen-light.html`.
- `radio under stairs`: opens `/play/radio-room.html`.
- `guestbook 1999`: opens `/play/guestbook-1999.html`.
- `do not open`: opens `/play/do-not-open.html`.
- `cassette`: opens `/play/cassette-log.html`.
- `return ticket`: opens `/play/train-platform.html`.
- `unfinished letter`: opens `/play/forgotten-draft.html`.
- `burnt sugar`: opens `/play/corrupted-recipe.html`.
- `cupboard`: opens `/play/terminal-cupboard.html`.
- `patience`: opens `/play/patience-game.html`.
- `marigold`, `luma`, `nadir`: lore toasts.
### Typed Phrase Secrets
- `remember`: continuity toast.
- `dream`: opens Dream Corridor.
- `leave the light on`: opens Kitchen Light.
- `oldweb`: old layout joke.
- `safiya`: biscuit tin joke.
- `luma`: plays a tiny original tone sequence.
### Rare Events
- First layer: rare page breeze and biscuit event.
- Second layer:
- 0.6% rare homepage old-web takeover.
- 0.2% cassette asks permission.
- 0.4% recurring dream line.
- 0.8% cryptic comforting warning.
- Rule: rare events should delight, not block navigation.
### Patience Rewards
- Trigger: 90 seconds of inactivity after repeat visits.
- Content: quiet no-achievement reward.
- Lore: patience and kindness count even when not announced.
### Source Relics and Alt Text
- Trigger: DOM load.
- Behavior: adds a source-code comment and fills empty image alt text with scrapbook-like hidden descriptions.
- Purpose: secrets inside source and accessibility-friendly hidden storytelling.
### Audio
- Trigger: user action only, never autoplay.
- Content: short original Web Audio tone sequence associated with Luma/cassettes.
- Note: no copyrighted lyrics or melodies.
## 5. Implemented Secret Pages
### `/play/left-behind.html`
- Trigger: seven clicks on corner object.
- Tone: familial, emotional.
- Lore: blue bowl, soup memory, Morrow at 2:13.
### `/play/dream-corridor.html`
- Trigger: logo taps or typed `dream`.
- Tone: surreal, safe.
- Lore: three shy doors, Nettle margin note.
### `/play/kitchen-light.html`
- Trigger: search `kitchen light` or phrase `leave the light on`.
- Tone: late-night comfort.
- Lore: rice in pot, towel on chair, light asks no questions.
### `/play/radio-room.html`
- Trigger: search `radio under stairs`; keepsake constellation hints it.
- Tone: nostalgic audio memory.
- Lore: Nadir backups, Luma humming, no autoplay manners.
### `/play/guestbook-1999.html`
- Trigger: search `guestbook 1999`.
- Tone: old-web funny/melancholy.
- Lore: fake users are recurring ghosts of the old internet.
### `/play/do-not-open.html`
- Trigger: search `do not open`.
- Tone: emotionally meaningful warning.
- Lore: Etta wanted to be found carefully.
### `/play/cassette-log.html`
- Trigger: search `cassette`.
- Tone: archive/audio.
- Interaction: buttons reveal cassette transcript toasts and play original tones.
### `/play/train-platform.html`
- Trigger: search `return ticket`.
- Tone: patient return.
- Lore: platform 3, Morrow, destinations as feelings.
### `/play/forgotten-draft.html`
- Trigger: search `unfinished letter`.
- Tone: unsent vulnerability.
- Lore: website as a way to recover rooms in pieces.
### `/play/corrupted-recipe.html`
- Trigger: search `burnt sugar`.
- Tone: funny family warmth.
- Lore: recipe corrupted into folklore.
### `/play/terminal-cupboard.html`
- Trigger: search `cupboard`.
- Tone: hidden terminal, domestic mystery.
- Commands: `help`, `listen`, `inventory`, `tea`, `open`, `remember`, `exit`.
### `/play/patience-game.html`
- Trigger: search `patience`.
- Tone: quiet game.
- Interaction: wait. Text changes at 30, 90, and 180 seconds.
## 6. Emotional Tone Categories
- Comfort: Kitchen Light, night messages, visitor milestones, patience rewards.
- Humor: biscuit tin, fake users, old-web jokes, fake errors.
- Melancholy: Forgotten Draft, train platform, cassettes, old usernames.
- Mystery: Dream Corridor, radio, cupboard, rare dreams.
- Familial warmth: corrupted recipe, Aunt Safiya, Nadir, blue bowl.
- Meta/web: source relics, under construction takeover, cache warnings.
## 7. Page/Section Categories
- Global: footer notes, toasts, hover whispers, keepsakes, weather window, source comments.
- Homepage: greetings, rare old-web takeover, logo taps.
- Search/nav: keyword portals.
- Play wing: secret rooms and tiny games.
- Comments: fake guestbook line when comments exist.
- Images/source: hidden alt text and source-code comment.
## 8. UI/UX Notes
- Hidden UI stays low opacity until hovered/focused.
- No feature should obscure reading for long.
- Audio requires explicit action.
- Weather requests require explicit click and have graceful fallback.
- Secret pages are buildable but marked `#+HIDDEN_PAGE: t` so they do not appear in index pages.
- Rewards are intentionally non-competitive; no public checklist.
## 9. Audio/Visual Suggestions
- Add tiny original ambient loops only after explicit button press.
- Use cassette hiss, lamp hum, rain-on-window synthesis, and soft train-platform chimes.
- Consider small generated bitmap textures: paper receipt, cassette label, pressed flower, old guestbook banner.
- Avoid horror stingers; even corrupted pages should be emotionally safe.
## 10. Long-Term Evolution
- Monthly train destination changes.
- Seasonal recipe variants.
- Yearly visitor anniversary letters.
- A slow Luma arc where the lamp becomes less shy after enough patient moments.
- Nettle's filing labels can become warmer over time.
- Morrow's return ticket can gain stamps after visits to secret rooms.
- Guestbook users can post new fake entries on real calendar dates.
## 11. Future Expansion Ideas
- Add a real local-only guestbook draft pad that stores messages in `localStorage`.
- Add a hidden "attic" page unlocked after visiting all secret rooms.
- Add generated cassette-label images for each log.
- Add a no-score cooking memory game using family recipe fragments.
- Add an RSS-feed-only hidden note.
- Add source-map comments or HTML comments with letter fragments.
- Add a "kindness route" where closing pages gently or waiting unlocks softer endings.
## 12. Maintaining Mystery
- Do not explain all triggers in the public UI.
- Keep most secrets optional and small.
- Never punish users for missing content.
- Use recurrence, not randomness alone: objects and characters should reappear with altered context.
- Make clues emotionally legible, not puzzle-hostile.
- Avoid completion language. The site is a place, not a checklist.
## 13. Implementation Notes
- Current state is client-side only through `localStorage` key `zxh_hidden_details_v1`.
- Secret routes are static Org pages.
- `#+HIDDEN_PAGE: t` excludes pages from sitemap/recently-updated via `lisp/sitemaps.el` and `lisp/recently-updated.el`.
- Add backend later only if real guestbook/comment persistence is desired.
- Keep hidden scripts dependency-free.
## 14. Canon Rule
The hidden soul should always imply: somebody lived here, somebody cared enough to label the drawer badly, and the visitor is welcome without needing to prove they found everything.
## 15. Second-Layer Text Registry
These are the newly added second-layer strings. The first-layer registry remains in `assets/scripts/hidden-details.js` as the original 102-detail foundation.
### Quotes
- The archive does not keep score. It keeps chairs.
- Some families pass down recipes. This one also passes down tabs left open for later.
- A page can be a room if enough people arrive softly.
- The oldest link on the site still expects a hand cursor.
- If a memory changes, it may be trying to fit through a smaller door.
- The guestbook is not empty. It is holding its breath between signatures.
- Every backup is a small superstition against forgetting.
- Somewhere in the CSS, a lamp is deciding whether to stay on.
- The radio under the stairs only plays songs it has already missed.
- A train can leave a station and still remain in the family story.
- Kindness is the only password the house accepts without checking case.
- The website learned your pace and stopped rushing the hallway.
- A draft is just a letter still gathering courage.
- The cursor is a visitor. The margin is a host.
- If you hover long enough, even a heading may remember being handwritten.
- The old forum banner says: welcome back, even if you are new.
- Not all broken pages need repair. Some need a name.
- The site keeps its mysteries low to the ground, where tired people can reach them.
- A year is a folder that keeps renaming itself.
- Some evenings are survived by the exact right mug.
### Poems
- A train in the rain / waits beside an empty platform / so memory can board.
- Grandma's radio / tuned between two stations / found us anyway.
- A flower pressed flat / still teaches the book / how spring once leaned.
- The lamp stayed awake / not because it was brave / but because someone might need it.
- Old usernames fade / but the welcome thread remains / warm at the top.
- I found a draft / with no ending / and felt less alone.
- The house at midnight / is mostly refrigerator hum / and unsent kindness.
- A calendar page / falls without bitterness / into tomorrow's hands.
- There is a road home / made entirely of small tabs / you meant to close.
- A cassette clicks / and the room becomes younger / for one more minute.
### Fake Conversations
- Nettle: The ticket drawer is humming again. Morrow: That is normal. It hums when someone is almost ready.
- Aunt Safiya: Who put the screwdriver in the biscuit tin? Nadir: A person with vision and no labelling discipline.
- Morrow: The lamp keeps following the visitor. Etta: Good. Lamps should have hobbies.
- Old Guest 14: is this page still alive? Webmaster: yes, but quietly, please. the baby is asleep.
- Nettle: Filed under: feelings disguised as infrastructure. Morrow: Approved without changes.
- Luma: The radio said my name. Aunt Safiya: Then say salaam back. Manners matter.
- Nadir: The server made soup noises. Etta: That means the deployment is nearly done.
- Morrow: I found a page that says do not open. Nettle: That usually means open gently.
### Journal Fragments
- Journal scrap, 2006: We changed the wallpaper and everyone pretended it was not emotional.
- Journal scrap, 2011: The old router blinked like it was trying to stay in the conversation.
- Journal scrap, 2018: I learned that homesickness can happen inside your own house.
- Journal scrap, 2024: Somebody laughed in the kitchen and the whole week became easier.
- Journal scrap, undated: The marigolds came back where nobody planted them.
- Journal scrap, undated: We kept the cracked mug because it knew too much.
- Journal scrap, winter: I saved a voicemail just to remember the background noise.
- Journal scrap, summer: The fan turned all afternoon and made the curtains look like boats.
### Comforting Warnings
- COMFORT WARNING: this page may contain traces of old carpet, warm rice, and unfinished errands.
- SOFT ERROR: the archive attempted to be impressive and became sincere instead.
- NOTICE: do not stack heavy feelings on the top shelf.
- LEGACY WARNING: this layout remembers 800x600 and asks you to be respectful.
- SYSTEM MESSAGE: no achievement unlocked. You were kind anyway.
- CACHE NOTE: the page kept a copy of the good part.
- RECOVERY MODE: make tea, rename file, try again.
- PERMISSION DENIED: the sadness may enter, but it must wipe its feet.
### Recurring Dreams
- Dream 01: a train full of empty mugs waited at a station called Later.
- Dream 02: the search box became a letterbox and returned every question with a family recipe.
- Dream 03: a radio under the stairs played tomorrow's weather in a grandparent's voice.
- Dream 04: the old homepage had a porch, and every link was a moth-wing-soft knock.
- Dream 05: someone had folded the sitemap into a paper flower and left it in a browser tab.
### Cassette Logs
- CASSETTE A / side 1: You can hear plates, rain, and someone saying, no, leave the light on.
- CASSETTE A / side 2: Three seconds of silence, then laughter too close to the microphone.
- CASSETTE B / side 1: Nadir explains backups to a child using rice grains and a spoon.
- CASSETTE C / side 2: A song with no words, hummed badly, loved completely.
- CASSETTE D / side ?: The tape label only says for when the house feels far away.
### Fake Users
- sunnydialup88: first!!!! but emotionally
- auntie-admin: please stop uploading files named newnewnewfinal
- railway_morrow: platform 3 smells like rain today
- nettle_mod: moved thread from Bugs to Feelings
- luma-lamp: does anyone else hear the margins singing
- guest_000: I signed this once and then grew up
### Rare Homepage Takeovers
- For one rare minute, the homepage believes it is an old family desktop.
- The dashboard has become a late-night noticeboard with one pin missing.
- The homepage found its old forum skin in a drawer and is trying it on.
### Seasonal Lines
- Winter: The archive put an extra blanket on the footer.
- Spring: A pressed flower appeared between two cached pages.
- Summer: The page opened a window and let the fan turn the silence.
- Autumn: The old links sound like leaves when the cursor passes.
### Secret Room Search Terms
- `radio under stairs` -> Radio Room.
- `guestbook 1999` -> Guestbook 1999.
- `do not open` -> Do Not Open.
- `cassette` -> Cassette Log.
- `return ticket` -> Train Platform.
- `unfinished letter` -> Forgotten Draft.
- `burnt sugar` -> Corrupted Recipe.
- `cupboard` -> Terminal Cupboard.
- `patience` -> Patience Game.
## 16. Maintenance Checklist
- When adding a new hidden page, include `#+HIDDEN_PAGE: t`.
- Add the route to the invisible link registry only if accidental discovery is acceptable.
- Add one lore connection, one emotional reason, and one non-obvious trigger.
- Keep text short in toasts; move longer writing into hidden rooms.
- Prefer local state and static pages. Add backend persistence only for explicitly public features.
- Rebuild with `emacs -Q --script build-site.el`.
- Verify secret pages build and do not appear in `sitemap.org` or `recently-updated.org`.

25
play/cassette-log.org Normal file
View File

@@ -0,0 +1,25 @@
#+TITLE: Cassette Log
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Audio shelf</p>
<h1>Cassette Log</h1>
<p class="play-lede">Transcripts from tapes with labels in three different handwritings.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<button class="play-primary" type="button" data-cassette="a">Play A</button>
<button class="play-primary" type="button" data-cassette="b">Play B</button>
<button class="play-primary" type="button" data-cassette="c">Play C</button>
<p>The buttons play tiny original tones and reveal transcripts. Nothing starts without being asked.</p>
</div>
</section>
</main>
#+END_EXPORT

26
play/corrupted-recipe.org Normal file
View File

@@ -0,0 +1,26 @@
#+TITLE: Corrupted Recipe
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Recovered file</p>
<h1>Corrupted Recipe</h1>
<p class="play-lede">The ingredients survived. The measurements became folklore.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p>1 onion, finely remembered</p>
<p>2 cups rice, or enough for whoever says they are not hungry</p>
<p>burnt sugar until the kitchen smells like almost</p>
<p>[DATA LOST] but Aunt Safiya says add patience here</p>
<p>Serve with the story about the train, but skip the sad part unless someone asks.</p>
</div>
</section>
</main>
#+END_EXPORT

25
play/do-not-open.org Normal file
View File

@@ -0,0 +1,25 @@
#+TITLE: Do Not Open
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Envelope</p>
<h1>Do Not Open</h1>
<p class="play-lede">The instruction was written by someone who hoped you would be careful, not obedient.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p>Dear whoever needed a door,</p>
<p>I wrote "do not open" because I was embarrassed by how much I wanted to be found.</p>
<p>If you are here, please take the small ordinary blessing: may the next room be kinder than the last one.</p>
<p>- Etta</p>
</div>
</section>
</main>
#+END_EXPORT

24
play/forgotten-draft.org Normal file
View File

@@ -0,0 +1,24 @@
#+TITLE: Forgotten Draft
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Unsent</p>
<h1>Forgotten Draft</h1>
<p class="play-lede">A letter that never became brave enough to be email.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p>I keep meaning to say that the house was not the same after everyone stopped being in the same room at once.</p>
<p>But then someone sends a photo of dinner, or a broken chair, or a sky that looks like 2007, and the room comes back in pieces.</p>
<p>I suppose this is what the website is for.</p>
</div>
</section>
</main>
#+END_EXPORT

26
play/guestbook-1999.org Normal file
View File

@@ -0,0 +1,26 @@
#+TITLE: Guestbook 1999
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Old web room</p>
<h1>Guestbook 1999</h1>
<p class="play-lede">Signatures from usernames that may never have existed.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p><strong>sunnydialup88:</strong> first!!!! but emotionally</p>
<p><strong>auntie-admin:</strong> stop making the background tiled unless you mean it.</p>
<p><strong>guest_000:</strong> I was here once, then became older, then came back.</p>
<p><strong>nettle_mod:</strong> thread moved from Bugs to Feelings.</p>
<p><strong>webmaster:</strong> if the counter breaks, count kindness manually.</p>
</div>
</section>
</main>
#+END_EXPORT

23
play/patience-game.org Normal file
View File

@@ -0,0 +1,23 @@
#+TITLE: Patience Game
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Tiny game</p>
<h1>Patience Game</h1>
<p class="play-lede">The only move is to stay a little while.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p data-patience-target>The lamp is on. Nothing else is required.</p>
<p><span data-patience-count>0</span> seconds kept company.</p>
</div>
</section>
</main>
#+END_EXPORT

27
play/radio-room.org Normal file
View File

@@ -0,0 +1,27 @@
#+TITLE: Radio Room
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Unlisted room</p>
<h1>Radio Room</h1>
<p class="play-lede">A room under the stairs where every station is almost home.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p class="play-console__eyebrow">Tuning log</p>
<h2>Between stations</h2>
<p>89.1: rain on the kitchen roof.</p>
<p>91.4: Nadir saying backups are just promises with timestamps.</p>
<p>99.9: Luma humming an original song about lamps, trains, and coming back slowly.</p>
<p>The dial is warm. It does not play automatically because even ghosts should ask permission.</p>
</div>
</section>
</main>
#+END_EXPORT

View File

@@ -0,0 +1,23 @@
#+TITLE: Terminal Cupboard
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Hidden terminal</p>
<h1>Terminal Cupboard</h1>
<p class="play-lede">A command line behind the jars.</p>
</div>
</section>
<section class="play-console hidden-terminal">
<div class="play-console__screen">
<div class="hidden-terminal-log" data-cupboard-log><p>cupboard v0.9. Type help.</p></div>
<input data-cupboard-input type="text" autocomplete="off" spellcheck="false" aria-label="Cupboard command" />
</div>
</section>
</main>
#+END_EXPORT

25
play/train-platform.org Normal file
View File

@@ -0,0 +1,25 @@
#+TITLE: Train Platform
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-09 Sat>
#+BEGIN_EXPORT html
<main class="play-root">
<section class="play-hero">
<div>
<p class="play-kicker">Platform 3</p>
<h1>Train Platform</h1>
<p class="play-lede">The departure board changes every month, but every destination is a feeling.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p>23:13 - Later - delayed by tenderness</p>
<p>00:04 - Home - expected when ready</p>
<p>06:30 - The Long Way - boarding quietly</p>
<p>Morrow is on the bench with a return ticket he refuses to explain.</p>
</div>
</section>
</main>
#+END_EXPORT