Release EgoSieve manifest inspector v0.1.0
Browse files- README.md +16 -5
- app.mjs +914 -0
- index.html +291 -17
- manifest.mjs +302 -0
- manifest.test.mjs +150 -0
- styles.css +1667 -0
README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
| 1 |
---
|
| 2 |
title: EgoSieve
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: EgoSieve
|
| 3 |
+
emoji: 🎞️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
+
models:
|
| 9 |
+
- itspublu/EgoSieve-S
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# EgoSieve manifest inspector
|
| 13 |
+
|
| 14 |
+
Inspect a versioned EgoSieve JSONL manifest alongside an optional local video.
|
| 15 |
+
The browser renders readiness curves, compiled segments, issue evidence, and
|
| 16 |
+
recorded run metadata. Segment selections seek the local video, and the loaded
|
| 17 |
+
manifest or a compact segment export can be downloaded for reuse.
|
| 18 |
+
|
| 19 |
+
This is a dependency-free Static Space. Files are processed only in browser
|
| 20 |
+
memory and are never uploaded. Model inference runs through the EgoSieve CLI;
|
| 21 |
+
the Space is deliberately an inspector, not an inference service.
|
app.mjs
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
READINESS_LABELS,
|
| 3 |
+
aggregateIssues,
|
| 4 |
+
buildSegmentExport,
|
| 5 |
+
decisionFor,
|
| 6 |
+
formatDuration,
|
| 7 |
+
parseManifest,
|
| 8 |
+
summarizeManifest,
|
| 9 |
+
} from "./manifest.mjs";
|
| 10 |
+
|
| 11 |
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
| 12 |
+
const MAX_MANIFEST_BYTES = 32 * 1024 * 1024;
|
| 13 |
+
const CHART = Object.freeze({ width: 1000, height: 360, left: 58, right: 20, top: 28, bottom: 45 });
|
| 14 |
+
const COLORS = Object.freeze({ KEEP: "#d7ff35", REVIEW: "#ff5b26", REJECT: "#898b83" });
|
| 15 |
+
|
| 16 |
+
const state = {
|
| 17 |
+
parsed: null,
|
| 18 |
+
manifestText: "",
|
| 19 |
+
manifestFileName: "",
|
| 20 |
+
manifestUrl: null,
|
| 21 |
+
videoUrl: null,
|
| 22 |
+
videoFileName: "",
|
| 23 |
+
videoDurationWarning: null,
|
| 24 |
+
selectedSegmentIndex: null,
|
| 25 |
+
routeFilter: "all",
|
| 26 |
+
toastTimer: null,
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
function byId(id) {
|
| 30 |
+
const element = document.getElementById(id);
|
| 31 |
+
if (!element) throw new Error(`Missing required element #${id}`);
|
| 32 |
+
return element;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function clear(element) {
|
| 36 |
+
element.replaceChildren();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function node(tagName, className = "", text = null) {
|
| 40 |
+
const element = document.createElement(tagName);
|
| 41 |
+
if (className) element.className = className;
|
| 42 |
+
if (text !== null) element.textContent = text;
|
| 43 |
+
return element;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function svgNode(tagName, attributes = {}) {
|
| 47 |
+
const element = document.createElementNS(SVG_NS, tagName);
|
| 48 |
+
for (const [name, value] of Object.entries(attributes)) {
|
| 49 |
+
element.setAttribute(name, String(value));
|
| 50 |
+
}
|
| 51 |
+
return element;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function safeFileName(value, fallback) {
|
| 55 |
+
const leaf = String(value || fallback).split(/[\\/]/u).pop() || fallback;
|
| 56 |
+
const safe = leaf.replace(/[^a-zA-Z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "");
|
| 57 |
+
return safe || fallback;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function displayPathLeaf(value) {
|
| 61 |
+
return String(value || "unknown source").split(/[\\/]/u).pop() || "unknown source";
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function formatPercent(value, digits = 0) {
|
| 65 |
+
return typeof value === "number" && Number.isFinite(value)
|
| 66 |
+
? `${(value * 100).toFixed(digits)}%`
|
| 67 |
+
: "—";
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function formatTimecode(seconds) {
|
| 71 |
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return "00:00.0";
|
| 72 |
+
const hours = Math.floor(seconds / 3600);
|
| 73 |
+
const minutes = Math.floor((seconds % 3600) / 60);
|
| 74 |
+
const remainder = seconds % 60;
|
| 75 |
+
const base = `${String(minutes).padStart(2, "0")}:${remainder.toFixed(1).padStart(4, "0")}`;
|
| 76 |
+
return hours > 0 ? `${hours}:${base}` : base;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function prettyIssue(name) {
|
| 80 |
+
return String(name).replaceAll("_", " ");
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function routeClass(route) {
|
| 84 |
+
return `route-${route === "discard" ? "discard" : route}`;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function decisionRoute(segment) {
|
| 88 |
+
if (["keep", "review", "discard"].includes(segment.route)) return segment.route;
|
| 89 |
+
return { KEEP: "keep", REVIEW: "review", REJECT: "discard" }[decisionFor(segment)] ?? "review";
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function showNotice(message, isError = false) {
|
| 93 |
+
const notice = byId("notice");
|
| 94 |
+
notice.textContent = message;
|
| 95 |
+
notice.classList.toggle("is-error", isError);
|
| 96 |
+
notice.hidden = false;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function hideNotice() {
|
| 100 |
+
byId("notice").hidden = true;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function showToast(message) {
|
| 104 |
+
const toast = byId("toast");
|
| 105 |
+
toast.textContent = message;
|
| 106 |
+
toast.hidden = false;
|
| 107 |
+
if (state.toastTimer !== null) window.clearTimeout(state.toastTimer);
|
| 108 |
+
state.toastTimer = window.setTimeout(() => {
|
| 109 |
+
toast.hidden = true;
|
| 110 |
+
state.toastTimer = null;
|
| 111 |
+
}, 2600);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
async function copyText(text, successMessage) {
|
| 115 |
+
try {
|
| 116 |
+
if (navigator.clipboard && window.isSecureContext) {
|
| 117 |
+
await navigator.clipboard.writeText(text);
|
| 118 |
+
} else {
|
| 119 |
+
const helper = document.createElement("textarea");
|
| 120 |
+
helper.value = text;
|
| 121 |
+
helper.setAttribute("readonly", "");
|
| 122 |
+
helper.style.position = "fixed";
|
| 123 |
+
helper.style.opacity = "0";
|
| 124 |
+
document.body.append(helper);
|
| 125 |
+
helper.select();
|
| 126 |
+
const copied = document.execCommand("copy");
|
| 127 |
+
helper.remove();
|
| 128 |
+
if (!copied) throw new Error("copy command was refused");
|
| 129 |
+
}
|
| 130 |
+
showToast(successMessage);
|
| 131 |
+
} catch {
|
| 132 |
+
showToast("Copy was blocked. Select the text and copy it manually.");
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
function revokeManifestUrl() {
|
| 137 |
+
if (state.manifestUrl) URL.revokeObjectURL(state.manifestUrl);
|
| 138 |
+
state.manifestUrl = null;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function updateManifestDownload() {
|
| 142 |
+
revokeManifestUrl();
|
| 143 |
+
const link = byId("download-manifest");
|
| 144 |
+
if (!state.manifestText) {
|
| 145 |
+
link.removeAttribute("href");
|
| 146 |
+
return;
|
| 147 |
+
}
|
| 148 |
+
state.manifestUrl = URL.createObjectURL(
|
| 149 |
+
new Blob([state.manifestText], { type: "application/x-ndjson;charset=utf-8" }),
|
| 150 |
+
);
|
| 151 |
+
link.href = state.manifestUrl;
|
| 152 |
+
link.download = safeFileName(state.manifestFileName, "egosieve-manifest.jsonl");
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function visibleSegments() {
|
| 156 |
+
if (!state.parsed) return [];
|
| 157 |
+
if (state.routeFilter === "all") return [...state.parsed.segments];
|
| 158 |
+
return state.parsed.segments.filter((segment) => decisionRoute(segment) === state.routeFilter);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function renderWarnings() {
|
| 162 |
+
const stack = byId("warning-stack");
|
| 163 |
+
clear(stack);
|
| 164 |
+
const warnings = [...(state.parsed?.warnings ?? [])];
|
| 165 |
+
if (state.videoDurationWarning) warnings.push(state.videoDurationWarning);
|
| 166 |
+
if (warnings.length === 0) {
|
| 167 |
+
stack.hidden = true;
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
for (const warning of warnings) stack.append(node("p", "", `⚠ ${warning}`));
|
| 171 |
+
stack.hidden = false;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function checkVideoDuration() {
|
| 175 |
+
const video = byId("video");
|
| 176 |
+
const manifestDuration = state.parsed?.header.source.duration_s;
|
| 177 |
+
if (!state.videoUrl || !manifestDuration || !Number.isFinite(video.duration)) {
|
| 178 |
+
state.videoDurationWarning = null;
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
const difference = Math.abs(video.duration - manifestDuration);
|
| 182 |
+
const tolerance = Math.max(0.5, manifestDuration * 0.01);
|
| 183 |
+
state.videoDurationWarning = difference > tolerance
|
| 184 |
+
? `Local video duration (${formatDuration(video.duration)}) differs from the manifest (${formatDuration(manifestDuration)}). Verify that the files match.`
|
| 185 |
+
: null;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
function readinessPoint(windowRecord, label) {
|
| 189 |
+
const value = windowRecord.readiness?.[label];
|
| 190 |
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function renderTimeline(parsed) {
|
| 194 |
+
const svg = byId("timeline");
|
| 195 |
+
clear(svg);
|
| 196 |
+
const title = svgNode("title", { id: "timeline-title" });
|
| 197 |
+
title.textContent = "Readiness probabilities over time";
|
| 198 |
+
const description = svgNode("desc", { id: "timeline-description" });
|
| 199 |
+
const evidenceWindows = parsed.windows.filter((windowRecord) =>
|
| 200 |
+
READINESS_LABELS.every((label) => readinessPoint(windowRecord, label) !== null),
|
| 201 |
+
);
|
| 202 |
+
description.textContent = evidenceWindows.length > 0
|
| 203 |
+
? `${evidenceWindows.length} scored windows across ${formatDuration(parsed.header.source.duration_s)}.`
|
| 204 |
+
: "This manifest contains no window-level readiness probability maps.";
|
| 205 |
+
svg.append(title, description);
|
| 206 |
+
|
| 207 |
+
const chartWidth = CHART.width - CHART.left - CHART.right;
|
| 208 |
+
const chartHeight = CHART.height - CHART.top - CHART.bottom;
|
| 209 |
+
const duration = Math.max(parsed.header.source.duration_s, 0.001);
|
| 210 |
+
const x = (seconds) => CHART.left + (Math.max(0, Math.min(duration, seconds)) / duration) * chartWidth;
|
| 211 |
+
const y = (probability) => CHART.top + (1 - probability) * chartHeight;
|
| 212 |
+
|
| 213 |
+
for (const probability of [0, 0.25, 0.5, 0.75, 1]) {
|
| 214 |
+
svg.append(
|
| 215 |
+
svgNode("line", {
|
| 216 |
+
x1: CHART.left,
|
| 217 |
+
y1: y(probability),
|
| 218 |
+
x2: CHART.left + chartWidth,
|
| 219 |
+
y2: y(probability),
|
| 220 |
+
stroke: "rgba(239,238,230,.13)",
|
| 221 |
+
"stroke-width": 1,
|
| 222 |
+
"vector-effect": "non-scaling-stroke",
|
| 223 |
+
}),
|
| 224 |
+
);
|
| 225 |
+
const label = svgNode("text", { x: CHART.left - 12, y: y(probability) + 4, "text-anchor": "end" });
|
| 226 |
+
label.textContent = `${Math.round(probability * 100)}`;
|
| 227 |
+
svg.append(label);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
const tickCount = duration > 900 ? 4 : 6;
|
| 231 |
+
for (let index = 0; index <= tickCount; index += 1) {
|
| 232 |
+
const seconds = (duration * index) / tickCount;
|
| 233 |
+
svg.append(
|
| 234 |
+
svgNode("line", {
|
| 235 |
+
x1: x(seconds),
|
| 236 |
+
y1: CHART.top,
|
| 237 |
+
x2: x(seconds),
|
| 238 |
+
y2: CHART.top + chartHeight,
|
| 239 |
+
stroke: "rgba(239,238,230,.075)",
|
| 240 |
+
"stroke-width": 1,
|
| 241 |
+
"vector-effect": "non-scaling-stroke",
|
| 242 |
+
}),
|
| 243 |
+
);
|
| 244 |
+
const label = svgNode("text", {
|
| 245 |
+
x: x(seconds),
|
| 246 |
+
y: CHART.top + chartHeight + 25,
|
| 247 |
+
"text-anchor": index === 0 ? "start" : index === tickCount ? "end" : "middle",
|
| 248 |
+
});
|
| 249 |
+
label.textContent = formatTimecode(seconds);
|
| 250 |
+
svg.append(label);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
if (evidenceWindows.length === 0) {
|
| 254 |
+
const empty = svgNode("text", {
|
| 255 |
+
x: CHART.left + chartWidth / 2,
|
| 256 |
+
y: CHART.top + chartHeight / 2,
|
| 257 |
+
"text-anchor": "middle",
|
| 258 |
+
});
|
| 259 |
+
empty.textContent = "NO WINDOW PROBABILITY CURVES IN THIS MANIFEST";
|
| 260 |
+
svg.append(empty);
|
| 261 |
+
} else {
|
| 262 |
+
for (const label of [...READINESS_LABELS].reverse()) {
|
| 263 |
+
const coordinates = evidenceWindows.map((windowRecord) => ({
|
| 264 |
+
x: x((windowRecord.start_s + windowRecord.end_s) / 2),
|
| 265 |
+
y: y(readinessPoint(windowRecord, label)),
|
| 266 |
+
}));
|
| 267 |
+
const path = svgNode("path", {
|
| 268 |
+
d: coordinates.map((point, index) => `${index === 0 ? "M" : "L"}${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(" "),
|
| 269 |
+
fill: "none",
|
| 270 |
+
stroke: COLORS[label],
|
| 271 |
+
"stroke-width": label === "KEEP" ? 3.5 : 2,
|
| 272 |
+
"stroke-linejoin": "round",
|
| 273 |
+
"stroke-linecap": "round",
|
| 274 |
+
opacity: label === "KEEP" ? 1 : 0.82,
|
| 275 |
+
"vector-effect": "non-scaling-stroke",
|
| 276 |
+
});
|
| 277 |
+
svg.append(path);
|
| 278 |
+
if (label === "KEEP") {
|
| 279 |
+
for (const point of coordinates) {
|
| 280 |
+
svg.append(
|
| 281 |
+
svgNode("circle", {
|
| 282 |
+
cx: point.x,
|
| 283 |
+
cy: point.y,
|
| 284 |
+
r: 3.5,
|
| 285 |
+
fill: COLORS.KEEP,
|
| 286 |
+
stroke: "#10110f",
|
| 287 |
+
"stroke-width": 1.5,
|
| 288 |
+
"vector-effect": "non-scaling-stroke",
|
| 289 |
+
}),
|
| 290 |
+
);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
svg.append(
|
| 297 |
+
svgNode("line", {
|
| 298 |
+
id: "playhead-line",
|
| 299 |
+
x1: CHART.left,
|
| 300 |
+
y1: CHART.top,
|
| 301 |
+
x2: CHART.left,
|
| 302 |
+
y2: CHART.top + chartHeight,
|
| 303 |
+
stroke: "#efeee6",
|
| 304 |
+
"stroke-width": 1,
|
| 305 |
+
"stroke-dasharray": "4 4",
|
| 306 |
+
opacity: 0,
|
| 307 |
+
"vector-effect": "non-scaling-stroke",
|
| 308 |
+
"pointer-events": "none",
|
| 309 |
+
}),
|
| 310 |
+
);
|
| 311 |
+
byId("timeline-end").textContent = formatTimecode(duration);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
function renderSegmentTrack(parsed) {
|
| 315 |
+
const track = byId("segment-track");
|
| 316 |
+
clear(track);
|
| 317 |
+
const duration = Math.max(parsed.header.source.duration_s, 0.001);
|
| 318 |
+
if (parsed.segments.length === 0) {
|
| 319 |
+
track.setAttribute("aria-label", "No compiled segments in this manifest");
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
+
for (const segment of parsed.segments) {
|
| 323 |
+
const route = decisionRoute(segment);
|
| 324 |
+
const button = node("button", `track-segment ${routeClass(route)}`);
|
| 325 |
+
button.type = "button";
|
| 326 |
+
button.dataset.segmentIndex = String(segment.segment_index);
|
| 327 |
+
button.style.left = `${Math.max(0, Math.min(100, (segment.start_s / duration) * 100))}%`;
|
| 328 |
+
button.style.width = `${Math.max(0.2, Math.min(100, ((segment.end_s - segment.start_s) / duration) * 100))}%`;
|
| 329 |
+
button.title = `${decisionFor(segment)} · ${formatTimecode(segment.start_s)}–${formatTimecode(segment.end_s)}`;
|
| 330 |
+
button.setAttribute("aria-label", `Select ${decisionFor(segment)} segment ${segment.segment_index}, ${formatTimecode(segment.start_s)} to ${formatTimecode(segment.end_s)}`);
|
| 331 |
+
button.addEventListener("click", () => selectSegment(segment, true));
|
| 332 |
+
track.append(button);
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
function renderSegments() {
|
| 337 |
+
const list = byId("segment-list");
|
| 338 |
+
clear(list);
|
| 339 |
+
const segments = visibleSegments();
|
| 340 |
+
byId("download-segments").textContent = state.routeFilter === "all"
|
| 341 |
+
? "Export all segments"
|
| 342 |
+
: `Export ${state.routeFilter === "discard" ? "reject" : state.routeFilter} segments`;
|
| 343 |
+
if (segments.length === 0) {
|
| 344 |
+
list.append(node("p", "empty-list", "No compiled segments match this route."));
|
| 345 |
+
return;
|
| 346 |
+
}
|
| 347 |
+
for (const segment of segments) {
|
| 348 |
+
const route = decisionRoute(segment);
|
| 349 |
+
const decision = decisionFor(segment);
|
| 350 |
+
const button = node("button", "segment-row");
|
| 351 |
+
button.type = "button";
|
| 352 |
+
button.dataset.segmentIndex = String(segment.segment_index);
|
| 353 |
+
button.setAttribute("aria-label", `Inspect ${decision} segment ${segment.segment_index} at ${formatTimecode(segment.start_s)}`);
|
| 354 |
+
|
| 355 |
+
button.append(node("span", "segment-row-index", String(segment.segment_index).padStart(2, "0")));
|
| 356 |
+
button.append(node("span", `segment-route ${routeClass(route)}`, decision));
|
| 357 |
+
|
| 358 |
+
const time = node("span", "segment-time");
|
| 359 |
+
time.append(
|
| 360 |
+
node("strong", "", `${formatTimecode(segment.start_s)} → ${formatTimecode(segment.end_s)}`),
|
| 361 |
+
node("small", "", `${formatDuration(segment.end_s - segment.start_s)} · ${segment.reason ?? "stable"}`),
|
| 362 |
+
);
|
| 363 |
+
button.append(time);
|
| 364 |
+
|
| 365 |
+
const confidence = segment.readiness?.[decision] ?? segment.mean_score;
|
| 366 |
+
const score = node("span", "segment-score");
|
| 367 |
+
score.append(node("strong", "", formatPercent(confidence)), node("small", "", "route confidence"));
|
| 368 |
+
button.append(score);
|
| 369 |
+
|
| 370 |
+
const reports = Array.isArray(segment.reported_issues) ? segment.reported_issues.length : 0;
|
| 371 |
+
button.append(node("span", "segment-issue-count", `${reports} SIGNAL${reports === 1 ? "" : "S"}`));
|
| 372 |
+
button.addEventListener("click", () => selectSegment(segment, true));
|
| 373 |
+
list.append(button);
|
| 374 |
+
}
|
| 375 |
+
updateSelectedClasses();
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function renderIssues(parsed) {
|
| 379 |
+
const list = byId("issue-list");
|
| 380 |
+
clear(list);
|
| 381 |
+
const issues = aggregateIssues(parsed.segments.length > 0 ? parsed.segments : parsed.windows);
|
| 382 |
+
if (issues.length === 0) {
|
| 383 |
+
list.append(node("p", "empty-list", "No issue probabilities were recorded."));
|
| 384 |
+
return;
|
| 385 |
+
}
|
| 386 |
+
for (const issue of issues) {
|
| 387 |
+
const row = node("div", "issue-row");
|
| 388 |
+
const heading = node("div", "issue-row-head");
|
| 389 |
+
heading.append(
|
| 390 |
+
node("span", "issue-name", prettyIssue(issue.name)),
|
| 391 |
+
node("span", "issue-value", `${formatPercent(issue.max)} / ${formatPercent(issue.mean)}`),
|
| 392 |
+
);
|
| 393 |
+
const meter = node("div", "issue-meter");
|
| 394 |
+
meter.title = `Maximum ${formatPercent(issue.max, 1)}; mean ${formatPercent(issue.mean, 1)} across ${issue.samples} record${issue.samples === 1 ? "" : "s"}`;
|
| 395 |
+
const maximum = node("span");
|
| 396 |
+
maximum.style.width = `${Math.min(100, issue.max * 100)}%`;
|
| 397 |
+
const mean = node("i");
|
| 398 |
+
mean.style.left = `${Math.min(100, issue.mean * 100)}%`;
|
| 399 |
+
meter.append(maximum, mean);
|
| 400 |
+
row.append(heading, meter);
|
| 401 |
+
list.append(row);
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
function metadataValue(value) {
|
| 406 |
+
if (value === null || value === undefined || value === "") return "—";
|
| 407 |
+
if (typeof value === "object") return JSON.stringify(value);
|
| 408 |
+
return String(value);
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
function renderMetadata(header) {
|
| 412 |
+
const source = header.source ?? {};
|
| 413 |
+
const model = header.model ?? {};
|
| 414 |
+
const sampling = header.sampling ?? {};
|
| 415 |
+
const policy = header.policy ?? {};
|
| 416 |
+
const geometry = source.display_width && source.display_height
|
| 417 |
+
? `${source.display_width} × ${source.display_height}${source.rotation_degrees ? ` · ${source.rotation_degrees}° display rotation` : ""}`
|
| 418 |
+
: "—";
|
| 419 |
+
const metadata = [
|
| 420 |
+
["Source file", displayPathLeaf(source.source_path)],
|
| 421 |
+
["Source SHA-256", source.source_sha256 ?? "not recorded"],
|
| 422 |
+
["Display geometry", geometry],
|
| 423 |
+
["Codec / FPS", `${source.codec_name ?? "unknown"} / ${source.fps ? Number(source.fps).toFixed(3) : "unknown"}`],
|
| 424 |
+
["Model", model.id ?? "not recorded"],
|
| 425 |
+
["Resolved revision", model.revision ?? "not recorded"],
|
| 426 |
+
["Processor revision", model.processor_revision ?? "not recorded"],
|
| 427 |
+
["Generator", header.generator ?? "not recorded"],
|
| 428 |
+
["Created", header.created_at ?? "not recorded"],
|
| 429 |
+
["Window sampling", sampling.window_duration_s ? `${sampling.window_duration_s}s / stride ${sampling.stride_s}s / ${sampling.frames_per_window} frames` : "not recorded"],
|
| 430 |
+
["Keep hysteresis", policy.enter_threshold !== undefined ? `enter ${policy.enter_threshold} / exit ${policy.exit_threshold}` : "not recorded"],
|
| 431 |
+
["Uncertainty route", policy.uncertainty_threshold !== undefined ? `${policy.uncertainty_route ?? "review"} at ${policy.uncertainty_threshold}` : "not recorded"],
|
| 432 |
+
];
|
| 433 |
+
const grid = byId("metadata-grid");
|
| 434 |
+
clear(grid);
|
| 435 |
+
for (const [label, value] of metadata) {
|
| 436 |
+
const item = node("div", "metadata-item");
|
| 437 |
+
item.append(node("span", "", label), node("strong", "", metadataValue(value)));
|
| 438 |
+
grid.append(item);
|
| 439 |
+
}
|
| 440 |
+
byId("raw-header").textContent = JSON.stringify(header, null, 2);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
function renderSelected(segment) {
|
| 444 |
+
const panel = byId("selected-panel");
|
| 445 |
+
if (!segment) {
|
| 446 |
+
panel.hidden = true;
|
| 447 |
+
return;
|
| 448 |
+
}
|
| 449 |
+
const decision = decisionFor(segment);
|
| 450 |
+
const route = decisionRoute(segment);
|
| 451 |
+
panel.hidden = false;
|
| 452 |
+
byId("selected-title").textContent = `Segment ${String(segment.segment_index).padStart(2, "0")}`;
|
| 453 |
+
const routeLabel = byId("selected-route");
|
| 454 |
+
routeLabel.textContent = decision;
|
| 455 |
+
routeLabel.className = routeClass(route);
|
| 456 |
+
byId("selected-time").textContent = `${formatTimecode(segment.start_s)} → ${formatTimecode(segment.end_s)}`;
|
| 457 |
+
byId("selected-reason").textContent = `${formatDuration(segment.end_s - segment.start_s)} · ${segment.reason ?? "stable"}`;
|
| 458 |
+
|
| 459 |
+
const scores = byId("selected-scores");
|
| 460 |
+
clear(scores);
|
| 461 |
+
for (const label of READINESS_LABELS) {
|
| 462 |
+
const item = node("div");
|
| 463 |
+
item.append(node("dt", "", label), node("dd", "", formatPercent(segment.readiness?.[label], 1)));
|
| 464 |
+
scores.append(item);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
const issues = byId("selected-issues");
|
| 468 |
+
clear(issues);
|
| 469 |
+
const ranked = Object.entries(segment.issues ?? {}).sort((left, right) => right[1] - left[1]);
|
| 470 |
+
if (ranked.length === 0) {
|
| 471 |
+
issues.append(node("span", "issue-chip", "No issue evidence recorded"));
|
| 472 |
+
} else {
|
| 473 |
+
for (const [name, probability] of ranked) {
|
| 474 |
+
issues.append(node("span", "issue-chip", `${prettyIssue(name)} ${formatPercent(probability)}`));
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
function updateSelectedClasses() {
|
| 480 |
+
for (const element of document.querySelectorAll("[data-segment-index]")) {
|
| 481 |
+
element.classList.toggle(
|
| 482 |
+
"is-selected",
|
| 483 |
+
Number(element.dataset.segmentIndex) === state.selectedSegmentIndex,
|
| 484 |
+
);
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
function selectSegment(segment, seekVideo) {
|
| 489 |
+
state.selectedSegmentIndex = segment.segment_index;
|
| 490 |
+
renderSelected(segment);
|
| 491 |
+
updateSelectedClasses();
|
| 492 |
+
if (seekVideo) seekTo(segment.start_s);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
function seekTo(seconds) {
|
| 496 |
+
const video = byId("video");
|
| 497 |
+
if (!state.videoUrl) {
|
| 498 |
+
showToast(`Segment starts at ${formatTimecode(seconds)}. Open the matching video to seek.`);
|
| 499 |
+
return;
|
| 500 |
+
}
|
| 501 |
+
const target = Number.isFinite(video.duration)
|
| 502 |
+
? Math.max(0, Math.min(seconds, Math.max(0, video.duration - 0.01)))
|
| 503 |
+
: Math.max(0, seconds);
|
| 504 |
+
video.currentTime = target;
|
| 505 |
+
byId("video-timecode").textContent = formatTimecode(target);
|
| 506 |
+
showToast(`Video set to ${formatTimecode(target)}.`);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
function updatePlayhead(seconds) {
|
| 510 |
+
const line = document.getElementById("playhead-line");
|
| 511 |
+
if (!line || !state.parsed) return;
|
| 512 |
+
const duration = Math.max(state.parsed.header.source.duration_s, 0.001);
|
| 513 |
+
const chartWidth = CHART.width - CHART.left - CHART.right;
|
| 514 |
+
const position = CHART.left + (Math.max(0, Math.min(duration, seconds)) / duration) * chartWidth;
|
| 515 |
+
line.setAttribute("x1", String(position));
|
| 516 |
+
line.setAttribute("x2", String(position));
|
| 517 |
+
line.setAttribute("opacity", "0.7");
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function renderWorkspace() {
|
| 521 |
+
const parsed = state.parsed;
|
| 522 |
+
if (!parsed) return;
|
| 523 |
+
const header = parsed.header;
|
| 524 |
+
const summary = summarizeManifest(parsed);
|
| 525 |
+
const source = header.source;
|
| 526 |
+
const model = header.model ?? {};
|
| 527 |
+
byId("identity-file").textContent = state.manifestFileName;
|
| 528 |
+
byId("identity-model").textContent = model.id
|
| 529 |
+
? `${model.id} @ ${model.revision ?? model.requested_revision ?? "unresolved revision"}`
|
| 530 |
+
: "Model identity not recorded";
|
| 531 |
+
byId("metric-duration").textContent = formatDuration(summary.duration);
|
| 532 |
+
byId("metric-geometry").textContent = source.display_width && source.display_height
|
| 533 |
+
? `${source.display_width} × ${source.display_height} · ${source.fps ? `${Number(source.fps).toFixed(2)} fps` : "fps unknown"}`
|
| 534 |
+
: "geometry not recorded";
|
| 535 |
+
byId("metric-windows").textContent = String(summary.windows).padStart(2, "0");
|
| 536 |
+
byId("metric-window-mix").textContent = `${summary.windowDecisions.KEEP} keep / ${summary.windowDecisions.REVIEW} review / ${summary.windowDecisions.REJECT} reject`;
|
| 537 |
+
byId("metric-segments").textContent = String(summary.segments).padStart(2, "0");
|
| 538 |
+
byId("metric-keep").textContent = formatDuration(summary.routedSeconds.KEEP);
|
| 539 |
+
byId("metric-review").textContent = `${formatDuration(summary.routedSeconds.REVIEW)} routed to review`;
|
| 540 |
+
checkVideoDuration();
|
| 541 |
+
renderWarnings();
|
| 542 |
+
renderTimeline(parsed);
|
| 543 |
+
renderSegmentTrack(parsed);
|
| 544 |
+
renderSegments();
|
| 545 |
+
renderIssues(parsed);
|
| 546 |
+
renderMetadata(header);
|
| 547 |
+
updateManifestDownload();
|
| 548 |
+
const selected = parsed.segments.find((segment) => segment.segment_index === state.selectedSegmentIndex)
|
| 549 |
+
?? parsed.segments[0]
|
| 550 |
+
?? null;
|
| 551 |
+
state.selectedSegmentIndex = selected?.segment_index ?? null;
|
| 552 |
+
renderSelected(selected);
|
| 553 |
+
updateSelectedClasses();
|
| 554 |
+
byId("workspace").hidden = false;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
function acceptManifestText(text, fileName, isDemo = false) {
|
| 558 |
+
const parsed = parseManifest(text);
|
| 559 |
+
state.parsed = parsed;
|
| 560 |
+
state.manifestText = text.endsWith("\n") ? text : `${text}\n`;
|
| 561 |
+
state.manifestFileName = safeFileName(fileName, "egosieve-manifest.jsonl");
|
| 562 |
+
state.selectedSegmentIndex = null;
|
| 563 |
+
state.routeFilter = "all";
|
| 564 |
+
for (const button of document.querySelectorAll("[data-route]")) {
|
| 565 |
+
button.setAttribute("aria-pressed", button.dataset.route === "all" ? "true" : "false");
|
| 566 |
+
}
|
| 567 |
+
byId("manifest-file-name").textContent = state.manifestFileName;
|
| 568 |
+
renderWorkspace();
|
| 569 |
+
showNotice(
|
| 570 |
+
isDemo
|
| 571 |
+
? "Illustrative evidence loaded. It demonstrates the inspector and is not a checkpoint result."
|
| 572 |
+
: `${parsed.windows.length} windows and ${parsed.segments.length} compiled segments loaded locally.`,
|
| 573 |
+
);
|
| 574 |
+
byId("workspace").scrollIntoView({ behavior: "smooth", block: "start" });
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
async function loadManifestFile(file) {
|
| 578 |
+
if (!file) return;
|
| 579 |
+
if (file.size > MAX_MANIFEST_BYTES) {
|
| 580 |
+
showNotice("That manifest is larger than 32 MB. Open a smaller EgoSieve JSONL file.", true);
|
| 581 |
+
return;
|
| 582 |
+
}
|
| 583 |
+
try {
|
| 584 |
+
acceptManifestText(await file.text(), file.name);
|
| 585 |
+
} catch (error) {
|
| 586 |
+
showNotice(error instanceof Error ? error.message : "The manifest could not be parsed.", true);
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
function loadVideoFile(file) {
|
| 591 |
+
if (!file) return;
|
| 592 |
+
if (file.type && !file.type.startsWith("video/")) {
|
| 593 |
+
showNotice("The optional media file must be a video supported by this browser.", true);
|
| 594 |
+
return;
|
| 595 |
+
}
|
| 596 |
+
if (state.videoUrl) URL.revokeObjectURL(state.videoUrl);
|
| 597 |
+
state.videoUrl = URL.createObjectURL(file);
|
| 598 |
+
state.videoFileName = file.name;
|
| 599 |
+
state.videoDurationWarning = null;
|
| 600 |
+
const video = byId("video");
|
| 601 |
+
video.src = state.videoUrl;
|
| 602 |
+
video.hidden = false;
|
| 603 |
+
video.load();
|
| 604 |
+
byId("video-empty").hidden = true;
|
| 605 |
+
byId("video-timecode").hidden = false;
|
| 606 |
+
byId("video-file-name").textContent = file.name;
|
| 607 |
+
byId("video-state").textContent = "LOCAL VIDEO";
|
| 608 |
+
byId("media-footnote").textContent = "Click any segment or timeline position to seek this local file.";
|
| 609 |
+
showToast(`${file.name} opened locally.`);
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
function resetWorkspace() {
|
| 613 |
+
state.parsed = null;
|
| 614 |
+
state.manifestText = "";
|
| 615 |
+
state.manifestFileName = "";
|
| 616 |
+
state.selectedSegmentIndex = null;
|
| 617 |
+
state.routeFilter = "all";
|
| 618 |
+
state.videoDurationWarning = null;
|
| 619 |
+
revokeManifestUrl();
|
| 620 |
+
if (state.videoUrl) URL.revokeObjectURL(state.videoUrl);
|
| 621 |
+
state.videoUrl = null;
|
| 622 |
+
state.videoFileName = "";
|
| 623 |
+
const video = byId("video");
|
| 624 |
+
video.pause();
|
| 625 |
+
video.removeAttribute("src");
|
| 626 |
+
video.load();
|
| 627 |
+
video.hidden = true;
|
| 628 |
+
byId("video-empty").hidden = false;
|
| 629 |
+
byId("video-timecode").hidden = true;
|
| 630 |
+
byId("video-state").textContent = "NO VIDEO";
|
| 631 |
+
byId("manifest-input").value = "";
|
| 632 |
+
byId("video-input").value = "";
|
| 633 |
+
byId("manifest-file-name").textContent = "Drop JSONL here or choose a file";
|
| 634 |
+
byId("video-file-name").textContent = "Drop video here or choose a file";
|
| 635 |
+
byId("media-footnote").textContent = "Segment selections still work without media; their timestamps remain inspectable.";
|
| 636 |
+
byId("workspace").hidden = true;
|
| 637 |
+
hideNotice();
|
| 638 |
+
showToast("Workspace cleared.");
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
function exportVisibleSegments() {
|
| 642 |
+
if (!state.parsed) return;
|
| 643 |
+
const selectedIndexes = new Set(visibleSegments().map((segment) => segment.segment_index));
|
| 644 |
+
const payload = JSON.stringify(buildSegmentExport(state.parsed, selectedIndexes), null, 2) + "\n";
|
| 645 |
+
const url = URL.createObjectURL(new Blob([payload], { type: "application/json;charset=utf-8" }));
|
| 646 |
+
const anchor = document.createElement("a");
|
| 647 |
+
const stem = safeFileName(state.manifestFileName, "egosieve-manifest").replace(/\.(jsonl|ndjson)$/iu, "");
|
| 648 |
+
anchor.href = url;
|
| 649 |
+
anchor.download = `${stem}.${state.routeFilter === "all" ? "segments" : state.routeFilter}.json`;
|
| 650 |
+
document.body.append(anchor);
|
| 651 |
+
anchor.click();
|
| 652 |
+
anchor.remove();
|
| 653 |
+
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 654 |
+
showToast(`${selectedIndexes.size} segment${selectedIndexes.size === 1 ? "" : "s"} exported.`);
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
function chartPosition(event) {
|
| 658 |
+
const svg = byId("timeline");
|
| 659 |
+
const bounds = svg.getBoundingClientRect();
|
| 660 |
+
const x = ((event.clientX - bounds.left) / Math.max(bounds.width, 1)) * CHART.width;
|
| 661 |
+
const duration = state.parsed?.header.source.duration_s ?? 0;
|
| 662 |
+
const fraction = (x - CHART.left) / (CHART.width - CHART.left - CHART.right);
|
| 663 |
+
return Math.max(0, Math.min(duration, fraction * duration));
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
function nearestWindow(seconds) {
|
| 667 |
+
if (!state.parsed || state.parsed.windows.length === 0) return null;
|
| 668 |
+
let nearest = null;
|
| 669 |
+
let nearestDistance = Infinity;
|
| 670 |
+
for (const windowRecord of state.parsed.windows) {
|
| 671 |
+
const midpoint = (windowRecord.start_s + windowRecord.end_s) / 2;
|
| 672 |
+
const distance = Math.abs(midpoint - seconds);
|
| 673 |
+
if (distance < nearestDistance) {
|
| 674 |
+
nearest = windowRecord;
|
| 675 |
+
nearestDistance = distance;
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
return nearest;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
function handleChartPointer(event) {
|
| 682 |
+
if (!state.parsed) return;
|
| 683 |
+
const windowRecord = nearestWindow(chartPosition(event));
|
| 684 |
+
const tooltip = byId("chart-tooltip");
|
| 685 |
+
if (!windowRecord?.readiness) {
|
| 686 |
+
tooltip.hidden = true;
|
| 687 |
+
return;
|
| 688 |
+
}
|
| 689 |
+
const stageBounds = byId("timeline-stage").getBoundingClientRect();
|
| 690 |
+
tooltip.textContent = [
|
| 691 |
+
`${formatTimecode(windowRecord.start_s)} → ${formatTimecode(windowRecord.end_s)}`,
|
| 692 |
+
...READINESS_LABELS.map((label) => `${label} ${formatPercent(windowRecord.readiness[label], 1)}`),
|
| 693 |
+
`UNCERTAINTY ${formatPercent(windowRecord.routing_uncertainty ?? windowRecord.uncertainty, 1)}`,
|
| 694 |
+
].join("\n");
|
| 695 |
+
tooltip.style.whiteSpace = "pre-line";
|
| 696 |
+
tooltip.style.left = `${Math.min(stageBounds.width - 175, Math.max(8, event.clientX - stageBounds.left + 14))}px`;
|
| 697 |
+
tooltip.style.top = `${Math.max(8, event.clientY - stageBounds.top - 88)}px`;
|
| 698 |
+
tooltip.hidden = false;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
function demoManifestText() {
|
| 702 |
+
const duration = 42;
|
| 703 |
+
const probabilities = [
|
| 704 |
+
[0.82, 0.13, 0.05],
|
| 705 |
+
[0.88, 0.09, 0.03],
|
| 706 |
+
[0.52, 0.41, 0.07],
|
| 707 |
+
[0.24, 0.63, 0.13],
|
| 708 |
+
[0.72, 0.23, 0.05],
|
| 709 |
+
[0.91, 0.07, 0.02],
|
| 710 |
+
[0.86, 0.1, 0.04],
|
| 711 |
+
[0.68, 0.28, 0.04],
|
| 712 |
+
[0.37, 0.55, 0.08],
|
| 713 |
+
[0.18, 0.32, 0.5],
|
| 714 |
+
];
|
| 715 |
+
const issueNames = [
|
| 716 |
+
"acting_hand_not_visible",
|
| 717 |
+
"low_hand_activity",
|
| 718 |
+
"camera_instability",
|
| 719 |
+
"blur",
|
| 720 |
+
"exposure",
|
| 721 |
+
"scene_cut",
|
| 722 |
+
"duplicate_frames",
|
| 723 |
+
];
|
| 724 |
+
const windows = probabilities.map((values, index) => {
|
| 725 |
+
const start = index * 4;
|
| 726 |
+
const end = Math.min(duration, start + 6);
|
| 727 |
+
const readiness = Object.fromEntries(READINESS_LABELS.map((label, labelIndex) => [label, values[labelIndex]]));
|
| 728 |
+
const issues = Object.fromEntries(issueNames.map((name, issueIndex) => [
|
| 729 |
+
name,
|
| 730 |
+
Math.min(0.92, 0.03 + (((index + 2) * (issueIndex + 3)) % 11) * 0.035),
|
| 731 |
+
]));
|
| 732 |
+
if (index === 3) issues.blur = 0.68;
|
| 733 |
+
if (index === 8) issues.duplicate_frames = 0.57;
|
| 734 |
+
return {
|
| 735 |
+
record_type: "window",
|
| 736 |
+
schema_version: 1,
|
| 737 |
+
window_index: index,
|
| 738 |
+
start_s: start,
|
| 739 |
+
end_s: end,
|
| 740 |
+
source_start_s: start,
|
| 741 |
+
source_end_s: end,
|
| 742 |
+
timestamps_s: Array.from({ length: 12 }, (_, frame) => start + ((frame + 0.5) * (end - start)) / 12),
|
| 743 |
+
decision: READINESS_LABELS[values.indexOf(Math.max(...values))],
|
| 744 |
+
readiness,
|
| 745 |
+
score: readiness.KEEP,
|
| 746 |
+
uncertainty: 0.2 + index * 0.025,
|
| 747 |
+
routing_uncertainty: Math.max(0.2 + index * 0.025, readiness.REVIEW),
|
| 748 |
+
issues,
|
| 749 |
+
reported_issues: Object.entries(issues).filter(([, value]) => value >= 0.35).map(([name]) => name),
|
| 750 |
+
boundary: [],
|
| 751 |
+
};
|
| 752 |
+
});
|
| 753 |
+
const makeSegment = (segment_index, start_s, end_s, route, window_indices, readiness, issues, reported_issues = []) => ({
|
| 754 |
+
record_type: "segment",
|
| 755 |
+
schema_version: 1,
|
| 756 |
+
segment_index,
|
| 757 |
+
segment_id: `sha256:example-${segment_index}`,
|
| 758 |
+
start_s,
|
| 759 |
+
end_s,
|
| 760 |
+
source_start_s: start_s,
|
| 761 |
+
source_end_s: end_s,
|
| 762 |
+
duration_s: end_s - start_s,
|
| 763 |
+
route,
|
| 764 |
+
decision: { keep: "KEEP", review: "REVIEW", discard: "REJECT" }[route],
|
| 765 |
+
reason: route === "review" ? "uncertain" : "stable",
|
| 766 |
+
window_indices,
|
| 767 |
+
mean_score: readiness.KEEP,
|
| 768 |
+
min_score: Math.max(0, readiness.KEEP - 0.08),
|
| 769 |
+
max_score: Math.min(1, readiness.KEEP + 0.08),
|
| 770 |
+
readiness,
|
| 771 |
+
issues,
|
| 772 |
+
reported_issues,
|
| 773 |
+
});
|
| 774 |
+
const baseIssues = Object.fromEntries(issueNames.map((name) => [name, 0.12]));
|
| 775 |
+
const segments = [
|
| 776 |
+
makeSegment(0, 0, 10, "keep", [0, 1], { KEEP: 0.85, REVIEW: 0.11, REJECT: 0.04 }, { ...baseIssues, low_hand_activity: 0.08 }),
|
| 777 |
+
makeSegment(1, 10, 16, "review", [2, 3], { KEEP: 0.38, REVIEW: 0.52, REJECT: 0.1 }, { ...baseIssues, blur: 0.68, camera_instability: 0.41 }, ["blur", "camera_instability"]),
|
| 778 |
+
makeSegment(2, 16, 30, "keep", [4, 5, 6, 7], { KEEP: 0.79, REVIEW: 0.17, REJECT: 0.04 }, { ...baseIssues, exposure: 0.29 }),
|
| 779 |
+
makeSegment(3, 30, 36, "review", [7, 8], { KEEP: 0.39, REVIEW: 0.53, REJECT: 0.08 }, { ...baseIssues, duplicate_frames: 0.57 }, ["duplicate_frames"]),
|
| 780 |
+
];
|
| 781 |
+
const header = {
|
| 782 |
+
record_type: "manifest",
|
| 783 |
+
schema: "egosieve.video-compilation",
|
| 784 |
+
schema_version: 1,
|
| 785 |
+
created_at: "2026-08-23T12:00:00Z",
|
| 786 |
+
generator: "egosieve static-inspector example",
|
| 787 |
+
example: "illustrative interface data; not a checkpoint result",
|
| 788 |
+
source: {
|
| 789 |
+
source_path: "example/assembly-pass.mp4",
|
| 790 |
+
source_sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
| 791 |
+
source_size_bytes: 18420000,
|
| 792 |
+
duration_s: duration,
|
| 793 |
+
start_time_s: 0,
|
| 794 |
+
width: 1920,
|
| 795 |
+
height: 1080,
|
| 796 |
+
display_width: 1920,
|
| 797 |
+
display_height: 1080,
|
| 798 |
+
rotation_degrees: 0,
|
| 799 |
+
fps: 29.97,
|
| 800 |
+
frame_count: 1259,
|
| 801 |
+
codec_name: "h264",
|
| 802 |
+
},
|
| 803 |
+
model: {
|
| 804 |
+
id: "itspublu/EgoSieve-S",
|
| 805 |
+
revision: "illustrative-example",
|
| 806 |
+
requested_revision: "illustrative-example",
|
| 807 |
+
processor_revision: "illustrative-example",
|
| 808 |
+
readiness_labels: READINESS_LABELS,
|
| 809 |
+
},
|
| 810 |
+
counts: {
|
| 811 |
+
windows: windows.length,
|
| 812 |
+
unique_samples: 84,
|
| 813 |
+
segments: segments.length,
|
| 814 |
+
window_decisions: { KEEP: 7, REVIEW: 2, REJECT: 1 },
|
| 815 |
+
segment_decisions: { KEEP: 2, REVIEW: 2, REJECT: 0 },
|
| 816 |
+
},
|
| 817 |
+
sampling: {
|
| 818 |
+
window_duration_s: 6,
|
| 819 |
+
stride_s: 4,
|
| 820 |
+
frames_per_window: 12,
|
| 821 |
+
window_count: windows.length,
|
| 822 |
+
unique_sample_count: 84,
|
| 823 |
+
},
|
| 824 |
+
policy: {
|
| 825 |
+
enter_threshold: 0.7,
|
| 826 |
+
exit_threshold: 0.5,
|
| 827 |
+
merge_gap_s: 0.5,
|
| 828 |
+
min_duration_s: 1,
|
| 829 |
+
uncertainty_threshold: 0.5,
|
| 830 |
+
uncertainty_route: "review",
|
| 831 |
+
short_segment_route: "discard",
|
| 832 |
+
include_discard: false,
|
| 833 |
+
},
|
| 834 |
+
issue_labels: issueNames,
|
| 835 |
+
issue_reporting_thresholds: Object.fromEntries(issueNames.map((name) => [name, 0.35])),
|
| 836 |
+
};
|
| 837 |
+
return [header, ...windows, ...segments].map((record) => JSON.stringify(record)).join("\n") + "\n";
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
function configureDropZone(zoneId, handler) {
|
| 841 |
+
const zone = byId(zoneId);
|
| 842 |
+
for (const eventName of ["dragenter", "dragover"]) {
|
| 843 |
+
zone.addEventListener(eventName, (event) => {
|
| 844 |
+
event.preventDefault();
|
| 845 |
+
event.dataTransfer.dropEffect = "copy";
|
| 846 |
+
zone.classList.add("is-dragging");
|
| 847 |
+
});
|
| 848 |
+
}
|
| 849 |
+
for (const eventName of ["dragleave", "drop"]) {
|
| 850 |
+
zone.addEventListener(eventName, (event) => {
|
| 851 |
+
event.preventDefault();
|
| 852 |
+
zone.classList.remove("is-dragging");
|
| 853 |
+
});
|
| 854 |
+
}
|
| 855 |
+
zone.addEventListener("drop", (event) => handler(event.dataTransfer.files?.[0] ?? null));
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
function init() {
|
| 859 |
+
byId("manifest-input").addEventListener("change", (event) => loadManifestFile(event.target.files?.[0]));
|
| 860 |
+
byId("video-input").addEventListener("change", (event) => loadVideoFile(event.target.files?.[0]));
|
| 861 |
+
configureDropZone("manifest-drop", loadManifestFile);
|
| 862 |
+
configureDropZone("video-drop", loadVideoFile);
|
| 863 |
+
byId("demo-button").addEventListener("click", () => {
|
| 864 |
+
try {
|
| 865 |
+
acceptManifestText(demoManifestText(), "illustrative.egosieve.jsonl", true);
|
| 866 |
+
} catch (error) {
|
| 867 |
+
showNotice(error instanceof Error ? error.message : "The example could not be loaded.", true);
|
| 868 |
+
}
|
| 869 |
+
});
|
| 870 |
+
byId("reset-button").addEventListener("click", resetWorkspace);
|
| 871 |
+
byId("copy-cli").addEventListener("click", () => copyText(byId("cli-command").textContent, "CLI command copied."));
|
| 872 |
+
byId("copy-segment").addEventListener("click", () => {
|
| 873 |
+
const segment = state.parsed?.segments.find((row) => row.segment_index === state.selectedSegmentIndex);
|
| 874 |
+
if (segment) copyText(JSON.stringify(segment, null, 2), "Segment JSON copied.");
|
| 875 |
+
});
|
| 876 |
+
byId("download-segments").addEventListener("click", exportVisibleSegments);
|
| 877 |
+
|
| 878 |
+
for (const button of document.querySelectorAll("[data-route]")) {
|
| 879 |
+
button.addEventListener("click", () => {
|
| 880 |
+
state.routeFilter = button.dataset.route;
|
| 881 |
+
for (const peer of document.querySelectorAll("[data-route]")) {
|
| 882 |
+
peer.setAttribute("aria-pressed", peer === button ? "true" : "false");
|
| 883 |
+
}
|
| 884 |
+
renderSegments();
|
| 885 |
+
});
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
const video = byId("video");
|
| 889 |
+
video.addEventListener("loadedmetadata", () => {
|
| 890 |
+
checkVideoDuration();
|
| 891 |
+
renderWarnings();
|
| 892 |
+
});
|
| 893 |
+
video.addEventListener("timeupdate", () => {
|
| 894 |
+
byId("video-timecode").textContent = formatTimecode(video.currentTime);
|
| 895 |
+
updatePlayhead(video.currentTime);
|
| 896 |
+
});
|
| 897 |
+
video.addEventListener("error", () => showNotice("This browser could not decode the selected video. The manifest remains available.", true));
|
| 898 |
+
|
| 899 |
+
const timeline = byId("timeline");
|
| 900 |
+
timeline.addEventListener("pointermove", handleChartPointer);
|
| 901 |
+
timeline.addEventListener("pointerleave", () => {
|
| 902 |
+
byId("chart-tooltip").hidden = true;
|
| 903 |
+
});
|
| 904 |
+
timeline.addEventListener("click", (event) => {
|
| 905 |
+
if (state.parsed) seekTo(chartPosition(event));
|
| 906 |
+
});
|
| 907 |
+
|
| 908 |
+
window.addEventListener("beforeunload", () => {
|
| 909 |
+
revokeManifestUrl();
|
| 910 |
+
if (state.videoUrl) URL.revokeObjectURL(state.videoUrl);
|
| 911 |
+
});
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
init();
|
index.html
CHANGED
|
@@ -1,19 +1,293 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<meta name="color-scheme" content="dark">
|
| 7 |
+
<meta
|
| 8 |
+
http-equiv="Content-Security-Policy"
|
| 9 |
+
content="default-src 'self' blob:; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"
|
| 10 |
+
>
|
| 11 |
+
<meta
|
| 12 |
+
name="description"
|
| 13 |
+
content="Inspect EgoSieve video-readiness manifests locally in your browser."
|
| 14 |
+
>
|
| 15 |
+
<title>EgoSieve — Manifest Inspector</title>
|
| 16 |
+
<link rel="stylesheet" href="styles.css">
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<a class="skip-link" href="#main">Skip to inspector</a>
|
| 20 |
+
|
| 21 |
+
<header class="masthead">
|
| 22 |
+
<a class="wordmark" href="#top" aria-label="EgoSieve home">
|
| 23 |
+
<span class="wordmark-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
| 24 |
+
<span>EGO/SIEVE</span>
|
| 25 |
+
</a>
|
| 26 |
+
<div class="masthead-status">
|
| 27 |
+
<span class="status-lamp" aria-hidden="true"></span>
|
| 28 |
+
LOCAL EVIDENCE CONSOLE
|
| 29 |
+
</div>
|
| 30 |
+
<a class="masthead-link" href="https://huggingface.co/itspublu/EgoSieve-S">MODEL CARD ↗</a>
|
| 31 |
+
</header>
|
| 32 |
+
|
| 33 |
+
<main id="main">
|
| 34 |
+
<section class="hero" id="top" aria-labelledby="hero-title">
|
| 35 |
+
<div class="hero-index" aria-hidden="true">INSPECTOR / 01</div>
|
| 36 |
+
<div class="hero-copy">
|
| 37 |
+
<p class="eyebrow"><span>VIDEO READINESS</span> / MANIFEST WORKBENCH</p>
|
| 38 |
+
<h1 id="hero-title">READ THE<br><em>USEFUL</em> SECONDS.</h1>
|
| 39 |
+
<p class="hero-deck">
|
| 40 |
+
Open an EgoSieve evidence manifest. Trace each routing decision, audit observable
|
| 41 |
+
failure signals, and jump straight to the corresponding moment in your local video.
|
| 42 |
+
</p>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="hero-stamp" aria-label="Private by design">
|
| 45 |
+
<span>PRIVATE</span>
|
| 46 |
+
<strong>BY<br>DESIGN</strong>
|
| 47 |
+
<small>NO FILE TRANSFER</small>
|
| 48 |
+
</div>
|
| 49 |
+
</section>
|
| 50 |
+
|
| 51 |
+
<section class="load-bay" aria-labelledby="load-title">
|
| 52 |
+
<div class="section-heading">
|
| 53 |
+
<span class="section-number">01</span>
|
| 54 |
+
<div>
|
| 55 |
+
<p class="eyebrow">SOURCE MATERIAL</p>
|
| 56 |
+
<h2 id="load-title">Open evidence</h2>
|
| 57 |
+
</div>
|
| 58 |
+
<p class="section-note">Both files stay in this browser tab. The video is optional.</p>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="file-grid">
|
| 62 |
+
<label class="file-bay manifest-bay" id="manifest-drop">
|
| 63 |
+
<input id="manifest-input" type="file" accept=".jsonl,.ndjson,application/x-ndjson,application/json">
|
| 64 |
+
<span class="file-number">A</span>
|
| 65 |
+
<span class="file-icon" aria-hidden="true">↳</span>
|
| 66 |
+
<strong>EgoSieve manifest</strong>
|
| 67 |
+
<span id="manifest-file-name">Drop JSONL here or choose a file</span>
|
| 68 |
+
<small>Required · schema v1 · maximum 32 MB</small>
|
| 69 |
+
</label>
|
| 70 |
+
|
| 71 |
+
<label class="file-bay video-bay" id="video-drop">
|
| 72 |
+
<input id="video-input" type="file" accept="video/*">
|
| 73 |
+
<span class="file-number">B</span>
|
| 74 |
+
<span class="file-icon" aria-hidden="true">▷</span>
|
| 75 |
+
<strong>Matching local video</strong>
|
| 76 |
+
<span id="video-file-name">Drop video here or choose a file</span>
|
| 77 |
+
<small>Optional · used only for playback and seeking</small>
|
| 78 |
+
</label>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div class="load-actions">
|
| 82 |
+
<button class="button button-primary" id="demo-button" type="button">Load example evidence</button>
|
| 83 |
+
<button class="button button-quiet" id="reset-button" type="button">Reset workspace</button>
|
| 84 |
+
<div class="privacy-note">
|
| 85 |
+
<span aria-hidden="true">◉</span>
|
| 86 |
+
Processing is local. This page has no network connection.
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="notice" id="notice" role="status" aria-live="polite" hidden></div>
|
| 90 |
+
</section>
|
| 91 |
+
|
| 92 |
+
<section class="cli-band" aria-labelledby="cli-title">
|
| 93 |
+
<div class="cli-label">
|
| 94 |
+
<span class="section-number">02</span>
|
| 95 |
+
<div>
|
| 96 |
+
<p class="eyebrow">CREATE A MANIFEST</p>
|
| 97 |
+
<h2 id="cli-title">Inference runs in the CLI</h2>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
<p>
|
| 101 |
+
This static inspector does not run the model. Scan a video on your machine, then open
|
| 102 |
+
the resulting JSONL here.
|
| 103 |
+
</p>
|
| 104 |
+
<div class="command-shell">
|
| 105 |
+
<span class="prompt" aria-hidden="true">$</span>
|
| 106 |
+
<code id="cli-command">egosieve scan INPUT_VIDEO --model itspublu/EgoSieve-S --revision 40f8549166c9c64f205e63c0836cd88f4208d112 --output OUTPUT.egosieve.jsonl</code>
|
| 107 |
+
<button class="copy-button" id="copy-cli" type="button" aria-label="Copy CLI command">COPY</button>
|
| 108 |
+
</div>
|
| 109 |
+
</section>
|
| 110 |
+
|
| 111 |
+
<section class="workspace" id="workspace" aria-labelledby="workspace-title" hidden>
|
| 112 |
+
<div class="section-heading workspace-heading">
|
| 113 |
+
<span class="section-number">03</span>
|
| 114 |
+
<div>
|
| 115 |
+
<p class="eyebrow">EVIDENCE LOADED</p>
|
| 116 |
+
<h2 id="workspace-title">Inspection desk</h2>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="manifest-identity">
|
| 119 |
+
<strong id="identity-file">—</strong>
|
| 120 |
+
<span id="identity-model">—</span>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<div class="metric-grid" aria-label="Manifest summary">
|
| 125 |
+
<article class="metric-card metric-acid">
|
| 126 |
+
<span>Source duration</span>
|
| 127 |
+
<strong id="metric-duration">—</strong>
|
| 128 |
+
<small id="metric-geometry">—</small>
|
| 129 |
+
</article>
|
| 130 |
+
<article class="metric-card">
|
| 131 |
+
<span>Sampled windows</span>
|
| 132 |
+
<strong id="metric-windows">—</strong>
|
| 133 |
+
<small id="metric-window-mix">—</small>
|
| 134 |
+
</article>
|
| 135 |
+
<article class="metric-card">
|
| 136 |
+
<span>Compiled segments</span>
|
| 137 |
+
<strong id="metric-segments">—</strong>
|
| 138 |
+
<small>exclusive routed spans</small>
|
| 139 |
+
</article>
|
| 140 |
+
<article class="metric-card metric-orange">
|
| 141 |
+
<span>Keep time</span>
|
| 142 |
+
<strong id="metric-keep">—</strong>
|
| 143 |
+
<small id="metric-review">—</small>
|
| 144 |
+
</article>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="warning-stack" id="warning-stack" aria-live="polite" hidden></div>
|
| 148 |
+
|
| 149 |
+
<div class="analysis-grid">
|
| 150 |
+
<article class="panel media-panel">
|
| 151 |
+
<div class="panel-heading">
|
| 152 |
+
<div>
|
| 153 |
+
<span class="panel-index">A / LOCAL MEDIA</span>
|
| 154 |
+
<h3>Source monitor</h3>
|
| 155 |
+
</div>
|
| 156 |
+
<span class="panel-state" id="video-state">NO VIDEO</span>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="video-stage" id="video-stage">
|
| 159 |
+
<video
|
| 160 |
+
id="video"
|
| 161 |
+
controls
|
| 162 |
+
preload="metadata"
|
| 163 |
+
playsinline
|
| 164 |
+
aria-label="Local video matched to the manifest"
|
| 165 |
+
hidden
|
| 166 |
+
></video>
|
| 167 |
+
<div class="video-empty" id="video-empty">
|
| 168 |
+
<span aria-hidden="true">B</span>
|
| 169 |
+
<strong>Video is optional</strong>
|
| 170 |
+
<p>Open the matching local file above to enable timecode seeking.</p>
|
| 171 |
+
</div>
|
| 172 |
+
<div class="video-timecode" id="video-timecode" hidden>00:00.0</div>
|
| 173 |
+
</div>
|
| 174 |
+
<p class="media-footnote" id="media-footnote">
|
| 175 |
+
Segment selections still work without media; their timestamps remain inspectable.
|
| 176 |
+
</p>
|
| 177 |
+
</article>
|
| 178 |
+
|
| 179 |
+
<article class="panel timeline-panel">
|
| 180 |
+
<div class="panel-heading">
|
| 181 |
+
<div>
|
| 182 |
+
<span class="panel-index">B / MODEL OUTPUT</span>
|
| 183 |
+
<h3>Readiness trace</h3>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="legend" aria-label="Readiness legend">
|
| 186 |
+
<span class="legend-keep">KEEP</span>
|
| 187 |
+
<span class="legend-review">REVIEW</span>
|
| 188 |
+
<span class="legend-reject">REJECT</span>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="timeline-stage" id="timeline-stage">
|
| 192 |
+
<svg
|
| 193 |
+
id="timeline"
|
| 194 |
+
viewBox="0 0 1000 360"
|
| 195 |
+
role="img"
|
| 196 |
+
aria-labelledby="timeline-title timeline-description"
|
| 197 |
+
preserveAspectRatio="none"
|
| 198 |
+
>
|
| 199 |
+
<title id="timeline-title">Readiness probabilities over time</title>
|
| 200 |
+
<desc id="timeline-description">Load a manifest to view the readiness trace.</desc>
|
| 201 |
+
</svg>
|
| 202 |
+
<div class="chart-tooltip" id="chart-tooltip" role="status" hidden></div>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="segment-track" id="segment-track" aria-label="Clickable compiled segment timeline"></div>
|
| 205 |
+
<div class="timeline-axis">
|
| 206 |
+
<span>00:00</span>
|
| 207 |
+
<span id="timeline-end">END</span>
|
| 208 |
+
</div>
|
| 209 |
+
</article>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
<div class="evidence-grid">
|
| 213 |
+
<article class="panel segments-panel">
|
| 214 |
+
<div class="panel-heading">
|
| 215 |
+
<div>
|
| 216 |
+
<span class="panel-index">C / ROUTING</span>
|
| 217 |
+
<h3>Compiled segments</h3>
|
| 218 |
+
</div>
|
| 219 |
+
<div class="route-filters" aria-label="Filter segments">
|
| 220 |
+
<button type="button" data-route="all" aria-pressed="true">ALL</button>
|
| 221 |
+
<button type="button" data-route="keep" aria-pressed="false">KEEP</button>
|
| 222 |
+
<button type="button" data-route="review" aria-pressed="false">REVIEW</button>
|
| 223 |
+
<button type="button" data-route="discard" aria-pressed="false">REJECT</button>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="segment-list" id="segment-list"></div>
|
| 227 |
+
</article>
|
| 228 |
+
|
| 229 |
+
<aside class="panel issue-panel" aria-labelledby="issues-title">
|
| 230 |
+
<div class="panel-heading">
|
| 231 |
+
<div>
|
| 232 |
+
<span class="panel-index">D / OBSERVABLE SIGNALS</span>
|
| 233 |
+
<h3 id="issues-title">Issue evidence</h3>
|
| 234 |
+
</div>
|
| 235 |
+
<span class="panel-state">MAX / MEAN</span>
|
| 236 |
+
</div>
|
| 237 |
+
<p class="issue-deck">Probabilities are model evidence, not verified ground truth.</p>
|
| 238 |
+
<div class="issue-list" id="issue-list"></div>
|
| 239 |
+
</aside>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<article class="panel selected-panel" id="selected-panel" hidden>
|
| 243 |
+
<div class="panel-heading">
|
| 244 |
+
<div>
|
| 245 |
+
<span class="panel-index">SELECTED EVIDENCE</span>
|
| 246 |
+
<h3 id="selected-title">Segment</h3>
|
| 247 |
+
</div>
|
| 248 |
+
<button class="copy-button dark" id="copy-segment" type="button">COPY JSON</button>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="selected-grid">
|
| 251 |
+
<div class="selected-decision">
|
| 252 |
+
<span id="selected-route">KEEP</span>
|
| 253 |
+
<strong id="selected-time">00:00.0 → 00:06.0</strong>
|
| 254 |
+
<small id="selected-reason">stable</small>
|
| 255 |
+
</div>
|
| 256 |
+
<dl class="selected-scores" id="selected-scores"></dl>
|
| 257 |
+
<div class="selected-issues" id="selected-issues"></div>
|
| 258 |
+
</div>
|
| 259 |
+
</article>
|
| 260 |
+
|
| 261 |
+
<details class="metadata-drawer">
|
| 262 |
+
<summary>
|
| 263 |
+
<span><b>04</b> Run identity & recorded policy</span>
|
| 264 |
+
<small>inspect raw header</small>
|
| 265 |
+
</summary>
|
| 266 |
+
<div class="metadata-grid" id="metadata-grid"></div>
|
| 267 |
+
<pre id="raw-header" tabindex="0" aria-label="Raw manifest header"></pre>
|
| 268 |
+
</details>
|
| 269 |
+
|
| 270 |
+
<div class="export-dock">
|
| 271 |
+
<div>
|
| 272 |
+
<p class="eyebrow">TAKE THE EVIDENCE WITH YOU</p>
|
| 273 |
+
<h3>Download. Diff. Reuse.</h3>
|
| 274 |
+
<p>The original JSONL stays untouched; the segment export is a compact JSON selection.</p>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="export-actions">
|
| 277 |
+
<a class="button button-primary" id="download-manifest" download>Download manifest</a>
|
| 278 |
+
<button class="button button-quiet light" id="download-segments" type="button">Export visible segments</button>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</section>
|
| 282 |
+
</main>
|
| 283 |
+
|
| 284 |
+
<footer>
|
| 285 |
+
<span>EGO/SIEVE · MANIFEST INSPECTOR</span>
|
| 286 |
+
<span>STATIC / LOCAL / REPRODUCIBLE</span>
|
| 287 |
+
</footer>
|
| 288 |
+
|
| 289 |
+
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
| 290 |
+
<noscript>This inspector requires JavaScript to parse local JSONL files.</noscript>
|
| 291 |
+
<script type="module" src="app.mjs"></script>
|
| 292 |
+
</body>
|
| 293 |
</html>
|
manifest.mjs
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const MANIFEST_SCHEMA = "egosieve.video-compilation";
|
| 2 |
+
export const MANIFEST_VERSION = 1;
|
| 3 |
+
export const READINESS_LABELS = Object.freeze(["KEEP", "REVIEW", "REJECT"]);
|
| 4 |
+
export const ROUTE_TO_DECISION = Object.freeze({
|
| 5 |
+
keep: "KEEP",
|
| 6 |
+
review: "REVIEW",
|
| 7 |
+
discard: "REJECT",
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
export class ManifestValidationError extends Error {
|
| 11 |
+
constructor(message, line = null) {
|
| 12 |
+
super(line === null ? message : `Line ${line}: ${message}`);
|
| 13 |
+
this.name = "ManifestValidationError";
|
| 14 |
+
this.line = line;
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function isObject(value) {
|
| 19 |
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function finiteNumber(value, name, line, { min = -Infinity, max = Infinity } = {}) {
|
| 23 |
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
| 24 |
+
throw new ManifestValidationError(`${name} must be a finite number`, line);
|
| 25 |
+
}
|
| 26 |
+
if (value < min || value > max) {
|
| 27 |
+
throw new ManifestValidationError(`${name} must be between ${min} and ${max}`, line);
|
| 28 |
+
}
|
| 29 |
+
return value;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function validateInterval(record, line) {
|
| 33 |
+
const start = finiteNumber(record.start_s, "start_s", line, { min: 0 });
|
| 34 |
+
const end = finiteNumber(record.end_s, "end_s", line, { min: 0 });
|
| 35 |
+
if (end <= start) {
|
| 36 |
+
throw new ManifestValidationError("end_s must be greater than start_s", line);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function validateProbabilityMap(value, name, line, warnings, requireReadiness = false) {
|
| 41 |
+
if (!isObject(value)) {
|
| 42 |
+
throw new ManifestValidationError(`${name} must be an object`, line);
|
| 43 |
+
}
|
| 44 |
+
if (requireReadiness) {
|
| 45 |
+
for (const label of READINESS_LABELS) {
|
| 46 |
+
if (!Object.hasOwn(value, label)) {
|
| 47 |
+
throw new ManifestValidationError(`${name} is missing ${label}`, line);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
const entries = Object.entries(value);
|
| 52 |
+
for (const [key, probability] of entries) {
|
| 53 |
+
finiteNumber(probability, `${name}.${key}`, line, { min: 0, max: 1 });
|
| 54 |
+
}
|
| 55 |
+
if (requireReadiness) {
|
| 56 |
+
const total = READINESS_LABELS.reduce((sum, label) => sum + value[label], 0);
|
| 57 |
+
if (Math.abs(total - 1) > 0.02) {
|
| 58 |
+
warnings.push(`Line ${line}: readiness probabilities sum to ${total.toFixed(3)}, not 1.`);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function validateHeader(header, line) {
|
| 64 |
+
if (!isObject(header)) {
|
| 65 |
+
throw new ManifestValidationError("the first non-empty line must be an object", line);
|
| 66 |
+
}
|
| 67 |
+
if (header.record_type !== "manifest") {
|
| 68 |
+
throw new ManifestValidationError("the first record must have record_type \"manifest\"", line);
|
| 69 |
+
}
|
| 70 |
+
if (header.schema !== MANIFEST_SCHEMA) {
|
| 71 |
+
throw new ManifestValidationError(`unsupported schema ${JSON.stringify(header.schema)}`, line);
|
| 72 |
+
}
|
| 73 |
+
if (header.schema_version !== MANIFEST_VERSION) {
|
| 74 |
+
throw new ManifestValidationError(
|
| 75 |
+
`unsupported schema version ${JSON.stringify(header.schema_version)}; expected ${MANIFEST_VERSION}`,
|
| 76 |
+
line,
|
| 77 |
+
);
|
| 78 |
+
}
|
| 79 |
+
if (!isObject(header.source)) {
|
| 80 |
+
throw new ManifestValidationError("manifest.source must be an object", line);
|
| 81 |
+
}
|
| 82 |
+
finiteNumber(header.source.duration_s, "source.duration_s", line, { min: 0 });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function validateWindow(record, line, warnings) {
|
| 86 |
+
if (!Number.isInteger(record.window_index) || record.window_index < 0) {
|
| 87 |
+
throw new ManifestValidationError("window_index must be a non-negative integer", line);
|
| 88 |
+
}
|
| 89 |
+
validateInterval(record, line);
|
| 90 |
+
if (record.decision !== undefined && !READINESS_LABELS.includes(record.decision)) {
|
| 91 |
+
throw new ManifestValidationError("window decision must be KEEP, REVIEW, or REJECT", line);
|
| 92 |
+
}
|
| 93 |
+
if (record.readiness !== undefined) {
|
| 94 |
+
validateProbabilityMap(record.readiness, "readiness", line, warnings, true);
|
| 95 |
+
}
|
| 96 |
+
if (record.issues !== undefined) {
|
| 97 |
+
validateProbabilityMap(record.issues, "issues", line, warnings);
|
| 98 |
+
}
|
| 99 |
+
if (record.boundary !== undefined && !Array.isArray(record.boundary)) {
|
| 100 |
+
throw new ManifestValidationError("boundary must be an array", line);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function validateSegment(record, line, warnings) {
|
| 105 |
+
if (!Number.isInteger(record.segment_index) || record.segment_index < 0) {
|
| 106 |
+
throw new ManifestValidationError("segment_index must be a non-negative integer", line);
|
| 107 |
+
}
|
| 108 |
+
validateInterval(record, line);
|
| 109 |
+
if (!Object.hasOwn(ROUTE_TO_DECISION, record.route)) {
|
| 110 |
+
throw new ManifestValidationError("segment route must be keep, review, or discard", line);
|
| 111 |
+
}
|
| 112 |
+
if (record.decision !== undefined && !READINESS_LABELS.includes(record.decision)) {
|
| 113 |
+
throw new ManifestValidationError("segment decision must be KEEP, REVIEW, or REJECT", line);
|
| 114 |
+
}
|
| 115 |
+
if (record.readiness !== undefined) {
|
| 116 |
+
validateProbabilityMap(record.readiness, "readiness", line, warnings, true);
|
| 117 |
+
}
|
| 118 |
+
if (record.issues !== undefined) {
|
| 119 |
+
validateProbabilityMap(record.issues, "issues", line, warnings);
|
| 120 |
+
}
|
| 121 |
+
if (!Array.isArray(record.window_indices) || record.window_indices.length === 0) {
|
| 122 |
+
throw new ManifestValidationError("segment.window_indices must be a non-empty array", line);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function declaredCount(header, key) {
|
| 127 |
+
const value = header.counts?.[key];
|
| 128 |
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/** Parse and validate an EgoSieve v1 JSONL manifest without mutating it. */
|
| 132 |
+
export function parseManifest(text) {
|
| 133 |
+
if (typeof text !== "string" || text.trim() === "") {
|
| 134 |
+
throw new ManifestValidationError("manifest is empty");
|
| 135 |
+
}
|
| 136 |
+
const records = [];
|
| 137 |
+
const sourceLines = [];
|
| 138 |
+
for (const [offset, rawLine] of text.split(/\r?\n/u).entries()) {
|
| 139 |
+
if (rawLine.trim() === "") continue;
|
| 140 |
+
const line = offset + 1;
|
| 141 |
+
let record;
|
| 142 |
+
try {
|
| 143 |
+
record = JSON.parse(rawLine);
|
| 144 |
+
} catch (error) {
|
| 145 |
+
throw new ManifestValidationError(`invalid JSON (${error.message})`, line);
|
| 146 |
+
}
|
| 147 |
+
if (!isObject(record)) {
|
| 148 |
+
throw new ManifestValidationError("record must be a JSON object", line);
|
| 149 |
+
}
|
| 150 |
+
records.push(record);
|
| 151 |
+
sourceLines.push(line);
|
| 152 |
+
}
|
| 153 |
+
if (records.length === 0) {
|
| 154 |
+
throw new ManifestValidationError("manifest is empty");
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
const header = records[0];
|
| 158 |
+
validateHeader(header, sourceLines[0]);
|
| 159 |
+
const windows = [];
|
| 160 |
+
const segments = [];
|
| 161 |
+
const warnings = [];
|
| 162 |
+
const windowIndexes = new Set();
|
| 163 |
+
const segmentIndexes = new Set();
|
| 164 |
+
|
| 165 |
+
for (let index = 1; index < records.length; index += 1) {
|
| 166 |
+
const record = records[index];
|
| 167 |
+
const line = sourceLines[index];
|
| 168 |
+
if (record.schema_version !== MANIFEST_VERSION) {
|
| 169 |
+
throw new ManifestValidationError("record schema_version does not match the header", line);
|
| 170 |
+
}
|
| 171 |
+
if (record.record_type === "window") {
|
| 172 |
+
validateWindow(record, line, warnings);
|
| 173 |
+
if (windowIndexes.has(record.window_index)) {
|
| 174 |
+
throw new ManifestValidationError(`duplicate window_index ${record.window_index}`, line);
|
| 175 |
+
}
|
| 176 |
+
windowIndexes.add(record.window_index);
|
| 177 |
+
windows.push(record);
|
| 178 |
+
} else if (record.record_type === "segment") {
|
| 179 |
+
validateSegment(record, line, warnings);
|
| 180 |
+
if (segmentIndexes.has(record.segment_index)) {
|
| 181 |
+
throw new ManifestValidationError(`duplicate segment_index ${record.segment_index}`, line);
|
| 182 |
+
}
|
| 183 |
+
segmentIndexes.add(record.segment_index);
|
| 184 |
+
segments.push(record);
|
| 185 |
+
} else {
|
| 186 |
+
throw new ManifestValidationError(`unknown record_type ${JSON.stringify(record.record_type)}`, line);
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
windows.sort((left, right) => left.start_s - right.start_s || left.window_index - right.window_index);
|
| 191 |
+
segments.sort((left, right) => left.start_s - right.start_s || left.segment_index - right.segment_index);
|
| 192 |
+
|
| 193 |
+
const declaredWindows = declaredCount(header, "windows");
|
| 194 |
+
if (declaredWindows !== null && declaredWindows !== windows.length) {
|
| 195 |
+
warnings.push(`Header declares ${declaredWindows} windows; ${windows.length} were found.`);
|
| 196 |
+
}
|
| 197 |
+
const declaredSegments = declaredCount(header, "segments");
|
| 198 |
+
if (declaredSegments !== null && declaredSegments !== segments.length) {
|
| 199 |
+
warnings.push(`Header declares ${declaredSegments} segments; ${segments.length} were found.`);
|
| 200 |
+
}
|
| 201 |
+
const maxEnd = Math.max(0, ...windows.map((row) => row.end_s), ...segments.map((row) => row.end_s));
|
| 202 |
+
if (maxEnd > header.source.duration_s + 0.05) {
|
| 203 |
+
warnings.push(`Evidence extends to ${maxEnd.toFixed(2)}s, beyond the declared source duration.`);
|
| 204 |
+
}
|
| 205 |
+
for (const segment of segments) {
|
| 206 |
+
const unknown = segment.window_indices.filter((value) => !windowIndexes.has(value));
|
| 207 |
+
if (unknown.length > 0) {
|
| 208 |
+
warnings.push(`Segment ${segment.segment_index} references missing window indexes: ${unknown.join(", ")}.`);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
return Object.freeze({
|
| 213 |
+
records: Object.freeze(records),
|
| 214 |
+
header,
|
| 215 |
+
windows: Object.freeze(windows),
|
| 216 |
+
segments: Object.freeze(segments),
|
| 217 |
+
warnings: Object.freeze(warnings),
|
| 218 |
+
});
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export function decisionFor(record) {
|
| 222 |
+
return record.decision ?? ROUTE_TO_DECISION[record.route] ?? "REVIEW";
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
export function aggregateIssues(records) {
|
| 226 |
+
const buckets = new Map();
|
| 227 |
+
for (const record of records) {
|
| 228 |
+
if (!isObject(record.issues)) continue;
|
| 229 |
+
for (const [name, probability] of Object.entries(record.issues)) {
|
| 230 |
+
if (typeof probability !== "number" || !Number.isFinite(probability)) continue;
|
| 231 |
+
const bucket = buckets.get(name) ?? { name, max: 0, sum: 0, count: 0, reports: 0 };
|
| 232 |
+
bucket.max = Math.max(bucket.max, probability);
|
| 233 |
+
bucket.sum += probability;
|
| 234 |
+
bucket.count += 1;
|
| 235 |
+
if (record.reported_issues?.includes(name)) bucket.reports += 1;
|
| 236 |
+
buckets.set(name, bucket);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
return [...buckets.values()]
|
| 240 |
+
.map((bucket) => ({
|
| 241 |
+
name: bucket.name,
|
| 242 |
+
max: bucket.max,
|
| 243 |
+
mean: bucket.sum / bucket.count,
|
| 244 |
+
reports: bucket.reports,
|
| 245 |
+
samples: bucket.count,
|
| 246 |
+
}))
|
| 247 |
+
.sort((left, right) => right.max - left.max || left.name.localeCompare(right.name));
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
export function summarizeManifest(parsed) {
|
| 251 |
+
const duration = parsed.header.source.duration_s;
|
| 252 |
+
const windowDecisions = Object.fromEntries(READINESS_LABELS.map((label) => [label, 0]));
|
| 253 |
+
for (const window of parsed.windows) {
|
| 254 |
+
const decision = decisionFor(window);
|
| 255 |
+
if (Object.hasOwn(windowDecisions, decision)) windowDecisions[decision] += 1;
|
| 256 |
+
}
|
| 257 |
+
const routedSeconds = Object.fromEntries(READINESS_LABELS.map((label) => [label, 0]));
|
| 258 |
+
for (const segment of parsed.segments) {
|
| 259 |
+
const decision = decisionFor(segment);
|
| 260 |
+
if (Object.hasOwn(routedSeconds, decision)) {
|
| 261 |
+
routedSeconds[decision] += segment.end_s - segment.start_s;
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
return {
|
| 265 |
+
duration,
|
| 266 |
+
windows: parsed.windows.length,
|
| 267 |
+
segments: parsed.segments.length,
|
| 268 |
+
windowDecisions,
|
| 269 |
+
routedSeconds,
|
| 270 |
+
issues: aggregateIssues(parsed.segments.length > 0 ? parsed.segments : parsed.windows),
|
| 271 |
+
};
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
export function formatDuration(seconds) {
|
| 275 |
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return "—";
|
| 276 |
+
const hours = Math.floor(seconds / 3600);
|
| 277 |
+
const minutes = Math.floor((seconds % 3600) / 60);
|
| 278 |
+
const remainder = seconds % 60;
|
| 279 |
+
if (hours > 0) {
|
| 280 |
+
return `${hours}:${String(minutes).padStart(2, "0")}:${remainder.toFixed(1).padStart(4, "0")}`;
|
| 281 |
+
}
|
| 282 |
+
return `${minutes}:${remainder.toFixed(1).padStart(4, "0")}`;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
export function buildSegmentExport(parsed, selectedIndexes = null) {
|
| 286 |
+
const selected = selectedIndexes === null
|
| 287 |
+
? parsed.segments
|
| 288 |
+
: parsed.segments.filter((segment) => selectedIndexes.has(segment.segment_index));
|
| 289 |
+
return {
|
| 290 |
+
schema: "egosieve.segment-selection/v1",
|
| 291 |
+
exported_at: new Date().toISOString(),
|
| 292 |
+
source: parsed.header.source,
|
| 293 |
+
model: parsed.header.model ?? null,
|
| 294 |
+
manifest: {
|
| 295 |
+
schema: parsed.header.schema,
|
| 296 |
+
schema_version: parsed.header.schema_version,
|
| 297 |
+
created_at: parsed.header.created_at ?? null,
|
| 298 |
+
generator: parsed.header.generator ?? null,
|
| 299 |
+
},
|
| 300 |
+
segments: selected,
|
| 301 |
+
};
|
| 302 |
+
}
|
manifest.test.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
ManifestValidationError,
|
| 6 |
+
aggregateIssues,
|
| 7 |
+
buildSegmentExport,
|
| 8 |
+
decisionFor,
|
| 9 |
+
formatDuration,
|
| 10 |
+
parseManifest,
|
| 11 |
+
summarizeManifest,
|
| 12 |
+
} from "./manifest.mjs";
|
| 13 |
+
|
| 14 |
+
function fixture(overrides = {}) {
|
| 15 |
+
const header = {
|
| 16 |
+
record_type: "manifest",
|
| 17 |
+
schema: "egosieve.video-compilation",
|
| 18 |
+
schema_version: 1,
|
| 19 |
+
created_at: "2026-08-23T12:00:00Z",
|
| 20 |
+
generator: "egosieve test",
|
| 21 |
+
source: {
|
| 22 |
+
source_path: "source.mp4",
|
| 23 |
+
source_sha256: "f".repeat(64),
|
| 24 |
+
duration_s: 12,
|
| 25 |
+
display_width: 1280,
|
| 26 |
+
display_height: 720,
|
| 27 |
+
fps: 30,
|
| 28 |
+
},
|
| 29 |
+
model: { id: "example/EgoSieve-S", revision: "a".repeat(40) },
|
| 30 |
+
counts: { windows: 2, segments: 2 },
|
| 31 |
+
...overrides,
|
| 32 |
+
};
|
| 33 |
+
const windows = [
|
| 34 |
+
{
|
| 35 |
+
record_type: "window",
|
| 36 |
+
schema_version: 1,
|
| 37 |
+
window_index: 0,
|
| 38 |
+
start_s: 0,
|
| 39 |
+
end_s: 6,
|
| 40 |
+
decision: "KEEP",
|
| 41 |
+
readiness: { KEEP: 0.8, REVIEW: 0.15, REJECT: 0.05 },
|
| 42 |
+
issues: { blur: 0.1, exposure: 0.2 },
|
| 43 |
+
reported_issues: [],
|
| 44 |
+
boundary: [],
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
record_type: "window",
|
| 48 |
+
schema_version: 1,
|
| 49 |
+
window_index: 1,
|
| 50 |
+
start_s: 6,
|
| 51 |
+
end_s: 12,
|
| 52 |
+
decision: "REVIEW",
|
| 53 |
+
readiness: { KEEP: 0.25, REVIEW: 0.65, REJECT: 0.1 },
|
| 54 |
+
issues: { blur: 0.7, exposure: 0.3 },
|
| 55 |
+
reported_issues: ["blur"],
|
| 56 |
+
boundary: [],
|
| 57 |
+
},
|
| 58 |
+
];
|
| 59 |
+
const segments = [
|
| 60 |
+
{
|
| 61 |
+
record_type: "segment",
|
| 62 |
+
schema_version: 1,
|
| 63 |
+
segment_index: 0,
|
| 64 |
+
start_s: 0,
|
| 65 |
+
end_s: 6,
|
| 66 |
+
route: "keep",
|
| 67 |
+
decision: "KEEP",
|
| 68 |
+
reason: "stable",
|
| 69 |
+
window_indices: [0],
|
| 70 |
+
readiness: { KEEP: 0.8, REVIEW: 0.15, REJECT: 0.05 },
|
| 71 |
+
issues: { blur: 0.1, exposure: 0.2 },
|
| 72 |
+
reported_issues: [],
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
record_type: "segment",
|
| 76 |
+
schema_version: 1,
|
| 77 |
+
segment_index: 1,
|
| 78 |
+
start_s: 6,
|
| 79 |
+
end_s: 12,
|
| 80 |
+
route: "review",
|
| 81 |
+
decision: "REVIEW",
|
| 82 |
+
reason: "uncertain",
|
| 83 |
+
window_indices: [1],
|
| 84 |
+
readiness: { KEEP: 0.25, REVIEW: 0.65, REJECT: 0.1 },
|
| 85 |
+
issues: { blur: 0.7, exposure: 0.3 },
|
| 86 |
+
reported_issues: ["blur"],
|
| 87 |
+
},
|
| 88 |
+
];
|
| 89 |
+
return [header, ...windows, ...segments].map((record) => JSON.stringify(record)).join("\n");
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
test("parses a versioned manifest and preserves sorted evidence", () => {
|
| 93 |
+
const parsed = parseManifest(fixture());
|
| 94 |
+
assert.equal(parsed.header.schema_version, 1);
|
| 95 |
+
assert.deepEqual(parsed.windows.map((row) => row.window_index), [0, 1]);
|
| 96 |
+
assert.deepEqual(parsed.segments.map((row) => row.segment_index), [0, 1]);
|
| 97 |
+
assert.deepEqual(parsed.warnings, []);
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
test("summarizes routing and issue evidence", () => {
|
| 101 |
+
const summary = summarizeManifest(parseManifest(fixture()));
|
| 102 |
+
assert.deepEqual(summary.windowDecisions, { KEEP: 1, REVIEW: 1, REJECT: 0 });
|
| 103 |
+
assert.deepEqual(summary.routedSeconds, { KEEP: 6, REVIEW: 6, REJECT: 0 });
|
| 104 |
+
assert.equal(summary.issues[0].name, "blur");
|
| 105 |
+
assert.equal(summary.issues[0].max, 0.7);
|
| 106 |
+
assert.ok(Math.abs(summary.issues[0].mean - 0.4) < 1e-12);
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
test("rejects malformed JSON, schemas, intervals, and probabilities", () => {
|
| 110 |
+
assert.throws(() => parseManifest("{oops"), /Line 1: invalid JSON/u);
|
| 111 |
+
assert.throws(
|
| 112 |
+
() => parseManifest(fixture({ schema: "unknown" })),
|
| 113 |
+
(error) => error instanceof ManifestValidationError && /unsupported schema/u.test(error.message),
|
| 114 |
+
);
|
| 115 |
+
assert.throws(
|
| 116 |
+
() => parseManifest(fixture().replace('"end_s":6', '"end_s":0')),
|
| 117 |
+
/end_s must be greater/u,
|
| 118 |
+
);
|
| 119 |
+
assert.throws(
|
| 120 |
+
() => parseManifest(fixture().replace('"blur":0.1', '"blur":1.1')),
|
| 121 |
+
/issues\.blur must be between/u,
|
| 122 |
+
);
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
test("reports count and reference mismatches without hiding the evidence", () => {
|
| 126 |
+
const text = fixture({ counts: { windows: 8, segments: 9 } }).replace(
|
| 127 |
+
'"window_indices":[1]',
|
| 128 |
+
'"window_indices":[1,99]',
|
| 129 |
+
);
|
| 130 |
+
const parsed = parseManifest(text);
|
| 131 |
+
assert.equal(parsed.warnings.length, 3);
|
| 132 |
+
assert.match(parsed.warnings[0], /declares 8 windows/u);
|
| 133 |
+
assert.match(parsed.warnings[2], /missing window indexes: 99/u);
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
test("exports only selected segments with source and model identity", () => {
|
| 137 |
+
const parsed = parseManifest(fixture());
|
| 138 |
+
const exported = buildSegmentExport(parsed, new Set([1]));
|
| 139 |
+
assert.equal(exported.schema, "egosieve.segment-selection/v1");
|
| 140 |
+
assert.equal(exported.source.source_sha256, "f".repeat(64));
|
| 141 |
+
assert.equal(exported.model.revision, "a".repeat(40));
|
| 142 |
+
assert.deepEqual(exported.segments.map((row) => row.segment_index), [1]);
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
test("formatters and route fallback remain deterministic", () => {
|
| 146 |
+
assert.equal(formatDuration(65.25), "1:05.3");
|
| 147 |
+
assert.equal(formatDuration(3661.2), "1:01:01.2");
|
| 148 |
+
assert.equal(decisionFor({ route: "discard" }), "REJECT");
|
| 149 |
+
assert.deepEqual(aggregateIssues([]), []);
|
| 150 |
+
});
|
styles.css
ADDED
|
@@ -0,0 +1,1667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--ink: #10110f;
|
| 3 |
+
--ink-soft: #191a17;
|
| 4 |
+
--ink-raised: #22231f;
|
| 5 |
+
--paper: #efeee6;
|
| 6 |
+
--paper-dim: #c2c1b8;
|
| 7 |
+
--grid: rgba(239, 238, 230, 0.075);
|
| 8 |
+
--acid: #d7ff35;
|
| 9 |
+
--acid-dark: #8cae00;
|
| 10 |
+
--orange: #ff5b26;
|
| 11 |
+
--red: #ff3158;
|
| 12 |
+
--steel: #898b83;
|
| 13 |
+
--line: rgba(239, 238, 230, 0.22);
|
| 14 |
+
--display: "Franklin Gothic Medium", "Avenir Next Condensed", "DIN Condensed", Impact, sans-serif;
|
| 15 |
+
--mono: "SFMono-Regular", "Cascadia Code", "Liberation Mono", Menlo, monospace;
|
| 16 |
+
--shadow: 10px 10px 0 rgba(0, 0, 0, 0.26);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
* {
|
| 20 |
+
box-sizing: border-box;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
html {
|
| 24 |
+
scroll-behavior: smooth;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
body {
|
| 28 |
+
min-width: 320px;
|
| 29 |
+
margin: 0;
|
| 30 |
+
color: var(--paper);
|
| 31 |
+
background:
|
| 32 |
+
linear-gradient(var(--grid) 1px, transparent 1px),
|
| 33 |
+
linear-gradient(90deg, var(--grid) 1px, transparent 1px),
|
| 34 |
+
var(--ink);
|
| 35 |
+
background-size: 32px 32px;
|
| 36 |
+
font-family: var(--mono);
|
| 37 |
+
font-size: 14px;
|
| 38 |
+
line-height: 1.55;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
button,
|
| 42 |
+
input {
|
| 43 |
+
font: inherit;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
button,
|
| 47 |
+
a {
|
| 48 |
+
-webkit-tap-highlight-color: transparent;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
button:focus-visible,
|
| 52 |
+
a:focus-visible,
|
| 53 |
+
input:focus-visible,
|
| 54 |
+
summary:focus-visible,
|
| 55 |
+
[tabindex="0"]:focus-visible {
|
| 56 |
+
outline: 3px solid var(--acid);
|
| 57 |
+
outline-offset: 4px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
[hidden] {
|
| 61 |
+
display: none !important;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.skip-link {
|
| 65 |
+
position: fixed;
|
| 66 |
+
z-index: 100;
|
| 67 |
+
top: 12px;
|
| 68 |
+
left: 12px;
|
| 69 |
+
padding: 10px 14px;
|
| 70 |
+
color: var(--ink);
|
| 71 |
+
background: var(--acid);
|
| 72 |
+
transform: translateY(-160%);
|
| 73 |
+
transition: transform 140ms ease;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.skip-link:focus {
|
| 77 |
+
transform: translateY(0);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.masthead,
|
| 81 |
+
main,
|
| 82 |
+
footer {
|
| 83 |
+
width: min(100% - 48px, 1500px);
|
| 84 |
+
margin-inline: auto;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.masthead {
|
| 88 |
+
min-height: 72px;
|
| 89 |
+
display: grid;
|
| 90 |
+
grid-template-columns: 1fr auto 1fr;
|
| 91 |
+
align-items: center;
|
| 92 |
+
border-bottom: 1px solid var(--line);
|
| 93 |
+
font-size: 11px;
|
| 94 |
+
letter-spacing: 0.13em;
|
| 95 |
+
text-transform: uppercase;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.wordmark,
|
| 99 |
+
.masthead-link {
|
| 100 |
+
color: var(--paper);
|
| 101 |
+
text-decoration: none;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.wordmark {
|
| 105 |
+
display: inline-flex;
|
| 106 |
+
gap: 12px;
|
| 107 |
+
align-items: center;
|
| 108 |
+
width: max-content;
|
| 109 |
+
font-family: var(--display);
|
| 110 |
+
font-size: 18px;
|
| 111 |
+
letter-spacing: -0.03em;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.wordmark-mark {
|
| 115 |
+
width: 22px;
|
| 116 |
+
height: 22px;
|
| 117 |
+
display: grid;
|
| 118 |
+
grid-template-columns: repeat(3, 1fr);
|
| 119 |
+
gap: 2px;
|
| 120 |
+
transform: skewX(-12deg);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.wordmark-mark i {
|
| 124 |
+
background: var(--paper);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.wordmark-mark i:nth-child(2) {
|
| 128 |
+
background: var(--acid);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.wordmark-mark i:nth-child(3) {
|
| 132 |
+
height: 55%;
|
| 133 |
+
align-self: end;
|
| 134 |
+
background: var(--orange);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.masthead-status {
|
| 138 |
+
display: flex;
|
| 139 |
+
gap: 9px;
|
| 140 |
+
align-items: center;
|
| 141 |
+
color: var(--paper-dim);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.status-lamp {
|
| 145 |
+
width: 8px;
|
| 146 |
+
height: 8px;
|
| 147 |
+
border-radius: 50%;
|
| 148 |
+
background: var(--acid);
|
| 149 |
+
box-shadow: 0 0 0 4px rgba(215, 255, 53, 0.1);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.masthead-link {
|
| 153 |
+
justify-self: end;
|
| 154 |
+
padding-block: 4px;
|
| 155 |
+
border-bottom: 1px solid transparent;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.masthead-link:hover {
|
| 159 |
+
border-color: var(--orange);
|
| 160 |
+
color: var(--orange);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.hero {
|
| 164 |
+
position: relative;
|
| 165 |
+
min-height: 650px;
|
| 166 |
+
padding: clamp(84px, 10vw, 148px) 0 72px;
|
| 167 |
+
display: grid;
|
| 168 |
+
grid-template-columns: 70px minmax(0, 1fr) 180px;
|
| 169 |
+
gap: clamp(24px, 4vw, 72px);
|
| 170 |
+
align-items: end;
|
| 171 |
+
overflow: hidden;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.hero::before {
|
| 175 |
+
content: "";
|
| 176 |
+
position: absolute;
|
| 177 |
+
inset: 60px 7% auto auto;
|
| 178 |
+
width: clamp(210px, 35vw, 510px);
|
| 179 |
+
aspect-ratio: 1;
|
| 180 |
+
border: 1px solid rgba(215, 255, 53, 0.3);
|
| 181 |
+
border-radius: 50%;
|
| 182 |
+
box-shadow:
|
| 183 |
+
0 0 0 48px rgba(215, 255, 53, 0.018),
|
| 184 |
+
0 0 0 96px rgba(215, 255, 53, 0.012);
|
| 185 |
+
pointer-events: none;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.hero-index {
|
| 189 |
+
align-self: center;
|
| 190 |
+
writing-mode: vertical-rl;
|
| 191 |
+
transform: rotate(180deg);
|
| 192 |
+
color: var(--steel);
|
| 193 |
+
font-size: 10px;
|
| 194 |
+
letter-spacing: 0.25em;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.eyebrow {
|
| 198 |
+
margin: 0;
|
| 199 |
+
color: var(--paper-dim);
|
| 200 |
+
font-size: 10px;
|
| 201 |
+
font-weight: 700;
|
| 202 |
+
letter-spacing: 0.2em;
|
| 203 |
+
text-transform: uppercase;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.eyebrow span {
|
| 207 |
+
color: var(--orange);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.hero h1 {
|
| 211 |
+
max-width: 1100px;
|
| 212 |
+
margin: 20px 0 38px;
|
| 213 |
+
font-family: var(--display);
|
| 214 |
+
font-size: clamp(66px, 10.5vw, 166px);
|
| 215 |
+
font-weight: 900;
|
| 216 |
+
font-stretch: condensed;
|
| 217 |
+
letter-spacing: -0.065em;
|
| 218 |
+
line-height: 0.78;
|
| 219 |
+
text-transform: uppercase;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.hero h1 em {
|
| 223 |
+
position: relative;
|
| 224 |
+
z-index: 1;
|
| 225 |
+
color: var(--ink);
|
| 226 |
+
font-style: normal;
|
| 227 |
+
-webkit-text-stroke: 2px var(--acid);
|
| 228 |
+
text-shadow: 7px 7px 0 var(--orange);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.hero-deck {
|
| 232 |
+
max-width: 720px;
|
| 233 |
+
margin: 0 0 0 clamp(0px, 7vw, 110px);
|
| 234 |
+
padding-left: 24px;
|
| 235 |
+
border-left: 4px solid var(--acid);
|
| 236 |
+
color: var(--paper-dim);
|
| 237 |
+
font-size: clamp(14px, 1.4vw, 18px);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.hero-stamp {
|
| 241 |
+
position: relative;
|
| 242 |
+
z-index: 1;
|
| 243 |
+
width: 156px;
|
| 244 |
+
padding: 18px;
|
| 245 |
+
border: 1px solid var(--paper);
|
| 246 |
+
background: var(--ink);
|
| 247 |
+
transform: rotate(3deg);
|
| 248 |
+
box-shadow: 6px 6px 0 var(--orange);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.hero-stamp span,
|
| 252 |
+
.hero-stamp small {
|
| 253 |
+
display: block;
|
| 254 |
+
color: var(--acid);
|
| 255 |
+
font-size: 8px;
|
| 256 |
+
letter-spacing: 0.19em;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.hero-stamp strong {
|
| 260 |
+
display: block;
|
| 261 |
+
margin: 8px 0;
|
| 262 |
+
font-family: var(--display);
|
| 263 |
+
font-size: 31px;
|
| 264 |
+
line-height: 0.84;
|
| 265 |
+
letter-spacing: -0.04em;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.load-bay,
|
| 269 |
+
.cli-band,
|
| 270 |
+
.workspace {
|
| 271 |
+
scroll-margin-top: 18px;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.load-bay {
|
| 275 |
+
margin-bottom: 80px;
|
| 276 |
+
padding: clamp(28px, 4vw, 54px);
|
| 277 |
+
color: var(--ink);
|
| 278 |
+
background: var(--paper);
|
| 279 |
+
box-shadow: var(--shadow);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.section-heading {
|
| 283 |
+
display: grid;
|
| 284 |
+
grid-template-columns: 62px 1fr minmax(200px, 430px);
|
| 285 |
+
gap: 20px;
|
| 286 |
+
align-items: end;
|
| 287 |
+
margin-bottom: 35px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.section-number {
|
| 291 |
+
color: var(--orange);
|
| 292 |
+
font-family: var(--display);
|
| 293 |
+
font-size: 44px;
|
| 294 |
+
line-height: 0.9;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.section-heading h2,
|
| 298 |
+
.cli-label h2,
|
| 299 |
+
.workspace h2,
|
| 300 |
+
.panel h3,
|
| 301 |
+
.export-dock h3 {
|
| 302 |
+
margin: 2px 0 0;
|
| 303 |
+
font-family: var(--display);
|
| 304 |
+
font-weight: 900;
|
| 305 |
+
letter-spacing: -0.035em;
|
| 306 |
+
line-height: 0.95;
|
| 307 |
+
text-transform: uppercase;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.section-heading h2,
|
| 311 |
+
.cli-label h2,
|
| 312 |
+
.workspace h2 {
|
| 313 |
+
font-size: clamp(34px, 4vw, 58px);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.section-note {
|
| 317 |
+
justify-self: end;
|
| 318 |
+
max-width: 430px;
|
| 319 |
+
margin: 0;
|
| 320 |
+
color: #56574f;
|
| 321 |
+
font-size: 12px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.file-grid {
|
| 325 |
+
display: grid;
|
| 326 |
+
grid-template-columns: 1.45fr 1fr;
|
| 327 |
+
gap: 16px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.file-bay {
|
| 331 |
+
position: relative;
|
| 332 |
+
min-height: 235px;
|
| 333 |
+
padding: 30px;
|
| 334 |
+
display: grid;
|
| 335 |
+
grid-template-columns: auto 1fr;
|
| 336 |
+
grid-template-rows: 1fr auto auto auto;
|
| 337 |
+
column-gap: 28px;
|
| 338 |
+
border: 2px solid var(--ink);
|
| 339 |
+
cursor: pointer;
|
| 340 |
+
overflow: hidden;
|
| 341 |
+
transition: transform 150ms ease, box-shadow 150ms ease, background 150ms ease;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.file-bay:hover,
|
| 345 |
+
.file-bay:focus-within,
|
| 346 |
+
.file-bay.is-dragging {
|
| 347 |
+
transform: translate(-4px, -4px);
|
| 348 |
+
box-shadow: 8px 8px 0 var(--ink);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.manifest-bay:hover,
|
| 352 |
+
.manifest-bay:focus-within,
|
| 353 |
+
.manifest-bay.is-dragging {
|
| 354 |
+
background: var(--acid);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.video-bay {
|
| 358 |
+
color: var(--paper);
|
| 359 |
+
background: var(--ink);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.video-bay:hover,
|
| 363 |
+
.video-bay:focus-within,
|
| 364 |
+
.video-bay.is-dragging {
|
| 365 |
+
color: var(--ink);
|
| 366 |
+
background: var(--orange);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.file-bay input {
|
| 370 |
+
position: absolute;
|
| 371 |
+
width: 1px;
|
| 372 |
+
height: 1px;
|
| 373 |
+
overflow: hidden;
|
| 374 |
+
clip: rect(0 0 0 0);
|
| 375 |
+
clip-path: inset(50%);
|
| 376 |
+
white-space: nowrap;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.file-number {
|
| 380 |
+
position: absolute;
|
| 381 |
+
top: 15px;
|
| 382 |
+
right: 18px;
|
| 383 |
+
font-family: var(--display);
|
| 384 |
+
font-size: 72px;
|
| 385 |
+
line-height: 1;
|
| 386 |
+
opacity: 0.11;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.file-icon {
|
| 390 |
+
grid-row: 1 / 5;
|
| 391 |
+
align-self: center;
|
| 392 |
+
font-family: var(--display);
|
| 393 |
+
font-size: 68px;
|
| 394 |
+
line-height: 1;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.file-bay strong {
|
| 398 |
+
align-self: end;
|
| 399 |
+
font-family: var(--display);
|
| 400 |
+
font-size: clamp(24px, 2.6vw, 39px);
|
| 401 |
+
line-height: 0.95;
|
| 402 |
+
letter-spacing: -0.04em;
|
| 403 |
+
text-transform: uppercase;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.file-bay > span:not(.file-number, .file-icon) {
|
| 407 |
+
margin-top: 11px;
|
| 408 |
+
overflow: hidden;
|
| 409 |
+
text-overflow: ellipsis;
|
| 410 |
+
white-space: nowrap;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.file-bay small {
|
| 414 |
+
margin-top: 6px;
|
| 415 |
+
opacity: 0.65;
|
| 416 |
+
font-size: 10px;
|
| 417 |
+
letter-spacing: 0.05em;
|
| 418 |
+
text-transform: uppercase;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.load-actions {
|
| 422 |
+
display: flex;
|
| 423 |
+
gap: 14px;
|
| 424 |
+
align-items: center;
|
| 425 |
+
margin-top: 22px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.button {
|
| 429 |
+
min-height: 46px;
|
| 430 |
+
padding: 13px 19px;
|
| 431 |
+
display: inline-flex;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
border: 1px solid var(--ink);
|
| 435 |
+
border-radius: 0;
|
| 436 |
+
color: var(--ink);
|
| 437 |
+
background: transparent;
|
| 438 |
+
font-family: var(--display);
|
| 439 |
+
font-size: 15px;
|
| 440 |
+
font-weight: 900;
|
| 441 |
+
letter-spacing: 0.02em;
|
| 442 |
+
line-height: 1;
|
| 443 |
+
text-decoration: none;
|
| 444 |
+
text-transform: uppercase;
|
| 445 |
+
cursor: pointer;
|
| 446 |
+
transition: transform 130ms ease, box-shadow 130ms ease, color 130ms ease, background 130ms ease;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.button:hover {
|
| 450 |
+
transform: translate(-2px, -2px);
|
| 451 |
+
box-shadow: 4px 4px 0 currentColor;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.button-primary {
|
| 455 |
+
color: var(--ink);
|
| 456 |
+
border-color: var(--orange);
|
| 457 |
+
background: var(--orange);
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.button-quiet:hover {
|
| 461 |
+
color: var(--paper);
|
| 462 |
+
background: var(--ink);
|
| 463 |
+
box-shadow: 4px 4px 0 var(--orange);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.privacy-note {
|
| 467 |
+
margin-left: auto;
|
| 468 |
+
color: #5d5e56;
|
| 469 |
+
font-size: 11px;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.privacy-note span {
|
| 473 |
+
margin-right: 6px;
|
| 474 |
+
color: var(--acid-dark);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.notice {
|
| 478 |
+
margin-top: 22px;
|
| 479 |
+
padding: 13px 16px;
|
| 480 |
+
border-left: 5px solid var(--orange);
|
| 481 |
+
color: var(--paper);
|
| 482 |
+
background: var(--ink);
|
| 483 |
+
font-size: 12px;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.notice.is-error {
|
| 487 |
+
border-color: var(--red);
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.cli-band {
|
| 491 |
+
margin: 0 0 105px;
|
| 492 |
+
padding: 32px 0;
|
| 493 |
+
display: grid;
|
| 494 |
+
grid-template-columns: minmax(270px, 0.8fr) minmax(260px, 0.7fr) minmax(430px, 1.5fr);
|
| 495 |
+
gap: clamp(28px, 4vw, 64px);
|
| 496 |
+
align-items: center;
|
| 497 |
+
border-block: 1px solid var(--line);
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.cli-label {
|
| 501 |
+
display: grid;
|
| 502 |
+
grid-template-columns: 55px 1fr;
|
| 503 |
+
gap: 14px;
|
| 504 |
+
align-items: end;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.cli-label h2 {
|
| 508 |
+
font-size: 30px;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.cli-band > p {
|
| 512 |
+
margin: 0;
|
| 513 |
+
color: var(--paper-dim);
|
| 514 |
+
font-size: 11px;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.command-shell {
|
| 518 |
+
min-width: 0;
|
| 519 |
+
display: grid;
|
| 520 |
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
| 521 |
+
gap: 12px;
|
| 522 |
+
align-items: center;
|
| 523 |
+
padding: 14px 14px 14px 18px;
|
| 524 |
+
border: 1px solid var(--line);
|
| 525 |
+
background: var(--ink-soft);
|
| 526 |
+
box-shadow: 5px 5px 0 rgba(255, 91, 38, 0.25);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.prompt {
|
| 530 |
+
color: var(--acid);
|
| 531 |
+
font-weight: 700;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.command-shell code {
|
| 535 |
+
overflow-x: auto;
|
| 536 |
+
color: var(--paper);
|
| 537 |
+
font-size: 11px;
|
| 538 |
+
scrollbar-width: thin;
|
| 539 |
+
white-space: nowrap;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.copy-button {
|
| 543 |
+
min-width: 58px;
|
| 544 |
+
padding: 8px 10px;
|
| 545 |
+
border: 1px solid var(--acid);
|
| 546 |
+
border-radius: 0;
|
| 547 |
+
color: var(--acid);
|
| 548 |
+
background: transparent;
|
| 549 |
+
font-size: 9px;
|
| 550 |
+
font-weight: 800;
|
| 551 |
+
letter-spacing: 0.1em;
|
| 552 |
+
cursor: pointer;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.copy-button:hover {
|
| 556 |
+
color: var(--ink);
|
| 557 |
+
background: var(--acid);
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.copy-button.dark {
|
| 561 |
+
border-color: var(--ink);
|
| 562 |
+
color: var(--ink);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.copy-button.dark:hover {
|
| 566 |
+
color: var(--paper);
|
| 567 |
+
background: var(--ink);
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.workspace {
|
| 571 |
+
padding-bottom: 100px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.workspace-heading {
|
| 575 |
+
grid-template-columns: 62px 1fr minmax(200px, 460px);
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.manifest-identity {
|
| 579 |
+
justify-self: end;
|
| 580 |
+
min-width: 0;
|
| 581 |
+
text-align: right;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
.manifest-identity strong,
|
| 585 |
+
.manifest-identity span {
|
| 586 |
+
display: block;
|
| 587 |
+
overflow: hidden;
|
| 588 |
+
text-overflow: ellipsis;
|
| 589 |
+
white-space: nowrap;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.manifest-identity strong {
|
| 593 |
+
color: var(--acid);
|
| 594 |
+
font-size: 12px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.manifest-identity span {
|
| 598 |
+
margin-top: 4px;
|
| 599 |
+
color: var(--paper-dim);
|
| 600 |
+
font-size: 10px;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.metric-grid {
|
| 604 |
+
display: grid;
|
| 605 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 606 |
+
border-top: 1px solid var(--line);
|
| 607 |
+
border-left: 1px solid var(--line);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.metric-card {
|
| 611 |
+
min-height: 160px;
|
| 612 |
+
padding: 23px;
|
| 613 |
+
display: flex;
|
| 614 |
+
flex-direction: column;
|
| 615 |
+
border-right: 1px solid var(--line);
|
| 616 |
+
border-bottom: 1px solid var(--line);
|
| 617 |
+
background: rgba(16, 17, 15, 0.76);
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.metric-card > span,
|
| 621 |
+
.metric-card small {
|
| 622 |
+
color: var(--paper-dim);
|
| 623 |
+
font-size: 9px;
|
| 624 |
+
letter-spacing: 0.12em;
|
| 625 |
+
text-transform: uppercase;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
.metric-card strong {
|
| 629 |
+
margin: auto 0 7px;
|
| 630 |
+
font-family: var(--display);
|
| 631 |
+
font-size: clamp(37px, 4.2vw, 66px);
|
| 632 |
+
letter-spacing: -0.05em;
|
| 633 |
+
line-height: 0.8;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.metric-acid strong {
|
| 637 |
+
color: var(--acid);
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.metric-orange strong {
|
| 641 |
+
color: var(--orange);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.warning-stack {
|
| 645 |
+
margin-top: 16px;
|
| 646 |
+
padding: 13px 16px;
|
| 647 |
+
border-left: 5px solid #ffcf35;
|
| 648 |
+
color: #ffe49a;
|
| 649 |
+
background: rgba(255, 207, 53, 0.08);
|
| 650 |
+
font-size: 11px;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
.warning-stack p {
|
| 654 |
+
margin: 3px 0;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.analysis-grid,
|
| 658 |
+
.evidence-grid {
|
| 659 |
+
display: grid;
|
| 660 |
+
gap: 18px;
|
| 661 |
+
margin-top: 18px;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
.analysis-grid {
|
| 665 |
+
grid-template-columns: minmax(320px, 0.8fr) minmax(500px, 1.4fr);
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.evidence-grid {
|
| 669 |
+
grid-template-columns: minmax(540px, 1.45fr) minmax(300px, 0.7fr);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.panel {
|
| 673 |
+
min-width: 0;
|
| 674 |
+
border: 1px solid var(--line);
|
| 675 |
+
background: rgba(25, 26, 23, 0.95);
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
.panel-heading {
|
| 679 |
+
min-height: 77px;
|
| 680 |
+
padding: 18px 20px;
|
| 681 |
+
display: flex;
|
| 682 |
+
gap: 18px;
|
| 683 |
+
align-items: center;
|
| 684 |
+
justify-content: space-between;
|
| 685 |
+
border-bottom: 1px solid var(--line);
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.panel-index,
|
| 689 |
+
.panel-state {
|
| 690 |
+
color: var(--steel);
|
| 691 |
+
font-size: 8px;
|
| 692 |
+
font-weight: 700;
|
| 693 |
+
letter-spacing: 0.18em;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.panel h3 {
|
| 697 |
+
font-size: 25px;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.panel-state {
|
| 701 |
+
padding: 5px 7px;
|
| 702 |
+
border: 1px solid rgba(239, 238, 230, 0.2);
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.video-stage {
|
| 706 |
+
position: relative;
|
| 707 |
+
min-height: 410px;
|
| 708 |
+
display: grid;
|
| 709 |
+
place-items: center;
|
| 710 |
+
background:
|
| 711 |
+
radial-gradient(circle at center, rgba(215, 255, 53, 0.08), transparent 44%),
|
| 712 |
+
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.025) 0, rgba(255, 255, 255, 0.025) 1px, transparent 1px, transparent 4px),
|
| 713 |
+
#090a09;
|
| 714 |
+
overflow: hidden;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.video-stage::before,
|
| 718 |
+
.video-stage::after {
|
| 719 |
+
content: "";
|
| 720 |
+
position: absolute;
|
| 721 |
+
z-index: 1;
|
| 722 |
+
width: 26px;
|
| 723 |
+
height: 26px;
|
| 724 |
+
border-color: var(--acid);
|
| 725 |
+
pointer-events: none;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.video-stage::before {
|
| 729 |
+
top: 15px;
|
| 730 |
+
left: 15px;
|
| 731 |
+
border-top: 1px solid;
|
| 732 |
+
border-left: 1px solid;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.video-stage::after {
|
| 736 |
+
right: 15px;
|
| 737 |
+
bottom: 15px;
|
| 738 |
+
border-right: 1px solid;
|
| 739 |
+
border-bottom: 1px solid;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.video-stage video {
|
| 743 |
+
width: 100%;
|
| 744 |
+
max-height: 540px;
|
| 745 |
+
background: #000;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
.video-empty {
|
| 749 |
+
max-width: 280px;
|
| 750 |
+
padding: 45px 28px;
|
| 751 |
+
text-align: center;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.video-empty > span {
|
| 755 |
+
display: block;
|
| 756 |
+
color: var(--acid);
|
| 757 |
+
font-family: var(--display);
|
| 758 |
+
font-size: 70px;
|
| 759 |
+
line-height: 1;
|
| 760 |
+
opacity: 0.35;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.video-empty strong {
|
| 764 |
+
display: block;
|
| 765 |
+
margin: 5px 0 8px;
|
| 766 |
+
font-family: var(--display);
|
| 767 |
+
font-size: 24px;
|
| 768 |
+
text-transform: uppercase;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.video-empty p,
|
| 772 |
+
.media-footnote,
|
| 773 |
+
.issue-deck {
|
| 774 |
+
color: var(--paper-dim);
|
| 775 |
+
font-size: 10px;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.video-timecode {
|
| 779 |
+
position: absolute;
|
| 780 |
+
z-index: 2;
|
| 781 |
+
right: 15px;
|
| 782 |
+
bottom: 14px;
|
| 783 |
+
padding: 5px 8px;
|
| 784 |
+
color: var(--acid);
|
| 785 |
+
background: rgba(0, 0, 0, 0.8);
|
| 786 |
+
font-size: 10px;
|
| 787 |
+
pointer-events: none;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.media-footnote {
|
| 791 |
+
margin: 0;
|
| 792 |
+
padding: 13px 20px;
|
| 793 |
+
border-top: 1px solid var(--line);
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
.legend {
|
| 797 |
+
display: flex;
|
| 798 |
+
gap: 14px;
|
| 799 |
+
font-size: 8px;
|
| 800 |
+
font-weight: 800;
|
| 801 |
+
letter-spacing: 0.12em;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
.legend span::before {
|
| 805 |
+
content: "";
|
| 806 |
+
width: 14px;
|
| 807 |
+
height: 3px;
|
| 808 |
+
display: inline-block;
|
| 809 |
+
margin-right: 5px;
|
| 810 |
+
vertical-align: middle;
|
| 811 |
+
background: currentColor;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.legend-keep {
|
| 815 |
+
color: var(--acid);
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
.legend-review {
|
| 819 |
+
color: var(--orange);
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.legend-reject {
|
| 823 |
+
color: var(--steel);
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.timeline-stage {
|
| 827 |
+
position: relative;
|
| 828 |
+
min-height: 410px;
|
| 829 |
+
background:
|
| 830 |
+
linear-gradient(rgba(239, 238, 230, 0.035) 1px, transparent 1px),
|
| 831 |
+
linear-gradient(90deg, rgba(239, 238, 230, 0.035) 1px, transparent 1px),
|
| 832 |
+
#121310;
|
| 833 |
+
background-size: 34px 34px;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
#timeline {
|
| 837 |
+
width: 100%;
|
| 838 |
+
height: 410px;
|
| 839 |
+
display: block;
|
| 840 |
+
cursor: crosshair;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
#timeline text {
|
| 844 |
+
fill: var(--steel);
|
| 845 |
+
font-family: var(--mono);
|
| 846 |
+
font-size: 11px;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.chart-tooltip {
|
| 850 |
+
position: absolute;
|
| 851 |
+
z-index: 4;
|
| 852 |
+
min-width: 155px;
|
| 853 |
+
padding: 10px 12px;
|
| 854 |
+
border: 1px solid var(--acid);
|
| 855 |
+
color: var(--paper);
|
| 856 |
+
background: rgba(9, 10, 9, 0.96);
|
| 857 |
+
box-shadow: 4px 4px 0 rgba(215, 255, 53, 0.18);
|
| 858 |
+
font-size: 9px;
|
| 859 |
+
line-height: 1.65;
|
| 860 |
+
pointer-events: none;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
.segment-track {
|
| 864 |
+
position: relative;
|
| 865 |
+
height: 36px;
|
| 866 |
+
margin: 0 20px;
|
| 867 |
+
background: #0b0c0a;
|
| 868 |
+
border: 1px solid var(--line);
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
.track-segment {
|
| 872 |
+
position: absolute;
|
| 873 |
+
top: 0;
|
| 874 |
+
bottom: 0;
|
| 875 |
+
min-width: 2px;
|
| 876 |
+
padding: 0;
|
| 877 |
+
border: 0;
|
| 878 |
+
border-right: 1px solid var(--ink);
|
| 879 |
+
cursor: pointer;
|
| 880 |
+
transition: filter 120ms ease, transform 120ms ease;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
.track-segment:hover,
|
| 884 |
+
.track-segment.is-selected {
|
| 885 |
+
z-index: 2;
|
| 886 |
+
filter: brightness(1.45);
|
| 887 |
+
transform: scaleY(1.2);
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.track-segment.route-keep {
|
| 891 |
+
background: var(--acid);
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
.track-segment.route-review {
|
| 895 |
+
background: var(--orange);
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.track-segment.route-discard {
|
| 899 |
+
background: var(--steel);
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.timeline-axis {
|
| 903 |
+
padding: 7px 20px 16px;
|
| 904 |
+
display: flex;
|
| 905 |
+
justify-content: space-between;
|
| 906 |
+
color: var(--steel);
|
| 907 |
+
font-size: 8px;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.route-filters {
|
| 911 |
+
display: flex;
|
| 912 |
+
gap: 5px;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.route-filters button {
|
| 916 |
+
padding: 6px 8px;
|
| 917 |
+
border: 1px solid var(--line);
|
| 918 |
+
color: var(--paper-dim);
|
| 919 |
+
background: transparent;
|
| 920 |
+
font-size: 8px;
|
| 921 |
+
letter-spacing: 0.08em;
|
| 922 |
+
cursor: pointer;
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.route-filters button:hover,
|
| 926 |
+
.route-filters button[aria-pressed="true"] {
|
| 927 |
+
color: var(--ink);
|
| 928 |
+
border-color: var(--acid);
|
| 929 |
+
background: var(--acid);
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.segment-list {
|
| 933 |
+
max-height: 680px;
|
| 934 |
+
overflow-y: auto;
|
| 935 |
+
scrollbar-color: var(--steel) var(--ink);
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
.segment-row {
|
| 939 |
+
width: 100%;
|
| 940 |
+
min-height: 92px;
|
| 941 |
+
padding: 17px 20px;
|
| 942 |
+
display: grid;
|
| 943 |
+
grid-template-columns: 54px 92px minmax(160px, 1fr) minmax(120px, 0.7fr) auto;
|
| 944 |
+
gap: 14px;
|
| 945 |
+
align-items: center;
|
| 946 |
+
border: 0;
|
| 947 |
+
border-bottom: 1px solid var(--line);
|
| 948 |
+
color: var(--paper);
|
| 949 |
+
background: transparent;
|
| 950 |
+
text-align: left;
|
| 951 |
+
cursor: pointer;
|
| 952 |
+
transition: color 130ms ease, background 130ms ease;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
.segment-row:hover,
|
| 956 |
+
.segment-row.is-selected {
|
| 957 |
+
color: var(--ink);
|
| 958 |
+
background: var(--paper);
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.segment-row.is-selected {
|
| 962 |
+
box-shadow: inset 5px 0 var(--orange);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.segment-row-index {
|
| 966 |
+
color: var(--steel);
|
| 967 |
+
font-family: var(--display);
|
| 968 |
+
font-size: 28px;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.segment-route {
|
| 972 |
+
width: max-content;
|
| 973 |
+
padding: 4px 7px;
|
| 974 |
+
color: var(--ink);
|
| 975 |
+
font-size: 9px;
|
| 976 |
+
font-weight: 900;
|
| 977 |
+
letter-spacing: 0.1em;
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
.segment-route.route-keep {
|
| 981 |
+
background: var(--acid);
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
.segment-route.route-review {
|
| 985 |
+
background: var(--orange);
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
.segment-route.route-discard {
|
| 989 |
+
color: var(--paper);
|
| 990 |
+
background: var(--steel);
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.segment-time strong,
|
| 994 |
+
.segment-time small,
|
| 995 |
+
.segment-score strong,
|
| 996 |
+
.segment-score small {
|
| 997 |
+
display: block;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
.segment-time strong {
|
| 1001 |
+
font-size: 12px;
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
.segment-time small,
|
| 1005 |
+
.segment-score small {
|
| 1006 |
+
margin-top: 3px;
|
| 1007 |
+
color: var(--steel);
|
| 1008 |
+
font-size: 8px;
|
| 1009 |
+
text-transform: uppercase;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
.segment-row:hover small,
|
| 1013 |
+
.segment-row.is-selected small {
|
| 1014 |
+
color: #686961;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
.segment-score {
|
| 1018 |
+
text-align: right;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.segment-score strong {
|
| 1022 |
+
font-family: var(--display);
|
| 1023 |
+
font-size: 24px;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
.segment-issue-count {
|
| 1027 |
+
color: var(--orange);
|
| 1028 |
+
font-size: 9px;
|
| 1029 |
+
white-space: nowrap;
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
.empty-list {
|
| 1033 |
+
padding: 42px 20px;
|
| 1034 |
+
color: var(--paper-dim);
|
| 1035 |
+
text-align: center;
|
| 1036 |
+
font-size: 11px;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.issue-deck {
|
| 1040 |
+
margin: 18px 20px 4px;
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
.issue-list {
|
| 1044 |
+
padding: 8px 20px 24px;
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
.issue-row {
|
| 1048 |
+
padding: 13px 0;
|
| 1049 |
+
border-bottom: 1px solid var(--line);
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
.issue-row-head {
|
| 1053 |
+
display: grid;
|
| 1054 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 1055 |
+
gap: 10px;
|
| 1056 |
+
align-items: baseline;
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
.issue-name {
|
| 1060 |
+
overflow: hidden;
|
| 1061 |
+
font-size: 10px;
|
| 1062 |
+
text-overflow: ellipsis;
|
| 1063 |
+
text-transform: uppercase;
|
| 1064 |
+
white-space: nowrap;
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
.issue-value {
|
| 1068 |
+
color: var(--orange);
|
| 1069 |
+
font-size: 9px;
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
.issue-meter {
|
| 1073 |
+
position: relative;
|
| 1074 |
+
height: 5px;
|
| 1075 |
+
margin-top: 8px;
|
| 1076 |
+
background: rgba(239, 238, 230, 0.1);
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.issue-meter span {
|
| 1080 |
+
position: absolute;
|
| 1081 |
+
inset: 0 auto 0 0;
|
| 1082 |
+
background: var(--orange);
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
.issue-meter i {
|
| 1086 |
+
position: absolute;
|
| 1087 |
+
top: -2px;
|
| 1088 |
+
bottom: -2px;
|
| 1089 |
+
width: 1px;
|
| 1090 |
+
background: var(--paper);
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
.selected-panel {
|
| 1094 |
+
margin-top: 18px;
|
| 1095 |
+
color: var(--ink);
|
| 1096 |
+
border-color: var(--paper);
|
| 1097 |
+
background: var(--paper);
|
| 1098 |
+
box-shadow: 8px 8px 0 var(--orange);
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
.selected-panel .panel-heading {
|
| 1102 |
+
border-color: rgba(16, 17, 15, 0.25);
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
.selected-panel .panel-index {
|
| 1106 |
+
color: #616259;
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
.selected-grid {
|
| 1110 |
+
display: grid;
|
| 1111 |
+
grid-template-columns: minmax(230px, 0.7fr) minmax(280px, 1fr) minmax(240px, 1fr);
|
| 1112 |
+
gap: 30px;
|
| 1113 |
+
padding: 26px;
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
.selected-decision > span {
|
| 1117 |
+
display: block;
|
| 1118 |
+
width: max-content;
|
| 1119 |
+
margin-bottom: 18px;
|
| 1120 |
+
padding: 5px 9px;
|
| 1121 |
+
color: var(--ink);
|
| 1122 |
+
background: var(--acid);
|
| 1123 |
+
font-size: 10px;
|
| 1124 |
+
font-weight: 900;
|
| 1125 |
+
letter-spacing: 0.12em;
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
.selected-decision > span.route-review {
|
| 1129 |
+
background: var(--orange);
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
.selected-decision > span.route-discard {
|
| 1133 |
+
color: var(--paper);
|
| 1134 |
+
background: var(--steel);
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
.selected-decision strong,
|
| 1138 |
+
.selected-decision small {
|
| 1139 |
+
display: block;
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
.selected-decision strong {
|
| 1143 |
+
font-family: var(--display);
|
| 1144 |
+
font-size: 28px;
|
| 1145 |
+
line-height: 1;
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
.selected-decision small {
|
| 1149 |
+
margin-top: 7px;
|
| 1150 |
+
color: #62635b;
|
| 1151 |
+
font-size: 9px;
|
| 1152 |
+
text-transform: uppercase;
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
.selected-scores {
|
| 1156 |
+
margin: 0;
|
| 1157 |
+
display: grid;
|
| 1158 |
+
grid-template-columns: repeat(3, 1fr);
|
| 1159 |
+
gap: 8px;
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
.selected-scores div {
|
| 1163 |
+
min-width: 0;
|
| 1164 |
+
padding: 13px;
|
| 1165 |
+
border: 1px solid rgba(16, 17, 15, 0.25);
|
| 1166 |
+
}
|
| 1167 |
+
|
| 1168 |
+
.selected-scores dt {
|
| 1169 |
+
overflow: hidden;
|
| 1170 |
+
color: #62635b;
|
| 1171 |
+
font-size: 8px;
|
| 1172 |
+
text-overflow: ellipsis;
|
| 1173 |
+
white-space: nowrap;
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
.selected-scores dd {
|
| 1177 |
+
margin: 8px 0 0;
|
| 1178 |
+
font-family: var(--display);
|
| 1179 |
+
font-size: 25px;
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
.selected-issues {
|
| 1183 |
+
align-self: start;
|
| 1184 |
+
display: flex;
|
| 1185 |
+
flex-wrap: wrap;
|
| 1186 |
+
gap: 6px;
|
| 1187 |
+
}
|
| 1188 |
+
|
| 1189 |
+
.issue-chip {
|
| 1190 |
+
padding: 5px 7px;
|
| 1191 |
+
border: 1px solid rgba(16, 17, 15, 0.35);
|
| 1192 |
+
font-size: 8px;
|
| 1193 |
+
text-transform: uppercase;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
.metadata-drawer {
|
| 1197 |
+
margin-top: 55px;
|
| 1198 |
+
border-top: 1px solid var(--line);
|
| 1199 |
+
border-bottom: 1px solid var(--line);
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
.metadata-drawer summary {
|
| 1203 |
+
padding: 24px 2px;
|
| 1204 |
+
display: flex;
|
| 1205 |
+
align-items: center;
|
| 1206 |
+
justify-content: space-between;
|
| 1207 |
+
list-style: none;
|
| 1208 |
+
cursor: pointer;
|
| 1209 |
+
font-family: var(--display);
|
| 1210 |
+
font-size: 20px;
|
| 1211 |
+
text-transform: uppercase;
|
| 1212 |
+
}
|
| 1213 |
+
|
| 1214 |
+
.metadata-drawer summary::-webkit-details-marker {
|
| 1215 |
+
display: none;
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
.metadata-drawer summary::after {
|
| 1219 |
+
content: "+";
|
| 1220 |
+
margin-left: 20px;
|
| 1221 |
+
color: var(--acid);
|
| 1222 |
+
font-family: var(--mono);
|
| 1223 |
+
font-size: 26px;
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
.metadata-drawer[open] summary::after {
|
| 1227 |
+
content: "−";
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
.metadata-drawer summary b {
|
| 1231 |
+
margin-right: 16px;
|
| 1232 |
+
color: var(--orange);
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
.metadata-drawer summary small {
|
| 1236 |
+
margin-left: auto;
|
| 1237 |
+
color: var(--steel);
|
| 1238 |
+
font-family: var(--mono);
|
| 1239 |
+
font-size: 8px;
|
| 1240 |
+
letter-spacing: 0.1em;
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
.metadata-grid {
|
| 1244 |
+
display: grid;
|
| 1245 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 1246 |
+
border-top: 1px solid var(--line);
|
| 1247 |
+
border-left: 1px solid var(--line);
|
| 1248 |
+
}
|
| 1249 |
+
|
| 1250 |
+
.metadata-item {
|
| 1251 |
+
min-height: 100px;
|
| 1252 |
+
padding: 16px;
|
| 1253 |
+
border-right: 1px solid var(--line);
|
| 1254 |
+
border-bottom: 1px solid var(--line);
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.metadata-item span,
|
| 1258 |
+
.metadata-item strong {
|
| 1259 |
+
display: block;
|
| 1260 |
+
overflow-wrap: anywhere;
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
.metadata-item span {
|
| 1264 |
+
color: var(--steel);
|
| 1265 |
+
font-size: 8px;
|
| 1266 |
+
letter-spacing: 0.12em;
|
| 1267 |
+
text-transform: uppercase;
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
.metadata-item strong {
|
| 1271 |
+
margin-top: 11px;
|
| 1272 |
+
font-size: 11px;
|
| 1273 |
+
font-weight: 500;
|
| 1274 |
+
}
|
| 1275 |
+
|
| 1276 |
+
#raw-header {
|
| 1277 |
+
max-height: 400px;
|
| 1278 |
+
margin: 0;
|
| 1279 |
+
padding: 22px;
|
| 1280 |
+
overflow: auto;
|
| 1281 |
+
border: 1px solid var(--line);
|
| 1282 |
+
border-top: 0;
|
| 1283 |
+
color: var(--acid);
|
| 1284 |
+
background: #090a09;
|
| 1285 |
+
font: 10px/1.7 var(--mono);
|
| 1286 |
+
white-space: pre-wrap;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.export-dock {
|
| 1290 |
+
margin-top: 55px;
|
| 1291 |
+
padding: clamp(25px, 4vw, 48px);
|
| 1292 |
+
display: grid;
|
| 1293 |
+
grid-template-columns: 1fr auto;
|
| 1294 |
+
gap: 40px;
|
| 1295 |
+
align-items: center;
|
| 1296 |
+
color: var(--ink);
|
| 1297 |
+
background: var(--acid);
|
| 1298 |
+
box-shadow: 9px 9px 0 var(--orange);
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
.export-dock h3 {
|
| 1302 |
+
margin-top: 7px;
|
| 1303 |
+
font-size: clamp(33px, 5vw, 67px);
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
.export-dock .eyebrow,
|
| 1307 |
+
.export-dock p {
|
| 1308 |
+
color: #34362b;
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
.export-dock p:last-child {
|
| 1312 |
+
max-width: 680px;
|
| 1313 |
+
margin-bottom: 0;
|
| 1314 |
+
font-size: 10px;
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
.export-actions {
|
| 1318 |
+
display: flex;
|
| 1319 |
+
gap: 10px;
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
.export-actions .button-primary {
|
| 1323 |
+
color: var(--paper);
|
| 1324 |
+
border-color: var(--ink);
|
| 1325 |
+
background: var(--ink);
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
.button-quiet.light:hover {
|
| 1329 |
+
color: var(--paper);
|
| 1330 |
+
background: var(--ink);
|
| 1331 |
+
box-shadow: 4px 4px 0 var(--orange);
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
footer {
|
| 1335 |
+
min-height: 80px;
|
| 1336 |
+
display: flex;
|
| 1337 |
+
align-items: center;
|
| 1338 |
+
justify-content: space-between;
|
| 1339 |
+
border-top: 1px solid var(--line);
|
| 1340 |
+
color: var(--steel);
|
| 1341 |
+
font-size: 8px;
|
| 1342 |
+
letter-spacing: 0.16em;
|
| 1343 |
+
}
|
| 1344 |
+
|
| 1345 |
+
.toast {
|
| 1346 |
+
position: fixed;
|
| 1347 |
+
z-index: 90;
|
| 1348 |
+
right: 24px;
|
| 1349 |
+
bottom: 24px;
|
| 1350 |
+
max-width: min(380px, calc(100vw - 48px));
|
| 1351 |
+
padding: 13px 16px;
|
| 1352 |
+
color: var(--ink);
|
| 1353 |
+
background: var(--acid);
|
| 1354 |
+
box-shadow: 5px 5px 0 var(--orange);
|
| 1355 |
+
font-size: 10px;
|
| 1356 |
+
font-weight: 700;
|
| 1357 |
+
}
|
| 1358 |
+
|
| 1359 |
+
noscript {
|
| 1360 |
+
position: fixed;
|
| 1361 |
+
inset: auto 20px 20px;
|
| 1362 |
+
z-index: 100;
|
| 1363 |
+
padding: 18px;
|
| 1364 |
+
color: var(--ink);
|
| 1365 |
+
background: var(--orange);
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
@media (max-width: 1120px) {
|
| 1369 |
+
.hero {
|
| 1370 |
+
grid-template-columns: 45px minmax(0, 1fr) 140px;
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
.hero-stamp {
|
| 1374 |
+
width: 135px;
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
.cli-band {
|
| 1378 |
+
grid-template-columns: 1fr 1fr;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.command-shell {
|
| 1382 |
+
grid-column: 1 / -1;
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
.analysis-grid,
|
| 1386 |
+
.evidence-grid {
|
| 1387 |
+
grid-template-columns: 1fr;
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
.video-stage {
|
| 1391 |
+
min-height: 350px;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
.selected-grid {
|
| 1395 |
+
grid-template-columns: 0.7fr 1.3fr;
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
.selected-issues {
|
| 1399 |
+
grid-column: 1 / -1;
|
| 1400 |
+
}
|
| 1401 |
+
}
|
| 1402 |
+
|
| 1403 |
+
@media (max-width: 800px) {
|
| 1404 |
+
.masthead,
|
| 1405 |
+
main,
|
| 1406 |
+
footer {
|
| 1407 |
+
width: min(100% - 28px, 1500px);
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
.masthead {
|
| 1411 |
+
grid-template-columns: 1fr auto;
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
.masthead-status {
|
| 1415 |
+
display: none;
|
| 1416 |
+
}
|
| 1417 |
+
|
| 1418 |
+
.hero {
|
| 1419 |
+
min-height: 560px;
|
| 1420 |
+
padding-top: 85px;
|
| 1421 |
+
grid-template-columns: 1fr;
|
| 1422 |
+
align-items: start;
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
.hero-index {
|
| 1426 |
+
display: none;
|
| 1427 |
+
}
|
| 1428 |
+
|
| 1429 |
+
.hero-stamp {
|
| 1430 |
+
position: absolute;
|
| 1431 |
+
right: 9px;
|
| 1432 |
+
bottom: 45px;
|
| 1433 |
+
width: 115px;
|
| 1434 |
+
}
|
| 1435 |
+
|
| 1436 |
+
.hero-stamp strong {
|
| 1437 |
+
font-size: 23px;
|
| 1438 |
+
}
|
| 1439 |
+
|
| 1440 |
+
.hero-deck {
|
| 1441 |
+
max-width: calc(100% - 125px);
|
| 1442 |
+
margin-left: 0;
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
.section-heading,
|
| 1446 |
+
.workspace-heading {
|
| 1447 |
+
grid-template-columns: 50px 1fr;
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
.section-note,
|
| 1451 |
+
.manifest-identity {
|
| 1452 |
+
grid-column: 2;
|
| 1453 |
+
justify-self: start;
|
| 1454 |
+
text-align: left;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
.file-grid,
|
| 1458 |
+
.metric-grid {
|
| 1459 |
+
grid-template-columns: 1fr 1fr;
|
| 1460 |
+
}
|
| 1461 |
+
|
| 1462 |
+
.file-grid {
|
| 1463 |
+
gap: 10px;
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
.file-bay {
|
| 1467 |
+
min-height: 260px;
|
| 1468 |
+
padding: 23px;
|
| 1469 |
+
grid-template-columns: 1fr;
|
| 1470 |
+
}
|
| 1471 |
+
|
| 1472 |
+
.file-icon {
|
| 1473 |
+
grid-row: auto;
|
| 1474 |
+
font-size: 50px;
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
.file-bay > span:not(.file-number, .file-icon) {
|
| 1478 |
+
white-space: normal;
|
| 1479 |
+
}
|
| 1480 |
+
|
| 1481 |
+
.load-actions {
|
| 1482 |
+
flex-wrap: wrap;
|
| 1483 |
+
}
|
| 1484 |
+
|
| 1485 |
+
.privacy-note {
|
| 1486 |
+
width: 100%;
|
| 1487 |
+
margin: 5px 0 0;
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
.cli-band {
|
| 1491 |
+
grid-template-columns: 1fr;
|
| 1492 |
+
gap: 22px;
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
.command-shell {
|
| 1496 |
+
grid-column: auto;
|
| 1497 |
+
}
|
| 1498 |
+
|
| 1499 |
+
.segment-row {
|
| 1500 |
+
grid-template-columns: 44px 80px 1fr auto;
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
.segment-score {
|
| 1504 |
+
display: none;
|
| 1505 |
+
}
|
| 1506 |
+
|
| 1507 |
+
.metadata-grid {
|
| 1508 |
+
grid-template-columns: 1fr 1fr;
|
| 1509 |
+
}
|
| 1510 |
+
|
| 1511 |
+
.export-dock {
|
| 1512 |
+
grid-template-columns: 1fr;
|
| 1513 |
+
}
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
@media (max-width: 560px) {
|
| 1517 |
+
.masthead-link {
|
| 1518 |
+
font-size: 0;
|
| 1519 |
+
}
|
| 1520 |
+
|
| 1521 |
+
.masthead-link::after {
|
| 1522 |
+
content: "HF ↗";
|
| 1523 |
+
font-size: 10px;
|
| 1524 |
+
}
|
| 1525 |
+
|
| 1526 |
+
.hero {
|
| 1527 |
+
min-height: 520px;
|
| 1528 |
+
padding-top: 70px;
|
| 1529 |
+
}
|
| 1530 |
+
|
| 1531 |
+
.hero h1 {
|
| 1532 |
+
font-size: clamp(52px, 18vw, 88px);
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
.hero-deck {
|
| 1536 |
+
max-width: 100%;
|
| 1537 |
+
margin-right: 95px;
|
| 1538 |
+
font-size: 12px;
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
+
.hero-stamp {
|
| 1542 |
+
right: 5px;
|
| 1543 |
+
bottom: 28px;
|
| 1544 |
+
width: 94px;
|
| 1545 |
+
padding: 12px;
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
.load-bay {
|
| 1549 |
+
padding: 22px 16px;
|
| 1550 |
+
}
|
| 1551 |
+
|
| 1552 |
+
.section-heading,
|
| 1553 |
+
.workspace-heading {
|
| 1554 |
+
grid-template-columns: 40px 1fr;
|
| 1555 |
+
}
|
| 1556 |
+
|
| 1557 |
+
.section-number {
|
| 1558 |
+
font-size: 34px;
|
| 1559 |
+
}
|
| 1560 |
+
|
| 1561 |
+
.file-grid,
|
| 1562 |
+
.metric-grid {
|
| 1563 |
+
grid-template-columns: 1fr;
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
.file-bay {
|
| 1567 |
+
min-height: 200px;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
.button {
|
| 1571 |
+
width: 100%;
|
| 1572 |
+
}
|
| 1573 |
+
|
| 1574 |
+
.load-actions .button {
|
| 1575 |
+
flex: 1 1 100%;
|
| 1576 |
+
}
|
| 1577 |
+
|
| 1578 |
+
.command-shell {
|
| 1579 |
+
grid-template-columns: auto minmax(0, 1fr);
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
.command-shell .copy-button {
|
| 1583 |
+
grid-column: 1 / -1;
|
| 1584 |
+
width: 100%;
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
.panel-heading {
|
| 1588 |
+
align-items: flex-start;
|
| 1589 |
+
flex-direction: column;
|
| 1590 |
+
}
|
| 1591 |
+
|
| 1592 |
+
.timeline-panel .panel-heading,
|
| 1593 |
+
.segments-panel .panel-heading {
|
| 1594 |
+
min-height: 108px;
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
.legend,
|
| 1598 |
+
.route-filters {
|
| 1599 |
+
width: 100%;
|
| 1600 |
+
justify-content: space-between;
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
.video-stage,
|
| 1604 |
+
.timeline-stage,
|
| 1605 |
+
#timeline {
|
| 1606 |
+
min-height: 300px;
|
| 1607 |
+
height: 300px;
|
| 1608 |
+
}
|
| 1609 |
+
|
| 1610 |
+
.segment-row {
|
| 1611 |
+
grid-template-columns: 38px 74px 1fr;
|
| 1612 |
+
gap: 9px;
|
| 1613 |
+
padding-inline: 13px;
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
.segment-issue-count {
|
| 1617 |
+
display: none;
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
.selected-grid {
|
| 1621 |
+
grid-template-columns: 1fr;
|
| 1622 |
+
}
|
| 1623 |
+
|
| 1624 |
+
.selected-issues {
|
| 1625 |
+
grid-column: auto;
|
| 1626 |
+
}
|
| 1627 |
+
|
| 1628 |
+
.metadata-grid {
|
| 1629 |
+
grid-template-columns: 1fr;
|
| 1630 |
+
}
|
| 1631 |
+
|
| 1632 |
+
.metadata-drawer summary small {
|
| 1633 |
+
display: none;
|
| 1634 |
+
}
|
| 1635 |
+
|
| 1636 |
+
.export-actions {
|
| 1637 |
+
flex-direction: column;
|
| 1638 |
+
}
|
| 1639 |
+
|
| 1640 |
+
footer {
|
| 1641 |
+
flex-direction: column;
|
| 1642 |
+
justify-content: center;
|
| 1643 |
+
gap: 6px;
|
| 1644 |
+
align-items: flex-start;
|
| 1645 |
+
}
|
| 1646 |
+
}
|
| 1647 |
+
|
| 1648 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1649 |
+
*,
|
| 1650 |
+
*::before,
|
| 1651 |
+
*::after {
|
| 1652 |
+
scroll-behavior: auto !important;
|
| 1653 |
+
transition-duration: 0.01ms !important;
|
| 1654 |
+
animation-duration: 0.01ms !important;
|
| 1655 |
+
animation-iteration-count: 1 !important;
|
| 1656 |
+
}
|
| 1657 |
+
}
|
| 1658 |
+
|
| 1659 |
+
@media (forced-colors: active) {
|
| 1660 |
+
.status-lamp,
|
| 1661 |
+
.segment-route,
|
| 1662 |
+
.track-segment,
|
| 1663 |
+
.issue-meter span,
|
| 1664 |
+
.issue-meter i {
|
| 1665 |
+
forced-color-adjust: none;
|
| 1666 |
+
}
|
| 1667 |
+
}
|