diff --git a/assets/scripts/hidden-details.js b/assets/scripts/hidden-details.js
index c89ffa1..6942ce1 100644
--- a/assets/scripts/hidden-details.js
+++ b/assets/scripts/hidden-details.js
@@ -34,6 +34,75 @@
// Generated by the hidden narrative authoring page.
// Friendly source of truth: assets/content/hidden-details.json
+ const CHARACTER_REGISTRY = {
+ "young z": {
+ "id": "young z",
+ "displayLabel": "young z",
+ "aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
+ "territoryColor": "#d8a95a",
+ "symbol": "Y",
+ "avatar": {"shape": "sun-child", "accent": "#f1bc5b", "shadow": "#7a5630", "mood": "excited", "motif": "crayon sun", "description": "small crayon-sun spirit with oversized curiosity"},
+ "motifs": ["crayon sun", "blanket cape", "childhood desk"],
+ "themes": ["childhood", "play", "memory", "safety"],
+ "affinities": ["z", "future z", "aphy", "lima"]
+ },
+ "z": {
+ "id": "z",
+ "displayLabel": "z",
+ "aliases": ["Z", "z", "zaine"],
+ "territoryColor": "#d6c38a",
+ "symbol": "Z",
+ "avatar": {"shape": "traveler", "accent": "#d6c38a", "shadow": "#5d533a", "mood": "receptive", "motif": "small lantern", "description": "present observer spirit, quiet and receptive"},
+ "motifs": ["archive", "website", "home"],
+ "themes": ["selfhood", "return", "making"],
+ "affinities": ["lima", "young z", "future z"]
+ },
+ "aphy": {
+ "id": "aphy",
+ "displayLabel": "aphy",
+ "aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
+ "territoryColor": "#7fb089",
+ "symbol": "A",
+ "avatar": {"shape": "terminal", "accent": "#7fd0a0", "shadow": "#315944", "mood": "curious", "motif": "terminal cursor", "description": "curious tiny system companion with a soft terminal glow"},
+ "motifs": ["console", "diagnostic", "backup"],
+ "themes": ["humor", "systems", "care through tools"],
+ "affinities": ["z", "lima", "sensei chi"]
+ },
+ "lima": {
+ "id": "lima",
+ "displayLabel": "lima",
+ "aliases": ["Lima", "lima"],
+ "territoryColor": "#d06b78",
+ "symbol": "L",
+ "avatar": {"shape": "hearth", "accent": "#e78f8d", "shadow": "#6d3f43", "mood": "warm", "motif": "kitchen light", "description": "warm grounding spirit with calm eyes and a soft light"},
+ "motifs": ["warmth", "kitchen light", "ring"],
+ "themes": ["love", "home", "grounding"],
+ "affinities": ["z", "aphy", "future z", "young z"]
+ },
+ "sensei chi": {
+ "id": "sensei chi",
+ "displayLabel": "sensei chi",
+ "aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
+ "territoryColor": "#75a9bd",
+ "symbol": "S",
+ "avatar": {"shape": "sage", "accent": "#84b8c8", "shadow": "#36566a", "mood": "smiling", "motif": "tea lantern", "description": "patient elderly spirit in simple robes"},
+ "motifs": ["tea", "garden", "quiet lesson"],
+ "themes": ["reflection", "patience", "wisdom"],
+ "affinities": ["aphy", "future z"]
+ },
+ "future z": {
+ "id": "future z",
+ "displayLabel": "future z",
+ "aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
+ "territoryColor": "#a58ac9",
+ "symbol": "F",
+ "avatar": {"shape": "moon", "accent": "#b8a2d7", "shadow": "#51496a", "mood": "reflective", "motif": "distant clock", "description": "older reflective spirit, distant but caring"},
+ "motifs": ["clock", "age 40", "future log"],
+ "themes": ["time", "reassurance", "continuity"],
+ "affinities": ["z", "young z", "lima", "sensei chi"]
+ }
+};
+
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.",
@@ -498,7 +567,70 @@
// UI HELPERS
// -----------------------------
- function toast(message, duration) {
+ function characterFromText(value) {
+ const text = String(value || "").toLowerCase();
+ let best = "z";
+ let bestScore = 0;
+ Object.entries(CHARACTER_REGISTRY).forEach(([id, config]) => {
+ let score = text.includes(id) ? 6 : 0;
+ (config.aliases || []).forEach((alias) => {
+ if (text.includes(String(alias).toLowerCase())) score += 3;
+ });
+ (config.themes || []).forEach((theme) => {
+ if (text.includes(String(theme).toLowerCase())) score += 2;
+ });
+ (config.motifs || []).forEach((motif) => {
+ if (text.includes(String(motif).toLowerCase())) score += 3;
+ });
+ if (id === "lima" && /warm|home|safe|kitchen|ring|love|rest/.test(text)) score += 3;
+ if (id === "aphy" && /system|terminal|logic|error|debug|console|css|search|backup/.test(text)) score += 3;
+ if (id === "sensei chi" && /quiet|patience|tea|garden|lesson|wisdom|still/.test(text)) score += 3;
+ if (id === "young z" && /play|child|crayon|sun|blanket|game|desk|drawing/.test(text)) score += 3;
+ if (id === "future z" && /future|time|age 40|later|return|warning|ordinary/.test(text)) score += 3;
+ if (score > bestScore) {
+ best = id;
+ bestScore = score;
+ }
+ });
+ return best;
+ }
+
+ function avatarExpression(character, text) {
+ const value = String(text || "").toLowerCase();
+ if (character === "aphy" && /error|glitch|system|diagnostic|debug|console/.test(value)) return "glitching";
+ if (character === "lima" && /safe|home|warm|protect|care|eat|rest|light/.test(value)) return "concerned";
+ if (character === "young z" && /draw|crayon|game|play|sun|blanket/.test(value)) return "drawing";
+ if (character === "future z" && /time|future|return|ordinary|warning|later/.test(value)) return "reflective";
+ if (character === "sensei chi" && /quiet|tea|patience|lesson|garden/.test(value)) return "smiling";
+ return CHARACTER_REGISTRY[character]?.avatar?.mood || "soft";
+ }
+
+ function avatarSvg(character, expression) {
+ const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z;
+ const avatar = config.avatar || {};
+ const accent = avatar.accent || config.territoryColor || "#d3a64d";
+ const shadow = avatar.shadow || "#5d4632";
+ const symbol = escapeHtml(config.symbol || "?");
+ const shape = avatar.shape || "traveler";
+ const eye = expression === "reflective" || expression === "smiling" ? "M12 15h3M21 15h3" : expression === "glitching" ? "M11 14h5M21 13h3M22 17h4" : "M12 14h3M21 14h3";
+ const mouth = expression === "concerned" ? "M15 23q3-2 6 0" : expression === "excited" || expression === "drawing" || expression === "smiling" ? "M15 22q3 3 7 0" : "M16 22h6";
+ const motif = {
+ "sun-child": `
${escapeHtml(art)}`;
+ panel.innerHTML = `${escapeHtml(art)}`;
panel.querySelector("button").addEventListener("click", () => panel.remove());
document.body.appendChild(panel);
}
diff --git a/assets/styles/hidden-details.css b/assets/styles/hidden-details.css
index 668ecd9..8571ae2 100644
--- a/assets/styles/hidden-details.css
+++ b/assets/styles/hidden-details.css
@@ -31,6 +31,9 @@
right: 1rem;
bottom: 1rem;
max-width: min(23rem, calc(100vw - 2rem));
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
padding: 0.8rem 0.95rem;
border: 1px solid var(--border);
border-radius: 8px;
@@ -45,6 +48,27 @@
transition: opacity 220ms ease, transform 220ms ease;
}
+.hidden-avatar {
+ width: 2rem;
+ height: 2rem;
+ display: inline-grid;
+ place-items: center;
+ flex: 0 0 auto;
+ border-radius: 8px;
+ background: color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 16%, var(--surface));
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 42%, transparent),
+ 0 0 18px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 22%, transparent);
+ overflow: hidden;
+ animation: hidden-avatar-breathe 6s ease-in-out infinite;
+}
+
+.hidden-avatar svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ shape-rendering: crispEdges;
+}
+
.hidden-toast.is-visible {
opacity: 1;
transform: translateY(0);
@@ -97,6 +121,13 @@
z-index: 40;
}
+.hidden-drift-note__head {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.7rem;
+ align-items: center;
+}
+
.hidden-drift-note pre {
margin: 0.75rem 0 0;
white-space: pre;
@@ -213,6 +244,18 @@
box-shadow: 0 12px 28px color-mix(in oklab, #000 12%, transparent);
}
+@keyframes hidden-avatar-breathe {
+ 0%, 100% {
+ transform: translateY(0);
+ filter: saturate(0.96);
+ }
+
+ 50% {
+ transform: translateY(-1px);
+ filter: saturate(1.14);
+ }
+}
+
body.hidden-home-takeover .db-root {
filter: saturate(0.9) contrast(1.03);
}
diff --git a/authoring_server.py b/authoring_server.py
index c6b5c4f..55942eb 100755
--- a/authoring_server.py
+++ b/authoring_server.py
@@ -149,6 +149,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
"territoryColor": "#d8a95a",
"symbol": "Y",
+ "avatar": {
+ "shape": "sun-child",
+ "accent": "#f1bc5b",
+ "shadow": "#7a5630",
+ "mood": "excited",
+ "motif": "crayon sun",
+ "description": "small crayon-sun spirit with oversized curiosity",
+ },
"motifs": ["crayon sun", "blanket cape", "childhood desk"],
"themes": ["childhood", "play", "memory", "safety"],
"affinities": ["z", "future z", "aphy", "lima"],
@@ -159,6 +167,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a",
"symbol": "Z",
+ "avatar": {
+ "shape": "traveler",
+ "accent": "#d6c38a",
+ "shadow": "#5d533a",
+ "mood": "receptive",
+ "motif": "small lantern",
+ "description": "present observer spirit, quiet and receptive",
+ },
"motifs": ["archive", "website", "home"],
"themes": ["selfhood", "return", "making"],
"affinities": ["lima", "young z", "future z"],
@@ -169,6 +185,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089",
"symbol": "A",
+ "avatar": {
+ "shape": "terminal",
+ "accent": "#7fd0a0",
+ "shadow": "#315944",
+ "mood": "curious",
+ "motif": "terminal cursor",
+ "description": "curious tiny system companion with a soft terminal glow",
+ },
"motifs": ["console", "diagnostic", "backup"],
"themes": ["humor", "systems", "care through tools"],
"affinities": ["z", "lima", "sensei chi"],
@@ -179,6 +203,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Lima", "lima"],
"territoryColor": "#d06b78",
"symbol": "L",
+ "avatar": {
+ "shape": "hearth",
+ "accent": "#e78f8d",
+ "shadow": "#6d3f43",
+ "mood": "warm",
+ "motif": "kitchen light",
+ "description": "warm grounding spirit with calm eyes and a soft light",
+ },
"motifs": ["warmth", "kitchen light", "ring"],
"themes": ["love", "home", "grounding"],
"affinities": ["z", "aphy", "future z", "young z"],
@@ -189,6 +221,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
"territoryColor": "#75a9bd",
"symbol": "S",
+ "avatar": {
+ "shape": "sage",
+ "accent": "#84b8c8",
+ "shadow": "#36566a",
+ "mood": "smiling",
+ "motif": "tea lantern",
+ "description": "patient elderly spirit in simple robes",
+ },
"motifs": ["tea", "garden", "quiet lesson"],
"themes": ["reflection", "patience", "wisdom"],
"affinities": ["aphy", "future z"],
@@ -199,6 +239,14 @@ CHARACTER_REGISTRY = {
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
"territoryColor": "#a58ac9",
"symbol": "F",
+ "avatar": {
+ "shape": "moon",
+ "accent": "#b8a2d7",
+ "shadow": "#51496a",
+ "mood": "reflective",
+ "motif": "distant clock",
+ "description": "older reflective spirit, distant but caring",
+ },
"motifs": ["clock", "age 40", "future log"],
"themes": ["time", "reassurance", "continuity"],
"affinities": ["z", "young z", "lima", "sensei chi"],
@@ -1228,6 +1276,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
" // -----------------------------\n"
" // Generated by the hidden narrative authoring page.\n"
" // Friendly source of truth: assets/content/hidden-details.json\n\n"
+ f"{js_const('CHARACTER_REGISTRY', CHARACTER_REGISTRY)}\n"
f"{js_const('familyLayers', family_layers)}\n"
f"{js_const('details', details)}\n"
f"{js_const('poems', poems)}\n"
@@ -2525,6 +2574,54 @@ HIDDEN_APP_HTML = r"""
.filters { display: grid; gap: 9px; margin: 14px 0; }
.modebar, .actions, .pills { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }
.modebar button.active { background: rgba(211, 166, 77, 0.26); border-color: var(--gold); }
+ .avatar {
+ width: var(--avatar-size, 24px);
+ height: var(--avatar-size, 24px);
+ display: inline-grid;
+ place-items: center;
+ flex: 0 0 auto;
+ border-radius: 7px;
+ background: color-mix(in oklab, var(--avatar-color, #d3a64d) 20%, rgba(18, 16, 13, 0.72));
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, transparent), 0 0 14px color-mix(in oklab, var(--avatar-color, #d3a64d) 22%, transparent);
+ overflow: hidden;
+ vertical-align: middle;
+ }
+ .avatar svg { width: 100%; height: 100%; display: block; shape-rendering: crispEdges; }
+ .avatar--xs { --avatar-size: 18px; border-radius: 5px; }
+ .avatar--sm { --avatar-size: 24px; }
+ .avatar--md { --avatar-size: 34px; }
+ .avatar--lg { --avatar-size: 52px; border-radius: 10px; }
+ .avatar--breathing { animation: spirit-breathe 5.8s ease-in-out infinite; }
+ .character-chip, .avatar-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, var(--line));
+ border-radius: 999px;
+ padding: 3px 8px 3px 4px;
+ color: #f3ddb2;
+ background: color-mix(in oklab, var(--avatar-color, #d3a64d) 12%, rgba(251, 241, 220, 0.07));
+ font-size: 12px;
+ }
+ .drawer-presence, .character-preview, .timeline-presence {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 7px;
+ margin-top: 9px;
+ }
+ .drawer-heading {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 10px;
+ align-items: center;
+ }
+ .drawer-heading h2 { margin: 0; }
+ .guided-spirit {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ }
.pill {
border: 1px solid var(--line);
border-radius: 999px;
@@ -2577,6 +2674,9 @@ HIDDEN_APP_HTML = r"""
.memory-node:active { cursor: grabbing; }
.memory-node text, .node-label text, .cluster-label text { fill: #f7ead3; paint-order: stroke; stroke: rgba(14, 11, 8, 0.82); stroke-width: 3px; stroke-linejoin: round; }
.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)); transition: r 160ms ease, stroke 160ms ease, opacity 160ms ease; }
+ .memory-node .character-halo { fill: none; stroke: var(--node-character-color, rgba(211, 166, 77, 0.46)); stroke-width: 2; opacity: 0.42; stroke-dasharray: 2 5; animation: spirit-shimmer 8s ease-in-out infinite; }
+ .memory-node .node-avatar { opacity: 0; transform: scale(0.72); transition: opacity 220ms ease, transform 220ms ease; }
+ .zoom-2 .memory-node .node-avatar, .zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 0.96; transform: scale(1); }
.memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
.memory-node.compatible circle { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
@@ -2600,6 +2700,8 @@ HIDDEN_APP_HTML = r"""
}
.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; }
+ .territory-anchor .cluster-halo { animation: spirit-shimmer 7.5s ease-in-out infinite; }
+ .cluster-avatar { filter: drop-shadow(0 0 10px rgba(255, 232, 177, 0.24)); }
.drag-preview-line { stroke: rgba(255, 232, 177, 0.78); stroke-width: 3; stroke-dasharray: 8 8; pointer-events: none; }
.drag-preview-halo { fill: rgba(211, 166, 77, 0.1); stroke: rgba(255, 232, 177, 0.58); stroke-width: 2; stroke-dasharray: 4 7; pointer-events: none; }
.drag-preview-label rect { fill: rgba(18, 16, 13, 0.86); stroke: rgba(255, 232, 177, 0.42); rx: 7; }
@@ -2629,7 +2731,7 @@ HIDDEN_APP_HTML = r"""
gap: 6px;
max-width: min(720px, calc(100% - 40px));
}
- .breadcrumb button { min-height: 26px; padding: 0 8px; }
+ .breadcrumb button { min-height: 26px; padding: 0 8px; display: inline-flex; align-items: center; gap: 6px; }
.active-state {
position: absolute;
z-index: 2;
@@ -2744,6 +2846,14 @@ HIDDEN_APP_HTML = r"""
0%, 100% { opacity: 0.72; }
50% { opacity: 1; }
}
+ @keyframes spirit-breathe {
+ 0%, 100% { transform: translateY(0); filter: saturate(0.94); }
+ 50% { transform: translateY(-1px); filter: saturate(1.14); }
+ }
+ @keyframes spirit-shimmer {
+ 0%, 100% { opacity: 0.28; stroke-dashoffset: 0; }
+ 50% { opacity: 0.68; stroke-dashoffset: 11; }
+ }
.core-glow { animation: breathe 7s ease-in-out infinite; }
@media (max-width: 1120px) {
body { overflow: auto; }
@@ -2800,11 +2910,11 @@ HIDDEN_APP_HTML = r"""