Spaces:
Running
Running
Bright theme and live motion: hero, picture reel, decision stream, scan beam, egg result stamps
Browse files- app.js +53 -8
- index.html +21 -4
- style.css +275 -190
app.js
CHANGED
|
@@ -203,6 +203,8 @@ const samplesReady = fetch("examples/samples.json")
|
|
| 203 |
SAMPLES = list;
|
| 204 |
list.forEach((s, i) => $("sampleSelect").insertAdjacentHTML("beforeend", `<option value="${i}">${i + 1}. ${esc(s.name)}</option>`));
|
| 205 |
demoFiles = [...new Set(list.flatMap((s) => s.files))];
|
|
|
|
|
|
|
| 206 |
return list;
|
| 207 |
})
|
| 208 |
.catch((e) => console.warn("samples.json", e));
|
|
@@ -434,6 +436,14 @@ function eggShow(blob, r, gold) {
|
|
| 434 |
`<details class="prompts"><summary>Prompts sent to the model</summary><pre>input: ${esc(r.queryText)} + image\n${r.optTexts.map((t, i) => `option ${EGG.options[i].key}: ${esc(t)} (cos ${r.cos[i].toFixed(4)})`).join("\n")}</pre></details>`;
|
| 435 |
$("eggResult").hidden = false;
|
| 436 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
async function eggBusy(fn) {
|
| 438 |
const btns = [$("eggRunAll"), ...document.querySelectorAll(".eggthumb")];
|
| 439 |
btns.forEach((b) => (b.disabled = true));
|
|
@@ -444,9 +454,11 @@ $("eggGrid").querySelectorAll(".eggthumb").forEach((b) =>
|
|
| 444 |
b.addEventListener("click", () => eggBusy(async () => {
|
| 445 |
const p = EGG_PHOTOS[+b.dataset.i];
|
| 446 |
$("eggSummary").textContent = model ? "Deciding..." : "Loading the model...";
|
|
|
|
| 447 |
const blob = await fetchBlob(p.file);
|
| 448 |
-
const r = await eggDecide(blob);
|
| 449 |
eggShow(blob, r, p.gold);
|
|
|
|
| 450 |
$("eggSummary").textContent = "";
|
| 451 |
}))
|
| 452 |
);
|
|
@@ -462,14 +474,17 @@ async function eggRunAll() {
|
|
| 462 |
const tb = $("eggTable").querySelector("tbody");
|
| 463 |
tb.innerHTML = "";
|
| 464 |
$("eggTable").hidden = false;
|
|
|
|
| 465 |
let ok = 0, ms = 0;
|
| 466 |
const rows = [];
|
| 467 |
for (let i = 0; i < EGG_PHOTOS.length; i++) {
|
| 468 |
const p = EGG_PHOTOS[i];
|
| 469 |
$("eggSummary").textContent = model ? `Checking ${i + 1} / ${EGG_PHOTOS.length}...` : "Loading the model...";
|
|
|
|
| 470 |
const blob = await fetchBlob(p.file);
|
| 471 |
const r = await eggDecide(blob);
|
| 472 |
const pred = EGG.options[r.top].key;
|
|
|
|
| 473 |
ok += pred === p.gold ? 1 : 0;
|
| 474 |
ms += r.totalMs;
|
| 475 |
rows.push({ file: p.file, gold: p.gold, pred, probs: r.probs, cos: r.cos, ms: r.totalMs });
|
|
@@ -609,7 +624,9 @@ function paintOverlay() {
|
|
| 609 |
const sure = conf >= c.threshold;
|
| 610 |
ov.classList.toggle("uncertain", !sure);
|
| 611 |
$("ovQuestion").textContent = c.title;
|
| 612 |
-
|
|
|
|
|
|
|
| 613 |
$("ovPct").textContent = sure ? `${(conf * 100).toFixed(0)}% confidence` : `best guess ${c.options[top].key.replace(/_/g, " ")} at ${(conf * 100).toFixed(0)}%, below the ${Math.round(c.threshold * 100)}% gate: would escalate`;
|
| 614 |
$("ringFg").style.strokeDashoffset = RING_LEN * (1 - conf);
|
| 615 |
}
|
|
@@ -647,6 +664,31 @@ async function scoreDeck(embs) {
|
|
| 647 |
paintOverlay();
|
| 648 |
paintGate();
|
| 649 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
function hud(ms) {
|
| 651 |
const now = performance.now();
|
| 652 |
if (live.lastDone) {
|
|
@@ -698,7 +740,7 @@ function renderDemo(now) {
|
|
| 698 |
let t = (now - demoStart) / dur;
|
| 699 |
if (t >= 1) { demoIdx = (demoIdx + 1) % demoImgs.length; demoStart = now; t = 0; }
|
| 700 |
const img = demoImgs[demoIdx];
|
| 701 |
-
feedCtx.fillStyle = "#
|
| 702 |
feedCtx.fillRect(0, 0, feed.width, feed.height);
|
| 703 |
if (img && img.width) {
|
| 704 |
// slow zoom, like a handheld camera; the whole image stays visible
|
|
@@ -713,13 +755,13 @@ function renderDemo(now) {
|
|
| 713 |
function drawAudio() {
|
| 714 |
fitCanvas();
|
| 715 |
const a = live.analyser;
|
| 716 |
-
feedCtx.fillStyle = "#
|
| 717 |
feedCtx.fillRect(0, 0, feed.width, feed.height);
|
| 718 |
if (a) {
|
| 719 |
const buf = new Float32Array(a.fftSize);
|
| 720 |
a.getFloatTimeDomainData(buf);
|
| 721 |
let rms = 0; for (const x of buf) rms += x * x; rms = Math.sqrt(rms / buf.length);
|
| 722 |
-
feedCtx.strokeStyle = "#
|
| 723 |
feedCtx.lineWidth = Math.max(2, feed.height / 200);
|
| 724 |
feedCtx.beginPath();
|
| 725 |
for (let i = 0; i < buf.length; i++) {
|
|
@@ -729,11 +771,11 @@ function drawAudio() {
|
|
| 729 |
}
|
| 730 |
feedCtx.stroke();
|
| 731 |
const lvl = Math.min(1, rms * 8);
|
| 732 |
-
feedCtx.fillStyle = "rgba(
|
| 733 |
feedCtx.fillRect(14, feed.height * 0.2, 10, feed.height * 0.5);
|
| 734 |
-
feedCtx.fillStyle = "#
|
| 735 |
feedCtx.fillRect(14, feed.height * 0.2 + feed.height * 0.5 * (1 - lvl), 10, feed.height * 0.5 * lvl);
|
| 736 |
-
feedCtx.fillStyle = "rgba(
|
| 737 |
feedCtx.font = `${Math.round(feed.height / 34)}px system-ui`;
|
| 738 |
feedCtx.fillText(`Listening in 2 s windows (${live.src === "sound" ? "sound file" : "microphone"}), 16 kHz mono`, 34, feed.height * 0.16);
|
| 739 |
}
|
|
@@ -773,6 +815,7 @@ async function visionLoop(token) {
|
|
| 773 |
if (token !== live.token) break;
|
| 774 |
await scoreDeck(embs);
|
| 775 |
hud(performance.now() - t0);
|
|
|
|
| 776 |
} catch (e) { console.error(e); setStatus("error", e.message); await sleep(1000); }
|
| 777 |
}
|
| 778 |
const gap = targetGap() - (performance.now() - t0);
|
|
@@ -895,6 +938,7 @@ async function startLive(opts = {}) {
|
|
| 895 |
rafId = requestAnimationFrame(drawAudio);
|
| 896 |
}
|
| 897 |
live.running = true;
|
|
|
|
| 898 |
live.fps = live.lat = live.lastDone = 0;
|
| 899 |
$("idle").hidden = true;
|
| 900 |
$("stopBtn").disabled = false;
|
|
@@ -920,6 +964,7 @@ function pickFile(accept) {
|
|
| 920 |
function stopLive(showIdle = true) {
|
| 921 |
live.running = false;
|
| 922 |
live.token++;
|
|
|
|
| 923 |
cancelAnimationFrame(rafId);
|
| 924 |
if (live.stream) { live.stream.getTracks().forEach((t) => t.stop()); live.stream = null; }
|
| 925 |
if (live.audioCtx) { live.audioCtx.close().catch(() => {}); live.audioCtx = null; live.analyser = null; }
|
|
|
|
| 203 |
SAMPLES = list;
|
| 204 |
list.forEach((s, i) => $("sampleSelect").insertAdjacentHTML("beforeend", `<option value="${i}">${i + 1}. ${esc(s.name)}</option>`));
|
| 205 |
demoFiles = [...new Set(list.flatMap((s) => s.files))];
|
| 206 |
+
const reelFiles = [...demoFiles, ...EGG_PHOTOS.map((p) => p.file)];
|
| 207 |
+
$("reel").innerHTML = [...reelFiles, ...reelFiles].map((f) => `<img src="${f}" alt="" loading="lazy" />`).join("");
|
| 208 |
return list;
|
| 209 |
})
|
| 210 |
.catch((e) => console.warn("samples.json", e));
|
|
|
|
| 436 |
`<details class="prompts"><summary>Prompts sent to the model</summary><pre>input: ${esc(r.queryText)} + image\n${r.optTexts.map((t, i) => `option ${EGG.options[i].key}: ${esc(t)} (cos ${r.cos[i].toFixed(4)})`).join("\n")}</pre></details>`;
|
| 437 |
$("eggResult").hidden = false;
|
| 438 |
}
|
| 439 |
+
function eggThumb(i) { return $("eggGrid").querySelector(`.eggthumb[data-i="${i}"]`); }
|
| 440 |
+
function stampEgg(i, pred, gold, conf) {
|
| 441 |
+
const t = eggThumb(i);
|
| 442 |
+
if (!t) return;
|
| 443 |
+
t.classList.remove("busy");
|
| 444 |
+
t.querySelector(".stamp")?.remove();
|
| 445 |
+
t.insertAdjacentHTML("beforeend", `<span class="stamp ${pred === "good" ? "good" : "bad"} ${pred === gold ? "" : "miss"}" title="${pred === gold ? "matches gold" : "differs from gold"}">${esc(pred)} ${Math.round(conf * 100)}%</span>`);
|
| 446 |
+
}
|
| 447 |
async function eggBusy(fn) {
|
| 448 |
const btns = [$("eggRunAll"), ...document.querySelectorAll(".eggthumb")];
|
| 449 |
btns.forEach((b) => (b.disabled = true));
|
|
|
|
| 454 |
b.addEventListener("click", () => eggBusy(async () => {
|
| 455 |
const p = EGG_PHOTOS[+b.dataset.i];
|
| 456 |
$("eggSummary").textContent = model ? "Deciding..." : "Loading the model...";
|
| 457 |
+
b.classList.add("busy");
|
| 458 |
const blob = await fetchBlob(p.file);
|
| 459 |
+
const r = await eggDecide(blob).finally(() => b.classList.remove("busy"));
|
| 460 |
eggShow(blob, r, p.gold);
|
| 461 |
+
stampEgg(+b.dataset.i, EGG.options[r.top].key, p.gold, r.probs[r.top]);
|
| 462 |
$("eggSummary").textContent = "";
|
| 463 |
}))
|
| 464 |
);
|
|
|
|
| 474 |
const tb = $("eggTable").querySelector("tbody");
|
| 475 |
tb.innerHTML = "";
|
| 476 |
$("eggTable").hidden = false;
|
| 477 |
+
document.querySelectorAll(".eggthumb .stamp").forEach((x) => x.remove());
|
| 478 |
let ok = 0, ms = 0;
|
| 479 |
const rows = [];
|
| 480 |
for (let i = 0; i < EGG_PHOTOS.length; i++) {
|
| 481 |
const p = EGG_PHOTOS[i];
|
| 482 |
$("eggSummary").textContent = model ? `Checking ${i + 1} / ${EGG_PHOTOS.length}...` : "Loading the model...";
|
| 483 |
+
eggThumb(i)?.classList.add("busy");
|
| 484 |
const blob = await fetchBlob(p.file);
|
| 485 |
const r = await eggDecide(blob);
|
| 486 |
const pred = EGG.options[r.top].key;
|
| 487 |
+
stampEgg(i, pred, p.gold, r.probs[r.top]);
|
| 488 |
ok += pred === p.gold ? 1 : 0;
|
| 489 |
ms += r.totalMs;
|
| 490 |
rows.push({ file: p.file, gold: p.gold, pred, probs: r.probs, cos: r.cos, ms: r.totalMs });
|
|
|
|
| 624 |
const sure = conf >= c.threshold;
|
| 625 |
ov.classList.toggle("uncertain", !sure);
|
| 626 |
$("ovQuestion").textContent = c.title;
|
| 627 |
+
const label = sure ? c.options[top].key.replace(/_/g, " ") : `uncertain`;
|
| 628 |
+
const ovl = $("ovLabel");
|
| 629 |
+
if (ovl.textContent !== label) { ovl.textContent = label; ovl.classList.remove("pop"); void ovl.offsetWidth; ovl.classList.add("pop"); }
|
| 630 |
$("ovPct").textContent = sure ? `${(conf * 100).toFixed(0)}% confidence` : `best guess ${c.options[top].key.replace(/_/g, " ")} at ${(conf * 100).toFixed(0)}%, below the ${Math.round(c.threshold * 100)}% gate: would escalate`;
|
| 631 |
$("ringFg").style.strokeDashoffset = RING_LEN * (1 - conf);
|
| 632 |
}
|
|
|
|
| 664 |
paintOverlay();
|
| 665 |
paintGate();
|
| 666 |
}
|
| 667 |
+
// Decision stream: one row per real decision on the primary card, newest first.
|
| 668 |
+
// A row is added when the verdict changes, or at most every 1.5 s while it holds.
|
| 669 |
+
const thumbCv = document.createElement("canvas");
|
| 670 |
+
const stream = { last: "", lastAt: 0, max: 7 };
|
| 671 |
+
function pushTick(ms, srcCanvas) {
|
| 672 |
+
const c = deck().find((x) => x.id === primaryId[deckName()]);
|
| 673 |
+
if (!c || !c.probs) return;
|
| 674 |
+
const top = argmax(c.probs), conf = c.probs[top], sure = conf >= c.threshold;
|
| 675 |
+
const label = sure ? c.options[top].key.replace(/_/g, " ") : "uncertain";
|
| 676 |
+
const now = performance.now();
|
| 677 |
+
if (label === stream.last && now - stream.lastAt < 1500) return;
|
| 678 |
+
stream.last = label; stream.lastAt = now;
|
| 679 |
+
let thumb = "";
|
| 680 |
+
if (srcCanvas && srcCanvas.width) {
|
| 681 |
+
thumbCv.width = 104; thumbCv.height = Math.max(1, Math.round((104 * srcCanvas.height) / srcCanvas.width));
|
| 682 |
+
thumbCv.getContext("2d").drawImage(srcCanvas, 0, 0, thumbCv.width, thumbCv.height);
|
| 683 |
+
thumb = thumbCv.toDataURL("image/jpeg", 0.7);
|
| 684 |
+
}
|
| 685 |
+
const li = document.createElement("li");
|
| 686 |
+
li.className = `tick ${sure ? "" : "uncertain"}`;
|
| 687 |
+
li.innerHTML = `${thumb ? `<img src="${thumb}" alt="" />` : "<span></span>"}<div class="tl"><div class="tq">${esc(c.title)}</div><div class="tv">${esc(label)}${sure ? "" : ` <span class="note">(${esc(c.options[top].key.replace(/_/g, " "))})</span>`}</div></div><div class="tm"><b>${Math.round(conf * 100)}%</b>${ms.toFixed(0)} ms</div>`;
|
| 688 |
+
const list = $("stream");
|
| 689 |
+
list.prepend(li);
|
| 690 |
+
while (list.children.length > stream.max) list.lastChild.remove();
|
| 691 |
+
}
|
| 692 |
function hud(ms) {
|
| 693 |
const now = performance.now();
|
| 694 |
if (live.lastDone) {
|
|
|
|
| 740 |
let t = (now - demoStart) / dur;
|
| 741 |
if (t >= 1) { demoIdx = (demoIdx + 1) % demoImgs.length; demoStart = now; t = 0; }
|
| 742 |
const img = demoImgs[demoIdx];
|
| 743 |
+
feedCtx.fillStyle = "#e9eef5";
|
| 744 |
feedCtx.fillRect(0, 0, feed.width, feed.height);
|
| 745 |
if (img && img.width) {
|
| 746 |
// slow zoom, like a handheld camera; the whole image stays visible
|
|
|
|
| 755 |
function drawAudio() {
|
| 756 |
fitCanvas();
|
| 757 |
const a = live.analyser;
|
| 758 |
+
feedCtx.fillStyle = "#e9eef5";
|
| 759 |
feedCtx.fillRect(0, 0, feed.width, feed.height);
|
| 760 |
if (a) {
|
| 761 |
const buf = new Float32Array(a.fftSize);
|
| 762 |
a.getFloatTimeDomainData(buf);
|
| 763 |
let rms = 0; for (const x of buf) rms += x * x; rms = Math.sqrt(rms / buf.length);
|
| 764 |
+
feedCtx.strokeStyle = "#0b9c94";
|
| 765 |
feedCtx.lineWidth = Math.max(2, feed.height / 200);
|
| 766 |
feedCtx.beginPath();
|
| 767 |
for (let i = 0; i < buf.length; i++) {
|
|
|
|
| 771 |
}
|
| 772 |
feedCtx.stroke();
|
| 773 |
const lvl = Math.min(1, rms * 8);
|
| 774 |
+
feedCtx.fillStyle = "rgba(11,156,148,.18)";
|
| 775 |
feedCtx.fillRect(14, feed.height * 0.2, 10, feed.height * 0.5);
|
| 776 |
+
feedCtx.fillStyle = "#0b9c94";
|
| 777 |
feedCtx.fillRect(14, feed.height * 0.2 + feed.height * 0.5 * (1 - lvl), 10, feed.height * 0.5 * lvl);
|
| 778 |
+
feedCtx.fillStyle = "rgba(14,26,43,.6)";
|
| 779 |
feedCtx.font = `${Math.round(feed.height / 34)}px system-ui`;
|
| 780 |
feedCtx.fillText(`Listening in 2 s windows (${live.src === "sound" ? "sound file" : "microphone"}), 16 kHz mono`, 34, feed.height * 0.16);
|
| 781 |
}
|
|
|
|
| 815 |
if (token !== live.token) break;
|
| 816 |
await scoreDeck(embs);
|
| 817 |
hud(performance.now() - t0);
|
| 818 |
+
pushTick(performance.now() - t0, grab);
|
| 819 |
} catch (e) { console.error(e); setStatus("error", e.message); await sleep(1000); }
|
| 820 |
}
|
| 821 |
const gap = targetGap() - (performance.now() - t0);
|
|
|
|
| 938 |
rafId = requestAnimationFrame(drawAudio);
|
| 939 |
}
|
| 940 |
live.running = true;
|
| 941 |
+
$("stage").classList.add("running");
|
| 942 |
live.fps = live.lat = live.lastDone = 0;
|
| 943 |
$("idle").hidden = true;
|
| 944 |
$("stopBtn").disabled = false;
|
|
|
|
| 964 |
function stopLive(showIdle = true) {
|
| 965 |
live.running = false;
|
| 966 |
live.token++;
|
| 967 |
+
$("stage").classList.remove("running");
|
| 968 |
cancelAnimationFrame(rafId);
|
| 969 |
if (live.stream) { live.stream.getTracks().forEach((t) => t.stop()); live.stream = null; }
|
| 970 |
if (live.audioCtx) { live.audioCtx.close().catch(() => {}); live.audioCtx = null; live.analyser = null; }
|
index.html
CHANGED
|
@@ -4,17 +4,28 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
| 6 |
<meta name="referrer" content="no-referrer" />
|
| 7 |
-
<meta name="theme-color" content="#
|
| 8 |
<title>Decide Live</title>
|
| 9 |
<meta name="description" content="Decide Live: on-device decisions from camera and microphone. A multimodal embedding model runs in your browser and turns every frame into calibrated probabilities." />
|
| 10 |
-
<link rel="
|
|
|
|
|
|
|
|
|
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
<header class="top">
|
|
|
|
| 14 |
<div class="wrap topbar">
|
| 15 |
<div class="brand">
|
| 16 |
-
<
|
|
|
|
| 17 |
<p class="sub">On-device decisions from camera and microphone. Every frame is embedded in your browser by EmbeddingGemma 2, fine-tuned for decisions, and scored against the options you write. No text generation, no tokens, no server.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</div>
|
| 19 |
<div class="statusbox">
|
| 20 |
<div class="status" id="status"><span class="dot"></span><span id="statusText">Model not loaded</span></div>
|
|
@@ -23,6 +34,7 @@
|
|
| 23 |
<p class="warn" id="wasmNote" hidden>WebGPU is not available here, so the model runs on the CPU (WASM). The live rate is lowered automatically.</p>
|
| 24 |
</div>
|
| 25 |
</div>
|
|
|
|
| 26 |
<nav class="wrap tabs" role="tablist">
|
| 27 |
<button class="tab active" data-tab="live" role="tab">Live</button>
|
| 28 |
<button class="tab" data-tab="single" role="tab">Single input</button>
|
|
@@ -45,6 +57,7 @@
|
|
| 45 |
<div class="stage" id="stage">
|
| 46 |
<video id="cam" playsinline muted autoplay></video>
|
| 47 |
<canvas id="feed"></canvas>
|
|
|
|
| 48 |
<div class="badge"><span class="pulse"></span>Runs on your device. 0 bytes uploaded.</div>
|
| 49 |
<div class="hud" id="hud"><span id="fpsVal">0.0</span> fps <span class="sep">|</span> <span id="latVal">0</span> <span id="latUnit" class="unit">ms / frame</span></div>
|
| 50 |
<div class="overlay" id="overlay" hidden>
|
|
@@ -100,6 +113,10 @@
|
|
| 100 |
<div><span class="gnum accent" id="gRate">-</span><span class="glab">on-device rate</span></div>
|
| 101 |
<button class="ghost small" id="resetGate">Reset</button>
|
| 102 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<p class="note">When a card's top probability is below its threshold the frame is marked uncertain: in a production system it would go to a larger model. Here nothing is sent anywhere; it is only counted.</p>
|
| 104 |
</section>
|
| 105 |
|
|
@@ -279,6 +296,6 @@
|
|
| 279 |
<footer class="wrap foot">
|
| 280 |
<p>Based on EmbeddingGemma 2 by Google DeepMind (Apache-2.0), fine-tuned for decisions, run with transformers.js. This page sends no data anywhere.</p>
|
| 281 |
</footer>
|
| 282 |
-
<script type="module" src="./app.js?v=
|
| 283 |
</body>
|
| 284 |
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
| 6 |
<meta name="referrer" content="no-referrer" />
|
| 7 |
+
<meta name="theme-color" content="#f5f7fb" />
|
| 8 |
<title>Decide Live</title>
|
| 9 |
<meta name="description" content="Decide Live: on-device decisions from camera and microphone. A multimodal embedding model runs in your browser and turns every frame into calibrated probabilities." />
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 12 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,700;12..96,800&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" />
|
| 13 |
+
<link rel="stylesheet" href="./style.css?v=6" />
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
<header class="top">
|
| 17 |
+
<span class="glow a" aria-hidden="true"></span><span class="glow b" aria-hidden="true"></span><span class="glow c" aria-hidden="true"></span>
|
| 18 |
<div class="wrap topbar">
|
| 19 |
<div class="brand">
|
| 20 |
+
<span class="eyebrow"><span class="pulse"></span>Decide Live</span>
|
| 21 |
+
<h1>See it. <span class="lit">Decide it.</span><br />Right in your browser.</h1>
|
| 22 |
<p class="sub">On-device decisions from camera and microphone. Every frame is embedded in your browser by EmbeddingGemma 2, fine-tuned for decisions, and scored against the options you write. No text generation, no tokens, no server.</p>
|
| 23 |
+
<div class="stats">
|
| 24 |
+
<span class="stat"><b>95%</b> on 100 held-out egg photos</span>
|
| 25 |
+
<span class="stat"><b>0</b> bytes uploaded</span>
|
| 26 |
+
<span class="stat"><b>744M</b> parameters, 4-bit</span>
|
| 27 |
+
<span class="stat"><b>0</b> tokens generated</span>
|
| 28 |
+
</div>
|
| 29 |
</div>
|
| 30 |
<div class="statusbox">
|
| 31 |
<div class="status" id="status"><span class="dot"></span><span id="statusText">Model not loaded</span></div>
|
|
|
|
| 34 |
<p class="warn" id="wasmNote" hidden>WebGPU is not available here, so the model runs on the CPU (WASM). The live rate is lowered automatically.</p>
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
+
<div class="reel" aria-hidden="true"><div class="reel-track" id="reel"></div></div>
|
| 38 |
<nav class="wrap tabs" role="tablist">
|
| 39 |
<button class="tab active" data-tab="live" role="tab">Live</button>
|
| 40 |
<button class="tab" data-tab="single" role="tab">Single input</button>
|
|
|
|
| 57 |
<div class="stage" id="stage">
|
| 58 |
<video id="cam" playsinline muted autoplay></video>
|
| 59 |
<canvas id="feed"></canvas>
|
| 60 |
+
<div class="scan" aria-hidden="true"></div>
|
| 61 |
<div class="badge"><span class="pulse"></span>Runs on your device. 0 bytes uploaded.</div>
|
| 62 |
<div class="hud" id="hud"><span id="fpsVal">0.0</span> fps <span class="sep">|</span> <span id="latVal">0</span> <span id="latUnit" class="unit">ms / frame</span></div>
|
| 63 |
<div class="overlay" id="overlay" hidden>
|
|
|
|
| 113 |
<div><span class="gnum accent" id="gRate">-</span><span class="glab">on-device rate</span></div>
|
| 114 |
<button class="ghost small" id="resetGate">Reset</button>
|
| 115 |
</div>
|
| 116 |
+
<div class="stream">
|
| 117 |
+
<div class="streamhead"><h3>Decision stream</h3><span class="note">Each row is a real decision made in this browser just now.</span></div>
|
| 118 |
+
<ol class="streamlist" id="stream"></ol>
|
| 119 |
+
</div>
|
| 120 |
<p class="note">When a card's top probability is below its threshold the frame is marked uncertain: in a production system it would go to a larger model. Here nothing is sent anywhere; it is only counted.</p>
|
| 121 |
</section>
|
| 122 |
|
|
|
|
| 296 |
<footer class="wrap foot">
|
| 297 |
<p>Based on EmbeddingGemma 2 by Google DeepMind (Apache-2.0), fine-tuned for decisions, run with transformers.js. This page sends no data anywhere.</p>
|
| 298 |
</footer>
|
| 299 |
+
<script type="module" src="./app.js?v=6"></script>
|
| 300 |
</body>
|
| 301 |
</html>
|
style.css
CHANGED
|
@@ -1,196 +1,281 @@
|
|
| 1 |
-
:
|
| 2 |
-
|
| 3 |
-
--
|
| 4 |
-
--
|
| 5 |
-
--
|
| 6 |
-
--
|
| 7 |
-
--
|
| 8 |
-
--
|
| 9 |
-
--
|
| 10 |
-
--accent
|
| 11 |
-
--accent-
|
| 12 |
-
--
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.status.loading .dot { background: var(--warn); animation: blink 1s infinite; }
|
| 36 |
-
.status.error .dot { background: var(--bad); }
|
| 37 |
-
@keyframes blink { 50% { opacity: .3; } }
|
| 38 |
-
.chips { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
|
| 39 |
-
.chip { font-size: 12px; padding: 2px 9px; border-radius: 999px; background: var(--card2); color: var(--muted); border: 1px solid var(--line); }
|
| 40 |
-
.chip:empty { display: none; }
|
| 41 |
-
.chip.safe { color: var(--accent); border-color: rgba(45,212,191,.35); background: var(--accent-dim); }
|
| 42 |
-
.progress { height: 4px; background: var(--line); border-radius: 2px; margin-top: 8px; overflow: hidden; }
|
| 43 |
-
.progress div { height: 100%; width: 0; background: var(--accent); transition: width .25s ease; }
|
| 44 |
-
.warn { color: var(--warn); font-size: 12px; margin: 6px 0 0; }
|
| 45 |
-
.tabs { display: flex; gap: 2px; overflow-x: auto; }
|
| 46 |
-
.tab { background: none; border: none; color: var(--muted); padding: 10px 14px; font: inherit; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; }
|
| 47 |
-
.tab.active { color: var(--ink); border-bottom-color: var(--accent); }
|
| 48 |
-
|
| 49 |
-
/* buttons and fields */
|
| 50 |
-
button { font: inherit; cursor: pointer; border-radius: 10px; padding: 8px 14px; border: 1px solid var(--line); background: var(--card2); color: var(--ink); transition: background .15s, border-color .15s, transform .1s; }
|
| 51 |
-
button:hover:not(:disabled) { border-color: var(--accent); }
|
| 52 |
-
button:active:not(:disabled) { transform: scale(.98); }
|
| 53 |
-
button:disabled { opacity: .45; cursor: default; }
|
| 54 |
-
button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; width: 100%; margin-top: 10px; }
|
| 55 |
-
button.primary.inline { width: auto; margin-top: 0; }
|
| 56 |
-
button.ghost { background: transparent; }
|
| 57 |
-
button.small { padding: 4px 10px; font-size: 13px; }
|
| 58 |
-
select, input[type=text], textarea { font: inherit; color: var(--ink); background: var(--bg2); border: 1px solid var(--line); border-radius: 10px; padding: 7px 10px; width: 100%; }
|
| 59 |
-
select:focus, input:focus, textarea:focus { outline: 2px solid var(--accent-dim); border-color: var(--accent); }
|
| 60 |
-
textarea { resize: vertical; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 13px; }
|
| 61 |
-
input[type=range] { accent-color: var(--accent); width: 100%; }
|
| 62 |
-
.note { color: var(--muted); font-size: 13px; margin: 8px 0; }
|
| 63 |
-
|
| 64 |
-
/* live layout */
|
| 65 |
-
.view { padding-top: 16px; padding-bottom: 16px; }
|
| 66 |
-
.livegrid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 18px; align-items: start; }
|
| 67 |
-
@media (max-width: 900px) { .livegrid { grid-template-columns: 1fr; } }
|
| 68 |
-
.sources { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
|
| 69 |
-
.src.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
|
| 70 |
-
.stage { position: relative; aspect-ratio: 4 / 3; background: #05070a; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); }
|
| 71 |
-
.stage video, .stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 72 |
-
.stage video { opacity: 0; }
|
| 73 |
-
.stage.camera video { opacity: 1; }
|
| 74 |
-
.stage.camera canvas { opacity: 0; }
|
| 75 |
-
.stage.mirror video { transform: scaleX(-1); }
|
| 76 |
-
.badge { position: absolute; top: 12px; left: 12px; display: flex; align-items: center; gap: 8px; background: rgba(5,7,10,.72); color: var(--ink); border: 1px solid rgba(45,212,191,.45); padding: 5px 11px; border-radius: 999px; font-size: 12.5px; font-weight: 600; backdrop-filter: blur(6px); }
|
| 77 |
-
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 0 rgba(45,212,191,.7); animation: pulse 1.6s infinite; }
|
| 78 |
-
@keyframes pulse { 70% { box-shadow: 0 0 0 9px rgba(45,212,191,0); } 100% { box-shadow: 0 0 0 0 rgba(45,212,191,0); } }
|
| 79 |
-
.hud { position: absolute; top: 12px; right: 12px; background: rgba(5,7,10,.72); padding: 5px 10px; border-radius: 999px; font-size: 12.5px; font-variant-numeric: tabular-nums; color: var(--muted); }
|
| 80 |
-
.hud span:not(.sep):not(.unit) { color: var(--ink); font-weight: 700; }
|
| 81 |
-
.hud .sep { opacity: .4; margin: 0 4px; }
|
| 82 |
-
.overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; display: flex; align-items: center; gap: 14px; background: rgba(5,7,10,.74); border: 1px solid var(--line); border-radius: 16px; padding: 10px 14px; backdrop-filter: blur(8px); }
|
| 83 |
-
.ring { width: 76px; height: 76px; flex: none; transform: rotate(-90deg); }
|
| 84 |
-
.ring circle { fill: none; stroke-width: 10; }
|
| 85 |
-
.ring-bg { stroke: rgba(255,255,255,.08); }
|
| 86 |
-
.ring-fg { stroke: var(--accent); stroke-linecap: round; stroke-dasharray: 326.73; stroke-dashoffset: 326.73; transition: stroke-dashoffset .35s ease, stroke .35s; }
|
| 87 |
-
.overlay.uncertain .ring-fg { stroke: var(--warn); }
|
| 88 |
-
.ovtext { min-width: 0; }
|
| 89 |
-
.ovq { color: var(--muted); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 90 |
-
.ovlabel { font-size: clamp(22px, 4.2vw, 36px); font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 91 |
-
.overlay.uncertain .ovlabel { color: var(--warn); }
|
| 92 |
-
.ovpct { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
| 93 |
-
.idle { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 6px; background: radial-gradient(ellipse at center, rgba(45,212,191,.10), rgba(5,7,10,.85) 70%); padding: 16px; }
|
| 94 |
-
.idle p { color: var(--muted); font-size: 13px; margin: 0; }
|
| 95 |
-
button.big { background: var(--accent); color: var(--accent-ink); border: none; font-weight: 800; font-size: 17px; padding: 14px 26px; border-radius: 999px; box-shadow: 0 8px 30px rgba(45,212,191,.25); }
|
| 96 |
-
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; margin-top: 10px; }
|
| 97 |
-
.controls label { display: flex; flex-direction: column; font-size: 12px; color: var(--muted); gap: 3px; flex: 1; min-width: 120px; }
|
| 98 |
-
.gate { display: flex; gap: 10px; align-items: center; margin-top: 12px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 14px; flex-wrap: wrap; }
|
| 99 |
-
.gate > div { display: flex; flex-direction: column; flex: 1; min-width: 90px; }
|
| 100 |
-
.gnum { font-size: 24px; font-weight: 800; font-variant-numeric: tabular-nums; }
|
| 101 |
-
.gnum.accent { color: var(--accent); }
|
| 102 |
-
.gnum.warnc { color: var(--warn); }
|
| 103 |
-
.glab { font-size: 12px; color: var(--muted); }
|
| 104 |
-
|
| 105 |
-
/* cards */
|
| 106 |
-
.cardhead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
| 107 |
-
.cardhead h2 { margin: 0 0 10px; font-size: 17px; }
|
| 108 |
-
.cards { display: grid; gap: 12px; }
|
| 109 |
-
.dcard { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; transition: border-color .25s, box-shadow .25s; }
|
| 110 |
-
.dcard.primary { border-color: rgba(45,212,191,.55); box-shadow: 0 0 0 1px rgba(45,212,191,.15) inset; }
|
| 111 |
-
.dcard.uncertain { border-color: rgba(245,184,75,.55); }
|
| 112 |
-
.dtop { display: flex; justify-content: space-between; gap: 8px; align-items: flex-start; }
|
| 113 |
-
.dtitle { font-weight: 700; font-size: 14.5px; }
|
| 114 |
-
.dact { display: flex; gap: 4px; flex: none; }
|
| 115 |
-
.dact button { padding: 2px 8px; font-size: 12px; border-radius: 8px; }
|
| 116 |
-
.dverdict { margin: 6px 0 8px; font-size: 13px; color: var(--muted); min-height: 20px; }
|
| 117 |
-
.dverdict b { color: var(--accent); font-size: 15px; }
|
| 118 |
-
.dcard.uncertain .dverdict b { color: var(--warn); }
|
| 119 |
-
.obar { display: grid; grid-template-columns: minmax(70px, 34%) 1fr 48px; gap: 8px; align-items: center; margin: 4px 0; font-size: 13px; }
|
| 120 |
-
.obar .k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
| 121 |
-
.obar.top .k { color: var(--ink); font-weight: 600; }
|
| 122 |
-
.obar .track { height: 12px; background: rgba(255,255,255,.06); border-radius: 6px; overflow: hidden; position: relative; }
|
| 123 |
-
.obar .fill { height: 100%; width: 0; background: #3a4558; border-radius: 6px; transition: width .35s cubic-bezier(.2,.8,.2,1), background .3s; }
|
| 124 |
-
.obar.top .fill { background: var(--accent); }
|
| 125 |
-
.dcard.uncertain .obar.top .fill { background: var(--warn); }
|
| 126 |
-
.obar .p { text-align: right; font-variant-numeric: tabular-nums; font-size: 12.5px; }
|
| 127 |
-
.thr { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 12px; color: var(--muted); }
|
| 128 |
-
.thr input { flex: 1; }
|
| 129 |
-
.thr .tv { width: 34px; text-align: right; color: var(--ink); font-variant-numeric: tabular-nums; }
|
| 130 |
-
.dstats { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
| 131 |
-
.dedit { margin-top: 8px; display: grid; gap: 6px; }
|
| 132 |
-
.templates { margin-top: 16px; }
|
| 133 |
-
.templates h3 { font-size: 14px; margin: 0 0 8px; color: var(--muted); font-weight: 600; }
|
| 134 |
-
.tpls { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
| 135 |
-
.tpls button { font-size: 13px; padding: 5px 11px; border-radius: 999px; }
|
| 136 |
-
|
| 137 |
-
/* single / samples */
|
| 138 |
-
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 139 |
-
@media (max-width: 800px) { .grid2 { grid-template-columns: 1fr; } }
|
| 140 |
-
.panel { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; min-width: 0; }
|
| 141 |
-
.panel h2 { margin: 0 0 12px; font-size: 17px; }
|
| 142 |
-
.wide { grid-column: 1 / -1; }
|
| 143 |
-
.subtabs { display: flex; gap: 4px; margin-bottom: 10px; flex-wrap: wrap; }
|
| 144 |
-
.subtab { padding: 5px 12px; font-size: 13px; }
|
| 145 |
-
.subtab.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
|
| 146 |
-
.col { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; font-size: 13px; color: var(--muted); }
|
| 147 |
-
.drop { display: block; border: 1px dashed var(--line); border-radius: 10px; padding: 12px; color: var(--muted); cursor: pointer; margin: 8px 0; font-size: 13px; }
|
| 148 |
-
.drop input { display: block; margin-bottom: 4px; max-width: 100%; color: var(--ink); }
|
| 149 |
-
#imagePreview, #videoPreview { max-width: 100%; max-height: 300px; border-radius: 10px; display: block; margin-top: 8px; }
|
| 150 |
-
#audioPreview { width: 100%; margin-top: 8px; }
|
| 151 |
-
.seg { display: flex; gap: 16px; font-size: 14px; }
|
| 152 |
-
.seg input { accent-color: var(--accent); }
|
| 153 |
-
.adv { margin: 10px 0; border-top: 1px solid var(--line); padding-top: 8px; }
|
| 154 |
-
.adv summary { cursor: pointer; color: var(--muted); font-size: 14px; }
|
| 155 |
-
.decision { font-size: 22px; margin: 0 0 10px; font-weight: 800; }
|
| 156 |
-
.decision b { color: var(--accent); }
|
| 157 |
-
.meta { color: var(--muted); font-size: 13px; margin-top: 10px; }
|
| 158 |
-
.prompts { margin-top: 8px; font-size: 12px; color: var(--muted); }
|
| 159 |
-
.prompts pre { white-space: pre-wrap; word-break: break-word; background: var(--bg2); border: 1px solid var(--line); padding: 8px; border-radius: 8px; color: var(--ink); }
|
| 160 |
-
.btnrow { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 161 |
-
.summary { margin: 12px 0; font-size: 16px; }
|
| 162 |
-
.summary b { color: var(--accent); }
|
| 163 |
-
.tablewrap { overflow-x: auto; margin-top: 8px; }
|
| 164 |
-
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
| 165 |
-
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
| 166 |
-
th { color: var(--muted); font-weight: 600; }
|
| 167 |
-
td.ok { color: var(--accent); font-weight: 700; }
|
| 168 |
-
td.no { color: var(--bad); font-weight: 700; }
|
| 169 |
-
.prose { max-width: 860px; }
|
| 170 |
-
.prose li { margin: 4px 0; }
|
| 171 |
-
.prose h3 { margin: 18px 0 6px; font-size: 15px; }
|
| 172 |
-
.foot { color: var(--muted); font-size: 12.5px; padding-bottom: 24px; }
|
| 173 |
-
@media (max-width: 600px) {
|
| 174 |
-
.topbar { flex-direction: column; gap: 8px; }
|
| 175 |
-
.statusbox { min-width: 0; max-width: none; width: 100%; }
|
| 176 |
-
.brand .sub { display: none; }
|
| 177 |
-
.ring { width: 58px; height: 58px; }
|
| 178 |
-
.stage { aspect-ratio: 3 / 4; }
|
| 179 |
-
.badge { font-size: 11.5px; padding: 4px 9px; left: 8px; top: 8px; }
|
| 180 |
-
.hud { top: 44px; left: 8px; right: auto; font-size: 11.5px; }
|
| 181 |
-
.overlay { left: 8px; right: 8px; bottom: 8px; gap: 10px; padding: 8px 10px; }
|
| 182 |
-
}
|
| 183 |
|
| 184 |
-
/*
|
| 185 |
-
.
|
| 186 |
-
.
|
| 187 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
.btnrow .drop.inline { display: inline-block; margin: 0; padding: 8px 12px; }
|
| 189 |
.eggresult { display: grid; grid-template-columns: minmax(120px, 220px) 1fr; gap: 16px; align-items: start; margin: 12px 0; }
|
| 190 |
-
.eggresult img { width: 100%; border-radius:
|
| 191 |
@media (max-width: 560px) { .eggresult { grid-template-columns: 1fr; } }
|
| 192 |
-
.okc { color: var(--good); font-weight:
|
| 193 |
-
.noc { color: var(--bad); font-weight:
|
| 194 |
-
img.mini { width: 32px; height: 32px; object-fit: cover; border-radius:
|
| 195 |
.dctx { display: block; font-size: 12px; font-weight: 400; color: var(--muted); }
|
| 196 |
.dedit input + input { margin-top: 6px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Decide Live: bright theme. Light shone through an egg is the motif: warm candle amber + clear teal on a cool white ground. */
|
| 2 |
+
:root {
|
| 3 |
+
--bg: #f5f7fb;
|
| 4 |
+
--bg2: #eef2f8;
|
| 5 |
+
--card: #ffffff;
|
| 6 |
+
--card2: #f3f6fa;
|
| 7 |
+
--ink: #0e1a2b;
|
| 8 |
+
--muted: #5a6a80;
|
| 9 |
+
--line: #dfe6ef;
|
| 10 |
+
--accent: #0b9c94;
|
| 11 |
+
--accent-strong: #08807a;
|
| 12 |
+
--accent-ink: #ffffff;
|
| 13 |
+
--accent-dim: rgba(11, 156, 148, 0.12);
|
| 14 |
+
--amber: #f2a20c;
|
| 15 |
+
--amber-dim: rgba(242, 162, 12, 0.14);
|
| 16 |
+
--warn: #c97800;
|
| 17 |
+
--bad: #e5484d;
|
| 18 |
+
--good: #0b9c94;
|
| 19 |
+
--radius: 18px;
|
| 20 |
+
--shadow: 0 1px 2px rgba(14, 26, 43, 0.05), 0 8px 24px rgba(14, 26, 43, 0.06);
|
| 21 |
+
--shadow-lift: 0 2px 4px rgba(14, 26, 43, 0.06), 0 18px 40px rgba(14, 26, 43, 0.10);
|
| 22 |
+
--display: "Bricolage Grotesque", "DM Sans", system-ui, sans-serif;
|
| 23 |
+
--body: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
| 24 |
+
--mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
|
| 25 |
+
color-scheme: light;
|
| 26 |
+
}
|
| 27 |
+
* { box-sizing: border-box; }
|
| 28 |
+
html, body { margin: 0; background: var(--bg); color: var(--ink); }
|
| 29 |
+
body { font: 15px/1.55 var(--body); -webkit-font-smoothing: antialiased; overflow-x: hidden; }
|
| 30 |
+
a { color: var(--accent-strong); }
|
| 31 |
+
code { font: 0.86em var(--mono); background: var(--card2); padding: 1px 6px; border-radius: 6px; border: 1px solid var(--line); }
|
| 32 |
+
.wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px; }
|
| 33 |
+
[hidden] { display: none !important; }
|
| 34 |
+
:focus-visible { outline: 3px solid rgba(11, 156, 148, 0.45); outline-offset: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
/* ---------------------------------------------------------------- hero */
|
| 37 |
+
.top { position: relative; overflow: hidden; background: #fbfcfe; border-bottom: 1px solid var(--line); isolation: isolate; }
|
| 38 |
+
.glow { position: absolute; border-radius: 50%; filter: blur(60px); opacity: 0.55; z-index: -1; pointer-events: none; }
|
| 39 |
+
.glow.a { width: 520px; height: 520px; background: radial-gradient(circle, rgba(242, 162, 12, 0.55), rgba(242, 162, 12, 0) 70%); top: -240px; right: -80px; animation: drift-a 16s ease-in-out infinite alternate; }
|
| 40 |
+
.glow.b { width: 460px; height: 460px; background: radial-gradient(circle, rgba(11, 156, 148, 0.45), rgba(11, 156, 148, 0) 70%); top: -160px; left: -140px; animation: drift-b 19s ease-in-out infinite alternate; }
|
| 41 |
+
.glow.c { width: 380px; height: 380px; background: radial-gradient(circle, rgba(80, 160, 255, 0.35), rgba(80, 160, 255, 0) 70%); bottom: -220px; left: 40%; animation: drift-c 22s ease-in-out infinite alternate; }
|
| 42 |
+
@keyframes drift-a { to { transform: translate(-120px, 60px) scale(1.15); } }
|
| 43 |
+
@keyframes drift-b { to { transform: translate(160px, 40px) scale(0.9); } }
|
| 44 |
+
@keyframes drift-c { to { transform: translate(-180px, -40px) scale(1.2); } }
|
| 45 |
+
.topbar { display: flex; gap: 28px; justify-content: space-between; align-items: flex-end; padding-top: 28px; padding-bottom: 18px; }
|
| 46 |
+
.brand { min-width: 0; }
|
| 47 |
+
.eyebrow { display: inline-flex; align-items: center; gap: 8px; font: 700 12px/1 var(--body); letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-strong); background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(11, 156, 148, 0.25); padding: 6px 11px; border-radius: 999px; backdrop-filter: blur(6px); }
|
| 48 |
+
.eyebrow .pulse { width: 7px; height: 7px; }
|
| 49 |
+
.brand h1 { margin: 14px 0 0; font: 800 clamp(34px, 5.4vw, 60px)/1.02 var(--display); letter-spacing: -0.035em; text-wrap: balance; }
|
| 50 |
+
.brand h1 .lit { background: linear-gradient(100deg, var(--accent) 0%, #1fb7a8 40%, var(--amber) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; background-size: 200% 100%; animation: sheen 6s ease-in-out infinite alternate; }
|
| 51 |
+
@keyframes sheen { to { background-position: 100% 0; } }
|
| 52 |
+
.brand .sub { margin: 12px 0 0; color: var(--muted); max-width: 640px; font-size: 15.5px; }
|
| 53 |
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
| 54 |
+
.stat { display: inline-flex; align-items: baseline; gap: 6px; background: rgba(255, 255, 255, 0.78); border: 1px solid var(--line); border-radius: 12px; padding: 7px 12px; font-size: 13px; color: var(--muted); backdrop-filter: blur(6px); box-shadow: var(--shadow); }
|
| 55 |
+
.stat b { font: 800 17px/1 var(--display); color: var(--ink); letter-spacing: -0.02em; }
|
| 56 |
+
.statusbox { min-width: 270px; max-width: 360px; background: rgba(255, 255, 255, 0.82); border: 1px solid var(--line); border-radius: 16px; padding: 12px 14px; box-shadow: var(--shadow); backdrop-filter: blur(8px); }
|
| 57 |
+
.status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); font-weight: 600; }
|
| 58 |
+
.status .dot { width: 10px; height: 10px; border-radius: 50%; background: #b5c0cf; flex: none; }
|
| 59 |
+
.status.ready .dot { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-dim); }
|
| 60 |
+
.status.ready { color: var(--accent-strong); }
|
| 61 |
+
.status.loading .dot { background: var(--amber); animation: blink 1s infinite; }
|
| 62 |
+
.status.error .dot { background: var(--bad); }
|
| 63 |
+
@keyframes blink { 50% { opacity: 0.3; } }
|
| 64 |
+
.chips { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
| 65 |
+
.chip { font-size: 12px; padding: 3px 10px; border-radius: 999px; background: var(--card2); color: var(--muted); border: 1px solid var(--line); font-weight: 600; }
|
| 66 |
+
.chip:empty { display: none; }
|
| 67 |
+
.chip.safe { color: var(--accent-strong); border-color: rgba(11, 156, 148, 0.3); background: var(--accent-dim); }
|
| 68 |
+
.progress { height: 6px; background: var(--bg2); border-radius: 3px; margin-top: 10px; overflow: hidden; }
|
| 69 |
+
.progress div { height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), var(--amber)); transition: width 0.25s ease; }
|
| 70 |
+
.warn { color: var(--warn); font-size: 12px; margin: 6px 0 0; }
|
| 71 |
+
|
| 72 |
+
/* reel: the sample pictures drift past under the hero */
|
| 73 |
+
.reel { position: relative; overflow: hidden; padding: 4px 0 14px; mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
|
| 74 |
+
.reel-track { display: flex; gap: 10px; width: max-content; animation: reel 60s linear infinite; }
|
| 75 |
+
.reel:hover .reel-track { animation-play-state: paused; }
|
| 76 |
+
.reel-track img { height: 74px; width: 104px; object-fit: cover; border-radius: 12px; border: 2px solid #fff; box-shadow: var(--shadow); background: var(--bg2); }
|
| 77 |
+
@keyframes reel { to { transform: translateX(-50%); } }
|
| 78 |
+
|
| 79 |
+
.tabs { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 14px; }
|
| 80 |
+
.tab { background: rgba(255, 255, 255, 0.7); border: 1px solid var(--line); color: var(--muted); padding: 8px 16px; font: 700 14px/1.2 var(--body); cursor: pointer; border-radius: 999px; white-space: nowrap; }
|
| 81 |
+
.tab:hover { color: var(--ink); }
|
| 82 |
+
.tab.active { color: #fff; background: var(--ink); border-color: var(--ink); }
|
| 83 |
+
|
| 84 |
+
/* ---------------------------------------------------------------- buttons and fields */
|
| 85 |
+
button { font: inherit; cursor: pointer; border-radius: 12px; padding: 8px 14px; border: 1px solid var(--line); background: var(--card); color: var(--ink); transition: background 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s; }
|
| 86 |
+
button:hover:not(:disabled) { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
|
| 87 |
+
button:active:not(:disabled) { transform: scale(0.97); }
|
| 88 |
+
button:disabled { opacity: 0.45; cursor: default; }
|
| 89 |
+
button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; width: 100%; margin-top: 10px; }
|
| 90 |
+
button.primary:hover:not(:disabled) { background: var(--accent-strong); }
|
| 91 |
+
button.primary.inline { width: auto; margin-top: 0; }
|
| 92 |
+
button.ghost { background: transparent; }
|
| 93 |
+
button.small { padding: 4px 10px; font-size: 13px; }
|
| 94 |
+
select, input[type=text], textarea { font: inherit; color: var(--ink); background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 8px 11px; width: 100%; }
|
| 95 |
+
select:focus, input:focus, textarea:focus { outline: 3px solid var(--accent-dim); border-color: var(--accent); }
|
| 96 |
+
textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
| 97 |
+
input[type=range] { accent-color: var(--accent); width: 100%; }
|
| 98 |
+
.note { color: var(--muted); font-size: 13px; margin: 8px 0; }
|
| 99 |
+
|
| 100 |
+
/* ---------------------------------------------------------------- live layout */
|
| 101 |
+
.view { padding-top: 22px; padding-bottom: 18px; animation: rise 0.45s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
| 102 |
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
|
| 103 |
+
.livegrid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 22px; align-items: start; }
|
| 104 |
+
@media (max-width: 900px) { .livegrid { grid-template-columns: 1fr; } }
|
| 105 |
+
.sources { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
|
| 106 |
+
.src { border-radius: 999px; font-weight: 600; }
|
| 107 |
+
.src.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
|
| 108 |
+
.stage { position: relative; aspect-ratio: 4 / 3; background: #e9eef5; border-radius: 22px; overflow: hidden; border: 1px solid var(--line); box-shadow: var(--shadow-lift); }
|
| 109 |
+
.stage video, .stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 110 |
+
.stage video { opacity: 0; }
|
| 111 |
+
.stage.camera video { opacity: 1; }
|
| 112 |
+
.stage.camera canvas { opacity: 0; }
|
| 113 |
+
.stage.mirror video { transform: scaleX(-1); }
|
| 114 |
+
/* a soft beam sweeps over the picture while decisions are running */
|
| 115 |
+
.scan { position: absolute; left: 0; right: 0; height: 28%; top: -30%; pointer-events: none; background: linear-gradient(180deg, rgba(242, 162, 12, 0) 0%, rgba(242, 162, 12, 0.16) 70%, rgba(255, 214, 120, 0.55) 100%); border-bottom: 2px solid rgba(255, 200, 80, 0.85); opacity: 0; }
|
| 116 |
+
.stage.running .scan { opacity: 1; animation: sweep 2.6s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
|
| 117 |
+
@keyframes sweep { to { top: 102%; } }
|
| 118 |
+
.badge { position: absolute; top: 12px; left: 12px; display: flex; align-items: center; gap: 8px; background: rgba(255, 255, 255, 0.86); color: var(--ink); border: 1px solid rgba(11, 156, 148, 0.35); padding: 6px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 700; backdrop-filter: blur(8px); box-shadow: var(--shadow); }
|
| 119 |
+
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 0 rgba(11, 156, 148, 0.6); animation: pulse 1.6s infinite; flex: none; }
|
| 120 |
+
@keyframes pulse { 70% { box-shadow: 0 0 0 9px rgba(11, 156, 148, 0); } 100% { box-shadow: 0 0 0 0 rgba(11, 156, 148, 0); } }
|
| 121 |
+
.hud { position: absolute; top: 12px; right: 12px; background: rgba(255, 255, 255, 0.86); padding: 6px 11px; border-radius: 999px; font: 12.5px var(--mono); color: var(--muted); box-shadow: var(--shadow); backdrop-filter: blur(8px); }
|
| 122 |
+
.hud span:not(.sep):not(.unit) { color: var(--ink); font-weight: 700; }
|
| 123 |
+
.hud .sep { opacity: 0.4; margin: 0 4px; }
|
| 124 |
+
.overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; display: flex; align-items: center; gap: 14px; background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.9); border-radius: 18px; padding: 10px 16px; backdrop-filter: blur(10px); box-shadow: var(--shadow-lift); }
|
| 125 |
+
.ring { width: 76px; height: 76px; flex: none; transform: rotate(-90deg); }
|
| 126 |
+
.ring circle { fill: none; stroke-width: 10; }
|
| 127 |
+
.ring-bg { stroke: var(--bg2); }
|
| 128 |
+
.ring-fg { stroke: var(--accent); stroke-linecap: round; stroke-dasharray: 326.73; stroke-dashoffset: 326.73; transition: stroke-dashoffset 0.35s ease, stroke 0.35s; }
|
| 129 |
+
.overlay.uncertain .ring-fg { stroke: var(--amber); }
|
| 130 |
+
.ovtext { min-width: 0; }
|
| 131 |
+
.ovq { color: var(--muted); font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 132 |
+
.ovlabel { font: 800 clamp(24px, 4.4vw, 40px)/1.08 var(--display); letter-spacing: -0.03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--accent-strong); }
|
| 133 |
+
.ovlabel.pop { animation: pop 0.42s cubic-bezier(0.2, 1.4, 0.4, 1); }
|
| 134 |
+
@keyframes pop { 0% { transform: scale(0.82); opacity: 0.3; } 100% { transform: none; opacity: 1; } }
|
| 135 |
+
.overlay.uncertain .ovlabel { color: var(--warn); }
|
| 136 |
+
.ovpct { font: 12.5px var(--mono); color: var(--muted); }
|
| 137 |
+
.idle { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 10px; background: radial-gradient(ellipse at 50% 45%, rgba(255, 236, 190, 0.85), rgba(245, 247, 251, 0.94) 65%); padding: 16px; }
|
| 138 |
+
.idle p { color: var(--muted); font-size: 13px; margin: 0; max-width: 420px; }
|
| 139 |
+
button.big { background: linear-gradient(100deg, var(--accent) 0%, #17b3a6 55%, #e7a015 130%); color: #fff; border: none; font: 800 18px/1 var(--display); letter-spacing: -0.01em; padding: 17px 30px; border-radius: 999px; box-shadow: 0 10px 30px rgba(11, 156, 148, 0.32); animation: breathe 2.4s ease-in-out infinite; }
|
| 140 |
+
button.big:hover:not(:disabled) { box-shadow: 0 14px 36px rgba(11, 156, 148, 0.42); }
|
| 141 |
+
@keyframes breathe { 50% { transform: scale(1.04); } }
|
| 142 |
+
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; margin-top: 12px; }
|
| 143 |
+
.controls label { display: flex; flex-direction: column; font-size: 12px; color: var(--muted); gap: 3px; flex: 1; min-width: 120px; font-weight: 600; }
|
| 144 |
+
.gate { display: flex; gap: 10px; align-items: center; margin-top: 14px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 16px; flex-wrap: wrap; box-shadow: var(--shadow); }
|
| 145 |
+
.gate > div { display: flex; flex-direction: column; flex: 1; min-width: 90px; }
|
| 146 |
+
.gnum { font: 800 28px/1.1 var(--display); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
| 147 |
+
.gnum.accent { color: var(--accent-strong); }
|
| 148 |
+
.gnum.warnc { color: var(--warn); }
|
| 149 |
+
.glab { font-size: 12px; color: var(--muted); font-weight: 600; }
|
| 150 |
+
|
| 151 |
+
/* decision stream: one row per real decision on the primary card */
|
| 152 |
+
.stream { margin-top: 14px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; box-shadow: var(--shadow); }
|
| 153 |
+
.streamhead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
| 154 |
+
.streamhead h3 { margin: 0; font: 800 16px/1.2 var(--display); letter-spacing: -0.01em; }
|
| 155 |
+
.streamhead .note { margin: 0; font-size: 12px; }
|
| 156 |
+
.streamlist { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 6px; min-height: 52px; }
|
| 157 |
+
.streamlist:empty::before { content: "Start live decisions and each result appears here as it happens."; color: var(--muted); font-size: 13px; }
|
| 158 |
+
.tick { display: grid; grid-template-columns: 52px 1fr auto; gap: 10px; align-items: center; padding: 6px 8px; border-radius: 12px; background: var(--card2); animation: slidein 0.45s cubic-bezier(0.2, 0.9, 0.2, 1); }
|
| 159 |
+
.tick:first-child { background: linear-gradient(90deg, var(--accent-dim), var(--card2)); }
|
| 160 |
+
.tick.uncertain:first-child { background: linear-gradient(90deg, var(--amber-dim), var(--card2)); }
|
| 161 |
+
@keyframes slidein { from { opacity: 0; transform: translateY(-12px) scale(0.98); } }
|
| 162 |
+
.tick img { width: 52px; height: 40px; object-fit: cover; border-radius: 8px; display: block; background: var(--bg2); }
|
| 163 |
+
.tick .tl { min-width: 0; }
|
| 164 |
+
.tick .tq { font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 165 |
+
.tick .tv { font: 800 16px/1.2 var(--display); color: var(--accent-strong); }
|
| 166 |
+
.tick.uncertain .tv { color: var(--warn); }
|
| 167 |
+
.tick .tm { font: 12px var(--mono); color: var(--muted); text-align: right; white-space: nowrap; }
|
| 168 |
+
.tick .tm b { display: block; font-size: 14px; color: var(--ink); }
|
| 169 |
+
|
| 170 |
+
/* ---------------------------------------------------------------- cards */
|
| 171 |
+
.cardhead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
| 172 |
+
.cardhead h2 { margin: 0 0 12px; font: 800 20px/1.2 var(--display); letter-spacing: -0.02em; }
|
| 173 |
+
.cards { display: grid; gap: 12px; }
|
| 174 |
+
.dcard { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s; box-shadow: var(--shadow); animation: rise 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); }
|
| 175 |
+
.dcard:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
|
| 176 |
+
.dcard.primary { border-color: rgba(11, 156, 148, 0.55); box-shadow: 0 0 0 3px var(--accent-dim), var(--shadow); }
|
| 177 |
+
.dcard.uncertain { border-color: rgba(242, 162, 12, 0.6); }
|
| 178 |
+
.dtop { display: flex; justify-content: space-between; gap: 8px; align-items: flex-start; }
|
| 179 |
+
.dtitle { font-weight: 700; font-size: 15px; }
|
| 180 |
+
.dact { display: flex; gap: 4px; flex: none; }
|
| 181 |
+
.dact button { padding: 2px 9px; font-size: 12px; border-radius: 8px; }
|
| 182 |
+
.dverdict { margin: 6px 0 8px; font-size: 13px; color: var(--muted); min-height: 20px; }
|
| 183 |
+
.dverdict b { color: var(--accent-strong); font: 800 17px/1.2 var(--display); }
|
| 184 |
+
.dcard.uncertain .dverdict b { color: var(--warn); }
|
| 185 |
+
.obar { display: grid; grid-template-columns: minmax(70px, 34%) 1fr 48px; gap: 8px; align-items: center; margin: 5px 0; font-size: 13px; }
|
| 186 |
+
.obar .k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
| 187 |
+
.obar.top .k { color: var(--ink); font-weight: 700; }
|
| 188 |
+
.obar .track { height: 12px; background: var(--bg2); border-radius: 6px; overflow: hidden; position: relative; }
|
| 189 |
+
.obar .fill { height: 100%; width: 0; background: #c3cddb; border-radius: 6px; transition: width 0.35s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.3s; }
|
| 190 |
+
.obar.top .fill { background: linear-gradient(90deg, var(--accent), #2cc2b3); }
|
| 191 |
+
.dcard.uncertain .obar.top .fill { background: linear-gradient(90deg, #e39410, var(--amber)); }
|
| 192 |
+
.obar .p { text-align: right; font: 12.5px var(--mono); }
|
| 193 |
+
.thr { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 12px; color: var(--muted); }
|
| 194 |
+
.thr input { flex: 1; }
|
| 195 |
+
.thr .tv { width: 34px; text-align: right; color: var(--ink); font-family: var(--mono); }
|
| 196 |
+
.dstats { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
| 197 |
+
.dedit { margin-top: 8px; display: grid; gap: 6px; }
|
| 198 |
+
.templates { margin-top: 18px; }
|
| 199 |
+
.templates h3 { font-size: 13px; margin: 0 0 8px; color: var(--muted); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
| 200 |
+
.tpls { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
| 201 |
+
.tpls button { font-size: 13px; padding: 6px 12px; border-radius: 999px; }
|
| 202 |
+
|
| 203 |
+
/* ---------------------------------------------------------------- single / samples */
|
| 204 |
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
| 205 |
+
@media (max-width: 800px) { .grid2 { grid-template-columns: 1fr; } }
|
| 206 |
+
.panel { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px; min-width: 0; box-shadow: var(--shadow); }
|
| 207 |
+
.panel h2 { margin: 0 0 12px; font: 800 21px/1.2 var(--display); letter-spacing: -0.02em; }
|
| 208 |
+
.wide { grid-column: 1 / -1; }
|
| 209 |
+
.subtabs { display: flex; gap: 4px; margin-bottom: 10px; flex-wrap: wrap; }
|
| 210 |
+
.subtab { padding: 5px 13px; font-size: 13px; border-radius: 999px; }
|
| 211 |
+
.subtab.active { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }
|
| 212 |
+
.col { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; font-size: 13px; color: var(--muted); font-weight: 600; }
|
| 213 |
+
.drop { display: block; border: 2px dashed var(--line); border-radius: 14px; padding: 12px; color: var(--muted); cursor: pointer; margin: 8px 0; font-size: 13px; background: var(--card2); transition: border-color 0.15s; }
|
| 214 |
+
.drop:hover { border-color: var(--accent); }
|
| 215 |
+
.drop input { display: block; margin-bottom: 4px; max-width: 100%; color: var(--ink); }
|
| 216 |
+
#imagePreview, #videoPreview { max-width: 100%; max-height: 300px; border-radius: 12px; display: block; margin-top: 8px; }
|
| 217 |
+
#audioPreview { width: 100%; margin-top: 8px; }
|
| 218 |
+
.seg { display: flex; gap: 16px; font-size: 14px; }
|
| 219 |
+
.seg input { accent-color: var(--accent); }
|
| 220 |
+
.adv { margin: 10px 0; border-top: 1px solid var(--line); padding-top: 8px; }
|
| 221 |
+
.adv summary { cursor: pointer; color: var(--muted); font-size: 14px; font-weight: 600; }
|
| 222 |
+
.decision { font: 800 24px/1.2 var(--display); margin: 0 0 10px; letter-spacing: -0.02em; animation: pop 0.42s cubic-bezier(0.2, 1.4, 0.4, 1); }
|
| 223 |
+
.decision b { color: var(--accent-strong); }
|
| 224 |
+
.meta { color: var(--muted); font-size: 13px; margin-top: 10px; }
|
| 225 |
+
.prompts { margin-top: 8px; font-size: 12px; color: var(--muted); }
|
| 226 |
+
.prompts pre { white-space: pre-wrap; word-break: break-word; background: var(--card2); border: 1px solid var(--line); padding: 8px; border-radius: 10px; color: var(--ink); font-family: var(--mono); }
|
| 227 |
+
.btnrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
| 228 |
+
.summary { margin: 12px 0; font-size: 16px; }
|
| 229 |
+
.summary b { color: var(--accent-strong); font: 800 20px/1 var(--display); }
|
| 230 |
+
.tablewrap { overflow-x: auto; margin-top: 8px; }
|
| 231 |
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
| 232 |
+
th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
| 233 |
+
th { color: var(--muted); font-weight: 700; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
|
| 234 |
+
tbody tr { animation: slidein 0.35s ease; }
|
| 235 |
+
td { font-variant-numeric: tabular-nums; }
|
| 236 |
+
td.ok { color: var(--good); font-weight: 700; }
|
| 237 |
+
td.no { color: var(--bad); font-weight: 700; }
|
| 238 |
+
.prose { max-width: 860px; }
|
| 239 |
+
.prose li { margin: 6px 0; }
|
| 240 |
+
.prose h3 { margin: 20px 0 6px; font: 800 17px/1.2 var(--display); }
|
| 241 |
+
.foot { color: var(--muted); font-size: 12.5px; padding-bottom: 28px; }
|
| 242 |
+
@media (max-width: 760px) {
|
| 243 |
+
.topbar { flex-direction: column; align-items: stretch; gap: 14px; padding-top: 20px; }
|
| 244 |
+
.statusbox { min-width: 0; max-width: none; width: 100%; }
|
| 245 |
+
.brand .sub { font-size: 14px; }
|
| 246 |
+
.reel-track img { height: 56px; width: 80px; }
|
| 247 |
+
}
|
| 248 |
+
@media (max-width: 600px) {
|
| 249 |
+
.ring { width: 58px; height: 58px; }
|
| 250 |
+
.stage { aspect-ratio: 3 / 4; }
|
| 251 |
+
.badge { font-size: 11.5px; padding: 4px 9px; left: 8px; top: 8px; }
|
| 252 |
+
.hud { top: 44px; left: 8px; right: auto; font-size: 11.5px; }
|
| 253 |
+
.overlay { left: 8px; right: 8px; bottom: 8px; gap: 10px; padding: 8px 10px; }
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* ---------------------------------------------------------------- egg check */
|
| 257 |
+
.egggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 12px; margin: 14px 0; }
|
| 258 |
+
.eggthumb { position: relative; padding: 6px; display: flex; flex-direction: column; gap: 5px; align-items: stretch; font-size: 12px; color: var(--muted); border-radius: 16px; box-shadow: var(--shadow); }
|
| 259 |
+
.eggthumb img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 11px; background: #1b1b1b; display: block; }
|
| 260 |
+
.eggthumb.busy img { animation: candle 0.9s ease-in-out infinite alternate; }
|
| 261 |
+
@keyframes candle { from { filter: brightness(1); } to { filter: brightness(1.45) saturate(1.2); } }
|
| 262 |
+
.eggthumb .stamp { position: absolute; top: 12px; right: 12px; min-width: 30px; height: 30px; padding: 0 8px; border-radius: 999px; display: flex; align-items: center; justify-content: center; font: 800 12px/1 var(--body); color: #fff; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); animation: stamp 0.45s cubic-bezier(0.2, 1.5, 0.4, 1); }
|
| 263 |
+
.eggthumb .stamp.good { background: var(--good); }
|
| 264 |
+
.eggthumb .stamp.bad { background: var(--bad); }
|
| 265 |
+
.eggthumb .stamp.miss { outline: 3px solid var(--amber); }
|
| 266 |
+
@keyframes stamp { from { transform: scale(2.2) rotate(-12deg); opacity: 0; } }
|
| 267 |
.btnrow .drop.inline { display: inline-block; margin: 0; padding: 8px 12px; }
|
| 268 |
.eggresult { display: grid; grid-template-columns: minmax(120px, 220px) 1fr; gap: 16px; align-items: start; margin: 12px 0; }
|
| 269 |
+
.eggresult img { width: 100%; border-radius: 14px; background: #1b1b1b; box-shadow: var(--shadow); }
|
| 270 |
@media (max-width: 560px) { .eggresult { grid-template-columns: 1fr; } }
|
| 271 |
+
.okc { color: var(--good); font-weight: 700; margin-left: 6px; font-size: 15px; font-family: var(--body); }
|
| 272 |
+
.noc { color: var(--bad); font-weight: 700; margin-left: 6px; font-size: 15px; font-family: var(--body); }
|
| 273 |
+
img.mini { width: 32px; height: 32px; object-fit: cover; border-radius: 6px; vertical-align: middle; margin-right: 6px; }
|
| 274 |
.dctx { display: block; font-size: 12px; font-weight: 400; color: var(--muted); }
|
| 275 |
.dedit input + input { margin-top: 6px; }
|
| 276 |
+
|
| 277 |
+
@media (prefers-reduced-motion: reduce) {
|
| 278 |
+
*, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
|
| 279 |
+
.reel-track { animation: none; }
|
| 280 |
+
.stage.running .scan { opacity: 0; }
|
| 281 |
+
}
|