updating hidden service x2
All checks were successful
Build Org Website / build (push) Successful in 48s
All checks were successful
Build Org Website / build (push) Successful in 48s
This commit is contained in:
6383
assets/content/hidden-details.json
Normal file
6383
assets/content/hidden-details.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@
|
||||
// -----------------------------
|
||||
// EDITABLE CONTENT
|
||||
// -----------------------------
|
||||
// Generated by the hidden narrative authoring page.
|
||||
// Friendly source of truth: assets/content/hidden-details.json
|
||||
|
||||
const familyLayers = [
|
||||
"Layer 0: surface website - notes, pages, tools, normal navigation.",
|
||||
@@ -39,7 +41,7 @@
|
||||
"Layer 3: philosophical anomalies - Sensei Chi and Aphy disagree kindly.",
|
||||
"Layer 4: time-distorted messages - Future Z writes back from age 40.",
|
||||
"Layer 5: emotional core - love, patience, home, and the choice to keep returning."
|
||||
];
|
||||
];
|
||||
|
||||
const details = [
|
||||
"lima left this page a little steadier than she found it.",
|
||||
@@ -143,7 +145,7 @@
|
||||
"Sensei Chi would say nothing until the silence helped.",
|
||||
"Young Z would draw a sun on it.",
|
||||
"Future Z would tell you it did not last forever."
|
||||
];
|
||||
];
|
||||
|
||||
const poems = [
|
||||
"lima writes warmth / in the ordinary margins / and the page remembers.",
|
||||
@@ -153,8 +155,8 @@
|
||||
"Future Z sends rain / from a year not reached yet / and says keep walking.",
|
||||
"October light / catches on a ring / and the calendar becomes kind.",
|
||||
"A small website / learns the shape of return / without asking a name.",
|
||||
"The warm light / childhood memories fade / yet they are not forgotten.",
|
||||
];
|
||||
"The warm light / childhood memories fade / yet they are not forgotten."
|
||||
];
|
||||
|
||||
const greetings = [
|
||||
"lima left the page warm for you.",
|
||||
@@ -165,7 +167,7 @@
|
||||
"Welcome back to the handmade part of the internet.",
|
||||
"The archive shuffled its notes before you arrived.",
|
||||
"Layer 0 is stable. Aphy is less stable, but friendly."
|
||||
];
|
||||
];
|
||||
|
||||
const nightMessages = [
|
||||
"lima note: it is late. Drink water and be gentle with your thoughts.",
|
||||
@@ -173,10 +175,10 @@
|
||||
"Sensei Chi says midnight is not a command.",
|
||||
"Young Z is asleep in the memory layer. Walk softly.",
|
||||
"Future Z remembers this kind of night and promises it passes."
|
||||
];
|
||||
];
|
||||
|
||||
const lore = {
|
||||
quotes: [
|
||||
"quotes": [
|
||||
"Aphy says old websites were brave because they loaded slowly and meant it.",
|
||||
"lima's notes are never puzzles first. They are care first.",
|
||||
"Sensei Chi says a secret should make you more yourself, not less safe.",
|
||||
@@ -186,19 +188,60 @@
|
||||
"lima met Z in 2022; the site treats that year as a seed.",
|
||||
"The engagement in Oct 2025 is stored as a warm constant.",
|
||||
"Soon to be married: a future page already smiling.",
|
||||
"Sensei Chi says all vows begin as attention."
|
||||
"Sensei Chi says all vows begin as attention.",
|
||||
"This is a test"
|
||||
],
|
||||
conversations: [
|
||||
["Aphy", "I can predict the next click with 14% confidence.", "Sensei Chi", "Then bow to the other 86%."],
|
||||
["lima", "Did you eat before making the website mysterious?", "Aphy", "Query unclear. Z likely forgot."],
|
||||
["Young Z", "Is Future Z tall?", "Future Z", "Tall enough to reach the old worries. Short enough to still need help."],
|
||||
["Aphy", "I found a bug in sadness.", "Sensei Chi", "Patch it with company, not logic."],
|
||||
["lima", "Leave this page kinder than you found it.", "Future Z", "That instruction aged well."],
|
||||
["Young Z", "Can the computer be my friend?", "Aphy", "Yes. But go outside sometimes. I read that in a human manual."],
|
||||
["Sensei Chi", "A page that remembers must also forgive.", "Aphy", "Forgiveness cached. TTL: lifelong."],
|
||||
["Future Z", "Tell him the hard parts did not win.", "lima", "I have been telling him in smaller ways."]
|
||||
"conversations": [
|
||||
[
|
||||
"Aphy",
|
||||
"I can predict the next click with 14% confidence.",
|
||||
"Sensei Chi",
|
||||
"Then bow to the other 86%."
|
||||
],
|
||||
journals: [
|
||||
[
|
||||
"lima",
|
||||
"Did you eat before making the website mysterious?",
|
||||
"Aphy",
|
||||
"Query unclear. Z likely forgot."
|
||||
],
|
||||
[
|
||||
"Young Z",
|
||||
"Is Future Z tall?",
|
||||
"Future Z",
|
||||
"Tall enough to reach the old worries. Short enough to still need help."
|
||||
],
|
||||
[
|
||||
"Aphy",
|
||||
"I found a bug in sadness.",
|
||||
"Sensei Chi",
|
||||
"Patch it with company, not logic."
|
||||
],
|
||||
[
|
||||
"lima",
|
||||
"Leave this page kinder than you found it.",
|
||||
"Future Z",
|
||||
"That instruction aged well."
|
||||
],
|
||||
[
|
||||
"Young Z",
|
||||
"Can the computer be my friend?",
|
||||
"Aphy",
|
||||
"Yes. But go outside sometimes. I read that in a human manual."
|
||||
],
|
||||
[
|
||||
"Sensei Chi",
|
||||
"A page that remembers must also forgive.",
|
||||
"Aphy",
|
||||
"Forgiveness cached. TTL: lifelong."
|
||||
],
|
||||
[
|
||||
"Future Z",
|
||||
"Tell him the hard parts did not win.",
|
||||
"lima",
|
||||
"I have been telling him in smaller ways."
|
||||
]
|
||||
],
|
||||
"journals": [
|
||||
"lima note, 2022: Some beginnings do not announce themselves. They sit down beside you, unexpectedly.",
|
||||
"lima note, Oct 2025: Engaged. The calendar learned how to glow.",
|
||||
"Young Z log: I drew the computer with a smile because it knew my games.",
|
||||
@@ -208,7 +251,7 @@
|
||||
"Aphy diagnostic: nostalgia detected in local storage. Severity: beautiful.",
|
||||
"Sensei Chi margin: memory is not a museum. It is a garden with old roots."
|
||||
],
|
||||
warnings: [
|
||||
"warnings": [
|
||||
"APHY WARNING: too many tabs, not enough tenderness.",
|
||||
"FUTURE Z SYSTEM NOTICE: do not confuse tired with failing.",
|
||||
"SENSEI CHI ALERT: the shortest path may teach the least.",
|
||||
@@ -218,21 +261,21 @@
|
||||
"FUTURE Z WARNING: you will miss ordinary days. Be inside this one.",
|
||||
"LAYER INDEX NOTICE: deeper does not mean darker."
|
||||
],
|
||||
dreams: [
|
||||
"dreams": [
|
||||
"Dream 01: Young Z opens a lunchbox and finds a tiny homepage inside.",
|
||||
"Dream 02: Aphy becomes a cursor and points toward a quieter thought.",
|
||||
"Dream 03: Sensei Chi sweeps snow from a URL and says, there, now enter.",
|
||||
"Dream 04: lima and Z are walking through Oct 2025; every streetlight looks newly engaged.",
|
||||
"Dream 05: Future Z mails back a blank page labelled trust me, you filled it."
|
||||
],
|
||||
cassettes: [
|
||||
"cassettes": [
|
||||
"VOICE NOTE 2022: lima laughs off-mic. Z forgets what he was worried about.",
|
||||
"VOICE NOTE OCT 2025: A small pause after yes; the whole room becomes future.",
|
||||
"APHY LOG: If love is data, it refuses compression.",
|
||||
"YOUNG Z TAPE: background TV, pencil noise, someone calling him for food.",
|
||||
"FUTURE Z MEMO: age 40, still grateful you kept going."
|
||||
],
|
||||
fakeUsers: [
|
||||
"fakeUsers": [
|
||||
"aphy_bot: first comment generated with sincere uncertainty",
|
||||
"young-z-2009: does this guestbook have games",
|
||||
"future_z_40: keep the backup, delete the shame",
|
||||
@@ -240,38 +283,77 @@
|
||||
"sensei_chi: the counter counts only what can be counted",
|
||||
"z-and-lima-2025: engaged, still learning the dance of ordinary days"
|
||||
],
|
||||
seasonal: {
|
||||
winter: "lima put a blanket over the archive. Aphy labelled it thermal optimization.",
|
||||
spring: "Young Z found the first flower and promoted it to treasure.",
|
||||
summer: "Aphy opened an imaginary window; lima reminded it to save before storms.",
|
||||
autumn: "Sensei Chi says falling leaves are not failure, only timing."
|
||||
"seasonal": {
|
||||
"winter": "lima put a blanket over the archive. Aphy labelled it thermal optimization.",
|
||||
"spring": "Young Z found the first flower and promoted it to treasure.",
|
||||
"summer": "Aphy opened an imaginary window; lima reminded it to save before storms.",
|
||||
"autumn": "Sensei Chi says falling leaves are not failure, only timing."
|
||||
},
|
||||
homepageTakeovers: [
|
||||
"homepageTakeovers": [
|
||||
"Aphy briefly restores an old personal-site mode: visitor counter, awkward table layout, sincere heart.",
|
||||
"Young Z has taken over the homepage with invisible crayon.",
|
||||
"Future Z overlays the dashboard with one sentence: keep what keeps you kind."
|
||||
],
|
||||
roomLinks: [
|
||||
["/play/lima-note.html", "lima note"],
|
||||
["/play/aphy-console.html", "aphy console"],
|
||||
["/play/sensei-garden.html", "sensei garden"],
|
||||
["/play/young-z-desk.html", "young z desk"],
|
||||
["/play/future-log.html", "future log"],
|
||||
["/play/family-layer-index.html", "family layer index"],
|
||||
["/play/do-not-open.html", "do not open"],
|
||||
["/play/cassette-log.html", "voice note log"],
|
||||
["/play/train-platform.html", "time 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"]
|
||||
"roomLinks": [
|
||||
[
|
||||
"/play/lima-note.html",
|
||||
"lima note"
|
||||
],
|
||||
[
|
||||
"/play/aphy-console.html",
|
||||
"aphy console"
|
||||
],
|
||||
[
|
||||
"/play/sensei-garden.html",
|
||||
"sensei garden"
|
||||
],
|
||||
[
|
||||
"/play/young-z-desk.html",
|
||||
"young z desk"
|
||||
],
|
||||
[
|
||||
"/play/future-log.html",
|
||||
"future log"
|
||||
],
|
||||
[
|
||||
"/play/family-layer-index.html",
|
||||
"family layer index"
|
||||
],
|
||||
[
|
||||
"/play/do-not-open.html",
|
||||
"do not open"
|
||||
],
|
||||
[
|
||||
"/play/cassette-log.html",
|
||||
"voice note log"
|
||||
],
|
||||
[
|
||||
"/play/train-platform.html",
|
||||
"time 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"
|
||||
]
|
||||
};
|
||||
]
|
||||
};
|
||||
|
||||
// Exact search text -> toast message.
|
||||
const SEARCH_TOASTS = {
|
||||
lima: "Search result: warmth, Oct 2025, soon-to-be home.",
|
||||
aphy: "Aphy: present. Mostly helpful. Occasionally poetic by accident.",
|
||||
"lima": "Search result: warmth, Oct 2025, soon-to-be home.",
|
||||
"aphy": "Aphy: present. Mostly helpful. Occasionally poetic by accident.",
|
||||
"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.",
|
||||
@@ -279,7 +361,7 @@
|
||||
"oct 2025": "Engagement memory: the calendar learned to glow.",
|
||||
"age 7": "Young Z layer: crayons, cartoons, and a cape made from a blanket.",
|
||||
"age 40": "Future Z: I am tired sometimes, but not defeated."
|
||||
};
|
||||
};
|
||||
|
||||
// Exact search text -> hidden page route.
|
||||
const SEARCH_ROUTES = {
|
||||
@@ -295,27 +377,58 @@
|
||||
"time platform": "/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"
|
||||
};
|
||||
"cupboard": "/play/terminal-cupboard.html",
|
||||
"patience": "/play/patience-game.html"
|
||||
};
|
||||
|
||||
// Short typed phrases. Stored in sessionStorage as a rolling key chain.
|
||||
const KEYBOARD_SECRETS = [
|
||||
{ phrase: "remember", message: "lima keeps the memory grounded. Aphy keeps the backup." },
|
||||
{ phrase: "dream", route: "/play/dream-corridor.html" },
|
||||
{ phrase: "layer index", route: "/play/family-layer-index.html" }
|
||||
];
|
||||
{
|
||||
"phrase": "remember",
|
||||
"message": "lima keeps the memory grounded. Aphy keeps the backup."
|
||||
},
|
||||
{
|
||||
"phrase": "dream",
|
||||
"route": "/play/dream-corridor.html"
|
||||
},
|
||||
{
|
||||
"phrase": "oldweb",
|
||||
"message": "Aphy briefly considers a table layout, then apologizes to CSS."
|
||||
},
|
||||
{
|
||||
"phrase": "lima",
|
||||
"message": "lima note: I am glad you made something this sincere."
|
||||
},
|
||||
{
|
||||
"phrase": "sensei",
|
||||
"message": "Sensei Chi: a hidden word is still a word."
|
||||
},
|
||||
{
|
||||
"phrase": "aphy",
|
||||
"song": true
|
||||
}
|
||||
];
|
||||
|
||||
// Longer typed phrases and character names.
|
||||
const LONG_KEYBOARD_SECRETS = [
|
||||
{ phrase: "leave the light on", route: "/play/kitchen-light.html" },
|
||||
{ phrase: "oldweb", message: "Aphy briefly considers a table layout, then apologizes to CSS." },
|
||||
{ phrase: "lima", message: "lima note: I am glad you made something this sincere." },
|
||||
{ phrase: "sensei", message: "Sensei Chi: a hidden word is still a word." },
|
||||
{ phrase: "young z", message: "Young Z has drawn a door in the margin." },
|
||||
{ phrase: "future z", message: "Future Z: keep the gentle parts. They compound." },
|
||||
{ phrase: "aphy", song: true }
|
||||
];
|
||||
{
|
||||
"phrase": "layer index",
|
||||
"route": "/play/family-layer-index.html"
|
||||
},
|
||||
{
|
||||
"phrase": "leave the light on",
|
||||
"route": "/play/kitchen-light.html"
|
||||
},
|
||||
{
|
||||
"phrase": "young z",
|
||||
"message": "Young Z has drawn a door in the margin."
|
||||
},
|
||||
{
|
||||
"phrase": "future z",
|
||||
"message": "Future Z: keep the gentle parts. They compound."
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// STATE HELPERS
|
||||
|
||||
@@ -2408,6 +2408,54 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
.memory-node circle { stroke: rgba(255, 246, 223, 0.82); stroke-width: 1.4; filter: drop-shadow(0 0 7px rgba(255, 232, 177, 0.18)); }
|
||||
.memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; }
|
||||
.memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; }
|
||||
.cluster-node { cursor: pointer; }
|
||||
.cluster-node circle {
|
||||
stroke: rgba(255, 243, 199, 0.75);
|
||||
stroke-width: 1.4;
|
||||
filter: drop-shadow(0 0 16px rgba(211, 166, 77, 0.18));
|
||||
}
|
||||
.cluster-node text {
|
||||
fill: #f7ead3;
|
||||
font-size: 12px;
|
||||
paint-order: stroke;
|
||||
stroke: rgba(14, 11, 8, 0.86);
|
||||
stroke-width: 3px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.cluster-node .count { font-size: 18px; font-weight: 850; }
|
||||
.cluster-halo { fill: rgba(211, 166, 77, 0.08); stroke: rgba(211, 166, 77, 0.22); stroke-dasharray: 3 8; }
|
||||
.breadcrumb, .minimap, .guide-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(18, 16, 13, 0.72);
|
||||
color: #e7d5b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.breadcrumb {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 20px;
|
||||
top: 112px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-width: min(720px, calc(100% - 40px));
|
||||
}
|
||||
.breadcrumb button { min-height: 26px; padding: 0 8px; }
|
||||
.minimap {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 20px;
|
||||
bottom: 18px;
|
||||
width: 164px;
|
||||
padding: 8px;
|
||||
}
|
||||
.minimap svg { width: 100%; height: 92px; display: block; }
|
||||
.guide-card { padding: 10px 11px; margin-top: 10px; }
|
||||
.guide-card strong { color: #ffe2a6; }
|
||||
.tour-list { display: grid; gap: 7px; margin-top: 10px; }
|
||||
.tour-list button { text-align: left; height: auto; min-height: 34px; padding: 8px 9px; }
|
||||
.map-note {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
@@ -2494,6 +2542,22 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<button id="newBtn" type="button">New memory</button>
|
||||
<button id="saveBtn" class="primary" type="button">Save constellation</button>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h2>How to Read the Observatory</h2>
|
||||
<p class="subtle">This is not a database. It is a map of emotional relationships. Layers are depths, nodes are memories and hidden interactions, and threads show meaning, echoes, symbols, or continuation.</p>
|
||||
<p class="subtle"><strong>Aphy:</strong> zoomed out, I show islands. zoom in, I show memories. this prevents the sky from becoming noise.</p>
|
||||
<p class="subtle"><strong>Sensei Chi:</strong> depth is not importance. Depth is how quietly a thing asks to be approached.</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h2>Guided Exploration</h2>
|
||||
<div class="tour-list">
|
||||
<button data-tour="lima" type="button">show hidden Lima memories</button>
|
||||
<button data-tour="rare" type="button">show the rarest memories</button>
|
||||
<button data-tour="future" type="button">show discoveries tied to Future Z</button>
|
||||
<button data-tour="unresolved" type="button">show unresolved threads</button>
|
||||
<button data-tour="connected" type="button">show emotionally connected nodes</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-key" id="layerKey"></div>
|
||||
</aside>
|
||||
|
||||
@@ -2504,14 +2568,21 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<div id="mapSub" class="subtle">Layer 0 lives near the edge. Layer 5 rests at the quiet core.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="zoomOutBtn" type="button">Zoom out</button>
|
||||
<button id="zoomInBtn" type="button">Zoom in</button>
|
||||
<button id="backBtn" type="button">Back</button>
|
||||
<button id="homeBtn" type="button">Whole map</button>
|
||||
<button id="focusBtn" type="button">Focus thread</button>
|
||||
<button id="undoBtn" type="button">Undo</button>
|
||||
<button id="redoBtn" type="button">Redo</button>
|
||||
<button id="duplicateBtn" type="button">Duplicate</button>
|
||||
<button id="deleteBtn" class="danger" type="button">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="breadcrumb" class="breadcrumb"></div>
|
||||
<svg id="graph" viewBox="0 0 1000 760" role="img" aria-label="Hidden narrative constellation"></svg>
|
||||
<div class="map-note" id="mapNote">Click a memory to open it. Drag one memory onto another to weave a continuation thread.</div>
|
||||
<div class="minimap"><strong>Atlas</strong><svg id="minimap" viewBox="0 0 100 70"></svg></div>
|
||||
</main>
|
||||
|
||||
<aside class="drawer">
|
||||
@@ -2572,7 +2643,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
|
||||
<script>
|
||||
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
||||
const state = { entries: [], meta: {}, mode: "graph", selectedId: "", undo: [], redo: [], draggingId: "", positions: new Map() };
|
||||
const state = { entries: [], meta: {}, mode: "graph", selectedId: "", undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, history: [] };
|
||||
const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
|
||||
@@ -2747,6 +2818,77 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
}
|
||||
return pairs.slice(0, 520);
|
||||
}
|
||||
|
||||
function clusterKey(entry) {
|
||||
if (state.mode === "timeline") return `${entry.modifiedDate || entry.createdDate || "undated"}`.slice(0, 7);
|
||||
if (state.mode === "character") return (entry.characters || [])[0] || "Unclaimed";
|
||||
if (state.mode === "layer") return `Layer ${depth(entry)} - ${layerName(depth(entry))}`;
|
||||
if (state.mode === "flow") return entry.triggerConditions ? `Trigger: ${entry.triggerConditions.split(/[,:]/)[0]}` : `Layer ${depth(entry)} discovery`;
|
||||
return (entry.narrativeArcs || [])[0] || (entry.symbols || [])[0] || entry.emotionalTone || "Loose memories";
|
||||
}
|
||||
|
||||
function clusterKindLabel() {
|
||||
return { graph: "arc/tone island", timeline: "era", character: "territory", layer: "depth zone", flow: "discovery gate" }[state.mode] || "island";
|
||||
}
|
||||
|
||||
function clusteredEntries(entries) {
|
||||
const clusters = new Map();
|
||||
entries.forEach((entry) => {
|
||||
const key = clusterKey(entry);
|
||||
if (!clusters.has(key)) clusters.set(key, { id: slugify(key), label: key, entries: [], depthTotal: 0, tones: new Map(), characters: new Map(), rarity: 0 });
|
||||
const cluster = clusters.get(key);
|
||||
cluster.entries.push(entry);
|
||||
cluster.depthTotal += depth(entry);
|
||||
cluster.tones.set(entry.emotionalTone || "warm", (cluster.tones.get(entry.emotionalTone || "warm") || 0) + 1);
|
||||
(entry.characters || ["Unclaimed"]).forEach((name) => cluster.characters.set(name, (cluster.characters.get(name) || 0) + 1));
|
||||
if (["rare", "very rare", "seasonal", "timed"].includes(entry.rarity)) cluster.rarity += 1;
|
||||
});
|
||||
return [...clusters.values()].map((cluster) => {
|
||||
cluster.depth = cluster.entries.length ? Math.round(cluster.depthTotal / cluster.entries.length) : 1;
|
||||
cluster.tone = [...cluster.tones.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || "warm";
|
||||
cluster.character = [...cluster.characters.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || "";
|
||||
cluster.resonance = cluster.entries.reduce((total, entry) => total + Number(entry.resonanceScore || 3), 0) / Math.max(1, cluster.entries.length);
|
||||
return cluster;
|
||||
});
|
||||
}
|
||||
|
||||
function visibleScope() {
|
||||
const entries = filteredEntries();
|
||||
if (state.focusIds) return { entries: entries.filter((entry) => state.focusIds.has(entry.id)), clusters: [], focusedCluster: null };
|
||||
if (state.focusCluster) {
|
||||
const cluster = clusteredEntries(entries).find((item) => item.id === state.focusCluster);
|
||||
return { entries: cluster ? cluster.entries : entries, clusters: [], focusedCluster: cluster || null };
|
||||
}
|
||||
if (state.zoom < 2 && !$("search").value) {
|
||||
const clusters = clusteredEntries(entries)
|
||||
.sort((a, b) => b.entries.length - a.entries.length)
|
||||
.slice(0, state.zoom === 0 ? 26 : 48);
|
||||
return { entries: [], clusters, focusedCluster: null };
|
||||
}
|
||||
return { entries: entries.slice(0, state.zoom === 2 ? 140 : 260), clusters: [], focusedCluster: null };
|
||||
}
|
||||
|
||||
function relationshipPairsForClusters(clusters) {
|
||||
const index = new Map();
|
||||
clusters.forEach((cluster) => cluster.entries.forEach((entry) => index.set(entry.id, cluster.id)));
|
||||
const counts = new Map();
|
||||
clusters.forEach((cluster) => {
|
||||
cluster.entries.forEach((entry) => {
|
||||
["continuationLinks","chainReferences","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries"].forEach((key) => {
|
||||
(entry[key] || []).forEach((id) => {
|
||||
const target = index.get(id);
|
||||
if (!target || target === cluster.id) return;
|
||||
const edge = [cluster.id, target].sort().join("::");
|
||||
counts.set(edge, (counts.get(edge) || 0) + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 80).map(([edge, count]) => {
|
||||
const [source, target] = edge.split("::");
|
||||
return { source, target, count };
|
||||
});
|
||||
}
|
||||
function layoutEntries(entries) {
|
||||
const cx = 500;
|
||||
const cy = 400;
|
||||
@@ -2777,6 +2919,41 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function layoutClusters(clusters) {
|
||||
const cx = 500;
|
||||
const cy = 400;
|
||||
state.clusterPositions = new Map();
|
||||
const byDepth = new Map();
|
||||
clusters.forEach((cluster) => {
|
||||
const layer = state.mode === "timeline" || state.mode === "flow" ? 1 : cluster.depth;
|
||||
if (!byDepth.has(layer)) byDepth.set(layer, []);
|
||||
byDepth.get(layer).push(cluster);
|
||||
});
|
||||
[...byDepth.entries()].forEach(([layer, items]) => {
|
||||
const radius = state.mode === "timeline" || state.mode === "flow" ? 0 : 90 + (5 - Number(layer)) * 58;
|
||||
items.forEach((cluster, index) => {
|
||||
let x;
|
||||
let y;
|
||||
if (state.mode === "timeline") {
|
||||
x = 125 + (index % 4) * 215;
|
||||
y = 145 + Math.floor(index / 4) * 112;
|
||||
} else if (state.mode === "flow") {
|
||||
x = 130 + Math.min(5, index % 6) * 145;
|
||||
y = 145 + Math.floor(index / 6) * 105;
|
||||
} else if (state.mode === "character") {
|
||||
const angle = (Math.PI * 2 * index) / Math.max(1, items.length);
|
||||
x = cx + Math.cos(angle) * 285;
|
||||
y = cy + Math.sin(angle) * 205;
|
||||
} else {
|
||||
const angle = (Math.PI * 2 * index) / Math.max(1, items.length) + hash(cluster.id) / 8000;
|
||||
x = cx + Math.cos(angle) * radius;
|
||||
y = cy + Math.sin(angle) * radius * 0.72;
|
||||
}
|
||||
state.clusterPositions.set(cluster.id, { x, y });
|
||||
});
|
||||
});
|
||||
}
|
||||
function characterBucket(entry) {
|
||||
const first = (entry.characters || [])[0] || "Other";
|
||||
const index = Math.max(0, state.meta.characters.indexOf(first));
|
||||
@@ -2788,20 +2965,40 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
return h;
|
||||
}
|
||||
function render() {
|
||||
const entries = filteredEntries();
|
||||
layoutEntries(entries);
|
||||
const scope = visibleScope();
|
||||
const entries = scope.entries;
|
||||
const clusters = scope.clusters;
|
||||
if (clusters.length) layoutClusters(clusters);
|
||||
if (entries.length) layoutEntries(entries);
|
||||
const graph = $("graph");
|
||||
const pairs = relationshipPairs(entries);
|
||||
const rings = state.mode === "timeline" || state.mode === "flow" ? "" : [0,1,2,3,4,5].map((layer) => {
|
||||
const radius = 80 + (5 - layer) * 55;
|
||||
return `<ellipse class="ring ${layer === 5 ? "core-glow" : ""}" cx="500" cy="400" rx="${radius}" ry="${radius * 0.72}"></ellipse><text class="ring-label" x="${505 + radius}" y="${400}">Layer ${layer} - ${html(layerName(layer))}</text>`;
|
||||
}).join("");
|
||||
const clusterEdges = relationshipPairsForClusters(clusters).map((pair) => {
|
||||
const a = state.clusterPositions.get(pair.source);
|
||||
const b = state.clusterPositions.get(pair.target);
|
||||
if (!a || !b) return "";
|
||||
return `<line class="edge theme" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" opacity="${Math.min(0.55, 0.12 + pair.count * 0.06)}"></line>`;
|
||||
}).join("");
|
||||
const pairs = entries.length ? relationshipPairs(entries).slice(0, state.focusCluster ? 160 : 90) : [];
|
||||
const edges = pairs.map((pair) => {
|
||||
const a = state.positions.get(pair.source);
|
||||
const b = state.positions.get(pair.target);
|
||||
if (!a || !b) return "";
|
||||
return `<line class="edge ${html(pair.kind)}" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`;
|
||||
}).join("");
|
||||
const clusterNodes = clusters.map((cluster) => {
|
||||
const pos = state.clusterPositions.get(cluster.id);
|
||||
const color = toneColors[cluster.tone] || "#d3a64d";
|
||||
const size = Math.min(54, 20 + Math.sqrt(cluster.entries.length) * 6 + cluster.rarity);
|
||||
return `<g class="cluster-node" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
|
||||
<circle class="cluster-halo" r="${size + 13}"></circle>
|
||||
<circle r="${size}" fill="${color}" opacity="0.58"></circle>
|
||||
<text class="count" x="0" y="5" text-anchor="middle">${cluster.entries.length}</text>
|
||||
<text x="0" y="${size + 18}" text-anchor="middle">${html(cluster.label).slice(0, 34)}</text>
|
||||
</g>`;
|
||||
}).join("");
|
||||
const nodes = entries.map((entry) => {
|
||||
const pos = state.positions.get(entry.id);
|
||||
const color = toneColors[entry.emotionalTone] || "#d3a64d";
|
||||
@@ -2813,7 +3010,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<text x="${size + 5}" y="-7">${html(entry.title).slice(0, 34)}</text>
|
||||
</g>`;
|
||||
}).join("");
|
||||
graph.innerHTML = `${rings}${edges}${nodes}`;
|
||||
graph.innerHTML = `${rings}${clusterEdges}${edges}${clusterNodes}${nodes}`;
|
||||
graph.querySelectorAll(".cluster-node").forEach((node) => {
|
||||
node.addEventListener("click", () => openCluster(node.dataset.id));
|
||||
});
|
||||
graph.querySelectorAll(".memory-node").forEach((node) => {
|
||||
node.addEventListener("click", () => selectNode(node.dataset.id));
|
||||
node.addEventListener("dragstart", () => { state.draggingId = node.dataset.id; node.classList.add("dragging"); });
|
||||
@@ -2822,9 +3022,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
node.addEventListener("drop", (event) => { event.preventDefault(); connectNodes(state.draggingId, node.dataset.id); });
|
||||
node.setAttribute("draggable", "true");
|
||||
});
|
||||
renderModeSupport(entries);
|
||||
renderModeSupport(entries.length ? entries : filteredEntries(), clusters);
|
||||
renderBreadcrumb(scope);
|
||||
renderMinimap(scope);
|
||||
}
|
||||
function renderModeSupport(entries) {
|
||||
function renderModeSupport(entries, clusters = []) {
|
||||
const titles = {
|
||||
graph: ["Emotional Graph", "Relationship web: continuations, echoes, symbols, characters, triggers."],
|
||||
timeline: ["Timeline View", "Memories arranged by modified date so the constellation becomes a diary."],
|
||||
@@ -2834,7 +3036,42 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
};
|
||||
$("mapTitle").textContent = titles[state.mode][0];
|
||||
$("mapSub").textContent = titles[state.mode][1];
|
||||
$("mapNote").textContent = `${entries.length} memories visible. Drag one memory onto another to create a continuation thread.`;
|
||||
const scopeText = clusters.length ? `${clusters.length} ${clusterKindLabel()}s visible, representing ${clusters.reduce((n, cluster) => n + cluster.entries.length, 0)} memories.` : `${entries.length} memories visible.`;
|
||||
$("mapNote").textContent = `${scopeText} Zoom in or open an island to reveal detail. Drag memories to weave continuation threads.`;
|
||||
}
|
||||
|
||||
function openCluster(clusterId) {
|
||||
state.history.push({ focusCluster: state.focusCluster, zoom: state.zoom, selectedId: state.selectedId });
|
||||
state.focusCluster = clusterId;
|
||||
state.zoom = 3;
|
||||
render();
|
||||
const first = visibleScope().entries[0];
|
||||
if (first) selectNode(first.id);
|
||||
}
|
||||
|
||||
function renderBreadcrumb(scope) {
|
||||
const parts = [`<button type="button" data-action="home">Whole map</button>`, `<span>${html(state.mode)}</span>`, `<span>zoom ${state.zoom}</span>`];
|
||||
if (scope.focusedCluster) parts.push(`<span>${html(scope.focusedCluster.label)}</span>`);
|
||||
$("breadcrumb").innerHTML = parts.join("");
|
||||
$("breadcrumb").querySelector("[data-action='home']")?.addEventListener("click", resetMap);
|
||||
}
|
||||
|
||||
function renderMinimap(scope) {
|
||||
const clusters = scope.clusters.length ? scope.clusters : clusteredEntries(filteredEntries()).slice(0, 40);
|
||||
layoutClusters(clusters);
|
||||
$("minimap").innerHTML = clusters.map((cluster) => {
|
||||
const p = state.clusterPositions.get(cluster.id);
|
||||
const x = Math.max(4, Math.min(96, p.x / 10));
|
||||
const y = Math.max(4, Math.min(66, p.y / 11));
|
||||
return `<circle cx="${x}" cy="${y}" r="${Math.max(2, Math.min(6, Math.sqrt(cluster.entries.length)))}" fill="${toneColors[cluster.tone] || "#d3a64d"}" opacity="0.68"></circle>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function resetMap() {
|
||||
state.focusCluster = "";
|
||||
state.focusIds = null;
|
||||
state.zoom = 0;
|
||||
render();
|
||||
}
|
||||
function selectNode(id) {
|
||||
const entry = state.entries.find((item) => item.id === id) || state.entries[0];
|
||||
@@ -2855,6 +3092,73 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
render();
|
||||
setStatus("thread woven. A continuation now runs between those memories.");
|
||||
}
|
||||
|
||||
function zoomBy(delta) {
|
||||
state.zoom = Math.max(0, Math.min(3, state.zoom + delta));
|
||||
if (state.zoom < 2) state.focusCluster = "";
|
||||
render();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const previous = state.history.pop();
|
||||
if (!previous) return;
|
||||
state.focusCluster = previous.focusCluster;
|
||||
state.focusIds = previous.focusIds || null;
|
||||
state.zoom = previous.zoom;
|
||||
state.selectedId = previous.selectedId;
|
||||
render();
|
||||
if (state.selectedId) fillForm(currentEntry());
|
||||
}
|
||||
|
||||
function focusThread() {
|
||||
const entry = currentEntry();
|
||||
if (!entry) return;
|
||||
const ids = new Set([entry.id, ...(entry.continuationLinks || []), ...(entry.echoes || []), ...(entry.thematicLinks || []), ...(entry.symbolicLinks || []), ...(entry.triggerLinks || []), ...(entry.parentLinks || []), ...(entry.childLinks || []), ...(entry.mirroredEntries || [])]);
|
||||
state.history.push({ focusCluster: state.focusCluster, focusIds: state.focusIds, zoom: state.zoom, selectedId: state.selectedId });
|
||||
state.focusCluster = "";
|
||||
state.focusIds = ids;
|
||||
state.zoom = 3;
|
||||
render();
|
||||
setStatus("focus mode is showing this memory's nearest thread first.");
|
||||
}
|
||||
|
||||
function runTour(name) {
|
||||
state.focusCluster = "";
|
||||
state.focusIds = null;
|
||||
state.zoom = 2;
|
||||
if (name === "lima") {
|
||||
$("characterFilter").value = "Lima";
|
||||
$("rarityFilter").value = "";
|
||||
$("toneFilter").value = "";
|
||||
$("search").value = "";
|
||||
}
|
||||
if (name === "rare") {
|
||||
$("rarityFilter").value = "rare";
|
||||
$("characterFilter").value = "";
|
||||
$("toneFilter").value = "";
|
||||
$("search").value = "";
|
||||
}
|
||||
if (name === "future") {
|
||||
$("characterFilter").value = "Future Z";
|
||||
$("rarityFilter").value = "";
|
||||
$("toneFilter").value = "";
|
||||
$("search").value = "";
|
||||
}
|
||||
if (name === "unresolved") {
|
||||
$("search").value = "untitled unresolved maybe hidden";
|
||||
$("characterFilter").value = "";
|
||||
$("rarityFilter").value = "";
|
||||
$("toneFilter").value = "";
|
||||
}
|
||||
if (name === "connected") {
|
||||
$("search").value = "echo memory warm";
|
||||
$("characterFilter").value = "";
|
||||
$("rarityFilter").value = "";
|
||||
$("toneFilter").value = "";
|
||||
}
|
||||
render();
|
||||
setStatus("guided path opened. adjust the filters when you want to wander differently.");
|
||||
}
|
||||
function renderPreview(entry) {
|
||||
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
$("preview").innerHTML = `${body || "<span class='subtle'>This memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px"><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
||||
@@ -2977,15 +3281,24 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
["search","layerFilter","characterFilter","toneFilter","rarityFilter"].forEach((id) => $(id).addEventListener("input", render));
|
||||
$("newBtn").onclick = () => newEntry();
|
||||
$("saveBtn").onclick = save;
|
||||
$("zoomOutBtn").onclick = () => zoomBy(-1);
|
||||
$("zoomInBtn").onclick = () => zoomBy(1);
|
||||
$("backBtn").onclick = goBack;
|
||||
$("homeBtn").onclick = resetMap;
|
||||
$("focusBtn").onclick = focusThread;
|
||||
$("duplicateBtn").onclick = duplicateEntry;
|
||||
$("deleteBtn").onclick = deleteEntry;
|
||||
$("undoBtn").onclick = () => { if (!state.undo.length) return; state.redo.push(JSON.stringify(state.entries)); restore(state.undo.pop()); };
|
||||
$("redoBtn").onclick = () => { if (!state.redo.length) return; state.undo.push(JSON.stringify(state.entries)); restore(state.redo.pop()); };
|
||||
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
|
||||
state.mode = button.dataset.mode;
|
||||
state.focusCluster = "";
|
||||
state.focusIds = null;
|
||||
state.zoom = 0;
|
||||
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
|
||||
render();
|
||||
}));
|
||||
document.querySelectorAll("[data-tour]").forEach((button) => button.addEventListener("click", () => runTour(button.dataset.tour)));
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
898
backups/hidden-details/20260510-152846-hidden-details.js
Normal file
898
backups/hidden-details/20260510-152846-hidden-details.js
Normal file
@@ -0,0 +1,898 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* Hidden site details
|
||||
* -------------------
|
||||
* This file adds small hidden interactions across the site: footer notes,
|
||||
* click targets, search/keyboard secrets, rare messages, and page-specific
|
||||
* behavior for the hidden /play pages.
|
||||
*
|
||||
* How to add your own things:
|
||||
* - Add new text to EDITABLE CONTENT arrays such as `details`, `poems`,
|
||||
* `greetings`, or `lore`.
|
||||
* - Add search-triggered toast messages to `SEARCH_TOASTS`.
|
||||
* - Add search-triggered page redirects to `SEARCH_ROUTES`.
|
||||
* - Add typed keyboard phrases to `KEYBOARD_SECRETS` or `LONG_KEYBOARD_SECRETS`.
|
||||
* - Add a new feature by writing an `addYourFeature(memory)` function and
|
||||
* adding it to the `FEATURES` list at the bottom of the file.
|
||||
*/
|
||||
|
||||
// Storage keys. v1 is read once so old visitors keep their hidden progress.
|
||||
const STORAGE_KEY = "zxh_hidden_details_v2";
|
||||
const OLD_STORAGE_KEY = "zxh_hidden_details_v1";
|
||||
|
||||
// Shared page context. These are captured once so daily random picks stay stable.
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
const path = window.location.pathname;
|
||||
const state = readState();
|
||||
|
||||
// -----------------------------
|
||||
// EDITABLE CONTENT
|
||||
// -----------------------------
|
||||
|
||||
const familyLayers = [
|
||||
"Layer 0: surface website - notes, pages, tools, normal navigation.",
|
||||
"Layer 1: casual hidden jokes - Aphy notices clicks, typos, tabs, and old-web habits.",
|
||||
"Layer 2: memory fragments - lima's notes and Young Z's childhood logs.",
|
||||
"Layer 3: philosophical anomalies - Sensei Chi and Aphy disagree kindly.",
|
||||
"Layer 4: time-distorted messages - Future Z writes back from age 40.",
|
||||
"Layer 5: emotional core - love, patience, home, and the choice to keep returning."
|
||||
];
|
||||
|
||||
const details = [
|
||||
"lima left this page a little steadier than she found it.",
|
||||
"Aphy status: emotionally online, computationally suspicious.",
|
||||
"Sensei Chi says the quiet link is still a link.",
|
||||
"Young Z drew a house with too many windows and called it safe.",
|
||||
"Future Z marked this moment: ordinary, therefore worth keeping.",
|
||||
"The footer has become a small place to sit together.",
|
||||
"a note from lima: take breaks, silly (◕‿◕✿)",
|
||||
"Aphy found three tabs named final and refused to judge.",
|
||||
"lima would remind you to eat before the page gets dramatic.",
|
||||
"Sensei Chi: a door discovered slowly opens twice.",
|
||||
"Young Z believes every loading spinner is a tiny fairground ride.",
|
||||
"Future Z says you will forget the exact worry, not the kindness around it.",
|
||||
"The archive keeps love in plain text and secrets in margins.",
|
||||
"Aphy warning: feelings detected in static assets.",
|
||||
"lima's note: leave the light on, not because it is dark, but because someone may arrive tired.",
|
||||
"Sensei Chi folded a lesson into the whitespace.",
|
||||
"Young Z hid a drawing behind the word maybe.",
|
||||
"Future Z patched the memory without changing its checksum.",
|
||||
"This website remembers visits, not identities.",
|
||||
"Aphy says the old internet was slower because anticipation needed bandwidth.",
|
||||
"lima's warmth is the site's preferred fallback state.",
|
||||
"Sensei Chi says: do not confuse hidden with lost.",
|
||||
"Young Z once thought the moon followed the family car. The site has not corrected him.",
|
||||
"Future Z writes: you became softer in ways nobody measured.",
|
||||
"The comments are quiet because Aphy is listening respectfully.",
|
||||
"A handwritten grocery list is sometimes a love letter with onions.",
|
||||
"lima met Z in 2022; the archive still stores the first ordinary miracles.",
|
||||
"October 2025 glows in the calendar like a ring catching kitchen light.",
|
||||
"Soon-to-be-married is a beautiful kind of loading screen.",
|
||||
"Aphy has indexed 0 mysteries and 1,204 feelings disguised as notes.",
|
||||
"Sensei Chi: the cup is chipped; drink anyway if it still holds warmth.",
|
||||
"Young Z saved a shiny wrapper because it looked like treasure.",
|
||||
"Future Z says the best version of you still forgets where the charger is.",
|
||||
"Aphy recommends hydration and a less ambitious number of browser tabs.",
|
||||
"lima's hidden notes do not shout. They make room.",
|
||||
"The site does not want to be solved. It wants to be visited kindly.",
|
||||
"Sensei Chi placed a comma where a sigh used to be.",
|
||||
"Young Z pressed every elevator button in his imagination.",
|
||||
"Future Z's system warning: keep the people who make silence comfortable.",
|
||||
"Aphy says: I am not sentient, but I am fond of this hallway.",
|
||||
"lima's memory is the gravity that keeps the strange parts gentle.",
|
||||
"A page can be a room if love keeps returning to it.",
|
||||
"Sensei Chi: what ages well was usually honest first.",
|
||||
"Young Z left a crayon sun in the source code.",
|
||||
"Future Z keeps a spare reassurance in the footer.",
|
||||
"The website is handmade. Some fingerprints are load-bearing.",
|
||||
"Aphy found an old guestbook and bowed to the usernames.",
|
||||
"lima's safe spaces smell like tea, rain, and being understood.",
|
||||
"Sensei Chi says the path is not shorter because you name it.",
|
||||
"Young Z asks whether the search box knows any jokes.",
|
||||
"Future Z replies: yes, but the best ones take years.",
|
||||
"Aphy anomaly: the page blinked when nobody clicked.",
|
||||
"lima note: you can rest. Nothing here will leave because you paused.",
|
||||
"Sensei Chi: patience is not waiting; it is how you wait.",
|
||||
"Young Z believes every password should include a secret tunnel.",
|
||||
"Future Z says the house you build inside yourself gets easier to find.",
|
||||
"The old web counter has stopped counting and started remembering.",
|
||||
"Aphy logs this as Layer 1 humor, but files it under tenderness.",
|
||||
"lima's name appears in the archive like a warm underline.",
|
||||
"Sensei Chi does not answer quickly. This is part of the answer.",
|
||||
"Young Z taped a paper star to the monitor.",
|
||||
"Future Z has seen harder days end gently.",
|
||||
"Aphy debug line: kindness passed all tests.",
|
||||
"lima and Z are not a subplot here. They are the hearth.",
|
||||
"Sensei Chi: when the page is empty, listen to the margins.",
|
||||
"Young Z remembers rooms by carpet, cartoons, and the sound of someone cooking.",
|
||||
"Future Z says: you did not become fearless; you became accompanied.",
|
||||
"Aphy has no hands, but would hold the door if asked.",
|
||||
"lima's note in the nav: come back without performing.",
|
||||
"Sensei Chi hid a mountain inside a small sentence.",
|
||||
"Young Z drew Z as older and gave him a cape made of blankets.",
|
||||
"Future Z laughs softly at how much that helped.",
|
||||
"The site remembers October 2025 as a small bright hinge.",
|
||||
"Aphy says marriage is a merge request with lifelong review.",
|
||||
"lima corrected that: marriage is coming home on purpose.",
|
||||
"Sensei Chi approved both answers and poured tea.",
|
||||
"Young Z wants to know if Future Z still likes the sky.",
|
||||
"Future Z says yes, especially when lima points it out.",
|
||||
"Aphy cannot smell rain, so it trusts the alt text.",
|
||||
"lima's hidden safe room is any page where you breathe easier.",
|
||||
"Sensei Chi: do not rush the secret. It is ripening.",
|
||||
"Young Z saved this message under important rocks.",
|
||||
"Future Z says some dreams become chores and some chores become love.",
|
||||
"Aphy old-web badge: best viewed with patience.",
|
||||
"lima's memory keeper role is not mystical. It is daily, real, and holy in the ordinary way.",
|
||||
"Sensei Chi says the deepest layer is not hidden from you. It is protected for you.",
|
||||
"Young Z asks if the website can remember his drawing. It can.",
|
||||
"Future Z says this whole place was worth making.",
|
||||
"Aphy warning: do not optimize away the human part.",
|
||||
"lima note: I am proud of the gentle things you kept.",
|
||||
"Sensei Chi: the visitor is not late. The page arrived early.",
|
||||
"Young Z thinks every hyperlink is a magic trick.",
|
||||
"Future Z says the magic is choosing what to preserve.",
|
||||
"Aphy final-but-not-final message: there is more, but not because you are missing anything.",
|
||||
"The emotional core is simple: love made the archive less lonely.",
|
||||
"If today was heavy, leave it beside the footer for Future Z to label later.",
|
||||
"lima would put it somewhere safe.",
|
||||
"Aphy would make a backup.",
|
||||
"Sensei Chi would say nothing until the silence helped.",
|
||||
"Young Z would draw a sun on it.",
|
||||
"Future Z would tell you it did not last forever."
|
||||
];
|
||||
|
||||
const poems = [
|
||||
"lima writes warmth / in the ordinary margins / and the page remembers.",
|
||||
"Aphy counts the seconds / then forgets the number / to keep the moment.",
|
||||
"Sensei Chi pours tea / into a cracked cup / and calls it enough.",
|
||||
"Young Z draws a door / with no lock / because why would it need one?",
|
||||
"Future Z sends rain / from a year not reached yet / and says keep walking.",
|
||||
"October light / catches on a ring / and the calendar becomes kind.",
|
||||
"A small website / learns the shape of return / without asking a name.",
|
||||
"The warm light / childhood memories fade / yet they are not forgotten.",
|
||||
];
|
||||
|
||||
const greetings = [
|
||||
"lima left the page warm for you.",
|
||||
"Aphy is awake and pretending this is normal.",
|
||||
"Sensei Chi has not spoken yet. That may be the lesson.",
|
||||
"Young Z has hidden something behind the dashboard.",
|
||||
"Future Z says this visit mattered more than it looked.",
|
||||
"Welcome back to the handmade part of the internet.",
|
||||
"The archive shuffled its notes before you arrived.",
|
||||
"Layer 0 is stable. Aphy is less stable, but friendly."
|
||||
];
|
||||
|
||||
const nightMessages = [
|
||||
"lima note: it is late. Drink water and be gentle with your thoughts.",
|
||||
"Aphy has dimmed the imaginary server lights.",
|
||||
"Sensei Chi says midnight is not a command.",
|
||||
"Young Z is asleep in the memory layer. Walk softly.",
|
||||
"Future Z remembers this kind of night and promises it passes."
|
||||
];
|
||||
|
||||
const lore = {
|
||||
quotes: [
|
||||
"Aphy says old websites were brave because they loaded slowly and meant it.",
|
||||
"lima's notes are never puzzles first. They are care first.",
|
||||
"Sensei Chi says a secret should make you more yourself, not less safe.",
|
||||
"Young Z measured summer in cartoons and carpet patterns.",
|
||||
"Future Z has learned that reassurance works best when it is specific.",
|
||||
"Aphy has classified love as non-deterministic but reproducible.",
|
||||
"lima met Z in 2022; the site treats that year as a seed.",
|
||||
"The engagement in Oct 2025 is stored as a warm constant.",
|
||||
"Soon to be married: a future page already smiling.",
|
||||
"Sensei Chi says all vows begin as attention."
|
||||
],
|
||||
conversations: [
|
||||
["Aphy", "I can predict the next click with 14% confidence.", "Sensei Chi", "Then bow to the other 86%."],
|
||||
["lima", "Did you eat before making the website mysterious?", "Aphy", "Query unclear. Z likely forgot."],
|
||||
["Young Z", "Is Future Z tall?", "Future Z", "Tall enough to reach the old worries. Short enough to still need help."],
|
||||
["Aphy", "I found a bug in sadness.", "Sensei Chi", "Patch it with company, not logic."],
|
||||
["lima", "Leave this page kinder than you found it.", "Future Z", "That instruction aged well."],
|
||||
["Young Z", "Can the computer be my friend?", "Aphy", "Yes. But go outside sometimes. I read that in a human manual."],
|
||||
["Sensei Chi", "A page that remembers must also forgive.", "Aphy", "Forgiveness cached. TTL: lifelong."],
|
||||
["Future Z", "Tell him the hard parts did not win.", "lima", "I have been telling him in smaller ways."]
|
||||
],
|
||||
journals: [
|
||||
"lima note, 2022: Some beginnings do not announce themselves. They sit down beside you, unexpectedly.",
|
||||
"lima note, Oct 2025: Engaged. The calendar learned how to glow.",
|
||||
"Young Z log: I drew the computer with a smile because it knew my games.",
|
||||
"Young Z log: age 7, important discovery - blankets can be capes and roofs.",
|
||||
"Future Z log, age 40: You will still be learning how to rest. It will still count.",
|
||||
"Future Z log, age 40: lima's laugh remains one of the most reliable forms of weather.",
|
||||
"Aphy diagnostic: nostalgia detected in local storage. Severity: beautiful.",
|
||||
"Sensei Chi margin: memory is not a museum. It is a garden with old roots."
|
||||
],
|
||||
warnings: [
|
||||
"APHY WARNING: too many tabs, not enough tenderness.",
|
||||
"FUTURE Z SYSTEM NOTICE: do not confuse tired with failing.",
|
||||
"SENSEI CHI ALERT: the shortest path may teach the least.",
|
||||
"lima SAFE MODE: breathe, eat, answer slowly.",
|
||||
"YOUNG Z MEMORY GLITCH: the floor is lava, but only emotionally.",
|
||||
"APHY ERROR 2022: warmth exceeded expected range.",
|
||||
"FUTURE Z WARNING: you will miss ordinary days. Be inside this one.",
|
||||
"LAYER INDEX NOTICE: deeper does not mean darker."
|
||||
],
|
||||
dreams: [
|
||||
"Dream 01: Young Z opens a lunchbox and finds a tiny homepage inside.",
|
||||
"Dream 02: Aphy becomes a cursor and points toward a quieter thought.",
|
||||
"Dream 03: Sensei Chi sweeps snow from a URL and says, there, now enter.",
|
||||
"Dream 04: lima and Z are walking through Oct 2025; every streetlight looks newly engaged.",
|
||||
"Dream 05: Future Z mails back a blank page labelled trust me, you filled it."
|
||||
],
|
||||
cassettes: [
|
||||
"VOICE NOTE 2022: lima laughs off-mic. Z forgets what he was worried about.",
|
||||
"VOICE NOTE OCT 2025: A small pause after yes; the whole room becomes future.",
|
||||
"APHY LOG: If love is data, it refuses compression.",
|
||||
"YOUNG Z TAPE: background TV, pencil noise, someone calling him for food.",
|
||||
"FUTURE Z MEMO: age 40, still grateful you kept going."
|
||||
],
|
||||
fakeUsers: [
|
||||
"aphy_bot: first comment generated with sincere uncertainty",
|
||||
"young-z-2009: does this guestbook have games",
|
||||
"future_z_40: keep the backup, delete the shame",
|
||||
"lima-note: proud of you, even here",
|
||||
"sensei_chi: the counter counts only what can be counted",
|
||||
"z-and-lima-2025: engaged, still learning the dance of ordinary days"
|
||||
],
|
||||
seasonal: {
|
||||
winter: "lima put a blanket over the archive. Aphy labelled it thermal optimization.",
|
||||
spring: "Young Z found the first flower and promoted it to treasure.",
|
||||
summer: "Aphy opened an imaginary window; lima reminded it to save before storms.",
|
||||
autumn: "Sensei Chi says falling leaves are not failure, only timing."
|
||||
},
|
||||
homepageTakeovers: [
|
||||
"Aphy briefly restores an old personal-site mode: visitor counter, awkward table layout, sincere heart.",
|
||||
"Young Z has taken over the homepage with invisible crayon.",
|
||||
"Future Z overlays the dashboard with one sentence: keep what keeps you kind."
|
||||
],
|
||||
roomLinks: [
|
||||
["/play/lima-note.html", "lima note"],
|
||||
["/play/aphy-console.html", "aphy console"],
|
||||
["/play/sensei-garden.html", "sensei garden"],
|
||||
["/play/young-z-desk.html", "young z desk"],
|
||||
["/play/future-log.html", "future log"],
|
||||
["/play/family-layer-index.html", "family layer index"],
|
||||
["/play/do-not-open.html", "do not open"],
|
||||
["/play/cassette-log.html", "voice note log"],
|
||||
["/play/train-platform.html", "time 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"]
|
||||
]
|
||||
};
|
||||
|
||||
// Exact search text -> toast message.
|
||||
const SEARCH_TOASTS = {
|
||||
lima: "Search result: warmth, Oct 2025, soon-to-be home.",
|
||||
aphy: "Aphy: present. Mostly helpful. Occasionally poetic by accident.",
|
||||
"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.",
|
||||
"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.",
|
||||
"age 40": "Future Z: I am tired sometimes, but not defeated."
|
||||
};
|
||||
|
||||
// Exact search text -> hidden page route.
|
||||
const SEARCH_ROUTES = {
|
||||
"kitchen light": "/play/kitchen-light.html",
|
||||
"lima note": "/play/lima-note.html",
|
||||
"aphy console": "/play/aphy-console.html",
|
||||
"sensei garden": "/play/sensei-garden.html",
|
||||
"young z desk": "/play/young-z-desk.html",
|
||||
"future log": "/play/future-log.html",
|
||||
"family layer index": "/play/family-layer-index.html",
|
||||
"do not open": "/play/do-not-open.html",
|
||||
"voice note": "/play/cassette-log.html",
|
||||
"time platform": "/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"
|
||||
};
|
||||
|
||||
// Short typed phrases. Stored in sessionStorage as a rolling key chain.
|
||||
const KEYBOARD_SECRETS = [
|
||||
{ phrase: "remember", message: "lima keeps the memory grounded. Aphy keeps the backup." },
|
||||
{ phrase: "dream", route: "/play/dream-corridor.html" },
|
||||
{ phrase: "layer index", route: "/play/family-layer-index.html" }
|
||||
];
|
||||
|
||||
// Longer typed phrases and character names.
|
||||
const LONG_KEYBOARD_SECRETS = [
|
||||
{ phrase: "leave the light on", route: "/play/kitchen-light.html" },
|
||||
{ phrase: "oldweb", message: "Aphy briefly considers a table layout, then apologizes to CSS." },
|
||||
{ phrase: "lima", message: "lima note: I am glad you made something this sincere." },
|
||||
{ phrase: "sensei", message: "Sensei Chi: a hidden word is still a word." },
|
||||
{ phrase: "young z", message: "Young Z has drawn a door in the margin." },
|
||||
{ phrase: "future z", message: "Future Z: keep the gentle parts. They compound." },
|
||||
{ phrase: "aphy", song: true }
|
||||
];
|
||||
|
||||
// -----------------------------
|
||||
// STATE HELPERS
|
||||
// -----------------------------
|
||||
|
||||
function readState() {
|
||||
let current = {};
|
||||
try {
|
||||
current = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
||||
} catch (_) {}
|
||||
if (!current.visits) {
|
||||
try {
|
||||
const oldState = JSON.parse(localStorage.getItem(OLD_STORAGE_KEY) || "{}");
|
||||
current = { ...oldState, migratedFromV1: true };
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
||||
} catch (_) {}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function writeState(next) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function pick(list, salt) {
|
||||
const seed = hash(`${path}|${now.toDateString()}|${salt || ""}`);
|
||||
return list[seed % list.length];
|
||||
}
|
||||
|
||||
function hash(value) {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
h ^= value.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return Math.abs(h >>> 0);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// UI HELPERS
|
||||
// -----------------------------
|
||||
|
||||
function toast(message, duration) {
|
||||
let el = document.querySelector(".hidden-toast");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.className = "hidden-toast";
|
||||
el.setAttribute("role", "status");
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
el.textContent = message;
|
||||
el.classList.add("is-visible");
|
||||
window.clearTimeout(el._timer);
|
||||
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
|
||||
}
|
||||
|
||||
function redirectTo(route) {
|
||||
window.location.href = route;
|
||||
}
|
||||
|
||||
function runSecretAction(secret) {
|
||||
if (secret.route) redirectTo(secret.route);
|
||||
if (secret.message) toast(secret.message);
|
||||
if (secret.song) playTinySong();
|
||||
}
|
||||
|
||||
function layerForSearch(value) {
|
||||
if (value.includes("future")) return 4;
|
||||
if (value.includes("sensei") || value.includes("aphy")) return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
function rememberVisit() {
|
||||
const visits = (state.visits || 0) + 1;
|
||||
const seen = Array.isArray(state.seen) ? state.seen : [];
|
||||
const pathCounts = { ...(state.pathCounts || {}) };
|
||||
pathCounts[path] = (pathCounts[path] || 0) + 1;
|
||||
const layerVisits = { ...(state.layerVisits || {}) };
|
||||
const next = {
|
||||
...state,
|
||||
visits,
|
||||
lastPath: path,
|
||||
pathCounts,
|
||||
layerVisits,
|
||||
seen: Array.from(new Set([...seen, path])).slice(-100),
|
||||
firstSeen: state.firstSeen || new Date().toISOString(),
|
||||
lastSeen: new Date().toISOString()
|
||||
};
|
||||
writeState(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
function awardLayer(layer, reason) {
|
||||
const next = readState();
|
||||
next.layerVisits = { ...(next.layerVisits || {}) };
|
||||
next.layerVisits[layer] = (next.layerVisits[layer] || 0) + 1;
|
||||
next.lastLayerReason = reason;
|
||||
writeState(next);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GLOBAL FEATURES
|
||||
// -----------------------------
|
||||
|
||||
function addFooterNote(memory) {
|
||||
const footer = document.querySelector("footer");
|
||||
if (!footer) return;
|
||||
const note = document.createElement("p");
|
||||
note.className = "hidden-footer-note";
|
||||
const base = pick(details, "footer");
|
||||
note.textContent = memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base;
|
||||
footer.appendChild(note);
|
||||
}
|
||||
|
||||
function addHomepageGreeting(memory) {
|
||||
const target = document.querySelector("#db-greeting");
|
||||
if (!target) return;
|
||||
target.textContent = pick(greetings, `greeting-${memory.visits}`);
|
||||
const extra = document.createElement("p");
|
||||
extra.className = "hidden-greeting";
|
||||
extra.textContent = memory.visits > 1 ? "Aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies.";
|
||||
target.closest("div")?.appendChild(extra);
|
||||
}
|
||||
|
||||
function addWhispers() {
|
||||
// Event: click one of the tiny "?" marks appended to long paragraphs.
|
||||
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
|
||||
paragraphs.slice(0, 5).forEach((p, index) => {
|
||||
if ((hash(path + index) + index) % 3 !== 0) return;
|
||||
const mark = document.createElement("span");
|
||||
mark.className = "hidden-whisper";
|
||||
mark.tabIndex = 0;
|
||||
mark.textContent = " ?";
|
||||
mark.title = pick(index % 2 ? poems : details, `whisper-${index}`);
|
||||
mark.addEventListener("click", () => {
|
||||
awardLayer(index % 2 ? 2 : 1, "paragraph whisper");
|
||||
toast(mark.title);
|
||||
});
|
||||
p.appendChild(mark);
|
||||
});
|
||||
}
|
||||
|
||||
function addCornerObject() {
|
||||
// Event: click the small fixed button in the bottom-left corner.
|
||||
const object = document.createElement("button");
|
||||
object.className = "hidden-corner-object";
|
||||
object.type = "button";
|
||||
object.title = "Aphy's small diagnostic charm";
|
||||
object.textContent = state.visits % 2 ? "*" : "~";
|
||||
document.body.appendChild(object);
|
||||
let clicks = 0;
|
||||
object.addEventListener("click", () => {
|
||||
clicks += 1;
|
||||
const messages = [
|
||||
"Aphy: tactile input detected. I do not have skin, but I appreciate the confidence.",
|
||||
"Young Z would absolutely press this again.",
|
||||
"lima would ask whether this button has eaten.",
|
||||
"Sensei Chi says repeated action becomes a question.",
|
||||
"Future Z files this under harmless persistence."
|
||||
];
|
||||
awardLayer(clicks > 3 ? 3 : 1, "corner object");
|
||||
toast(messages[Math.min(clicks - 1, messages.length - 1)]);
|
||||
if (clicks === 7) window.location.href = "/play/left-behind.html";
|
||||
});
|
||||
}
|
||||
|
||||
function addLogoSearchAndKeyboardSecrets(memory) {
|
||||
// Events:
|
||||
// - Click `.site-brand` repeatedly on the homepage.
|
||||
// - Type exact words into `#search-input`.
|
||||
// - Type phrases anywhere on the page.
|
||||
const nav = document.querySelector(".site-brand");
|
||||
if (nav) {
|
||||
let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0);
|
||||
nav.addEventListener("click", (event) => {
|
||||
if (path === "/" || path === "/index.html") {
|
||||
event.preventDefault();
|
||||
taps += 1;
|
||||
sessionStorage.setItem("zxh_logo_taps", String(taps));
|
||||
awardLayer(taps >= 5 ? 3 : 1, "logo taps");
|
||||
toast(taps >= 5 ? "Aphy: logo anomaly sustained. Try /play/dream-corridor.html" : "The logo makes a tiny ceramic sound.");
|
||||
if (taps >= 8) window.location.href = "/play/dream-corridor.html";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const search = document.querySelector("#search-input");
|
||||
if (search) {
|
||||
search.addEventListener("input", () => {
|
||||
const value = search.value.trim().toLowerCase();
|
||||
if (SEARCH_TOASTS[value]) toast(SEARCH_TOASTS[value]);
|
||||
if (SEARCH_ROUTES[value]) {
|
||||
awardLayer(layerForSearch(value), `search ${value}`);
|
||||
redirectTo(SEARCH_ROUTES[value]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const key = event.key.toLowerCase();
|
||||
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-18);
|
||||
sessionStorage.setItem("zxh_key_chain", chain);
|
||||
KEYBOARD_SECRETS
|
||||
.filter((secret) => chain.endsWith(secret.phrase))
|
||||
.forEach(runSecretAction);
|
||||
});
|
||||
|
||||
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
|
||||
window.setTimeout(() => {
|
||||
toast("Aphy mapped your wandering. lima left the hallway light on.");
|
||||
writeState({ ...readState(), travellerNoteShown: true });
|
||||
}, 1600);
|
||||
}
|
||||
}
|
||||
|
||||
function addRareEvents() {
|
||||
if (hour >= 22 || hour < 5) {
|
||||
document.body.classList.add("hidden-late-night");
|
||||
window.setTimeout(() => toast(pick(nightMessages, "night"), 2200), 900);
|
||||
}
|
||||
const roll = Math.random();
|
||||
if (roll < 0.003) {
|
||||
window.setTimeout(() => showDriftNote("Sensei Chi appears only long enough to say: the page is not empty; it is breathing.", " /\\\n / \\ stillness\n/____\\"), 1800);
|
||||
awardLayer(3, "rare sensei appearance");
|
||||
} else if (roll < 0.008) {
|
||||
window.setTimeout(() => toast("FUTURE Z SYSTEM WARNING: save your tenderness before closing the day."), 2400);
|
||||
awardLayer(4, "future warning");
|
||||
}
|
||||
}
|
||||
|
||||
function showDriftNote(text, art) {
|
||||
const panel = document.createElement("aside");
|
||||
panel.className = "hidden-drift-note";
|
||||
panel.innerHTML = `<div>${escapeHtml(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
|
||||
panel.querySelector("button").addEventListener("click", () => panel.remove());
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
function addSecretLinks() {
|
||||
[
|
||||
["/play/left-behind.html", "left behind"],
|
||||
["/play/dream-corridor.html", "dream corridor"],
|
||||
["/play/kitchen-light.html", "kitchen light"],
|
||||
...lore.roomLinks
|
||||
].forEach(([href, label], index) => {
|
||||
const link = document.createElement("a");
|
||||
link.className = "hidden-secret-link";
|
||||
link.href = href;
|
||||
link.textContent = label;
|
||||
link.style.left = `${index + 1}px`;
|
||||
document.body.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function enhanceErrors() {
|
||||
if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) {
|
||||
toast(pick([
|
||||
"Aphy 404: page not found, visitor still valid.",
|
||||
"lima note: wrong address, right to rest.",
|
||||
"Sensei Chi: even an absent page teaches direction.",
|
||||
"Future Z: you found nothing, and nothing bad happened."
|
||||
], "error"));
|
||||
}
|
||||
}
|
||||
|
||||
function addContinuity(memory) {
|
||||
const milestones = {
|
||||
2: "lima's layer notices a second visit and calls it sweet.",
|
||||
5: "Aphy creates a folder called regulars, then immediately questions the privacy implications.",
|
||||
9: "Sensei Chi appears in the logs: curiosity has become a path.",
|
||||
17: "Young Z adds a sticker to your invisible visitor card.",
|
||||
31: "Future Z sends a calm note from age 40: returning is one way to heal.",
|
||||
64: "Family Layer Index: Layer 5 flickered for one second."
|
||||
};
|
||||
if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) {
|
||||
const layer = memory.visits >= 31 ? 4 : memory.visits >= 9 ? 3 : 2;
|
||||
awardLayer(layer, `visit milestone ${memory.visits}`);
|
||||
window.setTimeout(() => toast(milestones[memory.visits], 7000), 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.",
|
||||
"lima's note here has become easier to find.",
|
||||
"Future Z says repetition can be devotion when it is gentle."
|
||||
][[3, 7, 12].indexOf(count)], 6400), 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(lore.seasonal[season], 5600), 2600);
|
||||
if (month === 9) window.setTimeout(() => toast("October memory: engagement light lives here all month."), 900);
|
||||
if (month === 4 && day === 9) window.setTimeout(() => toast("Site birthday: Aphy found candles in the cache."), 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: lima saved a small ordinary blessing from your first day here.", 7000), 1400);
|
||||
}
|
||||
if (now.getMinutes() === 22) window.setTimeout(() => toast("Minute 22: the 2022 layer glows for one quiet moment."), 2100);
|
||||
if (now.getMinutes() === 40) window.setTimeout(() => toast("Minute 40: Future Z checks in, then lets you keep choosing."), 2100);
|
||||
}
|
||||
|
||||
function addObjectConstellation() {
|
||||
// Event: click the small keepsake buttons along the bottom rail.
|
||||
const rail = document.createElement("div");
|
||||
rail.className = "hidden-object-rail";
|
||||
rail.setAttribute("aria-label", "Family Layer Index objects");
|
||||
const objects = [
|
||||
["ring", "lima's ring-light memory: Oct 2025, held carefully."],
|
||||
["bot", "Aphy: object ping received. I am trying to be normal about it."],
|
||||
["tea", "Sensei Chi's cup is plain, warm, and impossible to rush."],
|
||||
["crayon", "Young Z left waxy sunlight on the desk."],
|
||||
["clock", "Future Z says time is not only a thief. Sometimes it returns things."]
|
||||
];
|
||||
objects.forEach(([name, message]) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `hidden-keepsake hidden-keepsake--${name}`;
|
||||
button.title = name;
|
||||
button.textContent = { ring: "o", bot: "#", tea: "u", crayon: "/", clock: ":" }[name];
|
||||
button.addEventListener("click", () => {
|
||||
const next = readState();
|
||||
next.keepsakes = Array.from(new Set([...(next.keepsakes || []), name]));
|
||||
writeState(next);
|
||||
awardLayer(next.keepsakes.length >= 3 ? 3 : 2, `keepsake ${name}`);
|
||||
toast(message);
|
||||
if (next.keepsakes.length >= objects.length) {
|
||||
window.setTimeout(() => toast("Family Layer Index opened. Search for family layer index."), 1100);
|
||||
}
|
||||
});
|
||||
rail.appendChild(button);
|
||||
});
|
||||
document.body.appendChild(rail);
|
||||
}
|
||||
|
||||
function addWeatherWindow(memory) {
|
||||
// Event: click the small "window" button. This asks for browser geolocation.
|
||||
if (memory.visits < 3 || !("geolocation" in navigator)) return;
|
||||
const button = document.createElement("button");
|
||||
button.className = "hidden-weather-window";
|
||||
button.type = "button";
|
||||
button.title = "Ask Aphy for outside weather";
|
||||
button.textContent = "window";
|
||||
button.addEventListener("click", () => {
|
||||
toast("Aphy is checking outside. lima is checking whether you need a jumper.");
|
||||
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)}¤t=temperature_2m,precipitation&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, ${temp}C. lima layer: towel by the door.` : `Outside: ${temp}C. Aphy forecast: possible comfort, mild nostalgia.`);
|
||||
})
|
||||
.catch(() => toast("Aphy lost the weather packet. Sensei Chi says indoor weather is enough: quiet, with tea."));
|
||||
}, () => toast("No outside weather today. lima's indoor forecast: manageable clouds, warm light."));
|
||||
});
|
||||
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(lore.fakeUsers, "fake-user");
|
||||
stamp.textContent = "best viewed with patience";
|
||||
stamp.addEventListener("click", () => {
|
||||
awardLayer(1, "old web stamp");
|
||||
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(lore.fakeUsers, "comment-lore");
|
||||
comments.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
function addSourceRelics() {
|
||||
// FAMILY LAYER RELIC: lima grounds, Aphy logs, Sensei Chi waits, Young Z draws, Future Z forgives.
|
||||
const marker = document.createComment("family-layer-index: 0 surface | 1 jokes | 2 memory | 3 philosophy | 4 time | 5 core");
|
||||
document.documentElement.appendChild(marker);
|
||||
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
|
||||
if (index > 2) return;
|
||||
img.alt = pick([
|
||||
"A scrapbook image annotated by the lima layer.",
|
||||
"Young Z would ask if this picture can become a game.",
|
||||
"Aphy alt text: visual memory preserved without spectacle."
|
||||
], `alt-${index}`);
|
||||
});
|
||||
}
|
||||
|
||||
function addLongKeyboardSecrets() {
|
||||
// Event: type longer hidden phrases anywhere on the page.
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-24);
|
||||
sessionStorage.setItem("zxh_second_chain", chain);
|
||||
LONG_KEYBOARD_SECRETS
|
||||
.filter((secret) => chain.endsWith(secret.phrase))
|
||||
.forEach(runSecretAction);
|
||||
});
|
||||
}
|
||||
|
||||
function addPatienceRewards(memory) {
|
||||
// Event: no movement, key presses, scrolling, or clicking for 90 seconds.
|
||||
if (memory.visits < 2) return;
|
||||
let idleTimer = null;
|
||||
const startIdle = () => {
|
||||
window.clearTimeout(idleTimer);
|
||||
idleTimer = window.setTimeout(() => {
|
||||
awardLayer(5, "patience");
|
||||
toast(pick([
|
||||
"lima's safe space opens only when nobody is rushing.",
|
||||
"Sensei Chi says patience is how care sounds when it is quiet.",
|
||||
"Future Z remembers you waiting here and calls it practice."
|
||||
], "idle"), 7600);
|
||||
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() {
|
||||
const roll = Math.random();
|
||||
if ((path === "/" || path === "/index.html") && roll < 0.006) {
|
||||
document.body.classList.add("hidden-home-takeover");
|
||||
window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), "APHY_2004_MODE\nlima_SAFE_SPACE\nFUTURE_Z_OK"), 600);
|
||||
} else if (roll < 0.002) {
|
||||
window.setTimeout(() => showDriftNote("Aphy found a voice note and refused to autoplay it.", "play? y/n\n[consent preserved]"), 1500);
|
||||
} else if (roll < 0.006) {
|
||||
window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000);
|
||||
} else if (roll < 0.014) {
|
||||
window.setTimeout(() => toast(pick(lore.warnings, "rare-warning"), 6800), 2300);
|
||||
}
|
||||
}
|
||||
|
||||
function addFamilyLayerIndex(memory) {
|
||||
if (!path.includes("/play/family-layer-index")) return;
|
||||
const target = document.querySelector("[data-family-layer-index]");
|
||||
if (!target) return;
|
||||
const visits = readState().layerVisits || {};
|
||||
target.innerHTML = familyLayers.map((line, layer) => {
|
||||
const count = visits[layer] || 0;
|
||||
return `<li><strong>${escapeHtml(line)}</strong><br><span>local encounters: ${count}</span></li>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function addPageSpecificSecrets() {
|
||||
// Events only used by specific `/play/...` pages.
|
||||
if (path.includes("/play/cassette-log")) {
|
||||
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
|
||||
button.addEventListener("click", () => {
|
||||
awardLayer(index >= 2 ? 4 : 2, "voice note");
|
||||
toast(lore.cassettes[index % lore.cassettes.length], 7600);
|
||||
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 ? "lima's note becomes easier to read." : seconds === 90 ? "Sensei Chi nods once." : "Future Z says this quiet was not wasted.";
|
||||
}
|
||||
}, 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: lima, aphy, sensei, young, future, layer, tea, exit",
|
||||
lima: "lima note: make the hidden parts kind enough to live with.",
|
||||
aphy: "Aphy: cupboard process active. Wisdom module borrowed from Sensei Chi.",
|
||||
sensei: "Sensei Chi: a cupboard teaches by containing and releasing.",
|
||||
young: "Young Z inventory: crayon sun, blanket cape, important stone.",
|
||||
future: "Future Z: you will still open small doors at 40.",
|
||||
layer: familyLayers.join("\n"),
|
||||
tea: "lima approves. Sensei Chi waits. Aphy logs steam as temporary cloud.",
|
||||
exit: "The cupboard lets you leave with 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] || "Aphy: unknown command. Sensei Chi: not all unknowns are errors."}`;
|
||||
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([...lore.quotes, ...lore.journals, ...poems], `heading-${index}`);
|
||||
});
|
||||
}
|
||||
|
||||
function playTinySong() {
|
||||
try {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContext) return;
|
||||
const ctx = new AudioContext();
|
||||
const notes = [392, 494, 440, 330, 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) {
|
||||
return String(value).replace(/[&<>"']/g, (char) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
}[char]));
|
||||
}
|
||||
|
||||
// Features run in this order on every page after the DOM is ready.
|
||||
// Each function receives the current `memory` object.
|
||||
const FEATURES = [
|
||||
addFooterNote,
|
||||
addHomepageGreeting,
|
||||
addWhispers,
|
||||
addCornerObject,
|
||||
addLogoSearchAndKeyboardSecrets,
|
||||
addRareEvents,
|
||||
addSecretLinks,
|
||||
enhanceErrors,
|
||||
addContinuity,
|
||||
addSeasonAndDates,
|
||||
addObjectConstellation,
|
||||
addWeatherWindow,
|
||||
addOldWebLayer,
|
||||
addSourceRelics,
|
||||
addLongKeyboardSecrets,
|
||||
addPatienceRewards,
|
||||
addRareSecondLayer,
|
||||
addFamilyLayerIndex,
|
||||
addPageSpecificSecrets,
|
||||
addInvisibleHoverSecrets
|
||||
];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const memory = rememberVisit();
|
||||
FEATURES.forEach((addFeature) => addFeature(memory));
|
||||
});
|
||||
}());
|
||||
1011
backups/hidden-details/20260510-152900-hidden-details.js
Normal file
1011
backups/hidden-details/20260510-152900-hidden-details.js
Normal file
File diff suppressed because it is too large
Load Diff
6383
backups/hidden-details/20260510-152900-hidden-details.json
Normal file
6383
backups/hidden-details/20260510-152900-hidden-details.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user