gamesssssssss
All checks were successful
Build Org Website / build (push) Successful in 52s

This commit is contained in:
2026-05-10 23:32:09 +01:00
parent 669eafadd3
commit 50399c2045
7 changed files with 761 additions and 1 deletions

View File

@@ -9615,6 +9615,165 @@
"thematicLinks": [],
"symbolicLinks": [],
"triggerLinks": []
},
{
"id": "search-route-gentle-constellarium",
"type": "search route",
"title": "Route: gentle constellarium",
"content": "/play/gentle-constellarium.html",
"characters": [
"lima",
"aphy",
"sensei chi"
],
"emotionalTone": "soft",
"rarity": "rare",
"triggerConditions": "gentle constellarium",
"tags": [
"observatory",
"thread",
"constellation"
],
"category": "search",
"pageLocation": "/play/gentle-constellarium.html",
"familyLayer": "2",
"enabled": true,
"createdDate": "2026-05-10",
"modifiedDate": "2026-05-10",
"notes": "Hidden entry into the constellation restoration ritual.",
"audioSettings": "",
"animationTrigger": "observatory-sky-soften",
"cssClassHooks": "observatory-ritual gentle-constellarium",
"chainReferences": [],
"continuationLinks": [],
"emotionalRole": "restoration",
"discoveryDifficulty": "gentle",
"mysteryLevel": "quiet",
"resonanceScore": 7,
"symbols": [
"thread",
"star",
"ring"
],
"narrativeArcs": [
"observatory rituals",
"emotional constellations"
],
"parentLinks": [],
"childLinks": [],
"echoes": [],
"mirroredEntries": [],
"thematicLinks": [],
"symbolicLinks": [],
"triggerLinks": []
},
{
"id": "search-route-future-z-return-log",
"type": "search route",
"title": "Route: future z return log",
"content": "/play/future-z-return-log.html",
"characters": [
"future z",
"sensei chi",
"lima"
],
"emotionalTone": "nostalgic",
"rarity": "timed",
"triggerConditions": "future z return log, return log, revisit",
"tags": [
"observatory",
"time",
"return"
],
"category": "search",
"pageLocation": "/play/future-z-return-log.html",
"familyLayer": "4",
"enabled": true,
"createdDate": "2026-05-10",
"modifiedDate": "2026-05-10",
"notes": "Hidden route for revisit-aware future z annotations.",
"audioSettings": "",
"animationTrigger": "temporal-echo",
"cssClassHooks": "observatory-ritual future-return-log",
"chainReferences": [],
"continuationLinks": [],
"emotionalRole": "revisit",
"discoveryDifficulty": "patient",
"mysteryLevel": "quietly alive",
"resonanceScore": 8,
"symbols": [
"clock",
"door",
"return"
],
"narrativeArcs": [
"time echoes",
"observatory rituals"
],
"parentLinks": [],
"childLinks": [],
"echoes": [],
"mirroredEntries": [],
"thematicLinks": [],
"symbolicLinks": [],
"triggerLinks": []
},
{
"id": "search-route-subconscious-index",
"type": "search route",
"title": "Route: subconscious index",
"content": "/play/subconscious-index.html",
"characters": [
"young z",
"aphy",
"sensei chi",
"lima",
"future z"
],
"emotionalTone": "strange",
"rarity": "very rare",
"triggerConditions": "subconscious index, dream index, recurring symbols",
"tags": [
"observatory",
"symbols",
"dream"
],
"category": "search",
"pageLocation": "/play/subconscious-index.html",
"familyLayer": "5",
"enabled": true,
"createdDate": "2026-05-10",
"modifiedDate": "2026-05-10",
"notes": "Hidden symbolic dream-navigation layer for recurring observatory objects.",
"audioSettings": "",
"animationTrigger": "symbolic-drift",
"cssClassHooks": "observatory-ritual subconscious-index",
"chainReferences": [],
"continuationLinks": [],
"emotionalRole": "symbolic synthesis",
"discoveryDifficulty": "hidden",
"mysteryLevel": "subconscious",
"resonanceScore": 9,
"symbols": [
"ring",
"tea",
"clock",
"door",
"mirror",
"crayon sun",
"console"
],
"narrativeArcs": [
"symbolic subconscious",
"observatory rituals"
],
"parentLinks": [],
"childLinks": [],
"echoes": [],
"mirroredEntries": [],
"thematicLinks": [],
"symbolicLinks": [],
"triggerLinks": []
}
]
}

