diff --git a/assets/content/hidden-details.json b/assets/content/hidden-details.json index 7b5a442..1b4c086 100644 --- a/assets/content/hidden-details.json +++ b/assets/content/hidden-details.json @@ -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": [] } ] } diff --git a/assets/scripts/hidden-details.js b/assets/scripts/hidden-details.js index 0e63312..c89ffa1 100644 --- a/assets/scripts/hidden-details.js +++ b/assets/scripts/hidden-details.js @@ -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 ]; diff --git a/assets/scripts/play.js b/assets/scripts/play.js index 90b15b1..a4da29c 100644 --- a/assets/scripts/play.js +++ b/assets/scripts/play.js @@ -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 = "

The observatory data is resting. Try again after the hidden details are available.

"; + 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 ``; + }).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) => `

${escapeHtml(thread.relation.replace("Links", ""))}: ${escapeHtml(thread.title)}

`).join("") + : "

No thread has been restored here yet.

"; + } + + $$("[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 ``; + }).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]) => ``).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]) => `

${escapeHtml(symbol)}: ${escapeHtml(info.meaning)}

`).join("") + : "

No object has admitted its meaning yet.

"; + } + + 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) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" })[char]); + } + + function escapeAttr(value) { + return escapeHtml(value).replace(/`/g, "`"); + } + function setupSortable(container, _render, after) { let dragged = null; container.addEventListener("dragstart", (event) => { diff --git a/assets/styles/play.css b/assets/styles/play.css index 5f16f40..62d3928 100644 --- a/assets/styles/play.css +++ b/assets/styles/play.css @@ -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; } diff --git a/play/future-z-return-log.org b/play/future-z-return-log.org new file mode 100644 index 0000000..41c7019 --- /dev/null +++ b/play/future-z-return-log.org @@ -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 +
+ Back to Play +
+

Layer 4 / return weather

+

Future Z's Return Log

+

This room changes by being visited. Future z does not hurry the message; the page keeps count quietly.

+
+ +
+
+

First opening

+

The log is still warming up.

+

future z has left the first note face down, so the ink does not smudge.

+

+
+
+ + + +
+
+ +
+
+ Temporal echoes are settling. +
+
+
+
+#+END_EXPORT diff --git a/play/gentle-constellarium.org b/play/gentle-constellarium.org new file mode 100644 index 0000000..bc78695 --- /dev/null +++ b/play/gentle-constellarium.org @@ -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 +
+ Back to Play +
+

Layer 2 / restored thread

+

Gentle Constellarium

+

The observatory has loosened into a sky. Choose a dim memory, then a second light that feels close enough to hold it.

+
+ +
+
+ The sky is gathering memory nodes. + +
+
+
+

Selected thread

+

Waiting for a first light

+

lima keeps a hand near the thread, not pulling, just steadying.

+
+ + + + +
+
+
+ +
+
+

Recovered constellations

+
+
+
+
+#+END_EXPORT diff --git a/play/subconscious-index.org b/play/subconscious-index.org new file mode 100644 index 0000000..3dc271b --- /dev/null +++ b/play/subconscious-index.org @@ -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 +
+ Back to Play +
+

Layer 5 / symbolic drift

+

The Subconscious Index

+

The website keeps speaking through objects. Choose recurring symbols until they begin arranging themselves into a sentence.

+
+ +
+
+ The index is reading the recurring objects. + +
+
+
+

Dream sentence

+

Nothing has combined yet.

+

young z has drawn a door in the margin and refuses to explain why that helps.

+
+
+ +
+
+

Subconscious routes

+
+
+
+
+#+END_EXPORT