From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/live-rules/web/app.js | 294 +++++++++++++++++++++++++++++++++++ prototypes/live-rules/web/core.wasm | Bin 0 -> 32544 bytes prototypes/live-rules/web/index.html | 101 ++++++++++++ prototypes/live-rules/web/style.css | 1 + 4 files changed, 396 insertions(+) create mode 100644 prototypes/live-rules/web/app.js create mode 100755 prototypes/live-rules/web/core.wasm create mode 100644 prototypes/live-rules/web/index.html create mode 100644 prototypes/live-rules/web/style.css (limited to 'prototypes/live-rules/web') diff --git a/prototypes/live-rules/web/app.js b/prototypes/live-rules/web/app.js new file mode 100644 index 0000000..d8ca815 --- /dev/null +++ b/prototypes/live-rules/web/app.js @@ -0,0 +1,294 @@ +"use strict"; + +(() => { + const $ = (id) => document.getElementById(id); + const field = Object.freeze({ tick: 0, y: 1, vy: 2, distance: 3, obstacleX: 4, passed: 5, hits: 6, jumps: 7, running: 8, revision: 9, reason: 10, source: 11, inputs: 12, accepted: 13, mode: 14, impulse: 15, enabled: 16, grounded: 17, obstacleHeight: 18 }); + const reasonText = ["아직 입력 없음", "점프를 받아들였어요.", "점프 입력이 꺼져 있어요.", "땅에 닿아야 다시 뛸 수 있어요.", "공중에서 쓸 점프를 모두 썼어요.", "알 수 없는 입력이에요."]; + const sourceText = ["키보드", "점프 버튼", "CLI"]; + const frameMs = 1000 / 60; + let core = null; + let ready = false; + let applied = null; + let lastTime = null; + let accumulator = 0; + let droppedTicks = 0; + let renderCount = 0; + let historySignature = ""; + let notice = ""; + const nodes = [...document.querySelectorAll("[data-node]")]; + + function text(id, value) { + const element = $(id); + const next = String(value); + if (element.textContent !== next) element.textContent = next; + } + + function get(name) { return core.get(field[name]); } + function counter(name) { return get(name) >>> 0; } + function say(message) { text("live-status", message); } + function formatBytes(value) { return `${(value >>> 0).toLocaleString("ko-KR")} B`; } + function draft() { + return { mode: Number(document.querySelector('input[name="mode"]:checked').value), impulse: Number($("impulse").value), enabled: $("enabled").checked ? 1 : 0 }; + } + function isDirty() { + if (!applied) return false; + const next = draft(); + return next.mode !== applied.mode || next.impulse !== applied.impulse || next.enabled !== applied.enabled; + } + function draftChanged() { + text("impulse-output", $("impulse").value); + const dirty = isDirty(); + text("draft-status", dirty ? "아직 적용하지 않은 변경" : "현재 규칙과 같음"); + $("draft-status").parentElement.classList.toggle("dirty", dirty); + $("apply-button").disabled = !ready || !dirty; + if (dirty) text("apply-feedback", "아직 이전 규칙으로 움직입니다. 적용하면 위치와 시간은 그대로 이어집니다."); + else if (ready) text("apply-feedback", "바꾼 뒤 적용을 눌러주세요. 위치와 진행 시간은 이어집니다."); + } + function syncApplied() { + applied = { mode: get("mode"), impulse: get("impulse"), enabled: get("enabled") }; + document.querySelector(`input[name="mode"][value="${applied.mode}"]`).checked = true; + $("impulse").value = String(applied.impulse); + $("enabled").checked = Boolean(applied.enabled); + draftChanged(); + } + + function renderHistory() { + const entries = []; + for (let i = 0, count = Math.min(8, core.history_count()); i < count; i++) { + entries.push([0, 1, 2, 3].map((part) => core.history_get(i, part) >>> 0)); + } + const signature = JSON.stringify(entries); + if (signature === historySignature) return; + historySignature = signature; + const list = $("history-list"); + list.replaceChildren(); + if (entries.length === 0) { + const empty = document.createElement("li"); + empty.className = "empty-history"; + empty.textContent = "점프하거나 규칙을 적용하면 이곳에 기록됩니다."; + list.append(empty); + return; + } + for (const [tick, kind, detail, revision] of entries) { + const row = document.createElement("li"); + const time = document.createElement("span"); + time.className = "history-time"; + time.textContent = `${tick}t`; + const label = document.createElement("span"); + label.className = "history-label"; + const descriptions = { 1: "점프 허용", 2: reasonText[detail] || "점프 거절", 3: "규칙 적용", 4: "땅에 착지", 5: "장애물에 닿음", 6: "장애물을 지남" }; + label.textContent = `${descriptions[kind] || "상태 변경"} · v${revision}`; + row.append(time, label); + list.append(row); + } + } + + function renderDecision() { + const reason = get("reason"); + const result = $("decision-result"); + result.classList.toggle("accepted", reason === 1); + result.classList.toggle("rejected", reason > 1); + text("decision-result", reason === 0 ? "첫 점프를 기다리고 있어요." : reasonText[reason] || "입력을 확인해주세요."); + if (reason === 0) { + text("decision-explanation", "한 번 누르면 입력이 규칙을 통과하는 과정을 볼 수 있습니다."); + text("decision-meta", "아직 입력 없음"); + $("stale-decision").hidden = true; + } else { + const preY = core.decision_meta(2); + const preJumps = core.decision_meta(3); + const revision = core.decision_meta(1) >>> 0; + text("decision-explanation", `누른 순간: 높이 ${(preY / 100).toFixed(1)} px · 사용한 점프 ${preJumps}회. ${get("running") ? "이후에도 세계는 움직입니다." : "멈춘 상태에서 판단을 살펴보고 있어요."}`); + text("decision-meta", `${sourceText[get("source")] || "알 수 없는 입력"} · ${core.decision_meta(0) >>> 0} tick · v${revision}`); + $("stale-decision").hidden = revision === counter("revision"); + } + for (const node of nodes) { + const value = core.node_value(Number(node.dataset.node)); + if (value < 0) delete node.dataset.result; + else node.dataset.result = String(Boolean(value)); + const output = node.querySelector(".node-value"); + const next = value < 0 ? "대기" : value ? "예" : "아니요"; + if (output.textContent !== next) output.textContent = next; + } + } + + function render() { + if (!ready) return; + renderCount++; + const y = get("y") / 100; + const velocity = get("vy") / 100; + const tick = counter("tick"); + const running = Boolean(get("running")); + const obstacleHeight = get("obstacleHeight"); + const cameraY = Math.min(0, 180 - y); + const viewWidth = $("stage").clientWidth < 500 ? 500 : 960; + $("world").setAttribute("viewBox", `0 ${cameraY} ${viewWidth} 350`); + $("dino").setAttribute("transform", `translate(120 ${230 - y})`); + $("dino-shadow").setAttribute("rx", String(Math.max(10, 27 - y / 12))); + $("dino-shadow").setAttribute("opacity", String(Math.max(0.05, 0.15 - y / 1600))); + $("obstacle").setAttribute("transform", `translate(${get("obstacleX")} ${278 - obstacleHeight})`); + $("obstacle-body").setAttribute("height", String(obstacleHeight)); + $("obstacle-mark").setAttribute("d", `M11 10v${Math.max(0, obstacleHeight - 20)}m10-${Math.max(0, obstacleHeight - 27)}v${Math.max(0, obstacleHeight - 27)}`); + text("running-badge", running ? "실행 중" : "멈춰 있음"); + $("running-badge").classList.toggle("running", running); + text("run-button", running ? "일시정지" : "실행"); + $("run-button").setAttribute("aria-pressed", String(running)); + $("step-button").disabled = running; + text("stage-hint", cameraY < 0 ? "높이에 맞춰 시점이 공룡을 따라갑니다." : running ? "Space 또는 점프 버튼 · 멈춰서 규칙을 바꿔도 좋아요." : "일시정지 상태에서도 규칙과 입력을 확인할 수 있어요."); + text("state-height", `${y.toFixed(1)} px`); + text("state-velocity", `${velocity > 0 ? "+" : ""}${velocity.toFixed(1)} px/틱`); + text("state-ground", get("grounded") ? "땅 위" : "공중"); + text("state-jumps", `${counter("jumps")}회`); + text("state-distance", `${counter("distance").toLocaleString("ko-KR")} px`); + text("state-time", `${(tick / 60).toFixed(2)}초`); + text("state-tick", tick); + text("state-passed", counter("passed")); + text("state-hits", counter("hits")); + text("revision-badge", `v${counter("revision")}`); + text("applied-label", `적용 v${counter("revision")}`); + text("runtime-note", notice || (droppedTicks > 0 ? `화면 지연으로 ${droppedTicks.toLocaleString("ko-KR")}틱을 생략했습니다. 진행 시간은 실제 시간보다 느릴 수 있어요.` : "실행하면 시간이 흐릅니다. 멈춘 뒤 한 프레임씩 살펴볼 수도 있어요.")); + text("scheduling-detail", `고정 60Hz · 한 화면 갱신당 최대 6틱 · 생략한 틱 ${droppedTicks.toLocaleString("ko-KR")}`); + text("bytes-linear", core.memory ? formatBytes(core.memory.buffer.byteLength) : "메모리 내보내기 없음"); + renderDecision(); + renderHistory(); + } + + function jump(source) { + if (!ready) return; + core.input(source); + render(); + say(reasonText[get("reason")] || "입력 처리 완료"); + } + + function setRunning(next) { + core.set_running(next ? 1 : 0); + accumulator = 0; + lastTime = null; + notice = ""; + render(); + say(next ? "실행했습니다." : "멈췄습니다. 규칙을 바꾸거나 한 프레임씩 살펴볼 수 있어요."); + } + + function frame(now) { + if (ready) { + if (get("running") && !document.hidden) { + if (lastTime !== null) { + accumulator += Math.max(0, now - lastTime); + const due = Math.floor(accumulator / frameMs); + if (due > 0) { + const ticks = Math.min(6, due); + droppedTicks += due - ticks; + accumulator -= due * frameMs; + core.advance(ticks); + render(); + } + } + lastTime = now; + } else { + lastTime = null; + accumulator = 0; + } + } + window.requestAnimationFrame(frame); + } + + $("rules-form").addEventListener("input", draftChanged); + $("rules-form").addEventListener("change", draftChanged); + $("rules-form").addEventListener("submit", (event) => { + event.preventDefault(); + if (!ready || !isDirty()) return; + const next = draft(); + if (core.apply_rules(next.mode, next.impulse, next.enabled) !== 0) { + text("apply-feedback", "적용하지 못했습니다. 점프 세기는 6~16이어야 합니다."); + say("규칙을 적용하지 못했습니다."); + return; + } + applied = next; + draftChanged(); + const feedback = `v${counter("revision")} 적용 완료. 위치·속도·진행 시간은 그대로 이어집니다.`; + text("apply-feedback", feedback); + render(); + say(feedback); + }); + $("run-button").addEventListener("click", () => { if (ready) setRunning(!get("running")); }); + $("step-button").addEventListener("click", () => { + if (!ready || get("running")) return; + core.step(); + render(); + say(`한 프레임 진행. 높이 ${(get("y") / 100).toFixed(1)} 픽셀.`); + }); + $("jump-button").addEventListener("click", () => jump(1)); + $("reset-button").addEventListener("click", () => { + if (!ready) return; + core.reset(); + core.set_running(0); + accumulator = 0; + lastTime = null; + droppedTicks = 0; + notice = "처음 상태로 돌아왔습니다. 멈춘 상태에서 다시 시작할 수 있어요."; + syncApplied(); + text("apply-feedback", "위치·진행 시간·규칙·기록을 초기 상태로 되돌렸습니다."); + render(); + say("세계와 규칙을 초기 상태로 되돌렸습니다."); + }); + document.addEventListener("keydown", (event) => { + if (!ready || (event.code !== "Space" && event.key !== " ") || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; + const target = event.target; + if (target instanceof Element && (target.isContentEditable || target.closest("input, textarea, select, button, a[href], summary, [contenteditable], [role=button]"))) return; + event.preventDefault(); + if (event.repeat) return; + jump(0); + }); + document.addEventListener("visibilitychange", () => { + if (!ready || !document.hidden) return; + const wasRunning = Boolean(get("running")); + core.set_running(0); + lastTime = null; + accumulator = 0; + if (wasRunning) { + notice = "다른 탭으로 이동해 자동으로 멈췄습니다. 실행을 누르면 이어집니다."; + render(); + say(notice); + } + }); + window.addEventListener("resize", render); + + Object.defineProperty(window, "pureDemo", { value: Object.freeze({ + snapshot() { + if (!ready) return Object.freeze({ ready: false }); + return Object.freeze({ ready: true, ...Object.fromEntries(Object.keys(field).map((name) => [name, get(name)])), draftDirty: isDirty(), droppedTicks, renderCount, wasmMemoryBytes: core.memory?.buffer.byteLength ?? null, logicalBytes: core.bytes(6), decisionTick: core.decision_meta(0), decisionRevision: core.decision_meta(1), decisionNodes: Object.freeze(nodes.map((node) => core.node_value(Number(node.dataset.node)))) }); + } + }), writable: false, configurable: false }); + + async function boot() { + try { + const response = await fetch("core.wasm", { cache: "no-store" }); + if (!response.ok) throw new Error(`core.wasm HTTP ${response.status}`); + const { instance } = await WebAssembly.instantiate(await response.arrayBuffer(), {}); + core = instance.exports; + for (const name of ["reset", "set_running", "advance", "step", "input", "apply_rules", "get", "node_value", "decision_meta", "history_count", "history_get", "bytes"]) { + if (typeof core[name] !== "function") throw new Error(`필요한 코어 함수가 없습니다: ${name}`); + } + core.reset(); + core.set_running(0); + ready = true; + $("rule-controls").disabled = false; + for (const id of ["run-button", "step-button", "jump-button", "reset-button"]) $(id).disabled = false; + $("workspace").setAttribute("aria-busy", "false"); + $("load-message").hidden = true; + syncApplied(); + ["rules", "state", "decision", "history", "engine", "graph", "total"].forEach((name, i) => text(`bytes-${name}`, formatBytes(core.bytes(i)))); + render(); + say("준비되었습니다. 멈춘 상태입니다. 점프 버튼을 눌러보세요."); + window.requestAnimationFrame(frame); + } catch (error) { + ready = false; + $("workspace").setAttribute("aria-busy", "false"); + $("load-message").hidden = true; + $("load-error").hidden = false; + text("running-badge", "실행기 없음"); + text("error-detail", error instanceof Error ? error.message : String(error)); + for (const control of document.querySelectorAll("button, #rule-controls")) control.disabled = true; + } + } + boot(); +})(); diff --git a/prototypes/live-rules/web/core.wasm b/prototypes/live-rules/web/core.wasm new file mode 100755 index 0000000..8c79903 Binary files /dev/null and b/prototypes/live-rules/web/core.wasm differ diff --git a/prototypes/live-rules/web/index.html b/prototypes/live-rules/web/index.html new file mode 100644 index 0000000..45ff700 --- /dev/null +++ b/prototypes/live-rules/web/index.html @@ -0,0 +1,101 @@ + + + + + + + Pure — 규칙 실험실 + + + + + + + +
+
+

작게 만들고, 직접 바꿔보기

뛰는 도중에, 규칙을 바꿔보세요.

같은 공룡, 같은 순간. 점프 규칙만 바꾸면 무엇이 달라질까요?

+

살아 있는 규칙
첫 번째 연습장

+
+ +
작은 실행기를 불러오고 있습니다…
+ + +
+
+

01연습장

불러오는 중
+
+ + 공룡 점프 연습장규칙을 적용하고 점프 버튼이나 스페이스 키로 움직여 보세요. 현재 상태는 연습장 아래에 표시됩니다. + + + + + + + + + + + + + + + PURE · RULE PLAYGROUND + +
일시정지 상태에서도 규칙과 입력을 확인할 수 있어요.
+
+
+
+ + +
+

실행하면 시간이 흐릅니다. 멈춘 뒤 한 프레임씩 살펴볼 수도 있어요.

+
+
높이—
세로 속도—
발 디딤—
사용한 점프—
진행 거리—
진행 시간—
+
+
지나간 장애물 0닿은 장애물 0tick 0
+
+ + +
+ +
+

03왜 그렇게 움직였나요?

마지막 점프 요청
+

첫 점프를 기다리고 있어요.

한 번 누르면 입력이 규칙을 통과하는 과정을 볼 수 있습니다.

아직 입력 없음
+ +
    +
  1. 01요청 도착대기
  2. 02입력 켜짐대기
  3. 03땅에 있음대기
  4. 04점프 여유대기
  5. 05방식에 맞음대기
  6. 06점프 허용대기
  7. +
+

요청과 입력 켜짐을 확인하고, 선택한 방식에 따라 ‘땅에 있음’ 또는 ‘점프 여유’를 판단합니다. 기록은 입력을 받은 순간의 상태입니다.

+

최근 변화 최대 8개 표시

  1. 점프하거나 규칙을 적용하면 이곳에 기록됩니다.
+
+ +
실험의 범위와 메모리 보기기술적인 내용은 여기에

무엇을 확인하는 실험인가요?

세계가 실행되는 동안 점프 규칙을 바꾸고, 상태를 유지하며, 입력 판단을 살펴보는 작은 시제품입니다. 물리 계산과 규칙 판단은 Zig로 작성한 동일한 WebAssembly 코어에서 처리합니다. 화면은 그 결과를 보여줍니다.

시간은 고정 60Hz 틱으로 진행합니다. 느린 화면에서는 한 번에 최대 6틱을 처리하며, 생략한 틱 수를 아래에 표시합니다. 실제 시간과 정확히 같다는 보장은 없습니다.

높이·속도·거리는 세계 좌표(px)로 표시합니다. 높은 점프에서는 시점이 공룡을 따라 위로 이동합니다. 화면 크기와 시점은 물리 계산을 바꾸지 않습니다.

아직 실행 전 · 생략한 틱 0

코어의 자료 구조 크기

코어가 보고한 논리 자료 크기 · 실제 프로세스 메모리와 다름
규칙—
세계 상태—
마지막 판단—
기록과 관리 정보—
엔진 전체 · 정렬 포함—
고정 규칙 그래프—
엔진 + 그래프 · 논리 합계—
WASM 선형 메모리 · 별도—

자료 구조 크기는 Zig 타입 배치 기준입니다. 고정 그래프는 컴파일 과정에서 코드에 통합될 수 있어, 이 수치를 실제 RAM 사용량으로 읽으면 안 됩니다. WASM 선형 메모리는 실제 할당된 별도 범위이며 위 합계와 더하지 않습니다. 브라우저·화면·코드·프로세스 전체 메모리(RSS)는 이 표에 포함되지 않습니다. M3 성능이나 최적화 우위를 입증하는 측정도 아닙니다.

+

+
+ + + diff --git a/prototypes/live-rules/web/style.css b/prototypes/live-rules/web/style.css new file mode 100644 index 0000000..95207c8 --- /dev/null +++ b/prototypes/live-rules/web/style.css @@ -0,0 +1 @@ +:root{color-scheme:light;--page:#f5f4ee;--card:#fffefa;--ink:#27372b;--muted:#74806f;--line:#e3e6dc;--green:#356548;--green-light:#edf3e9;--amber:#a36a31;--amber-light:#faf0df;--radius:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:15px}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--page)}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.46}a{color:inherit}button,input,a,summary,[tabindex]{-webkit-tap-highlight-color:transparent}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:3px solid #b37c39;outline-offset:4px}button,input{accent-color:var(--green)}[hidden]{display:none!important}.site-header{height:94px;max-width:1256px;padding:0 36px;margin:auto;display:flex;align-items:center;justify-content:space-between}.brand{display:inline-flex;align-items:center;gap:13px;font-size:19px;font-weight:750;text-decoration:none;letter-spacing:-.5px}.brand-mark{display:grid;place-items:center;width:35px;height:35px;background:var(--ink);color:var(--page);border-radius:9px;font:700 27px Georgia,serif;padding-bottom:4px}.brand-separator{margin:0 10px;color:#b1b7a9;font-weight:400}.brand-subtitle{font-size:14px;font-weight:500;letter-spacing:-.3px}.experiment-tag{display:inline-flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.status-dot,.tiny-dot{display:inline-block;width:6px;height:6px;background:#82a06e;border-radius:50%}main{max-width:1256px;margin:0 auto;padding:28px 36px 0}.intro{display:flex;align-items:center;justify-content:space-between;gap:32px;margin-bottom:30px}.eyebrow{font-size:11px;letter-spacing:1.25px;font-weight:650;color:var(--muted);margin:0 0 11px}h1{font-size:clamp(25px,2.6vw,34px);font-weight:650;line-height:1.35;letter-spacing:-1.3px;margin:0 0 11px}.intro-copy{color:var(--muted);font-size:14px;line-height:1.7;margin:0}.prototype-label{flex-shrink:0;text-align:right;color:var(--green);font-size:12px;font-weight:600;line-height:1.85;margin:0;border-left:1px solid var(--line);padding-left:35px}.prototype-label span{color:var(--muted);font-size:11px;font-weight:400}.notice,.load-error{border:1px solid var(--line);padding:16px 20px;border-radius:10px;margin-bottom:20px;font-size:13px;line-height:1.7;background:var(--card)}.load-error{background:#fff0e8;border-color:#e9c6b7;color:#7b392b}.load-error h2{font-size:17px}.load-error pre{overflow:auto;padding:12px;background:#fff9f3;border-radius:7px;font-size:12px}.error-detail{font-family:ui-monospace,monospace;font-size:11px;overflow-wrap:anywhere}.workbench{display:grid;grid-template-columns:minmax(0,1fr) 322px;gap:20px;align-items:start}.panel{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);overflow:hidden}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:22px 24px 19px}.panel-heading h2{font-size:14px;font-weight:650;margin:0;letter-spacing:-.3px;display:flex;align-items:center;gap:9px}.section-number{color:#9aa28f;font:11px ui-monospace,monospace;font-weight:400}.badge{font-size:10px;line-height:1;padding:6px 8px;background:#f0f1eb;color:#6e7967;border-radius:5px;font-variant-numeric:tabular-nums;white-space:nowrap}.badge.running{background:var(--green-light);color:var(--green)}.stage{position:relative;margin:0 14px;background:#f5f7f0;border:1px solid #e7ebdf;border-radius:10px;overflow:hidden;line-height:0}#world{display:block;width:100%;height:auto;min-height:230px}.stage-hint{position:absolute;top:16px;left:17px;right:15px;display:flex;align-items:center;gap:7px;color:#7a866f;font-size:10px;line-height:1.5}.tiny-dot{width:4px;height:4px;flex-shrink:0;background:#a5b496}.transport{display:flex;align-items:center;gap:9px;padding:20px 24px 0;flex-wrap:wrap}.transport-primary{display:flex;gap:7px}.button{border:1px solid transparent;border-radius:7px;line-height:1.25;font-size:12px;font-weight:550;padding:11px 15px;transition:background .15s,border-color .15s,transform .15s}.button:active:not(:disabled){transform:translateY(1px)}.button-dark{background:var(--ink);color:white;min-width:78px}.button-dark:hover:not(:disabled){background:#425544}.button-quiet{background:transparent;border-color:var(--line);color:var(--ink)}.button-quiet:hover:not(:disabled),.button-reset:hover:not(:disabled){background:#f2f3ec}.button-jump{margin-left:auto;background:var(--green-light);border-color:#dce6d5;color:var(--green);display:flex;align-items:center;gap:20px}.button-jump:hover:not(:disabled){background:#deead7}.button-jump kbd{font:9px ui-monospace,monospace;padding:2px 4px;border:1px solid #c7d5be;border-radius:3px;color:#6d865e}.button-reset{background:transparent;color:var(--muted);font-size:11px;padding-left:9px;padding-right:9px}.runtime-note{font-size:11px;color:var(--muted);line-height:1.7;margin:12px 24px 21px;min-height:19px}.state-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:23px 18px;padding:22px 24px;border-top:1px solid var(--line)}.state-grid>div{display:flex;flex-direction:column;gap:7px}.state-grid span{color:var(--muted);font-size:10px}.state-grid strong{font-size:16px;font-weight:550;font-variant-numeric:tabular-nums;letter-spacing:-.5px}.world-summary{display:flex;gap:22px;padding:14px 24px;border-top:1px solid var(--line);font-size:10px;color:var(--muted);background:#fcfcf7}.world-summary b{margin-left:5px;font-weight:550;color:#58694f;font-variant-numeric:tabular-nums}.tick-label{margin-left:auto;font-family:ui-monospace,monospace}.rules-panel .panel-heading{padding-bottom:22px}#rules-form{padding:0 23px}fieldset{border:0;padding:0;margin:0;min-width:0}.field-label{font-size:11px;font-weight:600;display:block;margin:0 0 12px}.mode-options{display:flex;flex-direction:column;gap:8px;margin-bottom:25px}.mode-option{position:relative;display:flex;align-items:center;gap:10px;padding:14px 12px;border:1px solid var(--line);border-radius:8px;cursor:pointer;background:var(--card);min-width:0}.mode-option:has(input:checked){border-color:#8fa783;background:#f1f5ec}.mode-option:has(input:focus-visible){outline:3px solid #b37c39;outline-offset:3px}.mode-option input{position:absolute;opacity:0;width:1px;height:1px}.mode-option>span:first-of-type{flex:1}.mode-option b,.enabled-option b{display:block;font-size:12px;font-weight:550}.mode-option small,.enabled-option small{display:block;font-size:10px;line-height:1.6;color:var(--muted);margin-top:5px;word-break:keep-all}.radio-mark{width:13px;height:13px;border:1px solid #c2cbb9;border-radius:50%;display:grid;place-items:center;flex-shrink:0}.mode-option input:checked~.radio-mark{border-color:var(--green)}.mode-option input:checked~.radio-mark:after{content:"";background:var(--green);width:5px;height:5px;border-radius:50%}.force-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.force-heading .field-label{margin:0}.force-heading output{background:#f1f3ec;font-size:12px;font-variant-numeric:tabular-nums;padding:4px 9px;border:1px solid var(--line);border-radius:5px;min-width:34px;text-align:center}input[type=range]{display:block;width:100%;margin:14px 0 9px;cursor:pointer;height:16px}.range-labels{display:flex;justify-content:space-between;font-size:9px;color:#8a937f}.field-help{font-size:10px;color:var(--muted);margin:10px 0 21px;line-height:1.6}.enabled-option{display:flex;align-items:center;position:relative;cursor:pointer;gap:15px;border-top:1px solid var(--line);padding-top:19px}.enabled-option>span:first-child{flex:1}.enabled-option input{position:absolute;right:4px;opacity:0;width:28px;height:20px}.switch{width:31px;height:19px;background:#ced4c7;border:1px solid #c5cdbe;border-radius:20px;padding:2px;flex-shrink:0;transition:background .15s}.switch:after{content:"";display:block;width:13px;height:13px;background:white;border-radius:50%;box-shadow:0 1px 2px #0002;transition:transform .15s}.enabled-option input:checked+.switch{background:var(--green);border-color:var(--green)}.enabled-option input:checked+.switch:after{transform:translateX(12px)}.enabled-option input:focus-visible+.switch{outline:3px solid #b37c39;outline-offset:4px}.apply-area{margin-top:23px}.draft-line{display:flex;justify-content:space-between;gap:10px;font-size:10px;color:var(--muted);margin-bottom:9px}.draft-line.dirty #draft-status{color:var(--amber);font-weight:600}.button-apply{display:block;width:100%;background:var(--green);color:white;font-size:12px;padding:12px}.button-apply:hover:not(:disabled){background:#284e36}#apply-feedback{font-size:10px;line-height:1.7;min-height:35px;margin:10px 0 22px;color:var(--muted)}.try-card{background:#f4f5ee;border-top:1px solid var(--line);padding:20px 23px}.try-card .eyebrow{font-size:10px;letter-spacing:.8px;display:block;margin-bottom:11px}.try-card ol{font-size:11px;line-height:1.8;padding-left:17px;margin:0;color:#627058}.try-card li+li{margin-top:5px}.try-card b{font-weight:600}.try-card p{font-size:10px;color:#859079;line-height:1.65;margin:12px 0 0}.decision-panel{margin-top:20px}.muted-small{font-size:10px;color:var(--muted)}.decision-summary{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:0 24px 19px}.decision-result{margin:0 0 7px;font-size:18px;letter-spacing:-.6px;font-weight:550}.decision-result.accepted{color:var(--green)}.decision-result.rejected{color:var(--amber)}.decision-explanation{font-size:11px;color:var(--muted);line-height:1.7;margin:0}.decision-meta{font-size:10px;color:var(--muted);text-align:right;line-height:1.9;white-space:nowrap;font-variant-numeric:tabular-nums}.stale-note{font-size:11px;color:var(--amber);background:var(--amber-light);margin:0 24px 17px;padding:10px 12px;border-radius:6px;line-height:1.7}.decision-path{list-style:none;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin:0;padding:0 24px}.decision-path li{position:relative;border:1px solid var(--line);background:#fafbf6;border-radius:8px;padding:12px;min-width:0}.decision-path li:not(:last-child):after{position:absolute;right:-10px;top:50%;content:"›";color:#a4af9a;font-size:12px;line-height:0}.decision-path li[data-result=true]{background:#f0f5eb;border-color:#d4e2ca}.decision-path li[data-result=false]{background:#fbf6ed;border-color:#ebdfc9}.node-step{display:block;font:9px ui-monospace,monospace;color:#a2aa97;margin-bottom:13px}.decision-path strong{display:block;font-size:11px;font-weight:550;white-space:nowrap}.node-value{display:block;font-size:10px;color:#8b9580;margin-top:6px}.decision-path li[data-result=true] .node-value{color:var(--green)}.decision-path li[data-result=false] .node-value{color:var(--amber)}.path-caption{font-size:10px;line-height:1.7;color:#85907b;padding:0 24px;margin:12px 0 21px}.history-section{border-top:1px solid var(--line);padding:19px 24px 20px;display:grid;grid-template-columns:145px minmax(0,1fr);gap:25px}.history-section h3{font-size:11px;font-weight:550;margin:3px 0;line-height:1.6}.history-section h3 span{display:block;font-size:9px;color:#939d88;font-weight:400;margin-top:4px}.history-list{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 25px}.history-list li{display:flex;gap:10px;align-items:baseline;font-size:10px;line-height:1.7;color:#69755e;min-width:0}.history-list .history-time{font:9px ui-monospace,monospace;color:#9aa38f;flex-shrink:0}.history-list .history-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-list .empty-history{grid-column:1/-1;color:#8d9781}.research-panel{margin-top:20px;border:1px solid var(--line);border-radius:12px;background:#f0f1e9;overflow:hidden}.research-panel summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:12px;padding:19px 23px;font-size:12px;font-weight:500}.research-panel summary::-webkit-details-marker{display:none}.research-panel summary:before{content:"+";font:16px ui-monospace,monospace;color:#8a947e}.research-panel[open] summary:before{content:"−"}.summary-caption{margin-left:auto;font-size:10px;font-weight:400;color:#8d9781}.research-body{border-top:1px solid var(--line);display:grid;grid-template-columns:1fr 1fr;gap:60px;padding:24px}.research-body h2{font-size:12px;margin:0 0 14px;font-weight:600}.research-body p{font-size:11px;line-height:1.85;color:#74806a;margin:0 0 13px}.memory-table{width:100%;border-collapse:collapse;font-size:10px;line-height:1.6;font-variant-numeric:tabular-nums}.memory-table caption{text-align:left;font-size:9px;color:#8c967f;padding-bottom:9px}.memory-table th{text-align:left;font-weight:400;padding:5px 0;color:#6b7762}.memory-table td{text-align:right;color:#4f6249;padding:5px 0;font-family:ui-monospace,monospace}.memory-table .total-row th,.memory-table .total-row td{border-top:1px solid #d6ddcd;font-weight:600;padding-top:9px}.research-body .metric-note{font-size:10px;margin-top:12px;color:#8a947f}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}footer{max-width:1256px;margin:0 auto;padding:30px 36px 35px;font-size:10px;color:#9ba48f;display:flex;align-items:center;gap:9px}footer span{color:#bdc4b4}@media(min-width:1100px){.state-grid{grid-template-columns:repeat(6,minmax(0,1fr));gap:10px}.state-grid strong{font-size:15px}.stage-hint{font-size:11px}}@media(max-width:900px){.site-header{height:77px;padding:0 24px}main{padding:22px 24px 0}.workbench{grid-template-columns:minmax(0,1fr) 286px;gap:15px}.panel-heading{padding:20px 19px 17px}#rules-form{padding:0 18px}.try-card{padding:18px}.transport{padding:17px 19px 0;gap:7px}.button{padding:10px 11px}.button-jump{gap:12px}.runtime-note{margin-left:19px;margin-right:19px}.state-grid{padding:19px}.world-summary{padding:13px 19px;gap:12px}.decision-path{gap:9px;padding:0 19px}.decision-path li{padding:10px 8px}.decision-path li:not(:last-child):after{right:-8px}.decision-path strong{font-size:10px}.decision-summary{padding-left:19px;padding-right:19px}.history-section{grid-template-columns:110px 1fr;padding-left:19px;padding-right:19px;gap:15px}.history-list{column-gap:14px}.intro{gap:20px}.prototype-label{display:none}.research-body{gap:32px}footer{padding-left:24px;padding-right:24px}}@media(max-width:700px){.site-header{height:70px;padding:0 18px}.brand{gap:9px;font-size:18px}.brand-separator{margin:0 6px}.brand-subtitle{font-size:12px}.experiment-tag{font-size:10px}main{padding:18px 16px 0}.intro{margin-bottom:24px}.eyebrow{font-size:10px}h1{font-size:25px;letter-spacing:-1px}.intro-copy{font-size:12px;word-break:keep-all}.workbench{grid-template-columns:1fr;gap:16px}.stage{margin:0 11px}#world{min-height:200px}.stage-hint{font-size:9px;left:11px;top:12px;max-width:280px}.transport{padding-left:17px;padding-right:17px;flex-wrap:nowrap;gap:6px}.button{font-size:11px;padding:11px 10px}.button-jump{gap:10px}.button-jump kbd{font-size:8px}.button-reset{font-size:10px;padding:10px 5px}.button-dark{min-width:69px}.state-grid{grid-template-columns:repeat(3,1fr);gap:20px}.state-grid strong{font-size:16px}.rules-panel .panel-heading{padding-bottom:19px}.mode-options{flex-direction:row;gap:9px;margin-bottom:21px}.mode-option{flex:1;padding:13px 11px;gap:7px}.mode-option b{font-size:11px}.mode-option small{font-size:9px}.radio-mark{width:12px;height:12px}.force-heading{margin-top:4px}.enabled-option{padding-top:15px}.apply-area{margin-top:21px}.try-card{padding:19px}.try-card li+li{margin-top:2px}.decision-panel{margin-top:16px}.decision-summary{align-items:flex-start;gap:15px}.decision-result{font-size:16px}.decision-explanation{font-size:10px}.decision-meta{font-size:9px}.decision-path{grid-template-columns:repeat(3,1fr);gap:9px}.decision-path li{padding:12px}.decision-path strong{font-size:11px}.decision-path li:nth-child(3):after{display:none}.node-step{margin-bottom:9px}.path-caption{padding:0 19px;font-size:9px}.history-section{display:block}.history-section h3{display:flex;justify-content:space-between;margin:0 0 13px}.history-section h3 span{margin:0}.history-list{grid-template-columns:1fr;gap:5px}.history-list li{font-size:10px}.stale-note{margin-left:19px;margin-right:19px;font-size:10px}.research-panel{margin-top:16px}.research-panel summary{padding:17px;font-size:11px}.summary-caption{font-size:9px}.research-body{grid-template-columns:1fr;gap:19px;padding:19px}.research-body>div+div{border-top:1px solid var(--line);padding-top:20px}footer{padding:24px 18px;font-size:9px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition:none!important}} -- cgit v1.2.3