View File

@@ -358,6 +358,9 @@
"sensei chi": "sensei chi: the search is also searching you.",
"young z": "Search result: age 7, blanket cape, crayon sun.",
"future z": "future z: I cannot spoil the ending. I can say keep going.",
"gentle constellarium": "The observatory sky loosens. A thread is waiting to be steadied.",
"return log": "future z has delayed the message just enough for it to mean something.",
"subconscious index": "The website answers in objects when ordinary search gets too direct.",
"2022": "lima layer: beginning stored as warmth, not trivia.",
"oct 2025": "Engagement memory: the calendar learned to glow.",
"age 7": "young z layer: crayons, cartoons, and a cape made from a blanket.",
@@ -372,6 +375,11 @@
"sensei garden": "/play/sensei-garden.html",
"young z desk": "/play/young-z-desk.html",
"future log": "/play/future-log.html",
"gentle constellarium": "/play/gentle-constellarium.html",
"return log": "/play/future-z-return-log.html",
"future z return log": "/play/future-z-return-log.html",
"subconscious index": "/play/subconscious-index.html",
"dream index": "/play/subconscious-index.html",
"family layer index": "/play/family-layer-index.html",
"do not open": "/play/do-not-open.html",
"voice note": "/play/cassette-log.html",
@@ -392,6 +400,10 @@
"phrase": "dream",
"route": "/play/dream-corridor.html"
},
{
"phrase": "thread",
"route": "/play/gentle-constellarium.html"
},
{
"phrase": "oldweb",
"message": "aphy briefly considers a table layout, then apologizes to CSS."
@@ -416,6 +428,18 @@
"phrase": "leave the light on",
"route": "/play/kitchen-light.html"
},
{
"phrase": "restore the thread",
"route": "/play/gentle-constellarium.html"
},
{
"phrase": "future z return log",
"route": "/play/future-z-return-log.html"
},
{
"phrase": "subconscious index",
"route": "/play/subconscious-index.html"
},
{
"phrase": "sensei chi",
"message": "sensei chi: a hidden word is still a word."
@@ -890,6 +914,32 @@
}).join("");
}
function addObservatoryPlayDoorways() {
let playState = {};
try {
playState = JSON.parse(localStorage.getItem("zxh_observatory_play_v1") || "{}");
} catch (_) {}
const restored = (playState.restoredThreads || []).length;
const symbols = Object.keys(playState.discoveredSymbols || {}).length;
const patient = Number(playState.patientMoments || 0);
const options = [];
if (restored > 0) options.push(["thread", "/play/future-z-return-log.html", "future z remembers the thread you restored."]);
if (symbols > 0) options.push(["symbol", "/play/subconscious-index.html", "a symbol you found is still casting a shadow."]);
if (patient > 0) options.push(["return", "/play/gentle-constellarium.html", "sensei chi notices the slower way you move through the site."]);
if (!options.length && memory.visits >= 5 && (playState.crossGameFlags || {})["future-z:observed"]) {
options.push(["echo", "/play/subconscious-index.html", "aphy found an echo between the return log and the dream index."]);
}
if (!options.length) return;
const [label, href, message] = pick(options, "observatory-doorway");
const link = document.createElement("a");
link.className = "hidden-oldweb-stamp";
link.href = href;
link.textContent = label;
link.title = message;
link.addEventListener("click", () => toast(message));
document.body.appendChild(link);
}
function addPageSpecificSecrets() {
// Events only used by specific `/play/...` pages.
if (path.includes("/play/cassette-log")) {
@@ -1001,6 +1051,7 @@
addPatienceRewards,
addRareSecondLayer,
addFamilyLayerIndex,
addObservatoryPlayDoorways,
addPageSpecificSecrets,
addInvisibleHoverSecrets
];

View File

@@ -14,7 +14,7 @@
const root = $(".play-root[data-play-page]");
if (!root) return;
const page = root.dataset.playPage;
const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg };
const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg, gentleConstellarium, futureZReturnLog, subconsciousIndex };
if (inits[page]) inits[page](root);
});
@@ -839,6 +839,314 @@
load();
}
const observatoryKey = "zxh_observatory_play_v1";
function readObservatoryState() {
try {
return JSON.parse(localStorage.getItem(observatoryKey) || "{}");
} catch (_error) {
return {};
}
}
function writeObservatoryState(next) {
localStorage.setItem(observatoryKey, JSON.stringify(next));
}
async function loadObservatory() {
try {
const response = await fetch("/assets/content/hidden-details.json", { cache: "no-store" });
if (!response.ok) throw new Error("hidden details unavailable");
const data = await response.json();
return (data.entries || []).filter((entry) => entry.enabled !== false);
} catch (_error) {
return [];
}
}
function entryDepth(entry) {
const layer = Number(entry.familyLayer);
if (Number.isFinite(layer)) return Math.max(0, Math.min(5, layer));
if ((entry.characters || []).includes("future z")) return 4;
if ((entry.characters || []).includes("sensei chi")) return 3;
if ((entry.characters || []).includes("young z") || (entry.characters || []).includes("lima")) return 2;
return 1;
}
function characterLine(entry, fallback) {
const chars = entry.characters || [];
if (chars.includes("lima")) return "lima steadies this without making it smaller.";
if (chars.includes("aphy")) return "aphy logs the relationship, then softens the label.";
if (chars.includes("sensei chi")) return "sensei chi waits until the pattern stops trying to impress you.";
if (chars.includes("young z")) return "young z thinks this memory needs a door and a sun.";
if (chars.includes("future z")) return "future z has seen this thread from farther down the path.";
return fallback || "the observatory accepts the shape for now.";
}
function relationScore(a, b) {
const shared = (left, right) => left.filter((item) => right.includes(item)).length;
let score = 0;
score += shared(a.characters || [], b.characters || []) * 3;
score += shared(a.symbols || [], b.symbols || []) * 4;
score += shared(a.narrativeArcs || [], b.narrativeArcs || []) * 3;
score += a.emotionalTone && a.emotionalTone === b.emotionalTone ? 2 : 0;
score += Math.max(0, 3 - Math.abs(entryDepth(a) - entryDepth(b)));
["continuationLinks", "chainReferences", "echoes", "thematicLinks", "symbolicLinks", "triggerLinks", "parentLinks", "childLinks", "mirroredEntries"].forEach((key) => {
if ((a[key] || []).includes(b.id) || (b[key] || []).includes(a.id)) score += 5;
});
return score;
}
function importantEntries(entries, wantedCharacters) {
return entries
.filter((entry) => entry.title && entry.content)
.filter((entry) => !wantedCharacters || (entry.characters || []).some((name) => wantedCharacters.includes(name)))
.sort((a, b) => Number(b.resonanceScore || 0) - Number(a.resonanceScore || 0) || entryDepth(b) - entryDepth(a))
.slice(0, 18);
}
async function gentleConstellarium(root) {
const entries = importantEntries(await loadObservatory(), ["lima", "aphy", "sensei chi", "young z", "future z", "z"]);
const sky = $("[data-constellarium-sky]", root);
const status = $("[data-constellarium-status]", root);
const title = $("[data-constellarium-title]", root);
const reading = $("[data-constellarium-reading]", root);
const log = $("[data-constellarium-log]", root);
let state = readObservatoryState();
let selected = [];
let relation = "continuationLinks";
function saveThread(a, b) {
const score = relationScore(a, b);
const thread = {
id: `${a.id}:${b.id}:${relation}`,
source: a.id,
target: b.id,
relation,
score,
title: `${a.title} / ${b.title}`,
restoredAt: new Date().toISOString()
};
const restoredThreads = [...(state.restoredThreads || []).filter((item) => item.id !== thread.id), thread].slice(-24);
state = { ...state, restoredThreads, lastRestoredThread: thread, characterTrust: bumpTrust(state.characterTrust, [...(a.characters || []), ...(b.characters || [])]) };
if (score >= 8) state.crossGameFlags = { ...(state.crossGameFlags || {}), "thread:strong-restored": true };
writeObservatoryState(state);
status.textContent = score >= 8 ? "The thread holds. The sky remembers that shape." : "The thread is gentle but uncertain. Lima keeps it from falling.";
reading.textContent = score >= 8 ? characterLine(b, "two memories agree without needing to be identical.") : "aphy marks this as possible. sensei chi says possible is a real room.";
renderLog();
}
function render() {
if (!entries.length) {
sky.innerHTML = "<p class=\"play-live-note\">The observatory data is resting. Try again after the hidden details are available.</p>";
return;
}
sky.innerHTML = entries.map((entry, index) => {
const angle = (index / entries.length) * Math.PI * 2 - Math.PI / 2;
const radius = 34 + entryDepth(entry) * 8;
const active = selected.some((item) => item.id === entry.id);
return `<button type="button" class="observatory-star${active ? " is-active" : ""}" data-id="${escapeAttr(entry.id)}" style="--x:${50 + Math.cos(angle) * radius}%;--y:${50 + Math.sin(angle) * radius * 0.74}%;--size:${18 + Number(entry.resonanceScore || 3) * 2}px">
<span>${escapeHtml((entry.characters || ["z"])[0].slice(0, 1))}</span>
</button>`;
}).join("");
$$("[data-id]", sky).forEach((button) => {
button.addEventListener("click", () => choose(entries.find((entry) => entry.id === button.dataset.id)));
});
renderLog();
}
function choose(entry) {
if (!entry) return;
if (selected.length === 2) selected = [];
selected.push(entry);
title.textContent = entry.title;
reading.textContent = characterLine(entry);
if (selected.length === 2) saveThread(selected[0], selected[1]);
render();
}
function renderLog() {
const threads = state.restoredThreads || [];
log.innerHTML = threads.length
? threads.slice(-6).reverse().map((thread) => `<p><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</p>`).join("")
: "<p>No thread has been restored here yet.</p>";
}
$$("[data-constellarium-relation]", root).forEach((button) => {
button.addEventListener("click", () => {
relation = button.dataset.constellariumRelation;
status.textContent = `The next thread will be a ${relation.replace("Links", "").replace("Entries", "")}.`;
});
});
$("[data-constellarium-reset]", root).addEventListener("click", () => {
selected = [];
status.textContent = "The sky rests without forgetting.";
title.textContent = "Waiting for a first light";
reading.textContent = "lima keeps a hand near the thread, not pulling, just steadying.";
render();
});
render();
}
async function futureZReturnLog(root) {
const entries = importantEntries(await loadObservatory(), ["future z", "young z", "lima", "sensei chi"]).slice(0, 9);
const now = Date.now();
let state = readObservatoryState();
const visits = { ...(state.returnVisits || {}) };
const page = visits["future-z-return-log"] || { count: 0, firstSeenAt: now, lastSeenAt: 0 };
const gap = page.lastSeenAt ? now - page.lastSeenAt : 0;
visits["future-z-return-log"] = { ...page, count: page.count + 1, lastSeenAt: now };
state = { ...state, returnVisits: visits };
writeObservatoryState(state);
const stage = $("[data-return-stage]", root);
const heading = $("[data-return-heading]", root);
const message = $("[data-return-message]", root);
const context = $("[data-return-context]", root);
const status = $("[data-return-status]", root);
const list = $("[data-return-list]", root);
const restored = (state.restoredThreads || []).length;
const symbols = Object.keys(state.discoveredSymbols || {}).length;
const patient = Number(state.patientMoments || 0);
function render() {
const count = visits["future-z-return-log"].count;
const minutes = Math.floor(gap / 60000);
stage.textContent = count <= 1 ? "First opening" : count < 4 ? `${count} returns` : "familiar return";
heading.textContent = restored ? "future z found one of your restored threads." : count > 1 ? "The page recognizes your shape in time." : "The log is still warming up.";
message.textContent = restored
? "future z: I remember the thread you helped hold together. Some repairs become routes."
: count > 2
? "future z: returning is not the same as being stuck. Sometimes it is how care checks the lock."
: "future z has left the first note face down, so the ink does not smudge.";
context.textContent = minutes
? `Last gap: about ${minutes} minutes. ${symbols ? "One discovered symbol is travelling with you." : "No symbol has followed you here yet."}`
: patient ? "sensei chi notices the quiet you brought from another room." : "aphy has no elapsed-time anomaly to report yet.";
list.innerHTML = entries.map((entry, index) => {
const seen = visits[entry.id] || {};
return `<button type="button" class="return-memory" data-id="${escapeAttr(entry.id)}">
<strong>${escapeHtml(entry.title)}</strong>
<span>${seen.count ? `observed ${seen.count} time${seen.count === 1 ? "" : "s"}` : "not observed here yet"}</span>
</button>`;
}).join("");
$$("[data-id]", list).forEach((button) => button.addEventListener("click", () => observe(button.dataset.id)));
}
function observe(id) {
const entry = entries.find((item) => item.id === id);
if (!entry) return;
const seen = visits[id] || { count: 0, firstSeenAt: now };
visits[id] = { ...seen, count: seen.count + 1, lastSeenAt: Date.now() };
state = { ...readObservatoryState(), returnVisits: visits };
if ((entry.characters || []).includes("future z")) state.crossGameFlags = { ...(state.crossGameFlags || {}), "future-z:observed": true };
writeObservatoryState(state);
status.textContent = characterLine(entry, "future z folds the observation into the log.");
render();
}
$("[data-return-observe]", root).addEventListener("click", () => entries[0] && observe(entries[0].id));
$("[data-return-wait]", root).addEventListener("click", () => {
state = readObservatoryState();
state.patientMoments = Number(state.patientMoments || 0) + 1;
state.crossGameFlags = { ...(state.crossGameFlags || {}), "patience:return-log": true };
writeObservatoryState(state);
status.textContent = "sensei chi notices that you did not rush the page.";
render();
});
$("[data-return-reset]", root).addEventListener("click", () => {
state = readObservatoryState();
delete state.returnVisits;
writeObservatoryState(state);
status.textContent = "The local drift was cleared. Future z leaves the kindness, not the counter.";
});
render();
}
async function subconsciousIndex(root) {
const entries = await loadObservatory();
let state = readObservatoryState();
let selected = [];
const cloud = $("[data-symbol-cloud]", root);
const status = $("[data-subconscious-status]", root);
const sentence = $("[data-dream-sentence]", root);
const reading = $("[data-dream-reading]", root);
const routes = $("[data-subconscious-routes]", root);
const counts = new Map();
entries.forEach((entry) => (entry.symbols || []).forEach((symbol) => counts.set(symbol, (counts.get(symbol) || 0) + 1)));
[["ring", 1], ["tea", 1], ["crayon sun", 1], ["clock", 1], ["door", 1], ["mirror", 1], ["console", 1], ["kitchen light", 1]].forEach(([symbol, count]) => {
if (!counts.has(symbol)) counts.set(symbol, count);
});
const symbols = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 16);
function render() {
cloud.innerHTML = symbols.map(([symbol, count]) => `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
<strong>${escapeHtml(symbol)}</strong><span>${count}</span>
</button>`).join("");
$$("[data-symbol]", cloud).forEach((button) => button.addEventListener("click", () => toggleSymbol(button.dataset.symbol)));
const known = state.discoveredSymbols || {};
routes.innerHTML = Object.keys(known).length
? Object.entries(known).slice(-8).map(([symbol, info]) => `<p><strong>${escapeHtml(symbol)}</strong>: ${escapeHtml(info.meaning)}</p>`).join("")
: "<p>No object has admitted its meaning yet.</p>";
}
function toggleSymbol(symbol) {
selected = selected.includes(symbol) ? selected.filter((item) => item !== symbol) : [...selected, symbol].slice(-3);
const phrase = dreamPhrase(selected);
sentence.textContent = phrase.title;
reading.textContent = phrase.reading;
if (selected.length >= 2) {
const discoveredSymbols = { ...(state.discoveredSymbols || {}) };
selected.forEach((item) => {
discoveredSymbols[item] = { meaning: phrase.meaning, discoveredAt: new Date().toISOString(), path: selected };
});
state = { ...state, discoveredSymbols, lastDreamPath: selected, crossGameFlags: { ...(state.crossGameFlags || {}), "symbol:dream-sentence": true } };
writeObservatoryState(state);
status.textContent = selected.length === 3 ? "The website answers in objects, then pretends it was only navigation." : "A symbolic route has begun to breathe.";
}
render();
}
$("[data-subconscious-clear]", root).addEventListener("click", () => {
selected = [];
sentence.textContent = "Nothing has combined yet.";
reading.textContent = "young z has drawn a door in the margin and refuses to explain why that helps.";
status.textContent = "The current dream closed without being lost.";
render();
});
render();
}
function dreamPhrase(symbols) {
const key = symbols.slice().sort().join("|");
const table = {
"clock|ring": ["A future kept warm", "future z recognizes the ring as a promise that learned to wait.", "promise and return"],
"crayon sun|door": ["Safety drawn before it was understood", "young z makes a door because a page can become a room.", "play and shelter"],
"console|tea": ["Logic learned to bow", "aphy accepts the cup from sensei chi and stops explaining for one line.", "systems softened by patience"],
"kitchen light|ring": ["Home on purpose", "lima turns the symbol back toward the ordinary room where it belongs.", "warmth and vow"],
"mirror|ring": ["The promise has a reflection", "the observatory shows the same memory from the side that was quiet.", "echo and vow"]
};
const picked = table[key] || (symbols.length
? [`${symbols.join(" / ")}`, "sensei chi says the meaning is not late. It is arriving by a slower path.", "private weather"]
: ["Nothing has combined yet.", "young z has drawn a door in the margin and refuses to explain why that helps.", "waiting"]);
return { title: picked[0], reading: picked[1], meaning: picked[2] };
}
function bumpTrust(current, characters) {
const next = { ...(current || {}) };
characters.forEach((name) => {
if (["lima", "aphy", "sensei chi", "young z", "future z"].includes(name)) next[name] = Number(next[name] || 0) + 1;
});
return next;
}
function escapeHtml(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" })[char]);
}
function escapeAttr(value) {
return escapeHtml(value).replace(/`/g, "&#96;");
}
function setupSortable(container, _render, after) {
let dragged = null;
container.addEventListener("dragstart", (event) => {

View File

@@ -495,6 +495,132 @@
background: var(--surface);
}
.observatory-ritual {
--ritual-sky: #17130f;
--ritual-gold: #d6b36a;
--ritual-blue: #75a9bd;
--ritual-red: #d06b78;
--ritual-green: #7fb089;
--ritual-violet: #a58ac9;
}
.observatory-panel {
display: grid;
gap: 1rem;
}
.observatory-sky {
position: relative;
min-height: clamp(340px, 58vw, 620px);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background:
radial-gradient(circle at 50% 50%, rgba(214, 179, 106, 0.16), transparent 12%),
radial-gradient(circle at 28% 34%, rgba(117, 169, 189, 0.14), transparent 18%),
radial-gradient(circle at 72% 70%, rgba(208, 107, 120, 0.12), transparent 16%),
repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07) 0 1px, transparent 1px 58px),
var(--ritual-sky);
}
.observatory-sky::after {
content: "";
position: absolute;
inset: 12%;
border: 1px dashed rgba(246, 216, 137, 0.16);
border-radius: 50%;
pointer-events: none;
}
.observatory-star {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--size);
height: var(--size);
min-height: 0;
padding: 0;
border-radius: 50%;
border-color: rgba(255, 248, 232, 0.58);
background: radial-gradient(circle, #fff8e8 0 20%, var(--ritual-gold) 21% 52%, rgba(214, 179, 106, 0.18) 53% 100%);
color: #17130f;
font-weight: 900;
transform: translate(-50%, -50%);
box-shadow: 0 0 18px rgba(214, 179, 106, 0.28);
}
.observatory-star:hover,
.observatory-star.is-active {
background: radial-gradient(circle, #fff8e8 0 18%, var(--ritual-red) 19% 56%, rgba(208, 107, 120, 0.22) 57% 100%);
color: #fff8e8;
box-shadow: 0 0 28px rgba(208, 107, 120, 0.46);
}
.observatory-star span {
display: grid;
place-items: center;
width: 100%;
height: 100%;
font-size: 0.78rem;
text-transform: uppercase;
}
.observatory-drawer,
.dream-sentence {
padding: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in oklab, var(--surface) 86%, var(--play-paper) 14%);
}
.observatory-thread-actions,
.symbol-cloud,
.return-memory-list {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.temporal-console {
grid-template-columns: minmax(0, 1fr) minmax(190px, auto);
}
.return-memory {
display: grid;
flex: 1 1 250px;
gap: 0.35rem;
min-height: 100px;
text-align: left;
}
.return-memory span,
.symbol-chip span {
color: var(--muted);
font-size: 0.86rem;
}
.symbol-cloud {
align-items: stretch;
}
.symbol-chip {
flex: 1 1 150px;
display: grid;
gap: 0.3rem;
text-align: left;
background:
linear-gradient(135deg, color-mix(in oklab, var(--surface) 88%, var(--ritual-blue) 12%), var(--surface));
}
.symbol-chip.is-active {
background: var(--play-ink);
color: #fff8e8;
}
.symbol-chip.is-active span {
color: rgba(255, 248, 232, 0.72);
}
.rpg-shell {
display: grid;
grid-template-columns: minmax(0, 960px) minmax(280px, 1fr);
@@ -804,6 +930,7 @@
.play-console,
.recipe-tool,
.sigil-tool,
.temporal-console,
.rpg-shell {
grid-template-columns: 1fr;
}

View File

@@ -0,0 +1,37 @@
#+TITLE: Future Z's Return Log
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-10 Sun>
#+BEGIN_EXPORT html
<main class="play-root play-page observatory-ritual" data-play-page="futureZReturnLog">
<a class="play-back" href="/play.html">Back to Play</a>
<header class="play-page-head">
<p class="play-kicker">Layer 4 / return weather</p>
<h1>Future Z's Return Log</h1>
<p>This room changes by being visited. Future z does not hurry the message; the page keeps count quietly.</p>
</header>
<section class="play-console temporal-console">
<div class="play-console__screen">
<p class="play-console__eyebrow" data-return-stage>First opening</p>
<h2 data-return-heading>The log is still warming up.</h2>
<p data-return-message>future z has left the first note face down, so the ink does not smudge.</p>
<p class="play-live-note" data-return-context></p>
</div>
<div class="play-console__controls">
<button type="button" data-return-observe>Observe a memory</button>
<button type="button" data-return-wait>Stay a moment</button>
<button type="button" data-return-reset>Clear local drift</button>
</div>
</section>
<section class="play-tool observatory-panel">
<div class="play-scorebar">
<span data-return-status>Temporal echoes are settling.</span>
</div>
<div class="return-memory-list" data-return-list></div>
</section>
</main>
#+END_EXPORT

View File

@@ -0,0 +1,42 @@
#+TITLE: Gentle Constellarium
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-10 Sun>
#+BEGIN_EXPORT html
<main class="play-root play-page observatory-ritual" data-play-page="gentleConstellarium">
<a class="play-back" href="/play.html">Back to Play</a>
<header class="play-page-head">
<p class="play-kicker">Layer 2 / restored thread</p>
<h1>Gentle Constellarium</h1>
<p>The observatory has loosened into a sky. Choose a dim memory, then a second light that feels close enough to hold it.</p>
</header>
<section class="play-tool observatory-panel">
<div class="play-scorebar">
<span data-constellarium-status>The sky is gathering memory nodes.</span>
<button type="button" data-constellarium-reset>Let the sky rest</button>
</div>
<div class="observatory-sky" data-constellarium-sky aria-label="Restorable emotional constellation"></div>
<div class="observatory-drawer">
<p class="play-console__eyebrow">Selected thread</p>
<h2 data-constellarium-title>Waiting for a first light</h2>
<p data-constellarium-reading>lima keeps a hand near the thread, not pulling, just steadying.</p>
<div class="observatory-thread-actions">
<button type="button" data-constellarium-relation="continuationLinks">Continue</button>
<button type="button" data-constellarium-relation="echoes">Echo</button>
<button type="button" data-constellarium-relation="symbolicLinks">Symbol</button>
<button type="button" data-constellarium-relation="mirroredEntries">Mirror</button>
</div>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p class="play-console__eyebrow">Recovered constellations</p>
<div data-constellarium-log></div>
</div>
</section>
</main>
#+END_EXPORT

View File

@@ -0,0 +1,36 @@
#+TITLE: The Subconscious Index
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+HIDDEN_PAGE: t
#+DATE: <2026-05-10 Sun>
#+BEGIN_EXPORT html
<main class="play-root play-page observatory-ritual" data-play-page="subconsciousIndex">
<a class="play-back" href="/play.html">Back to Play</a>
<header class="play-page-head">
<p class="play-kicker">Layer 5 / symbolic drift</p>
<h1>The Subconscious Index</h1>
<p>The website keeps speaking through objects. Choose recurring symbols until they begin arranging themselves into a sentence.</p>
</header>
<section class="play-tool observatory-panel">
<div class="play-scorebar">
<span data-subconscious-status>The index is reading the recurring objects.</span>
<button type="button" data-subconscious-clear>Close the current dream</button>
</div>
<div class="symbol-cloud" data-symbol-cloud aria-label="Recurring observatory symbols"></div>
<div class="dream-sentence">
<p class="play-console__eyebrow">Dream sentence</p>
<h2 data-dream-sentence>Nothing has combined yet.</h2>
<p data-dream-reading>young z has drawn a door in the margin and refuses to explain why that helps.</p>
</div>
</section>
<section class="play-console">
<div class="play-console__screen">
<p class="play-console__eyebrow">Subconscious routes</p>
<div data-subconscious-routes></div>
</div>
</section>
</main>
#+END_EXPORT