SeaWolf-AI commited on
Commit
f4f4513
·
verified ·
1 Parent(s): af0b0b7

Bright theme and live motion: hero, picture reel, decision stream, scan beam, egg result stamps

Browse files
Files changed (3) hide show
  1. app.js +53 -8
  2. index.html +21 -4
  3. 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
- $("ovLabel").textContent = sure ? c.options[top].key.replace(/_/g, " ") : `uncertain`;
 
 
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 = "#05070a";
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 = "#05070a";
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 = "#2dd4bf";
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(45,212,191,.25)";
733
  feedCtx.fillRect(14, feed.height * 0.2, 10, feed.height * 0.5);
734
- feedCtx.fillStyle = "#2dd4bf";
735
  feedCtx.fillRect(14, feed.height * 0.2 + feed.height * 0.5 * (1 - lvl), 10, feed.height * 0.5 * lvl);
736
- feedCtx.fillStyle = "rgba(255,255,255,.55)";
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="#0b0d12" />
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="stylesheet" href="./style.css?v=5" />
 
 
 
11
  </head>
12
  <body>
13
  <header class="top">
 
14
  <div class="wrap topbar">
15
  <div class="brand">
16
- <h1>Decide Live</h1>
 
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=5"></script>
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
- :root {
2
- --bg: #0b0d12;
3
- --bg2: #10131a;
4
- --card: #151922;
5
- --card2: #1b202b;
6
- --ink: #e9ecf2;
7
- --muted: #8d95a5;
8
- --line: #262c38;
9
- --accent: #2dd4bf;
10
- --accent-ink: #04201c;
11
- --accent-dim: rgba(45, 212, 191, 0.16);
12
- --warn: #f5b84b;
13
- --bad: #ff7a6b;
14
- --good: #2dd4bf;
15
- --radius: 14px;
16
- color-scheme: dark;
17
- }
18
- * { box-sizing: border-box; }
19
- html, body { margin: 0; background: var(--bg); color: var(--ink); }
20
- body { font: 15px/1.5 Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
21
- a { color: var(--accent); }
22
- code { font-size: 0.88em; background: var(--card2); padding: 1px 5px; border-radius: 5px; }
23
- .wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px; }
24
- [hidden] { display: none !important; }
25
-
26
- /* header */
27
- .top { background: linear-gradient(180deg, #0f1420 0%, var(--bg) 100%); border-bottom: 1px solid var(--line); }
28
- .topbar { display: flex; gap: 20px; justify-content: space-between; align-items: flex-start; padding-top: 16px; padding-bottom: 8px; }
29
- .brand h1 { margin: 0; font-size: 24px; letter-spacing: -0.02em; }
30
- .brand .sub { margin: 4px 0 0; color: var(--muted); max-width: 640px; font-size: 14px; }
31
- .statusbox { min-width: 260px; max-width: 360px; }
32
- .status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
33
- .status .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }
34
- .status.ready .dot { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-dim); }
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
- /* egg check */
185
- .egggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; margin: 12px 0; }
186
- .eggthumb { padding: 6px; display: flex; flex-direction: column; gap: 4px; align-items: stretch; font-size: 12px; color: var(--muted); }
187
- .eggthumb img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #000; display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 10px; background: #000; }
191
  @media (max-width: 560px) { .eggresult { grid-template-columns: 1fr; } }
192
- .okc { color: var(--good); font-weight: 600; margin-left: 6px; }
193
- .noc { color: var(--bad); font-weight: 600; margin-left: 6px; }
194
- img.mini { width: 32px; height: 32px; object-fit: cover; border-radius: 4px; vertical-align: middle; margin-right: 6px; }
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
+ }