Back to PHICodepen Playground
Codepen Playground

Alchemy

Map

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) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
    }[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>