itspublu commited on
Commit
9b4ace6
·
verified ·
1 Parent(s): 64139ab

Release EgoSieve manifest inspector v0.1.0

Browse files
Files changed (6) hide show
  1. README.md +16 -5
  2. app.mjs +914 -0
  3. index.html +291 -17
  4. manifest.mjs +302 -0
  5. manifest.test.mjs +150 -0
  6. styles.css +1667 -0
README.md CHANGED
@@ -1,10 +1,21 @@
1
  ---
2
  title: EgoSieve
3
- emoji: 🐠
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: static
7
- pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 &amp; 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
+ }