live renderhtml
htmluniphi-alchemy-map.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Uniphi-Alchemy Map</title>
<style>
:root{
--bg:#0b0f14;
--panel:#101824;
--panel2:#0f1620;
--ink:#e7eef9;
--muted:#9fb1c8;
--line:#213044;
--accent:#7dd3fc;
--accent2:#a78bfa;
--ok:#86efac;
--warn:#fca5a5;
--radius:14px;
--shadow: 0 10px 40px rgba(0,0,0,.35);
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}
*{box-sizing:border-box}
body{
margin:0;
font-family:var(--sans);
background:
radial-gradient(900px 500px at 20% 10%, rgba(167,139,250,.14), transparent 60%),
radial-gradient(900px 500px at 80% 0%, rgba(125,211,252,.12), transparent 55%),
radial-gradient(800px 600px at 30% 90%, rgba(134,239,172,.08), transparent 60%),
var(--bg);
color:var(--ink);
line-height:1.35;
}
header{
padding:28px 18px 6px;
max-width:1100px;
margin:0 auto;
}
h1{
margin:0 0 6px;
letter-spacing:.2px;
font-weight:800;
font-size:24px;
}
.subtitle{
color:var(--muted);
max-width:70ch;
font-size:14px;
}
main{
max-width:1100px;
margin:0 auto;
padding:12px 18px 60px;
display:grid;
gap:14px;
}
.grid{
display:grid;
grid-template-columns: 1.1fr .9fr;
gap:14px;
}
@media (max-width: 980px){
.grid{grid-template-columns:1fr}
}
.card{
background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02));
border:1px solid var(--line);
border-radius:var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
.card h2{
margin:0;
padding:14px 14px 10px;
font-size:14px;
letter-spacing:.3px;
text-transform:uppercase;
color:var(--muted);
border-bottom:1px solid var(--line);
background: rgba(0,0,0,.14);
}
.content{ padding:14px; }
label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 6px; }
input[type="text"], input[type="date"], textarea, select{
width:100%;
border:1px solid var(--line);
border-radius:12px;
background: rgba(0,0,0,.18);
color:var(--ink);
padding:10px 11px;
outline:none;
transition: border .15s ease;
}
textarea{ min-height:84px; resize:vertical; }
input:focus, textarea:focus, select:focus{ border-color: rgba(125,211,252,.6); }
.row{
display:grid;
grid-template-columns: 1fr 1fr;
gap:12px;
}
@media (max-width: 640px){
.row{ grid-template-columns:1fr; }
}
.chips{
display:flex; flex-wrap:wrap; gap:8px;
}
.chip{
display:flex;
gap:8px;
align-items:flex-start;
padding:10px 10px;
border:1px solid var(--line);
border-radius:999px;
background: rgba(0,0,0,.14);
font-size:12px;
color:var(--ink);
cursor:pointer;
user-select:none;
}
.chip input{ margin-top:2px; }
.chip small{
display:block;
color:var(--muted);
font-size:11px;
line-height:1.2;
margin-top:2px;
}
.btnbar{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
}
button{
border:1px solid var(--line);
background: rgba(0,0,0,.2);
color:var(--ink);
padding:10px 12px;
border-radius:12px;
cursor:pointer;
font-weight:600;
}
button:hover{ border-color: rgba(125,211,252,.55); }
button.primary{
border-color: rgba(125,211,252,.45);
background: linear-gradient(135deg, rgba(125,211,252,.20), rgba(167,139,250,.12));
}
button.good{
border-color: rgba(134,239,172,.45);
background: linear-gradient(135deg, rgba(134,239,172,.16), rgba(125,211,252,.10));
}
button.bad{
border-color: rgba(252,165,165,.45);
background: linear-gradient(135deg, rgba(252,165,165,.16), rgba(167,139,250,.08));
}
.hint{ color:var(--muted); font-size:12px; }
.phasewrap{
display:flex; gap:12px; align-items:center;
padding:10px 12px;
border:1px dashed rgba(125,211,252,.35);
border-radius:12px;
background: rgba(0,0,0,.12);
margin-top:8px;
}
.phasewrap input[type="range"]{ width:100%; }
.phasepill{
font-family:var(--mono);
font-size:12px;
padding:6px 10px;
border:1px solid var(--line);
border-radius:999px;
white-space:nowrap;
background: rgba(0,0,0,.16);
}
.list{
display:grid;
gap:10px;
}
.ingredient{
border:1px solid var(--line);
border-radius:14px;
background: rgba(0,0,0,.12);
padding:12px;
}
.ingredient .top{
display:flex; align-items:center; justify-content:space-between; gap:10px;
}
.ingredient strong{ font-size:13px; }
.rating{
display:flex; align-items:center; gap:10px;
font-family:var(--mono);
color:var(--muted);
font-size:12px;
}
.rating input[type="range"]{ width:180px; }
@media (max-width: 640px){
.rating input[type="range"]{ width:140px; }
}
details{
border:1px solid var(--line);
border-radius:14px;
background: rgba(0,0,0,.12);
padding:10px 12px;
}
summary{
cursor:pointer;
font-weight:700;
list-style:none;
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
}
summary::-webkit-details-marker{ display:none; }
.mono{ font-family:var(--mono); color:var(--muted); font-size:12px; }
.foot{
display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap;
margin-top:10px;
}
.status{
font-size:12px;
color:var(--muted);
}
.status b{ color:var(--ink); }
.file{
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
}
.file input[type="file"]{
border:1px solid var(--line);
border-radius:12px;
padding:8px;
background: rgba(0,0,0,.12);
color:var(--muted);
}
</style>
</head>
<body>
<header>
<h1>Uniphi-Alchemy Map</h1>
<div class="subtitle">
A single-page ritual for building worlds: <span style="color:var(--accent)">noise</span> becomes
<span style="color:var(--accent2)">constellations</span>, and structure becomes a gentle handrail.
Works offline. Exports Markdown + JSON.
</div>
</header>
<main class="grid">
<section class="card">
<h2>Identity</h2>
<div class="content">
<div class="row">
<div>
<label>Project / Work</label>
<input id="project" type="text" placeholder="e.g. Spectra — Nebula Field UI Pass" />
</div>
<div>
<label>Date</label>
<input id="date" type="date" />
</div>
</div>
<label>Phase (0–6)</label>
<div class="phasewrap">
<span id="phasepill" class="phasepill">0 • spark</span>
<input id="phase" type="range" min="0" max="6" step="1" value="0" />
</div>
<div class="hint" style="margin-top:8px">
0 spark • 1 calcination • 2 dissolution • 3 separation • 4 conjunction • 5 distillation • 6 coagulation
</div>
<label>One-line intent</label>
<textarea id="intent" placeholder="What is the smallest true north for this cycle?"></textarea>
<div class="foot">
<div class="status" id="autosave">Autosave: <b>on</b> (localStorage)</div>
<div class="status" id="wordcount">Words: <b>0</b></div>
</div>
</div>
</section>
<section class="card">
<h2>Axioms (pick 3)</h2>
<div class="content">
<div class="chips" id="axioms"></div>
<div class="hint" style="margin-top:10px">
Motto-style axioms: Latin + French + English. Keep it short enough to remember.
</div>
</div>
</section>
<section class="card">
<h2>9 Ingredients</h2>
<div class="content">
<div class="hint" style="margin-bottom:10px">
Rate each 0–5 and write one sentence. If it feels “super-artificial”: boost <b>Sulfur</b> + <b>Witness</b>, reduce <b>Vessel</b> + <b>Guardian</b>.
</div>
<div class="list" id="ingredients"></div>
</div>
</section>
<section class="card">
<h2>7 Operations</h2>
<div class="content">
<div class="list" id="operations"></div>
</div>
</section>
<section class="card">
<h2>Archetypes + Shadow</h2>
<div class="content">
<label>Active archetypes (2–3 is plenty)</label>
<div class="chips" id="archetypes"></div>
<label>Shadow loop watch</label>
<select id="shadow_loop">
<option value="none">(none)</option>
<option value="complexity_armor">Complexity as armor</option>
<option value="security_trance">Security trance (fence becomes cage)</option>
<option value="abstraction_no_ground">Abstraction without grounding</option>
<option value="release_shyness">Release shyness (“one more patch”)</option>
</select>
<label>Counter-spell (one sentence)</label>
<textarea id="counter_spell" placeholder="A tiny vow that breaks the loop."></textarea>
</div>
</section>
<section class="card">
<h2>Balance + 60-second ritual</h2>
<div class="content">
<label>Balance: smallest next step</label>
<textarea id="balance_next_step" placeholder="One actionable step that improves balance."></textarea>
<label>Anchor (sensory / concrete)</label>
<input id="ritual_anchor" type="text" placeholder="e.g. The laptop fan warms the room like a small sun." />
<label>Stake (why it matters)</label>
<input id="ritual_stake" type="text" placeholder="e.g. I want this to welcome curiosity." />
<label>Spell (cosmic lens)</label>
<input id="ritual_spell" type="text" placeholder="e.g. Noise becomes constellations; structure becomes a handrail." />
<label>As a single paragraph</label>
<textarea id="ritual_paragraph" placeholder="Auto-composed from Anchor + Stake + Spell (you can edit)."></textarea>
<div class="btnbar" style="margin-top:12px">
<button class="primary" id="btn_export_md">Export Markdown</button>
<button class="good" id="btn_download_json">Download JSON</button>
<button id="btn_copy_md">Copy Markdown</button>
<button class="bad" id="btn_reset">Reset</button>
</div>
<div class="file" style="margin-top:12px">
<label style="margin:0">Import JSON</label>
<input id="import_file" type="file" accept="application/json,.json" />
<button id="btn_load_example">Load example</button>
</div>
<div class="hint" style="margin-top:10px">
Tip: Keep your map in git alongside the project. It’s a living README for your own nervous system.
</div>
</div>
</section>
</main>
<script>
(() => {
const PHASES = ["spark","calcination","dissolution","separation","conjunction","distillation","coagulation"];
const AXIOMS = [
{ id:"solve_et_coagula", la:"Solve et Coagula", fr:"Dissoudre et coaguler", en:"Dissolve and coagulate" },
{ id:"ordo_ab_chao", la:"Ordo ab Chao", fr:"L’ordre du chaos", en:"Order from chaos" },
{ id:"iterare_est_distillare", la:"Iterare est Distillare", fr:"Itérer, c’est distiller", en:"To iterate is to distill" },
{ id:"veritas_in_structura", la:"Veritas in Structura", fr:"La vérité dans la structure", en:"Truth lives in structure" },
{ id:"nomen_est_clavis", la:"Nomen est Clavis", fr:"Le nom est clé", en:"Name is a key" },
{ id:"mensura_est_magia", la:"Mensura est Magia", fr:"La mesure est magie", en:"Measurement is magic" },
{ id:"custodia_sine_carcere", la:"Custodia sine Carcere", fr:"Garder sans enfermer", en:"Guard without imprisoning" },
{ id:"ludus_est_labor", la:"Ludus est Labor", fr:"Le jeu est travail", en:"Play is work" },
{ id:"testis_facit_mundum", la:"Testis facit Mundum", fr:"Le témoin fait le monde", en:"The witness makes the world" },
];
const INGREDIENTS = [
{ key:"aether", name:"Aether", desc:"Vision" },
{ key:"mercurius", name:"Mercurius", desc:"Flow / Interoperability" },
{ key:"sulfur", name:"Sulfur", desc:"Soul / Urgency" },
{ key:"sal", name:"Sal", desc:"Structure / Truth" },
{ key:"vessel", name:"Vessel", desc:"Infrastructure / Container" },
{ key:"catalyst", name:"Catalyst", desc:"Play / Risk" },
{ key:"noise", name:"Noise", desc:"Oracle / Randomness" },
{ key:"guardian", name:"Guardian", desc:"Security / Boundaries" },
{ key:"witness", name:"Witness", desc:"Story / Audience" },
];
const OPS = [
{ key:"calcination", title:"Calcination", cue:"burn illusions, keep essence" },
{ key:"dissolution", title:"Dissolution", cue:"let it flow, prototype freely" },
{ key:"separation", title:"Separation", cue:"isolate the core" },
{ key:"conjunction", title:"Conjunction", cue:"merge art + system" },
{ key:"fermentation", title:"Fermentation", cue:"invite surprise" },
{ key:"distillation", title:"Distillation", cue:"refine and clarify" },
{ key:"coagulation", title:"Coagulation", cue:"ship the artifact" },
];
const ARCHETYPES = [
{ id:"cosmic_chemist", label:"Cosmic Chemist", sub:"myth + physics into matter" },
{ id:"net_weaver", label:"Net Weaver", sub:"bridges, peers, tunnels" },
{ id:"vault_guardian", label:"Vault Guardian", sub:"keys, access, integrity" },
{ id:"trickster_debugger", label:"Trickster Debugger", sub:"chaos-wrangler" },
{ id:"archivist", label:"Archivist", sub:"names, versions, reproducibility" },
{ id:"poet_witness", label:"Poet-Witness", sub:"felt meaning, human door" },
];
// Elements
const $ = (id) => document.getElementById(id);
const elAxioms = $("axioms");
const elIngredients = $("ingredients");
const elOps = $("operations");
const elArchetypes = $("archetypes");
const stateKey = "uniphi_alchemy_map_v1";
function defaultState(){
const today = new Date();
const iso = today.toISOString().slice(0,10);
return {
project: "",
date: iso,
phase: 0,
intent: "",
axioms: [],
ingredients: INGREDIENTS.map(x => ({ key:x.key, rating: 3, note: "" })),
operations: OPS.map(x => ({ key:x.key, notes: "" })),
archetypes: [],
shadow_loop: "none",
counter_spell: "",
balance_next_step: "",
ritual: { anchor:"", stake:"", spell:"", paragraph:"" }
};
}
let state = load() || defaultState();
function save(){
localStorage.setItem(stateKey, JSON.stringify(state));
$("autosave").innerHTML = 'Autosave: <b>on</b> (localStorage)';
}
function load(){
try{
const raw = localStorage.getItem(stateKey);
if(!raw) return null;
return JSON.parse(raw);
}catch(e){
return null;
}
}
function renderChips(container, items, selectedIds, onToggle){
container.innerHTML = "";
items.forEach(item => {
const lbl = document.createElement("label");
lbl.className = "chip";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = selectedIds.includes(item.id);
cb.addEventListener("change", () => onToggle(item.id, cb.checked));
const txt = document.createElement("div");
txt.innerHTML = `<div><b>${escapeHtml(item.la || item.label)}</b><small>${escapeHtml(item.fr || item.sub)} — ${escapeHtml(item.en || "")}</small></div>`;
lbl.appendChild(cb);
lbl.appendChild(txt);
container.appendChild(lbl);
});
}
function escapeHtml(s){
return (s ?? "").toString().replace(/[&<>"']/g, (c) => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[c]));
}
function render(){
// Identity
$("project").value = state.project || "";
$("date").value = state.date || "";
$("phase").value = state.phase ?? 0;
$("intent").value = state.intent || "";
updatePhasePill();
// Axioms
renderChips(elAxioms, AXIOMS, state.axioms, (id, checked) => {
if(checked){
if(state.axioms.length >= 3){
// soft rule: keep 3
// uncheck immediately
const inputs = elAxioms.querySelectorAll("input[type=checkbox]");
inputs.forEach(inp => {
const parent = inp.closest("label");
const text = parent?.innerText || "";
// no-op, we'll just revert this one:
});
checked = false;
} else {
state.axioms.push(id);
}
}else{
state.axioms = state.axioms.filter(x => x !== id);
}
// hard enforce length <= 3
state.axioms = state.axioms.slice(0,3);
save();
render(); // re-render to reflect enforcement
});
// Ingredients
elIngredients.innerHTML = "";
INGREDIENTS.forEach((meta, idx) => {
const data = state.ingredients[idx] || { key: meta.key, rating: 3, note: "" };
const wrap = document.createElement("div");
wrap.className = "ingredient";
wrap.innerHTML = `
<div class="top">
<div>
<strong>${meta.name}</strong> <span class="mono">(${meta.desc})</span>
</div>
<div class="rating">
<span>0</span>
<input type="range" min="0" max="5" step="1" value="${data.rating ?? 3}" data-i="${idx}" />
<span>5</span>
<span class="phasepill" style="padding:4px 10px">${data.rating ?? 3}/5</span>
</div>
</div>
<label style="margin-top:10px">Sentence</label>
<textarea data-note="${idx}" placeholder="One sentence that makes it true.">${escapeHtml(data.note || "")}</textarea>
`;
elIngredients.appendChild(wrap);
});
// Operations
elOps.innerHTML = "";
OPS.forEach((meta, idx) => {
const data = state.operations[idx] || { key: meta.key, notes:"" };
const det = document.createElement("details");
det.open = idx < 2; // open first two by default
det.innerHTML = `
<summary>
<span>${meta.title} <span class="mono">— ${meta.cue}</span></span>
<span class="mono">${idx+1}/7</span>
</summary>
<div style="margin-top:10px">
<label>Notes</label>
<textarea data-op="${idx}" placeholder="Write in bullets or prose.">${escapeHtml(data.notes || "")}</textarea>
</div>
`;
elOps.appendChild(det);
});
// Archetypes
renderChips(elArchetypes, ARCHETYPES, state.archetypes, (id, checked) => {
if(checked){
if(!state.archetypes.includes(id)) state.archetypes.push(id);
}else{
state.archetypes = state.archetypes.filter(x => x !== id);
}
save();
updateWordCount();
});
// Shadow + other
$("shadow_loop").value = state.shadow_loop || "none";
$("counter_spell").value = state.counter_spell || "";
$("balance_next_step").value = state.balance_next_step || "";
$("ritual_anchor").value = state.ritual?.anchor || "";
$("ritual_stake").value = state.ritual?.stake || "";
$("ritual_spell").value = state.ritual?.spell || "";
$("ritual_paragraph").value = state.ritual?.paragraph || "";
updateWordCount();
}
function updatePhasePill(){
const p = parseInt($("phase").value,10) || 0;
$("phasepill").textContent = `${p} • ${PHASES[p] || "?"}`;
}
function bind(){
$("project").addEventListener("input", e => { state.project = e.target.value; save(); updateWordCount(); });
$("date").addEventListener("input", e => { state.date = e.target.value; save(); });
$("phase").addEventListener("input", e => { state.phase = parseInt(e.target.value,10) || 0; updatePhasePill(); save(); });
$("intent").addEventListener("input", e => { state.intent = e.target.value; save(); updateWordCount(); });
// Ingredients (delegated)
elIngredients.addEventListener("input", (e) => {
const t = e.target;
if(t.matches("input[type=range][data-i]")){
const idx = parseInt(t.getAttribute("data-i"),10);
state.ingredients[idx].rating = parseInt(t.value,10);
save();
render(); // update pill
}
if(t.matches("textarea[data-note]")){
const idx = parseInt(t.getAttribute("data-note"),10);
state.ingredients[idx].note = t.value;
save();
updateWordCount();
}
});
// Operations
elOps.addEventListener("input", (e) => {
const t = e.target;
if(t.matches("textarea[data-op]")){
const idx = parseInt(t.getAttribute("data-op"),10);
state.operations[idx].notes = t.value;
save();
updateWordCount();
}
});
$("shadow_loop").addEventListener("change", e => { state.shadow_loop = e.target.value; save(); });
$("counter_spell").addEventListener("input", e => { state.counter_spell = e.target.value; save(); updateWordCount(); });
$("balance_next_step").addEventListener("input", e => { state.balance_next_step = e.target.value; save(); updateWordCount(); });
// Ritual
["ritual_anchor","ritual_stake","ritual_spell"].forEach(id => {
$(id).addEventListener("input", () => {
state.ritual = state.ritual || {anchor:"",stake:"",spell:"",paragraph:""};
state.ritual.anchor = $("ritual_anchor").value;
state.ritual.stake = $("ritual_stake").value;
state.ritual.spell = $("ritual_spell").value;
// auto compose if paragraph empty or exactly previous auto
const auto = autoParagraph();
if(($("ritual_paragraph").value || "").trim() === "" || ($("ritual_paragraph").dataset.auto === "1")){
$("ritual_paragraph").value = auto;
$("ritual_paragraph").dataset.auto = "1";
state.ritual.paragraph = auto;
}
save();
updateWordCount();
});
});
$("ritual_paragraph").addEventListener("input", e => {
state.ritual = state.ritual || {anchor:"",stake:"",spell:"",paragraph:""};
state.ritual.paragraph = e.target.value;
e.target.dataset.auto = "0";
save();
updateWordCount();
});
// Buttons
$("btn_download_json").addEventListener("click", () => downloadJson());
$("btn_export_md").addEventListener("click", () => downloadMarkdown());
$("btn_copy_md").addEventListener("click", () => copyMarkdown());
$("btn_reset").addEventListener("click", () => {
if(confirm("Reset the map? (local draft will be cleared)")){
state = defaultState();
save();
render();
}
});
$("btn_load_example").addEventListener("click", () => loadExample());
$("import_file").addEventListener("change", async (e) => {
const file = e.target.files && e.target.files[0];
if(!file) return;
try{
const txt = await file.text();
const obj = JSON.parse(txt);
state = normalizeImported(obj);
save();
render();
e.target.value = "";
}catch(err){
alert("Could not import JSON: " + err);
}
});
}
function normalizeImported(obj){
// Light normalization; keep user content.
const s = defaultState();
s.project = obj.project ?? s.project;
s.date = obj.date ?? s.date;
s.phase = (typeof obj.phase === "number") ? obj.phase : s.phase;
s.intent = obj.intent ?? s.intent;
s.axioms = Array.isArray(obj.axioms) ? obj.axioms.slice(0,3) : s.axioms;
if(Array.isArray(obj.ingredients) && obj.ingredients.length === 9){
s.ingredients = obj.ingredients.map((x,i) => ({
key: INGREDIENTS[i].key,
rating: clampInt(x.rating ?? 3, 0, 5),
note: (x.note ?? "").toString()
}));
}
if(Array.isArray(obj.operations) && obj.operations.length === 7){
s.operations = obj.operations.map((x,i) => ({
key: OPS[i].key,
notes: (x.notes ?? "").toString()
}));
}
s.archetypes = Array.isArray(obj.archetypes) ? obj.archetypes.filter(Boolean) : s.archetypes;
s.shadow_loop = obj.shadow_loop ?? s.shadow_loop;
s.counter_spell = obj.counter_spell ?? s.counter_spell;
s.balance_next_step = obj.balance_next_step ?? s.balance_next_step;
s.ritual = obj.ritual ?? s.ritual;
if(!s.ritual) s.ritual = {anchor:"",stake:"",spell:"",paragraph:""};
return s;
}
function clampInt(n, a, b){
const x = parseInt(n,10);
if(Number.isNaN(x)) return a;
return Math.max(a, Math.min(b, x));
}
function autoParagraph(){
const a = ($("ritual_anchor").value || "").trim();
const s = ($("ritual_stake").value || "").trim();
const p = ($("ritual_spell").value || "").trim();
const bits = [a,s,p].filter(Boolean);
return bits.join(" ");
}
function downloadJson(){
const data = JSON.stringify(stateToExport(), null, 2);
const blob = new Blob([data], {type:"application/json"});
const name = fileSlug(state.project || "uniphi-alchemy-map") + ".json";
triggerDownload(blob, name);
}
function copyMarkdown(){
const md = toMarkdown();
navigator.clipboard.writeText(md).then(
() => alert("Markdown copied to clipboard."),
() => alert("Could not copy (browser permission). Use Export Markdown instead.")
);
}
function downloadMarkdown(){
const md = toMarkdown();
const blob = new Blob([md], {type:"text/markdown"});
const name = fileSlug(state.project || "uniphi-alchemy-map") + ".md";
triggerDownload(blob, name);
}
function triggerDownload(blob, filename){
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 500);
}
function fileSlug(s){
return s.toLowerCase()
.replace(/[^a-z0-9]+/g,"-")
.replace(/^-+|-+$/g,"")
.slice(0,64) || "uniphi-alchemy-map";
}
function stateToExport(){
// Keep stable ordering + keys
return {
project: state.project,
date: state.date,
phase: state.phase,
intent: state.intent,
axioms: (state.axioms || []).slice(0,3).map(id => {
const ax = AXIOMS.find(x => x.id === id);
return ax ? ax.la : id;
}),
ingredients: state.ingredients.map(x => ({ key:x.key, rating:x.rating, note:x.note })),
operations: state.operations.map(x => ({ key:x.key, notes:x.notes })),
archetypes: (state.archetypes || []).slice(),
shadow_loop: state.shadow_loop,
counter_spell: state.counter_spell,
balance_next_step: state.balance_next_step,
ritual: state.ritual
};
}
function toMarkdown(){
const d = stateToExport();
const phaseLabel = PHASES[d.phase] || "";
const axLines = (d.axioms || []).map(a => `- ${a}`).join("\n") || "- (none)";
const nameMap = {
aether:"Aether (Vision)",
mercurius:"Mercurius (Flow / Interoperability)",
sulfur:"Sulfur (Soul / Urgency)",
sal:"Sal (Structure / Truth)",
vessel:"Vessel (Infrastructure / Container)",
catalyst:"Catalyst (Play / Risk)",
noise:"Noise (Oracle / Randomness)",
guardian:"Guardian (Security / Boundaries)",
witness:"Witness (Story / Audience)",
};
const opMap = {
calcination:"Calcination",
dissolution:"Dissolution",
separation:"Separation",
conjunction:"Conjunction",
fermentation:"Fermentation",
distillation:"Distillation",
coagulation:"Coagulation",
};
const archMap = {
cosmic_chemist:"Cosmic Chemist",
net_weaver:"Net Weaver",
vault_guardian:"Vault Guardian",
trickster_debugger:"Trickster Debugger",
archivist:"Archivist",
poet_witness:"Poet-Witness",
};
const ing = d.ingredients.map(x => `### ${nameMap[x.key] || x.key} — ${x.rating}/5\n\n${x.note || ""}\n`).join("\n");
const ops = d.operations.map(x => `### ${opMap[x.key] || x.key}\n\n${x.notes || ""}\n`).join("\n");
const arch = (d.archetypes || []).map(a => archMap[a] || a).join(", ") || "(none)";
const r = d.ritual || {anchor:"",stake:"",spell:"",paragraph:""};
return `# Uniphi-Alchemy Map
**Project / Work:** ${d.project || ""}
**Date:** ${d.date || ""}
**Phase (0–6):** ${d.phase} (${phaseLabel})
**One-line intent:** ${d.intent || ""}
---
## Axioms
${axLines}
---
## 9 Ingredients
${ing}
---
## 7 Operations
${ops}
---
## Archetypes
${arch}
### Shadow Loop
${d.shadow_loop || "none"}
### Counter-spell
${d.counter_spell || ""}
---
## Balance Next Step
${d.balance_next_step || ""}
---
## 60-Second Ritual
**Anchor:** ${r.anchor || ""}
**Stake:** ${r.stake || ""}
**Spell:** ${r.spell || ""}
${r.paragraph || ""}
`;
}
function loadExample(){
// a gentle example, not overwriting if user cancels
if(!confirm("Load the example map? This will overwrite your current draft.")) return;
state = {
project: "Spectra — Nebula Field UI Pass",
date: new Date().toISOString().slice(0,10),
phase: 4,
intent: "Unify shader controls + observability into a single calm cockpit that still feels alive.",
axioms: ["solve_et_coagula","veritas_in_structura","testis_facit_mundum"],
ingredients: [
{key:"aether",rating:5,note:"The universe-feel is the north star: everything must glow with meaning."},
{key:"mercurius",rating:4,note:"Controls, telemetry, and visuals must flow together without friction."},
{key:"sulfur",rating:3,note:"Keep one human pulse in the interface: warmth, not just precision."},
{key:"sal",rating:4,note:"Naming + grouping is the truth layer; no slider without a reason."},
{key:"vessel",rating:4,note:"Single-file deployment, predictable state, no dependencies."},
{key:"catalyst",rating:4,note:"Allow playful toggles (hue, EM, magnetism) to invite discovery."},
{key:"noise",rating:3,note:"Seeded randomness as oracle, not as clutter."},
{key:"guardian",rating:3,note:"Safe defaults, but not a cage; keep the door open."},
{key:"witness",rating:4,note:"The ‘aha’ moment: one button that reveals the universe behind the numbers."},
],
operations: [
{key:"calcination",notes:"Kill duplicate controls; keep only what changes experience. Constraint: single-file, offline."},
{key:"dissolution",notes:"Prototype UI grouping fast; let the shader run wild while controls settle."},
{key:"separation",notes:"Core: (1) field params, (2) color/energy, (3) observability. Decorations later."},
{key:"conjunction",notes:"Bind UI -> shader uniforms -> telemetry in one loop, with stable naming."},
{key:"fermentation",notes:"Leave one unknown: emergent interference patterns from parameter coupling."},
{key:"distillation",notes:"Refactor: remove 2 sliders, merge 2 metrics, rename 5 params for clarity."},
{key:"coagulation",notes:"Ship: a single HTML file + a one-page readme; witness: screen-recorded 30s tour."},
],
archetypes: ["cosmic_chemist","net_weaver","poet_witness"],
shadow_loop: "release_shyness",
counter_spell: "Ship the smallest living ritual; refinement can orbit the release.",
balance_next_step: "Add one concrete anchor line in the UI (‘temperature of the void: …’) and publish the file.",
ritual: {
anchor:"The laptop fan warms the room like a small sun.",
stake:"I want the cockpit to welcome curiosity instead of intimidating it.",
spell:"So I let noise become constellations, and structure become a gentle handrail.",
paragraph:"The laptop fan warms the room like a small sun. I want the cockpit to welcome curiosity instead of intimidating it. So I let noise become constellations, and structure become a gentle handrail."
}
};
save();
render();
}
function updateWordCount(){
const md = toMarkdown();
const words = md.trim().split(/\s+/).filter(Boolean).length;
$("wordcount").innerHTML = `Words: <b>${words}</b>`;
}
// Boot
function ensureDate(){
if(!state.date){
state.date = new Date().toISOString().slice(0,10);
}
}
ensureDate();
bind();
render();
save();
})();
</script>
</body>
</html>