diff --git a/simulator/static/app.js b/simulator/static/app.js
new file mode 100644
index 0000000..12a77f2
--- /dev/null
+++ b/simulator/static/app.js
@@ -0,0 +1,111 @@
+const DIALS = ["left", "right", "dark", "light"];
+const MODEL = ["brain_weight", "mood_weight", "pool_size"];
+
+function buildGrid(el) {
+ el.innerHTML = "";
+ // rows = first axis 0..4 top->bottom, cols = second axis 0..4 left->right
+ for (let a = 0; a < 5; a++) {
+ for (let b = 0; b < 5; b++) {
+ const cell = document.createElement("div");
+ cell.className = "cell";
+ cell.dataset.a = a;
+ cell.dataset.b = b;
+ el.appendChild(cell);
+ }
+ }
+}
+
+function paintGrid(el, axisA, axisB, point, pool) {
+ // clear
+ el.querySelectorAll(".cell").forEach((c) => {
+ c.className = "cell";
+ c.innerHTML = "";
+ });
+ const counts = {};
+ pool.forEach((c) => {
+ const r = c.record;
+ const key = `${r[axisA]},${r[axisB]}`;
+ counts[key] = (counts[key] || 0) + 1;
+ });
+ el.querySelectorAll(".cell").forEach((c) => {
+ const a = +c.dataset.a, b = +c.dataset.b;
+ const key = `${a},${b}`;
+ if (counts[key]) {
+ c.classList.add("cand");
+ const n = document.createElement("span");
+ n.className = "n";
+ n.textContent = counts[key];
+ c.appendChild(n);
+ }
+ if (a === point[axisA] && b === point[axisB]) c.classList.add("point");
+ });
+}
+
+function readState() {
+ const s = { mode: document.getElementById("mode").value, approved_only: document.getElementById("approved_only").checked };
+ DIALS.forEach((d) => (s[d] = +document.getElementById(d).value));
+ s.brain_weight = +document.getElementById("brain_weight").value;
+ s.mood_weight = +document.getElementById("mood_weight").value;
+ s.pool_size = +document.getElementById("pool_size").value;
+ return s;
+}
+
+function syncOutputs() {
+ [...DIALS, ...MODEL].forEach((id) => {
+ const out = document.getElementById(`${id}-out`);
+ if (out) out.textContent = document.getElementById(id).value;
+ });
+}
+
+async function refresh() {
+ syncOutputs();
+ const state = readState();
+ const resp = await fetch("/api/select", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(state),
+ });
+ const data = await resp.json();
+
+ const pickEl = document.getElementById("pick");
+ if (!data.pick) {
+ pickEl.innerHTML = '
∅ Void / rest — walls dark, audio silent.
';
+ } else {
+ const p = data.pick;
+ pickEl.innerHTML =
+ `${p.title}
` +
+ `mode ${p.mode} · coord (${p.left},${p.right},${p.dark},${p.light})
` +
+ `${p.rationale || ""}
`;
+ }
+
+ const poolEl = document.getElementById("pool");
+ poolEl.innerHTML = "";
+ data.pool.forEach((c, i) => {
+ const li = document.createElement("li");
+ if (i === 0) li.className = "winner";
+ li.innerHTML = `${c.record.title} d=${c.distance.toFixed(2)}`;
+ poolEl.appendChild(li);
+ });
+
+ const point = { left: state.left, right: state.right, dark: state.dark, light: state.light };
+ paintGrid(document.getElementById("brain-grid"), "left", "right", point, data.pool);
+ paintGrid(document.getElementById("mood-grid"), "dark", "light", point, data.pool);
+}
+
+async function loadMeta() {
+ const data = await (await fetch("/api/catalog/meta")).json();
+ const byMode = Object.entries(data.by_mode).map(([k, v]) => `${k}:${v}`).join(" ");
+ document.getElementById("meta").textContent = `${data.total} records · ${byMode}`;
+}
+
+function init() {
+ buildGrid(document.getElementById("brain-grid"));
+ buildGrid(document.getElementById("mood-grid"));
+ document.querySelectorAll("input, select").forEach((el) =>
+ el.addEventListener("input", refresh)
+ );
+ loadMeta();
+ refresh();
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/simulator/static/index.html b/simulator/static/index.html
new file mode 100644
index 0000000..0a89203
--- /dev/null
+++ b/simulator/static/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ HEF — Curator's X-ray
+
+
+
+
+ Experience Filter — Curator's X-ray
+
+
+
+
+
+
+
+
+
+
Pool (nearest first)
+
+
+
+
+
+
+
+
+
diff --git a/simulator/static/style.css b/simulator/static/style.css
new file mode 100644
index 0000000..79371ce
--- /dev/null
+++ b/simulator/static/style.css
@@ -0,0 +1,26 @@
+:root { color-scheme: dark; }
+* { box-sizing: border-box; }
+body { margin: 0; font-family: -apple-system, system-ui, sans-serif; background: #0e0e16; color: #e6e6ee; }
+header { padding: 12px 20px; border-bottom: 1px solid #2a2a3a; display: flex; justify-content: space-between; align-items: baseline; }
+header h1 { font-size: 18px; margin: 0; }
+.meta { font-size: 12px; color: #9a9ab0; }
+main { display: grid; grid-template-columns: 280px 1fr; gap: 20px; padding: 20px; }
+.controls label { display: block; margin: 8px 0; font-size: 13px; }
+.controls input[type=range] { width: 100%; }
+.controls .check { display: flex; gap: 6px; align-items: center; }
+h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: #9a9ab0; }
+.xray { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
+.pick #pick { background: #1a1a2e; border: 1px solid #33334a; border-radius: 8px; padding: 14px; min-height: 80px; }
+.pick .title { font-size: 16px; font-weight: 600; }
+.pick .void { color: #7777aa; font-style: italic; }
+.pool ol { margin: 0; padding-left: 18px; font-size: 13px; }
+.pool li { margin: 4px 0; }
+.pool li.winner { color: #7fffd4; font-weight: 600; }
+.pool .dist { color: #9a9ab0; }
+.maps { grid-column: 1 / -1; display: flex; gap: 40px; }
+.grid5 { display: grid; grid-template-columns: repeat(5, 28px); grid-template-rows: repeat(5, 28px); gap: 3px; }
+.grid5 .cell { background: #1c1c2c; border: 1px solid #2a2a3a; border-radius: 3px; position: relative; }
+.grid5 .cell.cand { background: #3a3a66; }
+.grid5 .cell.point { outline: 2px solid #7fffd4; }
+.grid5 .cell .n { position: absolute; right: 2px; bottom: 1px; font-size: 9px; color: #aab; }
+.label { font-size: 11px; color: #9a9ab0; margin-bottom: 4px; }
diff --git a/tests/test_simulator_api.py b/tests/test_simulator_api.py
index e476617..57e6b24 100644
--- a/tests/test_simulator_api.py
+++ b/tests/test_simulator_api.py
@@ -88,3 +88,15 @@ def test_catalog_meta_reports_counts(client):
assert data["by_mode"]["video"] == 4
assert data["by_mode"]["audio"] == 1
assert set(data["by_status"]) == {"proposed", "approved"}
+
+
+from simulator.app import create_app as _create_app_for_static
+
+
+def test_index_is_served():
+ # The default app mounts the real static dir.
+ client = TestClient(_create_app_for_static())
+ resp = client.get("/")
+ assert resp.status_code == 200
+ assert "text/html" in resp.headers["content-type"]
+ assert "X-ray" in resp.text