@rig-c/review 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -359,15 +359,28 @@ Control capabilities are excluded from links and browser persistence.
359
359
 
360
360
  ## Workspace settings and panels
361
361
 
362
- The left, right and bottom panels are resizable. Drag a separator, or focus it
362
+ The review page has a left column (Parts explorer, with checkpoints and the
363
+ reference select folded above it), the stage, the Inspector below it with
364
+ **Setup**, **Animate** and **Feedback** sections, and a status footer. The
365
+ compare page keeps sources on the left and the Parts explorer on the right.
366
+
367
+ The view bar keeps the layer toggles in reach in every section: **Images**
368
+ (`I`), **Mesh edges** (`M`) and **Bone points** (`B`). The keys do nothing while
369
+ a text field, textarea, select or editable text has focus, with Ctrl/Alt/Cmd
370
+ held, or on key repeat; Space stays pan and Enter stays frame entry. A disabled
371
+ toggle shows why as a tooltip, and the footer shows the same layer status. The
372
+ footer also gathers the message, player, layout, renderer, idle, remote control
373
+ and draft state; **View region** opens the numeric view.
374
+
375
+ The side and bottom panels are resizable. Drag a separator, or focus it
363
376
  and use the arrow keys (Shift for larger steps) and Home/End. Escape, a
364
377
  cancelled pointer or window blur during a drag returns the previous size. The
365
- stage keeps a usable minimum. When the window is too narrow, the left and then
366
- the right panel, and when it is too short, the Inspector, become explicit
378
+ stage keeps a usable minimum. When the window is too narrow, the side columns
379
+ (left first on the compare page), and when it is too short, the Inspector, become explicit
367
380
  drawers opened from buttons above the stage instead of shrinking to nothing. Responsive clamping
368
381
  never overwrites the desired size, so a wider window restores it.
369
382
 
370
- Settings, in the lower left, holds browser defaults: Theme, Sync cameras by
383
+ Settings, at the bottom of the left column, holds browser defaults: Theme, Sync cameras by
371
384
  default, Repeat idle by default, and Reset layout. Defaults seed a new
372
385
  inspection only. A view link or source draft keeps its own appearance, camera
373
386
  sync and idle choice, and restoring it neither changes the saved defaults nor
@@ -390,9 +403,9 @@ serving use per-port origins and keep them separate.
390
403
 
391
404
  ## Inspect a part
392
405
 
393
- The right Parts explorer selects an actual model slot on a specific single/A/B side. All rows follow the public held snapshot's exact draw-order z, including hidden/null slots. Search matches the exact source slot names: a row is shown when its name contains the typed text, case-sensitively, with no case folding or normalization. Arrow keys/Home/End move between result buttons, Enter/Space selects, and Escape returns to search. Whole rig restores full drawing. Selecting a drawn row fits its conservative world bounds within the existing camera zoom range; Sync cameras applies the same normalized camera action to every visible pane, while independent mode changes only that selected pane. Hidden/empty geometry reports `PART_FIT_UNAVAILABLE` and keeps the camera.
406
+ The Parts explorer (left column; right on the compare page) selects an actual model slot on a specific single/A/B side. All rows follow the public held snapshot's exact draw-order z, including hidden/null slots. Search matches the exact source slot names: a row is shown when its name contains the typed text, case-sensitively, with no case folding or normalization. Arrow keys/Home/End move between result buttons, Enter/Space selects, and Escape returns to search. Whole rig restores full drawing. Selecting a drawn row fits its conservative world bounds within the existing camera zoom range; Sync cameras applies the same normalized camera action to every visible pane, while independent mode changes only that selected pane. Hidden/empty geometry reports `PART_FIT_UNAVAILABLE` and keeps the camera.
394
407
 
395
- Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The Rig inspector shows actual posed mesh counts or region corners, page/region, current skin/set context and declared attachment candidates. **Use slot for feedback** explicitly prefills the target; selecting a row alone submits nothing.
408
+ Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The Setup section shows actual posed mesh counts or region corners, page/region, current skin/set context and declared attachment candidates. **Use slot for feedback** explicitly prefills the target; selecting a row alone submits nothing.
396
409
 
397
410
  The pinned published `@rig-c/play`1.12.0 supplies `slotIdentity()` and frame generation. The explorer atomically retains the public identity snapshot with the same-generation held frame: exact attachment key, owning skin, active set/skin, full z order and explicit hidden/no-attachment/timeline-null/set-hide reasons. Hidden/null slots keep null keys with their reported reason; declarations and texture/UV names never replace that public result. An identity-known attachment need not have a visible primitive or positive fit bounds. The current UI follows its selected slot as animation changes, updating the displayed identity to the actual held frame.
398
411
 
package/docs/DESIGN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The delivery unit is an existing output folder or an optional curated review package handed from an agent to a person. This reusable app owns inspection, reference comparison, annotations and feedback export. Upstream compiler, splitter and runtime remain their respective tools. The existing producing agent needs no change to open its output here.
6
6
 
7
- The person uses one inspection workspace with **View**, **Rig**, **Playback** and **Feedback** sections. Rig covers setup geometry, source art, pivot/joint placement, meshes, weights, draw order and angle coverage; Playback covers frame intervals, reference video/poses, contact/deformation and audio inspection. Motion is strictly **view, annotate and export**: time/angle controls navigate inspection. This app does not author animation keys, retime motion or write `motion.json`. Future modeling touch-up remains in scope as a design goal; it is not implemented by this starter.
7
+ The person uses one inspection workspace with **Setup**, **Animate** and **Feedback** sections (the compare page keeps **View**, **Rig**, **Playback** and **Feedback**). Setup covers setup geometry, source art, pivot/joint placement, meshes, weights, draw order and angle coverage; Animate covers frame intervals, reference video/poses, contact/deformation and audio inspection. Motion is strictly **view, annotate and export**: time/angle controls navigate inspection. This app does not author animation keys, retime motion or write `motion.json`. Future modeling touch-up remains in scope as a design goal; it is not implemented by this starter.
8
8
 
9
9
  ## Implemented foundation
10
10
 
@@ -34,6 +34,17 @@ Any future modeling edit needs provenance, undo/redo, explicit units and an expo
34
34
 
35
35
  [design-prototype.html](design-prototype.html) preserves the interactive screen concept. It is **illustrative**, with sample controls/data, separate from the implemented app. Its older motion key/curve editing concept is superseded by the current view/annotate/export boundary. It does not demonstrate completed mesh, motion, angle or audio editing.
36
36
 
37
+ ## Workspace layout
38
+
39
+ The single review page is one inspection workspace: a left column, the stage, the Inspector under the stage and one status footer. There is no right panel.
40
+
41
+ - **Left column.** The Parts explorer fills it. Checkpoints, the current checkpoint title and the reference select fold into a compact strip above the part list; the strip opens when the package supplies checkpoints or references and stays folded for a plain output folder. Browser Settings sit at the bottom.
42
+ - **View bar.** Pane choice, the persistent image / mesh edges / bone points layer toggles (keys `I`, `M`, `B`), camera sync and zoom, then appearance, repeat idle and Copy view link at the right end. The toggles stay native checkboxes, so their checked state is the pressed state, the draft input and the `layers` control value. A disabled toggle names the footer's layer reason as its tooltip. Turning images off leaves the mesh/bone overlay alone.
43
+ - **Inspector.** Three sections: **Setup** (selected part, part presets, modeling-review proposals, angle panel), **Animate** (transport, frame navigation, motion-review proposals, sound cues, report events) and **Feedback** (review status, reviewed source with the read-only source-file list, selected checkpoint, structured feedback, frame-strip export). Their element ids and saved section names (`rig`, `playback`, `feedback`) are unchanged; a saved `view` section from older links restores without selecting a section.
44
+ - **Footer.** Message (truncated, full text as tooltip) on the left; player, effective layout, renderer, layers, idle, remote control and draft state in the middle; the numeric view region (behind **View region**) and the source hash on the right.
45
+
46
+ The compare page keeps its own markup and stylesheet: sources left, Parts explorer right and its four Inspector sections.
47
+
37
48
  ## Inspection strips
38
49
 
39
50
  An inclusive annotation interval can also become a derived frame strip. The single view selects one declared source video; compare retains its explicit shared video and chooses a producer clock basis for each interval. A batch preserves separate feedback intervals and side provenance. Source rows run down the sheet and requested producer frames run across it. A vertical batch repeats this grid rather than merging overlapping annotations.
@@ -45,7 +56,7 @@ The export uses dedicated real canvas players and leaves the live playhead/sessi
45
56
 
46
57
  ## Implemented slot inspection and public identity snapshots
47
58
 
48
- The right Parts explorer provides source-hash-bound slot selection, held-pose clipped Solo, Whole/context, conservative bounds fit, a diagnostic selection box, actual mesh/region counts and reproducible selection context. This is inspection rather than geometry editing. All rows use the public held identity snapshot's exact z order, including hidden/null slots. Current source-bound links/drafts and control observations retain side/slot/mode.
59
+ The Parts explorer (the review page's left column; the compare page's right one) provides source-hash-bound slot selection, held-pose clipped Solo, Whole/context, conservative bounds fit, a diagnostic selection box, actual mesh/region counts and reproducible selection context. This is inspection rather than geometry editing. All rows use the public held identity snapshot's exact z order, including hidden/null slots. Current source-bound links/drafts and control observations retain side/slot/mode.
49
60
 
50
61
  Published Play1.12.0 supplies a frozen `slotIdentity()` snapshot coherent with `posedFrame().generation`. Review retains copied frame/identity together only after equal-generation and source-slot/order checks, uses the player's default public filter, and never seeks/reposes/changes physics merely to read identity. Exact owning skin/key, active set/skin and null reasons come from that snapshot, not a unique-declaration or texture match. A resolved identity can remain known without visible geometry or fit bounds. Clip declarations do not invent keys when the API reports no drawable attachment.
51
62
 
@@ -55,16 +66,29 @@ A future optimization operation sends explicit source-bound selection/bounds to
55
66
 
56
67
  ## Workspace geometry and browser preferences
57
68
 
58
- Left, right and bottom panels have pointer and keyboard separators with separator semantics: orientation, the controlled panel, an accessible name and the actual reachable minimum/maximum/current pixels. Arrow keys act only when the separator itself has focus, so text-entry arrows are never intercepted. A drag holds pointer capture; cancel, lost capture, blur, Escape and disposal restore the previous desired size and release it.
69
+ Side and bottom panels have pointer and keyboard separators with separator semantics: orientation, the controlled panel, an accessible name and the actual reachable minimum/maximum/current pixels. Arrow keys act only when the separator itself has focus, so text-entry arrows are never intercepted. A drag holds pointer capture; cancel, lost capture, blur, Escape and disposal restore the previous desired size and release it.
59
70
 
60
- The allocator measures the layout rectangle minus stage chrome and keeps a 240×180 CSS-pixel stage minimum. It shrinks side panels proportionally toward their own minima; when the width still cannot hold them it turns the left, then the right panel, and when the height cannot hold it the Inspector, into a native modal drawer opened from a trigger outside it. Effective sizes are computed from desired sizes and never written back; a separator inverts the allocator so a clamped pixel value is not saved as the desktop desire.
71
+ The allocator measures the layout rectangle minus stage chrome and keeps a 240×180 CSS-pixel stage minimum. It shrinks side panels proportionally toward their own minima; when the width still cannot hold them it turns the left, then the right column, and when the height cannot hold it the Inspector, into a native modal drawer opened from a trigger outside it. Effective sizes are computed from desired sizes and never written back; a separator inverts the allocator so a clamped pixel value is not saved as the desktop desire.
61
72
 
62
- Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat and the three desired panel sizes. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
73
+ Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat and the three desired panel sizes. The saved `rightWidth` is the Parts explorer's width wherever the page shows it, so the review page's left parts column keeps the width chosen before the move; `leftWidth` is the compare page's sources column. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
63
74
 
64
75
  Content state stays source-bound. Each Inspector section's x/y scroll offset is captured before its panel hides and restored after it shows, after two frames of layout, and again when its content extent changes. Restoring clamps to the current extent without replacing the remembered offset, and intentional user scrolling supersedes a pending restore. Offsets are kept in the source draft's UI state, never in the global record or a copied view link.
65
76
 
66
77
  Browser storage is origin-scoped. Managed opens (the default) share one loopback front origin that routes `/instances/<key>/…` to each worker, so Settings, sizes and drafts carry across reopens; `--direct` keeps per-port origins. Because one origin cannot isolate its scripts, writes require a per-instance capability handed off only in the printed `#cap=` fragment and checked by both the front and the worker; a fragment-less copied link is read-only. Draft scopes carry the source hash, so revisions of one package never clobber each other's drafts.
67
78
 
79
+ ## Theme
80
+
81
+ The palette comes from the rig-c.org site stylesheet. Its dark `:root` values (`--bg`, `--fg`, `--fg-strong`, `--fg-soft`, `--fg-muted`, `--border`, `--border-strong`, `--hover-bg`, `--active-bg`, `--link`, `--link-hover`, `--btn-fg`, `--code-*`, `--footer-bg`, `--pass`, `--warn`, `--fail`, the font stacks and `--radius`) are copied into the `:root` block of `web/styles.css` and `web/compare.css`; nothing is fetched at runtime, so the viewer stays offline. The site loads Inter and JetBrains Mono from a font CDN; the review keeps only the family stacks and falls back to local fonts. The site has no light scheme, so the light values and every review-only surface token (`--rv-*`: bars, panels, controls, fields, pressed state, focus ring, backdrop, stage) are review-local derivations of the same palette.
82
+
83
+ Rules:
84
+
85
+ - No literal colour (`#hex`, `rgb()`, `hsl()`) outside the `:root` token blocks of either stylesheet. Rules use `var(--…)` only.
86
+ - Every colour token is defined for both schemes: `:root` holds dark (the default before any script runs) and `:root[data-theme=light]` redefines the same set. The appearance control resolves `system` through `prefers-color-scheme` and always writes `data-theme=dark|light` on the root element, so the stylesheets carry no per-component light overrides.
87
+ - State colours: pressed and current selections use the `--pass` family, primary actions and links `--link`, dirty and draft notices `--warn`, errors `--fail`.
88
+ - The stage (pane gradient, grid dots, pane labels, empty-state text, video letterbox) stays dark in both schemes so source pixels and overlays read the same; its tokens carry equal values in both blocks.
89
+
90
+ `tests/theme.test.ts` enforces the first two rules.
91
+
68
92
  ## Comparison eligibility
69
93
 
70
94
  A layout is offered only for inputs that actually loaded and rendered. One pure decision (`src/comparison-eligibility.ts`) takes load outcomes, never declared paths: the rig counts after its first successful draw, an image after `decode()`, a pose after it parses, and a video after `seeked` at the held time with a decoded frame. A new video stays pending until then; a held time outside its duration is refused as `REFERENCE_OUT_OF_DOMAIN`, never clamped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/review",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Local human rig inspection and structured feedback export",
6
6
  "license": "MIT",
package/web/compare.css CHANGED
@@ -1,8 +1,11 @@
1
- :root{font:13px/1.5 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e0eeec;background:#10212a;color-scheme:dark}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button{padding:7px 10px;border:1px solid #486574;border-radius:6px;color:inherit;background:#28414c;cursor:pointer}button:hover{background:#385763}button:disabled{opacity:.4;cursor:default}.primary{background:#b7dfcb;color:#183c2c;border-color:#b7dfcb}.primary:hover{background:#d0eddf}input,select{border:1px solid #4b6c7c;background:#102833;border-radius:6px;color:inherit;padding:8px;max-width:100%;width:100%}input[type=range],input[type=checkbox]{accent-color:#b6dec9;padding:0}input[type=checkbox]{width:auto}input[type=range]{min-width:60px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid #d0eada;outline-offset:3px}header{padding:15px 21px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #355563;background:#152c36;flex-wrap:wrap}.brand{font-size:19px;letter-spacing:-.5px;white-space:nowrap}.brand span{font-size:10px;letter-spacing:.08em;display:block;color:#b5dcc9}h1{font-size:14px;font-weight:500;margin:0}h2{font-size:13px;font-weight:500;margin:0 0 13px}.hint{font-size:11px;color:#a6c2cf}.actions{display:flex;align-items:center;gap:7px;margin-left:auto;flex-wrap:wrap}.actions button{font-size:11px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;min-height:calc(100vh - 118px)}main{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #34515e;font-size:11px;color:#b6cfd9}.row{display:flex;gap:5px;align-items:center;flex-wrap:wrap}.row button{font-size:11px;padding:4px 8px}.row output{font:11px ui-monospace,SFMono-Regular,monospace;padding:0 4px}.stages{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px;flex:1;min-height:500px}.pane{display:flex;flex-direction:column;min-width:0;border:1px solid #3b5d6c;border-radius:8px;overflow:hidden;background:#152d39}.pane-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid #385762;font-size:11px;color:#b4cfd9;min-height:45px;flex-wrap:wrap}.pane-head strong{font-weight:500}.side-label{display:inline-grid;place-items:center;width:23px;height:23px;border:1px solid #7aab95;border-radius:5px;color:#ccead9;flex:none}.rig-viewport,.reference-viewport{position:relative;min-height:420px;overflow:hidden;flex:1;background:radial-gradient(ellipse at 50% 40%,#294754,#102631 75%)}.rig-viewport{background-image:radial-gradient(#8eb7c637 .7px,transparent .7px);background-size:22px 22px}.rig-root{position:absolute;transform-origin:0 0}video{width:100%;height:100%;position:absolute;inset:0;object-fit:contain;background:#07171f}.empty{position:absolute;inset:50px 16px 30px;display:grid;place-items:center;text-align:center;font-size:12px;color:#a9c7d4;line-height:1.7;overflow-wrap:anywhere}.pane-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 11px;border-top:1px solid #385762;color:#b5ccd7;font:11px ui-monospace,SFMono-Regular,monospace;min-height:42px}.pane-foot label{display:flex;align-items:center;gap:4px;font:11px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.transport{display:flex;gap:11px;align-items:center;flex-wrap:wrap;padding:13px 17px;border-top:1px solid #3b5b69;background:#17313d}.transport input[type=range]{flex:1}.transport input[type=number]{width:100px}.transport output{font:11px ui-monospace,SFMono-Regular,monospace;white-space:nowrap;color:#c2dae2}.compare-info{display:flex;justify-content:space-between;gap:9px;flex-wrap:wrap;padding:9px 17px;color:#a4c0ce;font-size:11px}.trace{font-size:11px;color:#abc6d1;padding:4px 17px}.trace summary,.provenance summary{cursor:pointer}.trace pre{font:11px/1.6 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.boundary{font-size:11px;color:#91b2c1;padding:7px 17px;margin:0}aside{background:#192f3b;padding:21px 17px;border-left:1px solid #3b5b69}.eyebrow{font-size:11px;letter-spacing:.08em;color:#b3ccd4;margin-bottom:15px}aside label{display:block;font-size:11px;color:#b1cbd5;margin:12px 0 6px}.provenance{padding:13px 11px;margin-top:22px;background:#223c47;border:1px solid #4b6a77;border-radius:7px;font-size:11px}.provenance p{margin:7px 0;color:#b8d1db;overflow-wrap:anywhere}.provenance details{margin-top:12px}.provenance pre{font:11px/1.6 ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere;white-space:pre-wrap}.feedback-form{border-top:1px solid #405f6c;margin-top:24px;padding-top:21px}.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.block{width:100%;margin-top:14px}.feedback-item{font-size:11px;overflow-wrap:anywhere;padding:12px 0;border-top:1px solid #426372;margin-top:13px}.feedback-item strong{display:block;font-weight:500;color:#d2e7e2}.feedback-item button{padding:3px 7px;font-size:11px;margin-top:7px}.dirty{color:#e1c697!important}.error{color:#f2baab!important}footer{display:flex;justify-content:space-between;gap:13px;flex-wrap:wrap;padding:11px 18px;background:#142a35;border-top:1px solid #365966;font-size:11px;color:#b5cfd9}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
1
+ /* Theme tokens: site values copied from rig-c.org styles.css (dark); light and --rv-* are review-local derivations. No literal colour outside these blocks. */
2
+ :root{--font-sans:Inter,system-ui,sans-serif;--font-display:Inter,system-ui,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;--radius:10px;--bg:#070b18;--fg:#c9d4e8;--fg-strong:#f3f6fc;--fg-soft:#aebbd4;--fg-muted:#7f8ba6;--border:#18213a;--border-strong:#2a3657;--hover-bg:#0f1730;--active-bg:#142042;--link:#7cc8ff;--link-hover:#b3e0ff;--btn-fg:#08101f;--code-bg:#050814;--code-fg:#d6e2f5;--code-border:#18213a;--code-inline-bg:#121a30;--footer-bg:#050813;--pass:#30d158;--warn:#ffcc00;--fail:#ff453a;--rv-bar:#0b1124;--rv-panel:#0d152b;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:var(--active-bg);--rv-field:var(--code-bg);--rv-segmented:var(--code-bg);--rv-pressed-bg:#133733;--rv-pressed-border:#207c44;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link-hover);--rv-backdrop:rgba(7,11,24,.82);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
3
+ :root[data-theme=light]{--bg:#f5f7fb;--fg:#26324a;--fg-strong:#0c1324;--fg-soft:#3b4864;--fg-muted:#5d6982;--border:#d5dcea;--border-strong:#b3bed3;--hover-bg:#e8edf6;--active-bg:#dbe3f3;--link:#1a6fc0;--link-hover:#0f528f;--btn-fg:#ffffff;--code-bg:#eef1f7;--code-fg:#1d2940;--code-border:#d5dcea;--code-inline-bg:#e3e8f2;--footer-bg:#eceff6;--pass:#1e8a3c;--warn:#8a6400;--fail:#b3261e;--rv-bar:#edf1f7;--rv-panel:#e9eef6;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:#d5ddec;--rv-field:#ffffff;--rv-segmented:var(--code-inline-bg);--rv-pressed-bg:#c9ded8;--rv-pressed-border:#6fb286;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link);--rv-backdrop:rgba(12,19,36,.45);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
4
+ :root{font:13px/1.5 var(--font-sans);color:var(--fg);background:var(--bg);color-scheme:dark}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button{padding:7px 10px;border:1px solid var(--border-strong);border-radius:6px;color:inherit;background:var(--rv-control);cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.4;cursor:default}.primary{background:var(--link);color:var(--btn-fg);border-color:var(--link)}.primary:hover{background:var(--link-hover)}input,select{border:1px solid var(--border-strong);background:var(--rv-field);border-radius:6px;color:inherit;padding:8px;max-width:100%;width:100%}input[type=range],input[type=checkbox]{accent-color:var(--link);padding:0}input[type=checkbox]{width:auto}input[type=range]{min-width:60px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}header{padding:15px 21px;display:flex;align-items:center;gap:24px;border-bottom:1px solid var(--border);background:var(--rv-bar);flex-wrap:wrap}.brand{font-size:19px;letter-spacing:-.5px;white-space:nowrap}.brand span{font-size:10px;letter-spacing:.08em;display:block;color:var(--fg-muted)}h1{font-size:14px;font-weight:500;margin:0}h2{font-size:13px;font-weight:500;margin:0 0 13px}.hint{font-size:11px;color:var(--fg-muted)}.actions{display:flex;align-items:center;gap:7px;margin-left:auto;flex-wrap:wrap}.actions button{font-size:11px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;min-height:calc(100vh - 118px)}main{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-muted)}.row{display:flex;gap:5px;align-items:center;flex-wrap:wrap}.row button{font-size:11px;padding:4px 8px}.row output{font:11px var(--font-mono);padding:0 4px}.stages{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px;flex:1;min-height:500px}.pane{display:flex;flex-direction:column;min-width:0;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:var(--rv-panel)}.pane-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-soft);min-height:45px;flex-wrap:wrap}.pane-head strong{font-weight:500}.side-label{display:inline-grid;place-items:center;width:23px;height:23px;border:1px solid var(--rv-pressed-border);border-radius:5px;color:var(--rv-pressed-fg);flex:none}.rig-viewport,.reference-viewport{position:relative;min-height:420px;overflow:hidden;flex:1;background:radial-gradient(ellipse at 50% 40%,var(--rv-pane-hi),var(--rv-pane-lo) 75%)}.rig-viewport{background-image:radial-gradient(var(--rv-grid-dot) .7px,transparent .7px);background-size:22px 22px}.rig-root{position:absolute;transform-origin:0 0}video{width:100%;height:100%;position:absolute;inset:0;object-fit:contain;background:var(--rv-media-bg)}.empty{position:absolute;inset:50px 16px 30px;display:grid;place-items:center;text-align:center;font-size:12px;color:var(--rv-stage-fg);line-height:1.7;overflow-wrap:anywhere}.pane-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 11px;border-top:1px solid var(--border);color:var(--fg-muted);font:11px var(--font-mono);min-height:42px}.pane-foot label{display:flex;align-items:center;gap:4px;font:11px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.transport{display:flex;gap:11px;align-items:center;flex-wrap:wrap;padding:13px 17px;border-top:1px solid var(--border);background:var(--rv-panel)}.transport input[type=range]{flex:1}.transport input[type=number]{width:100px}.transport output{font:11px var(--font-mono);white-space:nowrap;color:var(--fg-muted)}.compare-info{display:flex;justify-content:space-between;gap:9px;flex-wrap:wrap;padding:9px 17px;color:var(--fg-muted);font-size:11px}.trace{font-size:11px;color:var(--fg-muted);padding:4px 17px}.trace summary,.provenance summary{cursor:pointer}.trace pre{font:11px/1.6 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere}.boundary{font-size:11px;color:var(--fg-muted);padding:7px 17px;margin:0}aside{background:var(--rv-panel);padding:21px 17px;border-left:1px solid var(--border)}.eyebrow{font-size:11px;letter-spacing:.08em;color:var(--fg-muted);margin-bottom:15px}aside label{display:block;font-size:11px;color:var(--fg-muted);margin:12px 0 6px}.provenance{padding:13px 11px;margin-top:22px;background:var(--rv-raised);border:1px solid var(--border-strong);border-radius:7px;font-size:11px}.provenance p{margin:7px 0;color:var(--fg-muted);overflow-wrap:anywhere}.provenance details{margin-top:12px}.provenance pre{font:11px/1.6 var(--font-mono);overflow-wrap:anywhere;white-space:pre-wrap}.feedback-form{border-top:1px solid var(--border);margin-top:24px;padding-top:21px}.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.block{width:100%;margin-top:14px}.feedback-item{font-size:11px;overflow-wrap:anywhere;padding:12px 0;border-top:1px solid var(--border);margin-top:13px}.feedback-item strong{display:block;font-weight:500;color:var(--fg-strong)}.feedback-item button{padding:3px 7px;font-size:11px;margin-top:7px}.dirty{color:var(--warn)!important}.error{color:var(--fail)!important}footer{display:flex;justify-content:space-between;gap:13px;flex-wrap:wrap;padding:11px 18px;background:var(--footer-bg);border-top:1px solid var(--border);font-size:11px;color:var(--fg-soft)}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
2
5
  @media(min-width:1600px){.rig-viewport,.reference-viewport{min-height:620px}.layout{grid-template-columns:minmax(0,1fr) 320px}}
3
6
  @media(max-width:1100px){.layout{grid-template-columns:minmax(0,1fr) 260px}aside{padding:17px 12px}.stages{gap:7px;padding:9px;min-height:430px}.rig-viewport,.reference-viewport{min-height:380px}.pane-head{padding:9px}.pane-foot{padding:9px 7px}}
4
- @media(max-width:850px){.layout{grid-template-columns:1fr}aside{border-left:0;border-top:1px solid #3b5b69;display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}aside>.eyebrow,aside>label,aside>select{grid-column:1/-1}.feedback-form,.provenance{margin-top:0}.feedback-form{padding-top:0;border-top:0}.actions{margin-left:0}.rig-viewport,.reference-viewport{min-height:420px}}
5
- @media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid #405f6c}.toolbar{padding:11px 13px}.transport{padding:12px;gap:8px}.transport input[type=range]{min-width:130px}.transport input[type=number]{width:84px}.provenance{margin-top:18px}}
7
+ @media(max-width:850px){.layout{grid-template-columns:1fr}aside{border-left:0;border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}aside>.eyebrow,aside>label,aside>select{grid-column:1/-1}.feedback-form,.provenance{margin-top:0}.feedback-form{padding-top:0;border-top:0}.actions{margin-left:0}.rig-viewport,.reference-viewport{min-height:420px}}
8
+ @media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid var(--border)}.toolbar{padding:11px 13px}.transport{padding:12px;gap:8px}.transport input[type=range]{min-width:130px}.transport input[type=number]{width:84px}.provenance{margin-top:18px}}
6
9
  @media(pointer:coarse){button{min-height:42px}input,select{font-size:16px}.row button{min-width:38px}}
7
10
 
8
11
  /* Inspection layout is bounded independently of pane content and review forms. */
@@ -13,37 +16,19 @@
13
16
  #stage-layout [data-panning=true]{cursor:grabbing!important}
14
17
  #stage-layout img{-webkit-user-drag:none;user-select:none}
15
18
  .rig-overlay{position:absolute;inset:0;pointer-events:none;width:100%;height:100%;z-index:1}
16
- .inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid #35515e;font-size:11px;background:#172f3b}
19
+ .inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--border);font-size:11px;background:var(--rv-panel)}
17
20
  .inspection-controls label{display:inline-flex;align-items:center;gap:5px;margin:0;white-space:nowrap}
18
21
  .inspection-controls select{width:auto;padding:4px;font-size:11px}
19
22
  .inspection-controls input[type=checkbox]{width:auto;margin:0}
20
23
  .inspection-controls fieldset{display:flex;gap:10px;align-items:center;border:0;padding:0;margin:0;min-width:0;flex-wrap:wrap}
21
24
  .inspection-controls legend{float:left;margin-right:9px;font-size:11px}
22
25
  #control-state:empty{display:none}
23
- :root[data-theme=light]{color-scheme:light;color:#233c47;background:#f4f7f9}
26
+ :root[data-theme=light]{color-scheme:light}
24
27
  :root[data-theme=dark]{color-scheme:dark}
25
- [data-theme=light] body,[data-theme=light] main{background:#f4f7f9;color:#233c47}
26
- [data-theme=light] header,[data-theme=light] .topbar,[data-theme=light] .workspaces,[data-theme=light] footer{background:#edf3f5;border-color:#bacbd3;color:#294552}
27
- [data-theme=light] aside,[data-theme=light] .transport,[data-theme=light] .frame-controls,[data-theme=light] .inspection-controls,[data-theme=light] .sound-panel,[data-theme=light] .angle-panel,[data-theme=light] .report-panel{background:#e9f0f3;border-color:#b7c9d1;color:#294552}
28
- [data-theme=light] button{background:#e2ebef;border-color:#a6bdc8;color:#264652}
29
- [data-theme=light] button:hover{background:#d0e2e8}
30
- [data-theme=light] button.primary,[data-theme=light] .primary{background:#24694d;border-color:#24694d;color:#fff}
31
- [data-theme=light] input,[data-theme=light] select,[data-theme=light] textarea{background:#fff;border-color:#8faab7;color:#243e4a;accent-color:#276c50}
32
- [data-theme=light] input::placeholder,[data-theme=light] textarea::placeholder{color:#607986}
33
- [data-theme=light] .hint,[data-theme=light] .subtle,[data-theme=light] .label,[data-theme=light] .eyebrow,[data-theme=light] aside label,[data-theme=light] .tool small,[data-theme=light] .stage-footer,[data-theme=light] .compare-info,[data-theme=light] .boundary,[data-theme=light] .trace,[data-theme=light] .transport output,[data-theme=light] .zoom-controls output,[data-theme=light] .frame-range label,[data-theme=light] .frame-controls label{color:#496774}
34
- [data-theme=light] .workspaces button,[data-theme=light] .segmented button{color:#3e5c69}
35
- [data-theme=light] .workspaces button[aria-pressed=true]{color:#185738;border-bottom-color:#276c50}
36
- [data-theme=light] .workspaces button span{color:#58727e}
37
- [data-theme=light] .segmented,[data-theme=light] .provenance,[data-theme=light] .report-selection pre{background:#e1ebef;border-color:#9cb5c0;color:#294552}
38
- [data-theme=light] .segmented button[aria-pressed=true],[data-theme=light] .checkpoint[aria-current=true],[data-theme=light] .report-event-list button[aria-current=true]{background:#cde4d8;border-color:#56866e;color:#1f4937}
39
- [data-theme=light] .checkpoint small{color:#4b6c59}
40
- [data-theme=light] .provenance p,[data-theme=light] .provenance-row span,[data-theme=light] .provenance details,[data-theme=light] .report-selection details,[data-theme=light] .report-event-list small{color:#486470}
41
- [data-theme=light] .report-event-list button,[data-theme=light] .report-empty-categories span{background:#e5eef2;border-color:#acbfca;color:#294552}
42
- [data-theme=light] .feedback-item strong{color:#1e4e3b}
43
- [data-theme=light] a{color:#175f48}
44
- [data-theme=light] .error{color:#a03225!important}
45
- [data-theme=light] .dirty,[data-theme=light] .report-event-list .no-time{color:#875800!important}
46
- [data-theme=light] :focus-visible{outline-color:#24694d}
28
+
29
+ input::placeholder,textarea::placeholder{color:var(--fg-muted)}
30
+
31
+ :focus-visible{outline-color:var(--rv-focus)}
47
32
  /* Source pixels retain their original color; the neutral stage stays dark in both themes. */
48
33
 
49
34
  #stage-layout{grid-template-columns:repeat(3,minmax(0,1fr))}#stage-layout[data-orientation=vertical]{grid-template-columns:minmax(0,1fr);grid-template-rows:repeat(3,minmax(0,1fr))}
@@ -55,12 +40,12 @@ html,body{height:100%;overflow:hidden}body{height:100dvh}#app{height:100dvh;disp
55
40
  main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
56
41
  .left-panel{min-height:0;overflow:auto;padding:13px 10px}
57
42
  #stage-layout{height:auto;min-height:0;flex:1;overflow:hidden;padding:8px;gap:7px}
58
- .inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid #49616d;background:#182d38;overflow:hidden}
59
- .inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid #3e5c69;flex:none;font-size:11px}
43
+ .inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid var(--border);background:var(--rv-panel);overflow:hidden}
44
+ .inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid var(--border);flex:none;font-size:11px}
60
45
  .inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
61
46
  .inspector-heading [role=tablist]{display:flex;gap:4px}
62
47
  .inspector-heading button{padding:4px 9px;font-size:11px}
63
- .inspector-heading button[aria-selected=true]{background:#3d5d4d;color:#e0f3e7;border-color:#7fa98e}
48
+ .inspector-heading button[aria-selected=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg);border-color:var(--rv-pressed-border)}
64
49
  .inspector-body{flex:1;min-height:0;overflow:hidden}
65
50
  .inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
66
51
  .inspector .inspection-controls{padding:0;border:0;background:transparent}
@@ -77,9 +62,7 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
77
62
  #inspector-rig #part-set{max-width:190px}
78
63
  #inspector-playback #motion-tools{display:flex;gap:8px;flex-wrap:wrap}
79
64
  #inspector-playback .tool{width:auto}
80
- [data-theme=light] .inspector{background:#edf3f5;border-color:#a6bec8;color:#294552}
81
- [data-theme=light] .inspector-heading{border-color:#b3c6d0}
82
- [data-theme=light] .inspector-heading button[aria-selected=true]{background:#cde4d8;color:#1e4d36;border-color:#59836b}
65
+
83
66
  @media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-meta{padding:6px 10px 0}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
84
67
  @media(max-width:540px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:block;padding:8px;font-size:11px}.layout{grid-template-columns:105px minmax(0,1fr)!important}.topbar,header{padding:7px 10px;gap:8px}.topbar .brand,header .brand{font-size:14px}.top-actions,.actions{gap:4px}.top-actions button,.actions button{padding:4px 6px;font-size:10px}.workspaces button{padding:6px}.inspector{height:40dvh;min-height:150px}.inspector-heading{gap:8px;padding:5px 8px}.inspector-heading button{padding:4px 7px}.stage-footer{display:none}}
85
68
 
@@ -91,12 +74,12 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
91
74
 
92
75
  .rig-layer-controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:11px}.rig-layer-controls fieldset{display:flex;gap:12px;align-items:center;flex-wrap:wrap;border:0;margin:0;padding:0}.rig-layer-controls legend{float:left;margin-right:10px}.rig-layer-controls label{display:inline-flex;gap:5px;align-items:center}.rig-layer-controls input[type=checkbox]{width:auto}
93
76
 
94
- .numeric-view{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:#14232c;color:#c9dfe7;border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:#e0c58e}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}[data-theme=light] .numeric-view{background:#e1eaee;color:#264653}[data-theme=light] .draft-notice{color:#80521a}[data-theme=light] .outcome legend,[data-theme=light] .brand span,[data-theme=light] .toolbar{color:#496774}
77
+ .numeric-view{font:11px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:var(--code-bg);color:var(--code-fg);border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:var(--warn)}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}
95
78
 
96
79
  .camera-sync{display:inline-flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}.camera-sync input[type=checkbox]{width:auto;flex:none;margin:0}
97
80
 
98
81
  /* Slot explorer shares the stage row; bottom Inspector remains in the main column. */
99
- .layout{grid-template-columns:150px minmax(0,1fr) 235px!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid #49616d;background:#182d38;padding:12px;overscroll-behavior:contain}.parts-explorer h2{font-size:12px;margin:0 0 10px}.parts-explorer>label{display:block;font-size:11px;margin-top:12px}.parts-explorer input[type=search]{width:100%;min-width:0}.parts-explorer .part-solo{display:flex;gap:6px;align-items:center}.parts-explorer ul{list-style:none;margin:8px 0;padding:0;display:grid;gap:4px}.parts-explorer li{min-width:0}.parts-explorer li button{display:block;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}.parts-explorer button[aria-pressed=true]{background:#355445;border-color:#9cd3b4}.parts-explorer h2,#selected-part h3{font-weight:600}#selected-part{margin-bottom:12px}#selected-part h3{margin:0 0 8px;overflow-wrap:anywhere}#selected-part p{overflow-wrap:anywhere}[data-theme=light] .parts-explorer{background:#edf3f5;border-color:#a6bec8;color:#294552}[data-theme=light] .parts-explorer button[aria-pressed=true]{background:#c5ddcd;color:#183c2c}
82
+ .layout{grid-template-columns:150px minmax(0,1fr) 235px!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid var(--border);background:var(--rv-panel);padding:12px;overscroll-behavior:contain}.parts-explorer h2{font-size:12px;margin:0 0 10px}.parts-explorer>label{display:block;font-size:11px;margin-top:12px}.parts-explorer input[type=search]{width:100%;min-width:0}.parts-explorer .part-solo{display:flex;gap:6px;align-items:center}.parts-explorer ul{list-style:none;margin:8px 0;padding:0;display:grid;gap:4px}.parts-explorer li{min-width:0}.parts-explorer li button{display:block;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}.parts-explorer button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border)}.parts-explorer h2,#selected-part h3{font-weight:600}#selected-part{margin-bottom:12px}#selected-part h3{margin:0 0 8px;overflow-wrap:anywhere}#selected-part p{overflow-wrap:anywhere}
100
83
  @media(max-width:1050px){.layout{grid-template-columns:110px minmax(0,1fr) 185px!important}.parts-explorer{padding:8px}}
101
84
  @media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr) 165px!important}.left-panel{display:none!important}}
102
85
  @media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr) 132px!important}.parts-explorer{padding:6px}.parts-explorer li button{padding:6px}.parts-explorer>.hint{font-size:10px}}
@@ -108,9 +91,9 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
108
91
  .workspace-layout>main{display:grid;min-width:0;min-height:0;overflow:hidden;grid-column:3;grid-row:1}
109
92
  .workspace-stage-stack{grid-row:1;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
110
93
  .workspace-stage-stack>#stage-layout{flex:1;min-height:0;height:auto;overflow:hidden}
111
- .workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid #49616d}
94
+ .workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid var(--border)}
112
95
  .workspace-sources{flex:1;min-height:0;overflow:auto;padding:10px;overscroll-behavior:contain}
113
- .workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid #49616d}
96
+ .workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid var(--border)}
114
97
  .workspace-settings h2{font-size:12px;margin:0 0 8px}
115
98
  .workspace-settings label{display:flex;align-items:center;gap:6px;font-size:11px;margin:7px 0}
116
99
  .workspace-settings select{width:auto;min-width:0;flex:1}
@@ -118,23 +101,22 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
118
101
  .workspace-settings input[type=checkbox]{width:auto;flex:none}
119
102
  .workspace-layout>.parts-explorer{display:block;grid-template-columns:none;grid-column:5;grid-row:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
120
103
  .workspace-layout .inspector{grid-row:3;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
121
- .workspace-separator{position:relative;z-index:3;background:#34515e;touch-action:none;cursor:col-resize;min-width:0;min-height:0}
104
+ .workspace-separator{position:relative;z-index:3;background:var(--border-strong);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
122
105
  .workspace-separator::after{content:'';position:absolute;inset:0 -4px}
123
- .workspace-separator:focus-visible{outline:2px solid #c8e8d6;outline-offset:-2px;background:#719581}
124
- .workspace-separator[data-resizing=true]{background:#8ac9a3}
106
+ .workspace-separator:focus-visible{outline:2px solid var(--rv-focus);outline-offset:-2px;background:var(--link)}
107
+ .workspace-separator[data-resizing=true]{background:var(--link-hover)}
125
108
  .workspace-leftWidth{grid-column:2;grid-row:1}.workspace-rightWidth{grid-column:4;grid-row:1}
126
109
  .workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-4px 0}
127
110
  .workspace-drawer-triggers{display:flex;gap:6px;flex-wrap:wrap;flex:none;padding:4px 8px;min-height:0}
128
111
  .workspace-drawer-triggers:not(:has(button:not([hidden]))){display:none}
129
112
  .workspace-drawer-triggers button{padding:4px 7px;font-size:11px;min-height:28px}
130
- .workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid #739383;border-radius:8px;background:#182d38;color:inherit;overflow:hidden;z-index:20}
113
+ .workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-panel);color:inherit;overflow:hidden;z-index:20}
131
114
  .workspace-drawer:not([open]){display:none}.workspace-drawer[open]{display:flex;flex-direction:column;gap:8px}
132
- .workspace-drawer::backdrop{background:#07141ed0}
115
+ .workspace-drawer::backdrop{background:var(--rv-backdrop)}
133
116
  .workspace-drawer-close{align-self:flex-end;flex:none}
134
117
  .workspace-drawer>.left-panel{display:flex!important;flex-direction:column;flex:1;overflow:hidden;min-height:0;padding:0}
135
118
  .workspace-drawer>.parts-explorer{display:block;grid-template-columns:none;flex:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
136
119
  .workspace-drawer>.inspector{flex:1;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
137
- [data-theme=light] .workspace-settings,[data-theme=light] .workspace-drawer{background:#edf3f5;color:#294552;border-color:#a6bec8}
138
- [data-theme=light] .workspace-separator{background:#b2c7cf}[data-theme=light] .workspace-separator:focus-visible{outline-color:#24694d;background:#70a58c}
120
+
139
121
  @media(pointer:coarse){.workspace-separator::after{inset:0 -7px}.workspace-inspectorHeight::after{inset:-7px 0}.workspace-drawer-triggers button{min-height:38px}}
140
122
  #stage-layout[data-panes="1"]{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
package/web/index.html CHANGED
@@ -5,9 +5,10 @@
5
5
  <div id="app">
6
6
  <header class="topbar"><div class="brand"><span class="brand-mark">r</span>rig review</div><div><strong id="package-title">Review package</strong><span id="revision" class="subtle"></span></div><div class="top-actions"><span id="dirty" class="subtle">Loading…</span><button id="export">Feedback JSON</button><button id="export-markdown">Issue Markdown</button><button id="save" class="primary" disabled>Submit review</button></div></header>
7
7
  <div class="layout">
8
- <aside class="left-panel"><div class="eyebrow">CHECKPOINTS</div><nav id="checkpoints" aria-label="Checkpoints"></nav><section><label for="reference-select" class="label">Reference</label><select id="reference-select"></select><div id="reference-info" class="hint"></div><div id="person-control" hidden><label for="person-select" class="label space-top">Person in input pose</label><select id="person-select"></select></div></section><section><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></section></aside>
9
- <main><div class="viewbar"><div class="segmented" aria-label="View"><button data-view="compare" aria-pressed="true">Side by side</button><button data-view="reference" aria-pressed="false">Reference</button><button data-view="rig" aria-pressed="false">Rig</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div></div><div class="stage-meta"><span id="checkpoint-title">Select a checkpoint</span><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections"><button role="tab" id="tab-view" data-inspector="view" aria-controls="inspector-view" aria-selected="true" tabindex="0">View</button><button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="false" tabindex="-1">Rig</button><button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Playback</button><button role="tab" id="tab-feedback" data-inspector="feedback" aria-controls="inspector-feedback" aria-selected="false" tabindex="-1">Feedback</button></div><span class="hint">Local view controls · source files stay unchanged</span></div><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div><div class="inspector-body"><section id="inspector-view" role="tabpanel" aria-labelledby="tab-view" data-inspector-panel="view"><div class="view-link-controls"><button id="copy-view" type="button">Copy view link</button><span class="hint">Numeric source region · opening this link restores a paused inspection</span></div><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><div class="inspection-controls" aria-label="Inspection display controls"><label>Appearance <select id="appearance"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span></div><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></section><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig" hidden><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><div class="rig-layer-controls"><fieldset id="layer-controls"><legend>Rig layers</legend><label><input id="layer-images" type="checkbox" checked disabled>Images</label><label><input id="layer-mesh" type="checkbox" disabled>Mesh edges</label><label><input id="layer-bones" type="checkbox" disabled>Bone points</label></fieldset><span id="layer-state" class="hint">No rig supplied</span></div><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section></section><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · READ ONLY</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section></div></section></main><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><h2>PARTS</h2><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Type icons and main preview represent actual slots; atlas pages are not part thumbnails.</p></aside>
10
-
11
- </div><footer><span id="message" role="status" aria-live="polite">Loading local review package…</span><span id="source-hash" class="subtle"></span></footer>
12
- </div><script type="module" src="main.js"></script>
8
+ <aside class="left-panel"><details id="package-context" class="package-context"><summary><span class="eyebrow">CHECKPOINTS</span><span id="checkpoint-title">Select a checkpoint</span></summary><nav id="checkpoints" aria-label="Checkpoints"></nav><section><label for="reference-select" class="label">Reference</label><select id="reference-select"></select><div id="reference-info" class="hint"></div><div id="person-control" hidden><label for="person-select" class="label space-top">Person in input pose</label><select id="person-select"></select></div></section></details><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><h2>PARTS</h2><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Type icons and main preview represent actual slots; atlas pages are not part thumbnails.</p></aside></aside>
9
+ <main><div class="viewbar"><div class="segmented" aria-label="View"><button data-view="compare" aria-pressed="true">Side by side</button><button data-view="reference" aria-pressed="false">Reference</button><button data-view="rig" aria-pressed="false">Rig</button></div><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><div class="view-options" aria-label="Inspection display controls"><label>Appearance <select id="appearance"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><button id="copy-view" type="button">Copy view link</button></div></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections">
10
+ <button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="true" tabindex="0">Setup</button>
11
+ <button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Animate</button>
12
+ <button role="tab" id="tab-feedback" data-inspector="feedback" aria-controls="inspector-feedback" aria-selected="false" tabindex="-1">Feedback</button></div><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig"><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section></section><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · READ ONLY</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer></div>
13
+ <script type="module" src="main.js"></script>
13
14
  </body></html>
package/web/main.ts CHANGED
@@ -2,7 +2,7 @@ import {PartExplorer,loadPartCatalog} from './part-selection.ts';
2
2
  import { loadModel, RigPlayer, type ModelAssets, type PoseBounds } from '@rig-c/play';
3
3
  import {cameraFrame,cameraSignature,cameraFromSignature,assertCameraView,boundsFitSignature,dragSignature,zoomSignature,type CameraFrame,type CameraSignature} from './camera-view.ts';
4
4
  import {rendererStatus} from './renderer-status.ts';
5
- import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,type ViewPoint} from './view-controls.ts';
5
+ import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,bindLayerShortcuts,describeLayerToggles,type ViewPoint} from './view-controls.ts';
6
6
  import {RigOverlays} from './rig-overlays.ts';
7
7
  import {bindWorkspaceUI,type WorkspaceUI} from './workspace-ui.ts';
8
8
  import {connectControl} from './control.ts';
@@ -92,6 +92,7 @@ let selectedReportTarget: number | undefined;
92
92
 
93
93
  function message(value: string, error = false): void {
94
94
  text('message', value);
95
+ el('message').title = value; // the footer truncates long messages
95
96
  el('message').classList.toggle('error', error);
96
97
  }
97
98
 
@@ -553,6 +554,7 @@ async function initPlayer(initialCheckpoint?:Checkpoint): Promise<void> {
553
554
  select('axis-select').replaceChildren(); for (const axis of validAxes) select('axis-select').append(option(axis.id, axis.label));
554
555
  input('axis-value').disabled = !validAxes.length;
555
556
  for(const id of ['layer-images','layer-mesh','layer-bones'])input(id).disabled=false;
557
+ describeLayerToggles(document,el('layer-state').textContent??'');
556
558
  fitCamera();
557
559
  } catch (error) { text('renderer-state',rendererStatus(null,'failed').label);player?.dispose(); player = null; assets?.dispose(); assets = null; text('player-state', 'Rig load failed'); text('rig-empty', `Could not open rig: ${errorText(error)}`); message(errorText(error), true); }
558
560
  }
@@ -783,6 +785,7 @@ function bind(): void {
783
785
  viewDisposers.push(bindStageLayout(el('stage-layout'),fitCamera));
784
786
  for(const target of ['rig','reference'] as const)viewDisposers.push(bindPan(el(target+'-viewport'),(dx,dy)=>cameraAction(()=>panCamera(target,dx,dy))));
785
787
  for(const [id,key] of [['layer-images','images'],['layer-mesh','mesh'],['layer-bones','bones']] as const)input(id).onchange=()=>{layers[key]=input(id).checked;redrawOverlay(false);};
788
+ viewDisposers.push(bindLayerShortcuts(document));describeLayerToggles(document,el('layer-state').textContent??'');
786
789
  input('idle-repeat').onchange=()=>{renderIdle();if(canRepeatIdle())void playPreview();};
787
790
  for(const id of ['proposal-panel','checkpoint-detail'])el(id).addEventListener('focusin',pause);
788
791
  bindInspector();
@@ -802,6 +805,8 @@ async function start(): Promise<void> {
802
805
  select('reference-select').replaceChildren(); for (const reference of data.manifest.references) select('reference-select').append(option(reference.id, `${reference.label} · ${reference.type}`));
803
806
  for (const reference of data.manifest.references.filter(item => item.type === 'video')) select('strip-reference').append(option(reference.id, reference.label));
804
807
  if (!data.manifest.references.length) { select('reference-select').append(option('', 'No references supplied')); select('reference-select').disabled = true; }
808
+ // Checkpoints and reference fold away when the package supplies neither.
809
+ (el('package-context') as HTMLDetailsElement).open = data.manifest.checkpoints.length > 0 || data.manifest.references.length > 0;
805
810
  const sourceFiles = el('source-files'); for (const file of data.manifest.authoring) { const a = document.createElement('a'); a.href = assetUrl(file.path); a.textContent = file.label; a.className = 'source-link'; a.target = '_blank'; a.rel = 'noopener'; sourceFiles.append(a); } if (!data.manifest.authoring.length) sourceFiles.textContent = 'No source files supplied';
806
811
  bind(); parts=new PartExplorer(el('parts-explorer'),el('selected-part'),previous=>{redrawOverlay(true);const part=parts?.value();if(part){activeCamera='rig';if(view==='reference')setView('rig');showInspector('rig');renderCameraControls();}if(!cameraRestoring&&(previous?.side!==part?.side||previous?.slot!==part?.slot))cameraAction(()=>fitActiveCamera(part?'rig':availableCamera()));persistence?.save();},part=>{select('feedback-target-kind').value='slot';renderFeedbackTargets(true);input('feedback-target-name').value=part.slot;showInspector('feedback');}); updateStripControls(); stripStatus('Choose the entered start–end frames or all feedback ranges to download. Unsubmitted feedback is included; exporting does not submit the review.'); renderFeedbackTargets(true); renderFeedbackRange(); renderCheckpoints(); renderProposals(); renderProvenance(); updateSavedState(); renderCues(); initReport();
807
812
  const requestedId = new URLSearchParams(location.search).get('checkpoint'),initial=initialReviewSelection(data.manifest,requestedId,data.explicitReference?.id,data.initialAnimation??undefined),first=initial.checkpoint;
@@ -127,7 +127,7 @@ export class PartExplorer extends PartInspection {
127
127
  return {title,info,declarations,bounds,unavailable,feedback,status};
128
128
  })();
129
129
  nodes.title.textContent=observation?`${observation.side==='single'?'':observation.side.toUpperCase()+' · '}${observation.slot}`:'Whole rig';
130
- nodes.info.textContent=observation?`${observation.mode==='solo'?'Solo':'In context'} · ${observation.drawn?observation.kind==='mesh'?`${observation.vertices} posed vertices · ${observation.triangles} triangles`:'Region · four posed corners':'Not drawn at current pose'} · Skin: ${observation.skin??'not set'} · Set: ${observation.set??'not set'}`:refusal?`Parts unavailable · ${refusal.message}. The whole rig stays drawn; no slot is selected, soloed or targeted from stale data.`:'Select an actual slot in the right explorer. Selection preserves animation, time and physics history.';
130
+ nodes.info.textContent=observation?`${observation.mode==='solo'?'Solo':'In context'} · ${observation.drawn?observation.kind==='mesh'?`${observation.vertices} posed vertices · ${observation.triangles} triangles`:'Region · four posed corners':'Not drawn at current pose'} · Skin: ${observation.skin??'not set'} · Set: ${observation.set??'not set'}`:refusal?`Parts unavailable · ${refusal.message}. The whole rig stays drawn; no slot is selected, soloed or targeted from stale data.`:'Select an actual slot in the parts explorer. Selection preserves animation, time and physics history.';
131
131
  for(const node of [nodes.declarations,nodes.bounds,nodes.unavailable,nodes.feedback])node.hidden=!observation;nodes.feedback.disabled=!observation;
132
132
  nodes.declarations.textContent=observation?`${observation.activeAttachment?'Resolved public attachment: '+observation.activeAttachment.skin+' / '+observation.activeAttachment.attachment+'.':(observation.identityUnavailable??'No active drawable attachment: '+observation.reason)+'.'} Declared: ${observation.declarations.map(d=>`${d.skin} / ${d.attachment} (${d.kind})`).join('; ')||'none'}. ${observation.page?'Drawn page: '+observation.page:''}${observation.region?' · region '+observation.region:''}${observation.sequence!==null?' · sequence '+observation.sequence:''}`:'';
133
133
  nodes.bounds.textContent=observation?observation.bounds?`Selection bounds · world units / Y up: x ${observation.bounds.x}, y ${observation.bounds.y}, width ${observation.bounds.width}, height ${observation.bounds.height}. Conservative geometry/clip bounds; fit uses the existing 0.25–8× camera range.`:'PART_FIT_UNAVAILABLE: no positive drawn bounds at the held pose.':'';
package/web/styles.css CHANGED
@@ -1,17 +1,20 @@
1
- :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e3eceb;background:#10181c;font-size:13px;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{color:inherit;background:#26383f;border:1px solid #3a535f;border-radius:6px;padding:7px 10px;cursor:pointer}button:hover{background:#344d58}button:disabled{opacity:.45;cursor:default}button[aria-pressed=true]{background:#355445;border-color:#729985;color:#dcf4e5}button.primary{background:#b1dec6;border-color:#b1dec6;color:#163728;font-weight:600}button.outline{background:transparent}button.block{width:100%;margin-top:10px}a{color:#b2dccb}input,select,textarea{color:#e3eceb;background:#12242d;border:1px solid #486472;border-radius:6px;padding:8px;max-width:100%}select,input[type=text],textarea{width:100%}textarea{resize:vertical;line-height:1.6}input[type=range]{accent-color:#b2dec7;padding:0;min-width:0}input[type=checkbox],input[type=radio]{accent-color:#b2dec7}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid #c6e7d7;outline-offset:3px}[hidden]{display:none!important}.topbar{min-height:72px;padding:15px 22px;display:flex;align-items:center;gap:25px;border-bottom:1px solid #30464f;flex-wrap:wrap;background:#15232a}.brand{font-size:19px;letter-spacing:-.6px;white-space:nowrap}.brand-mark{display:inline-grid;place-items:center;width:24px;height:24px;border:1px solid #afdcc5;color:#afdcc5;border-radius:50%;margin-right:9px;font-size:14px}.topbar strong{font-weight:500;font-size:13px}.topbar #revision{margin-left:9px}.top-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.subtle,.hint{color:#a2bac4;font-size:11px}.hint{line-height:1.6}.workspaces{display:flex;gap:10px;padding:0 18px;border-bottom:1px solid #30464f;background:#142129}.workspaces button{border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;padding:15px 13px;font-size:12px;letter-spacing:.06em;color:#a6bec8}.workspaces button[aria-pressed=true]{border-bottom-color:#b1dec6;color:#c8ead8;background:transparent}.workspaces button span{font-size:11px;margin-left:8px;letter-spacing:0;color:#8faab6}.layout{display:grid;grid-template-columns:190px minmax(250px,1fr) 286px;min-height:calc(100vh - 160px)}aside{background:#182a33;padding:20px 15px}.left-panel{border-right:1px solid #30464f}.right-panel{border-left:1px solid #30464f}.eyebrow{font-size:11px;letter-spacing:.08em;color:#aac0c8;margin:0 0 11px}.label{display:block;color:#acbec8;font-size:11px;margin:0 0 7px}.space-top{margin-top:15px}.left-panel section{margin-top:26px}.tool{width:100%;display:flex;justify-content:space-between;align-items:center;text-align:left;gap:5px;border-color:transparent;background:transparent;padding:9px 6px;margin:3px 0}.tool small{font-size:11px;color:#9bb4bf}.checkpoint{display:block;width:100%;background:transparent;border:1px solid transparent;text-align:left;margin:4px 0;padding:10px 9px;white-space:normal;line-height:1.5}.checkpoint[aria-current=true]{background:#294638;border-color:#658d76}.checkpoint small{display:block;font-size:11px;color:#9eb8aa;margin-top:3px}.source-link{display:block;font-size:11px;overflow-wrap:anywhere;margin:8px 0}main{min-width:0;display:flex;flex-direction:column;background:#12232c}.viewbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:12px;border-bottom:1px solid #2e4855}.segmented{display:flex;gap:3px;padding:3px;border:1px solid #3a5663;border-radius:8px;background:#11202a}.segmented button{border:0;background:transparent;font-size:11px;padding:5px 9px}.segmented button[aria-pressed=true]{background:#395560;color:#e3eceb}.zoom-controls{display:flex;gap:4px;align-items:center}.zoom-controls button{padding:4px 8px;font-size:12px}.zoom-controls output{font:11px ui-monospace,SFMono-Regular,monospace;min-width:39px;text-align:center;color:#abc1cc}.stage-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 16px 0;font-size:11px;flex-wrap:wrap}.stages{display:flex;gap:10px;padding:12px 12px 0;flex:1;min-height:460px}.stage-pane{position:relative;min-width:0;flex:1;border:1px solid #35535f;border-radius:8px;overflow:hidden;background:radial-gradient(ellipse at 50% 40%,#233d48,#10222c 74%)}.pane-label{position:absolute;left:10px;top:10px;z-index:2;background:#152a35d9;border-radius:4px;padding:4px 7px;color:#c0d4db;font-size:11px;pointer-events:none}.viewport{position:relative;overflow:hidden;width:100%;height:100%;min-height:450px;touch-action:none;background-image:radial-gradient(#8eb2c333 .7px,transparent .7px);background-size:22px 22px}.empty{position:absolute;inset:65px 22px 30px;display:grid;place-items:center;text-align:center;color:#9dbac7;font-size:12px;line-height:1.8}.reference-content,#rig-root{position:absolute;transform-origin:0 0}.reference-content img,.reference-content video{display:block;max-width:none}.reference-content svg{display:block;max-width:none}.reference-content video{object-fit:contain}.stage-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:11px 16px;color:#90afbd;font-size:11px}.transport{display:flex;align-items:center;gap:11px;padding:13px;border-top:1px solid #35515e;background:#162e3b}.transport select{width:auto;max-width:150px;font-size:11px}.transport input{flex:1}.transport output{font:11px ui-monospace,SFMono-Regular,monospace;white-space:nowrap;color:#c1d7df}.sound-panel,.angle-panel{padding:15px;border-top:1px solid #35515e;background:#172f3b}.row{display:flex;align-items:center;gap:11px;flex-wrap:wrap}.row .eyebrow{margin:0}.sound-panel .row{font-size:11px;margin-bottom:9px}.sound-panel input[type=range]{flex:1;max-width:160px}.cue{display:flex;gap:9px;align-items:center;justify-content:space-between;border-top:1px solid #35505c;padding:9px 0;font-size:11px;flex-wrap:wrap}.cue button{padding:4px 7px;font-size:11px}.angle-panel{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:11px}.angle-panel select{width:auto;max-width:150px}.angle-panel .hint{width:100%;margin:0}.angle-panel input{flex:1}.divider{height:1px;background:#38515e;margin:22px 0}h1{font-size:15px;font-weight:500;line-height:1.6;margin:0 0 7px}.outcome{border:0;padding:0;margin:18px 0 0;display:flex;gap:11px;flex-wrap:wrap}.outcome legend{font-size:11px;color:#aec6cf;margin-bottom:8px;padding:0}.outcome label{display:flex;align-items:center;gap:3px;font-size:11px}.proposal-item{border-top:1px solid #3b5664;padding:11px 0;margin-top:12px;font-size:11px;overflow-wrap:anywhere}.proposal-item strong{display:block;font-weight:500;margin-bottom:5px}.proposal-item button{font-size:11px;padding:3px 6px;margin-top:7px}footer{display:flex;justify-content:space-between;gap:15px;flex-wrap:wrap;padding:11px 18px;border-top:1px solid #344e5a;background:#14242c;font-size:11px;color:#b4cad3}.error{color:#f0baac!important}.dirty{color:#e4c99a!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
1
+ /* Theme tokens: site values copied from rig-c.org styles.css (dark); light and --rv-* are review-local derivations. No literal colour outside these blocks. */
2
+ :root{--font-sans:Inter,system-ui,sans-serif;--font-display:Inter,system-ui,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;--radius:10px;--bg:#070b18;--fg:#c9d4e8;--fg-strong:#f3f6fc;--fg-soft:#aebbd4;--fg-muted:#7f8ba6;--border:#18213a;--border-strong:#2a3657;--hover-bg:#0f1730;--active-bg:#142042;--link:#7cc8ff;--link-hover:#b3e0ff;--btn-fg:#08101f;--code-bg:#050814;--code-fg:#d6e2f5;--code-border:#18213a;--code-inline-bg:#121a30;--footer-bg:#050813;--pass:#30d158;--warn:#ffcc00;--fail:#ff453a;--rv-bar:#0b1124;--rv-panel:#0d152b;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:var(--active-bg);--rv-field:var(--code-bg);--rv-segmented:var(--code-bg);--rv-pressed-bg:#133733;--rv-pressed-border:#207c44;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link-hover);--rv-backdrop:rgba(7,11,24,.82);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
3
+ :root[data-theme=light]{--bg:#f5f7fb;--fg:#26324a;--fg-strong:#0c1324;--fg-soft:#3b4864;--fg-muted:#5d6982;--border:#d5dcea;--border-strong:#b3bed3;--hover-bg:#e8edf6;--active-bg:#dbe3f3;--link:#1a6fc0;--link-hover:#0f528f;--btn-fg:#ffffff;--code-bg:#eef1f7;--code-fg:#1d2940;--code-border:#d5dcea;--code-inline-bg:#e3e8f2;--footer-bg:#eceff6;--pass:#1e8a3c;--warn:#8a6400;--fail:#b3261e;--rv-bar:#edf1f7;--rv-panel:#e9eef6;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:#d5ddec;--rv-field:#ffffff;--rv-segmented:var(--code-inline-bg);--rv-pressed-bg:#c9ded8;--rv-pressed-border:#6fb286;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link);--rv-backdrop:rgba(12,19,36,.45);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
4
+ :root{font-family:var(--font-sans);color:var(--fg);background:var(--bg);font-size:13px;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{color:inherit;background:var(--rv-control);border:1px solid var(--border-strong);border-radius:6px;padding:7px 10px;cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.45;cursor:default}button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border);color:var(--rv-pressed-fg)}button.primary{background:var(--link);border-color:var(--link);color:var(--btn-fg);font-weight:600}button.outline{background:transparent}button.block{width:100%;margin-top:10px}a{color:var(--link)}input,select,textarea{color:var(--fg);background:var(--rv-field);border:1px solid var(--border-strong);border-radius:6px;padding:8px;max-width:100%}select,input[type=text],textarea{width:100%}textarea{resize:vertical;line-height:1.6}input[type=range]{accent-color:var(--link);padding:0;min-width:0}input[type=checkbox],input[type=radio]{accent-color:var(--link)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}[hidden]{display:none!important}.topbar{min-height:72px;padding:15px 22px;display:flex;align-items:center;gap:25px;border-bottom:1px solid var(--border);flex-wrap:wrap;background:var(--rv-bar)}.brand{font-size:19px;letter-spacing:-.6px;white-space:nowrap}.brand-mark{display:inline-grid;place-items:center;width:24px;height:24px;border:1px solid var(--link);color:var(--link);border-radius:50%;margin-right:9px;font-size:14px}.topbar strong{font-weight:500;font-size:13px}.topbar #revision{margin-left:9px}.top-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.subtle,.hint{color:var(--fg-muted);font-size:11px}.hint{line-height:1.6}.workspaces{display:flex;gap:10px;padding:0 18px;border-bottom:1px solid var(--border);background:var(--rv-bar)}.workspaces button{border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;padding:15px 13px;font-size:12px;letter-spacing:.06em;color:var(--fg-soft)}.workspaces button[aria-pressed=true]{border-bottom-color:var(--link);color:var(--fg-strong);background:transparent}.workspaces button span{font-size:11px;margin-left:8px;letter-spacing:0;color:var(--fg-muted)}.layout{display:grid;grid-template-columns:190px minmax(250px,1fr) 286px;min-height:calc(100vh - 160px)}aside{background:var(--rv-panel);padding:20px 15px}.left-panel{border-right:1px solid var(--border)}.right-panel{border-left:1px solid var(--border)}.eyebrow{font-size:11px;letter-spacing:.08em;color:var(--fg-muted);margin:0 0 11px}.label{display:block;color:var(--fg-muted);font-size:11px;margin:0 0 7px}.space-top{margin-top:15px}.left-panel section{margin-top:26px}.tool{width:100%;display:flex;justify-content:space-between;align-items:center;text-align:left;gap:5px;border-color:transparent;background:transparent;padding:9px 6px;margin:3px 0}.tool small{font-size:11px;color:var(--fg-muted)}.checkpoint{display:block;width:100%;background:transparent;border:1px solid transparent;text-align:left;margin:4px 0;padding:10px 9px;white-space:normal;line-height:1.5}.checkpoint[aria-current=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border)}.checkpoint small{display:block;font-size:11px;color:var(--fg-muted);margin-top:3px}.source-link{display:block;font-size:11px;overflow-wrap:anywhere;margin:8px 0}main{min-width:0;display:flex;flex-direction:column;background:var(--bg)}.viewbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:12px;border-bottom:1px solid var(--border)}.segmented{display:flex;gap:3px;padding:3px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-segmented)}.segmented button{border:0;background:transparent;font-size:11px;padding:5px 9px}.segmented button[aria-pressed=true]{background:var(--rv-pressed-bg);color:var(--fg)}.zoom-controls{display:flex;gap:4px;align-items:center}.zoom-controls button{padding:4px 8px;font-size:12px}.zoom-controls output{font:11px var(--font-mono);min-width:39px;text-align:center;color:var(--fg-muted)}.stages{display:flex;gap:10px;padding:12px 12px 0;flex:1;min-height:460px}.stage-pane{position:relative;min-width:0;flex:1;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:radial-gradient(ellipse at 50% 40%,var(--rv-pane-hi),var(--rv-pane-lo) 74%)}.pane-label{position:absolute;left:10px;top:10px;z-index:2;background:var(--rv-pane-label-bg);border-radius:4px;padding:4px 7px;color:var(--rv-stage-fg);font-size:11px;pointer-events:none}.viewport{position:relative;overflow:hidden;width:100%;height:100%;min-height:450px;touch-action:none;background-image:radial-gradient(var(--rv-grid-dot) .7px,transparent .7px);background-size:22px 22px}.empty{position:absolute;inset:65px 22px 30px;display:grid;place-items:center;text-align:center;color:var(--rv-stage-fg);font-size:12px;line-height:1.8}.reference-content,#rig-root{position:absolute;transform-origin:0 0}.reference-content img,.reference-content video{display:block;max-width:none}.reference-content svg{display:block;max-width:none}.reference-content video{object-fit:contain}.stage-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:11px 16px;color:var(--fg-muted);font-size:11px}.transport{display:flex;align-items:center;gap:11px;padding:13px;border-top:1px solid var(--border);background:var(--rv-panel)}.transport select{width:auto;max-width:150px;font-size:11px}.transport input{flex:1}.transport output{font:11px var(--font-mono);white-space:nowrap;color:var(--fg-muted)}.sound-panel,.angle-panel{padding:15px;border-top:1px solid var(--border);background:var(--rv-panel)}.row{display:flex;align-items:center;gap:11px;flex-wrap:wrap}.row .eyebrow{margin:0}.sound-panel .row{font-size:11px;margin-bottom:9px}.sound-panel input[type=range]{flex:1;max-width:160px}.cue{display:flex;gap:9px;align-items:center;justify-content:space-between;border-top:1px solid var(--border);padding:9px 0;font-size:11px;flex-wrap:wrap}.cue button{padding:4px 7px;font-size:11px}.angle-panel{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:11px}.angle-panel select{width:auto;max-width:150px}.angle-panel .hint{width:100%;margin:0}.angle-panel input{flex:1}.divider{height:1px;background:var(--border-strong);margin:22px 0}h1{font-size:15px;font-weight:500;line-height:1.6;margin:0 0 7px}.outcome{border:0;padding:0;margin:18px 0 0;display:flex;gap:11px;flex-wrap:wrap}.outcome legend{font-size:11px;color:var(--fg-muted);margin-bottom:8px;padding:0}.outcome label{display:flex;align-items:center;gap:3px;font-size:11px}.proposal-item{border-top:1px solid var(--border);padding:11px 0;margin-top:12px;font-size:11px;overflow-wrap:anywhere}.proposal-item strong{display:block;font-weight:500;margin-bottom:5px}.proposal-item button{font-size:11px;padding:3px 6px;margin-top:7px}footer{display:flex;justify-content:space-between;align-items:center;gap:6px 15px;flex-wrap:wrap;padding:8px 18px;border-top:1px solid var(--border);background:var(--footer-bg);font-size:11px;color:var(--fg-soft)}.error{color:var(--fail)!important}.dirty{color:var(--warn)!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
2
5
  @media(min-width:1500px){.layout{grid-template-columns:218px minmax(250px,1fr) 310px}.stages{min-height:600px}}
3
6
  @media(max-width:1000px){.layout{grid-template-columns:158px minmax(250px,1fr) 250px}aside{padding:17px 12px}.topbar{gap:14px}.topbar #revision{display:block;margin-left:0}.stages{min-height:400px}.viewport{min-height:400px}.workspaces span{display:none}}
4
- @media(max-width:780px){.layout{grid-template-columns:150px minmax(0,1fr)}.right-panel{grid-column:1/-1;border-left:0;border-top:1px solid #30464f;display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}.right-panel>.eyebrow,.right-panel>.label,.right-panel>#review-status{grid-column:1/-1}.right-panel>.divider{display:none}#checkpoint-detail,#proposal-panel{min-width:0}.top-actions{margin-left:0}.stages{min-height:420px}.viewport{min-height:420px}.transport{flex-wrap:wrap}.transport input{min-width:130px}.topbar{padding:14px 16px}}
5
- @media(max-width:540px){.layout{grid-template-columns:1fr}.left-panel{border-right:0;border-bottom:1px solid #30464f;display:grid;grid-template-columns:1fr 1fr;gap:12px}.left-panel>.eyebrow,.left-panel>#checkpoints{grid-column:1/-1}.left-panel section{margin-top:8px}#checkpoints{display:flex;gap:5px;flex-wrap:wrap}.checkpoint{width:auto;flex:1;min-width:120px}.right-panel{display:block}#proposal-panel{margin-top:25px}.stages{gap:6px;padding:8px}.stage-pane{border-radius:5px}.viewport{min-height:365px}.stages{min-height:365px}.pane-label{left:5px;top:6px;font-size:11px;padding:3px}.viewbar{gap:8px}.workspaces{padding:0 8px}.workspaces button{padding:13px 9px}.stage-footer{padding:9px 12px}.source-link{font-size:11px}footer{padding:11px 12px}}
7
+ @media(max-width:780px){.layout{grid-template-columns:150px minmax(0,1fr)}.right-panel{grid-column:1/-1;border-left:0;border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}.right-panel>.eyebrow,.right-panel>.label,.right-panel>#review-status{grid-column:1/-1}.right-panel>.divider{display:none}#checkpoint-detail,#proposal-panel{min-width:0}.top-actions{margin-left:0}.stages{min-height:420px}.viewport{min-height:420px}.transport{flex-wrap:wrap}.transport input{min-width:130px}.topbar{padding:14px 16px}}
8
+ @media(max-width:540px){.layout{grid-template-columns:1fr}.left-panel{border-right:0;border-bottom:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px}.left-panel>.eyebrow,.left-panel>#checkpoints{grid-column:1/-1}.left-panel section{margin-top:8px}#checkpoints{display:flex;gap:5px;flex-wrap:wrap}.checkpoint{width:auto;flex:1;min-width:120px}.right-panel{display:block}#proposal-panel{margin-top:25px}.stages{gap:6px;padding:8px}.stage-pane{border-radius:5px}.viewport{min-height:365px}.stages{min-height:365px}.pane-label{left:5px;top:6px;font-size:11px;padding:3px}.viewbar{gap:8px}.workspaces{padding:0 8px}.workspaces button{padding:13px 9px}.stage-footer{padding:9px 12px}.source-link{font-size:11px}footer{padding:11px 12px}}
6
9
  @media(pointer:coarse){button{min-height:42px}input,select,textarea{font-size:16px}.outcome label{min-height:40px}.zoom-controls button{min-width:38px}}
7
10
 
8
- .frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.frame-range label{font-size:11px;color:#aec6cf}.frame-range input{width:100%;margin-top:6px}
11
+ .frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.frame-range label{font-size:11px;color:var(--fg-muted)}.frame-range input{width:100%;margin-top:6px}
9
12
 
10
- .provenance{margin-top:20px;padding:12px;background:#1d3540;border:1px solid #45616c;border-radius:7px}.provenance-row{display:flex;gap:9px;justify-content:space-between;font-size:11px;margin:6px 0}.provenance-row span{color:#a9c0ca}.provenance-row strong{font-weight:500;overflow-wrap:anywhere}.provenance code{font:11px ui-monospace,SFMono-Regular,monospace}.provenance details{margin-top:10px;font-size:11px;color:#b9d0d9}.provenance summary{cursor:pointer}.provenance pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 ui-monospace,SFMono-Regular,monospace}.right-panel>.provenance{grid-column:1/-1}
13
+ .provenance{margin-top:20px;padding:12px;background:var(--rv-raised);border:1px solid var(--border-strong);border-radius:7px}.provenance-row{display:flex;gap:9px;justify-content:space-between;font-size:11px;margin:6px 0}.provenance-row span{color:var(--fg-muted)}.provenance-row strong{font-weight:500;overflow-wrap:anywhere}.provenance code{font:11px var(--font-mono)}.provenance details{margin-top:10px;font-size:11px;color:var(--fg-muted)}.provenance summary{cursor:pointer}.provenance pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 var(--font-mono)}.right-panel>.provenance{grid-column:1/-1}
11
14
 
12
- .report-panel{padding:15px;border-top:1px solid #35515e;background:#142a35}.report-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.report-header h2{font-size:13px;font-weight:500;margin:0}.report-header a{font-size:11px;margin-left:auto}.report-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px}.report-controls select{width:auto;max-width:250px;flex:1;font-size:11px}.report-controls button{font-size:11px;padding:5px 9px}.report-controls output{font:11px ui-monospace,SFMono-Regular,monospace;color:#b3cbd5}.report-empty-categories{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px}.report-empty-categories span{font-size:11px;color:#a8c2ce;background:#213943;padding:2px 6px;border-radius:4px}.report-event-list{list-style:none;padding:0;margin:10px 0 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}.report-event-list li{min-width:0}.report-event-list button{display:flex;flex-direction:column;gap:2px;text-align:left;width:100%;height:100%;padding:8px 9px;border-color:#304b58;background:#192f3b;white-space:normal;overflow-wrap:anywhere;line-height:1.5}.report-event-list button[aria-current=true]{border-color:#88b39a;background:#294537}.report-event-list strong{font-size:11px;font-weight:500}.report-event-list small{font-size:11px;color:#a4bdc8}.report-event-list .no-time{color:#dfc49c}.report-selection{border-top:1px solid #3e5b68;margin-top:12px;padding-top:12px}.report-selection h3{font-size:12px;font-weight:500;margin:0}.report-selection details{font-size:11px;color:#aac3ce;margin-top:10px}.report-selection summary{cursor:pointer}.report-selection pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 ui-monospace,SFMono-Regular,monospace;background:#11232d;padding:10px;border-radius:5px}.report-selection #report-time{max-width:100%}@media(max-width:1000px){.report-event-list{grid-template-columns:1fr}}@media(max-width:540px){.report-controls select{min-width:140px}.report-header a{margin-left:0}}
15
+ .report-panel{padding:15px;border-top:1px solid var(--border);background:var(--rv-panel)}.report-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.report-header h2{font-size:13px;font-weight:500;margin:0}.report-header a{font-size:11px;margin-left:auto}.report-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px}.report-controls select{width:auto;max-width:250px;flex:1;font-size:11px}.report-controls button{font-size:11px;padding:5px 9px}.report-controls output{font:11px var(--font-mono);color:var(--fg-muted)}.report-empty-categories{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px}.report-empty-categories span{font-size:11px;color:var(--fg-muted);background:var(--rv-raised);padding:2px 6px;border-radius:4px}.report-event-list{list-style:none;padding:0;margin:10px 0 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}.report-event-list li{min-width:0}.report-event-list button{display:flex;flex-direction:column;gap:2px;text-align:left;width:100%;height:100%;padding:8px 9px;border-color:var(--border);background:var(--rv-raised);white-space:normal;overflow-wrap:anywhere;line-height:1.5}.report-event-list button[aria-current=true]{border-color:var(--rv-pressed-border);background:var(--rv-pressed-bg)}.report-event-list strong{font-size:11px;font-weight:500}.report-event-list small{font-size:11px;color:var(--fg-muted)}.report-event-list .no-time{color:var(--warn)}.report-selection{border-top:1px solid var(--border);margin-top:12px;padding-top:12px}.report-selection h3{font-size:12px;font-weight:500;margin:0}.report-selection details{font-size:11px;color:var(--fg-muted);margin-top:10px}.report-selection summary{cursor:pointer}.report-selection pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 var(--font-mono);background:var(--code-bg);padding:10px;border-radius:5px}.report-selection #report-time{max-width:100%}@media(max-width:1000px){.report-event-list{grid-template-columns:1fr}}@media(max-width:540px){.report-controls select{min-width:140px}.report-header a{margin-left:0}}
13
16
 
14
- .transport #frame-label{color:#c6e6d4;font-size:11px}.frame-controls{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:10px 14px;background:#17303b;border-top:1px solid #35515e;font-size:11px}.frame-controls label{color:#b1c9d2}.frame-controls input[type=number]{width:86px;font-variant-numeric:tabular-nums}.frame-controls input[type=range]{flex:1;min-width:100px}.frame-controls button{font-size:11px;padding:5px 9px}.frame-controls .hint{margin:0}.frame-controls #frame-outside{width:100%}
17
+ .transport #frame-label{color:var(--fg-strong);font-size:11px}.frame-controls{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:10px 14px;background:var(--rv-panel);border-top:1px solid var(--border);font-size:11px}.frame-controls label{color:var(--fg-muted)}.frame-controls input[type=number]{width:86px;font-variant-numeric:tabular-nums}.frame-controls input[type=range]{flex:1;min-width:100px}.frame-controls button{font-size:11px;padding:5px 9px}.frame-controls .hint{margin:0}.frame-controls #frame-outside{width:100%}
15
18
 
16
19
  /* Inspection layout is bounded independently of pane content and review forms. */
17
20
  #stage-layout{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:minmax(0,1fr);height:clamp(480px,calc(100dvh - 300px),900px);min-height:0;flex:none;align-items:stretch}
@@ -21,37 +24,19 @@
21
24
  #stage-layout [data-panning=true]{cursor:grabbing!important}
22
25
  #stage-layout img{-webkit-user-drag:none;user-select:none}
23
26
  .rig-overlay{position:absolute;inset:0;pointer-events:none;width:100%;height:100%;z-index:1}
24
- .inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid #35515e;font-size:11px;background:#172f3b}
27
+ .inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--border);font-size:11px;background:var(--rv-panel)}
25
28
  .inspection-controls label{display:inline-flex;align-items:center;gap:5px;margin:0;white-space:nowrap}
26
29
  .inspection-controls select{width:auto;padding:4px;font-size:11px}
27
30
  .inspection-controls input[type=checkbox]{width:auto;margin:0}
28
31
  .inspection-controls fieldset{display:flex;gap:10px;align-items:center;border:0;padding:0;margin:0;min-width:0;flex-wrap:wrap}
29
32
  .inspection-controls legend{float:left;margin-right:9px;font-size:11px}
30
33
  #control-state:empty{display:none}
31
- :root[data-theme=light]{color-scheme:light;color:#233c47;background:#f4f7f9}
34
+ :root[data-theme=light]{color-scheme:light}
32
35
  :root[data-theme=dark]{color-scheme:dark}
33
- [data-theme=light] body,[data-theme=light] main{background:#f4f7f9;color:#233c47}
34
- [data-theme=light] header,[data-theme=light] .topbar,[data-theme=light] .workspaces,[data-theme=light] footer{background:#edf3f5;border-color:#bacbd3;color:#294552}
35
- [data-theme=light] aside,[data-theme=light] .transport,[data-theme=light] .frame-controls,[data-theme=light] .inspection-controls,[data-theme=light] .sound-panel,[data-theme=light] .angle-panel,[data-theme=light] .report-panel{background:#e9f0f3;border-color:#b7c9d1;color:#294552}
36
- [data-theme=light] button{background:#e2ebef;border-color:#a6bdc8;color:#264652}
37
- [data-theme=light] button:hover{background:#d0e2e8}
38
- [data-theme=light] button.primary,[data-theme=light] .primary{background:#24694d;border-color:#24694d;color:#fff}
39
- [data-theme=light] input,[data-theme=light] select,[data-theme=light] textarea{background:#fff;border-color:#8faab7;color:#243e4a;accent-color:#276c50}
40
- [data-theme=light] input::placeholder,[data-theme=light] textarea::placeholder{color:#607986}
41
- [data-theme=light] .hint,[data-theme=light] .subtle,[data-theme=light] .label,[data-theme=light] .eyebrow,[data-theme=light] aside label,[data-theme=light] .tool small,[data-theme=light] .stage-footer,[data-theme=light] .compare-info,[data-theme=light] .boundary,[data-theme=light] .trace,[data-theme=light] .transport output,[data-theme=light] .zoom-controls output,[data-theme=light] .frame-range label,[data-theme=light] .frame-controls label{color:#496774}
42
- [data-theme=light] .workspaces button,[data-theme=light] .segmented button{color:#3e5c69}
43
- [data-theme=light] .workspaces button[aria-pressed=true]{color:#185738;border-bottom-color:#276c50}
44
- [data-theme=light] .workspaces button span{color:#58727e}
45
- [data-theme=light] .segmented,[data-theme=light] .provenance,[data-theme=light] .report-selection pre{background:#e1ebef;border-color:#9cb5c0;color:#294552}
46
- [data-theme=light] .segmented button[aria-pressed=true],[data-theme=light] .checkpoint[aria-current=true],[data-theme=light] .report-event-list button[aria-current=true]{background:#cde4d8;border-color:#56866e;color:#1f4937}
47
- [data-theme=light] .checkpoint small{color:#4b6c59}
48
- [data-theme=light] .provenance p,[data-theme=light] .provenance-row span,[data-theme=light] .provenance details,[data-theme=light] .report-selection details,[data-theme=light] .report-event-list small{color:#486470}
49
- [data-theme=light] .report-event-list button,[data-theme=light] .report-empty-categories span{background:#e5eef2;border-color:#acbfca;color:#294552}
50
- [data-theme=light] .feedback-item strong{color:#1e4e3b}
51
- [data-theme=light] a{color:#175f48}
52
- [data-theme=light] .error{color:#a03225!important}
53
- [data-theme=light] .dirty,[data-theme=light] .report-event-list .no-time{color:#875800!important}
54
- [data-theme=light] :focus-visible{outline-color:#24694d}
36
+
37
+ input::placeholder,textarea::placeholder{color:var(--fg-muted)}
38
+
39
+ :focus-visible{outline-color:var(--rv-focus)}
55
40
  /* Source pixels retain their original color; the neutral stage stays dark in both themes. */
56
41
 
57
42
  /* The application shell never scrolls; each Inspector tab and the source list owns overflow. */
@@ -60,12 +45,12 @@ html,body{height:100%;overflow:hidden}body{height:100dvh}#app{height:100dvh;disp
60
45
  main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
61
46
  .left-panel{min-height:0;overflow:auto;padding:13px 10px}
62
47
  #stage-layout{height:auto;min-height:0;flex:1;overflow:hidden;padding:8px;gap:7px}
63
- .inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid #49616d;background:#182d38;overflow:hidden}
64
- .inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid #3e5c69;flex:none;font-size:11px}
48
+ .inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid var(--border);background:var(--rv-panel);overflow:hidden}
49
+ .inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid var(--border);flex:none;font-size:11px}
65
50
  .inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
66
51
  .inspector-heading [role=tablist]{display:flex;gap:4px}
67
52
  .inspector-heading button{padding:4px 9px;font-size:11px}
68
- .inspector-heading button[aria-selected=true]{background:#3d5d4d;color:#e0f3e7;border-color:#7fa98e}
53
+ .inspector-heading button[aria-selected=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg);border-color:var(--rv-pressed-border)}
69
54
  .inspector-body{flex:1;min-height:0;overflow:hidden}
70
55
  .inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
71
56
  .inspector .inspection-controls{padding:0;border:0;background:transparent}
@@ -82,35 +67,31 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
82
67
  #inspector-rig #part-set{max-width:190px}
83
68
  #inspector-playback #motion-tools{display:flex;gap:8px;flex-wrap:wrap}
84
69
  #inspector-playback .tool{width:auto}
85
- [data-theme=light] .inspector{background:#edf3f5;border-color:#a6bec8;color:#294552}
86
- [data-theme=light] .inspector-heading{border-color:#b3c6d0}
87
- [data-theme=light] .inspector-heading button[aria-selected=true]{background:#cde4d8;color:#1e4d36;border-color:#59836b}
88
- @media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-meta{padding:6px 10px 0}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
70
+
71
+ @media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
89
72
  @media(max-width:540px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:block;padding:8px;font-size:11px}.layout{grid-template-columns:105px minmax(0,1fr)!important}.topbar,header{padding:7px 10px;gap:8px}.topbar .brand,header .brand{font-size:14px}.top-actions,.actions{gap:4px}.top-actions button,.actions button{padding:4px 6px;font-size:10px}.workspaces button{padding:6px}.inspector{height:40dvh;min-height:150px}.inspector-heading{gap:8px;padding:5px 8px}.inspector-heading button{padding:4px 7px}.stage-footer{display:none}}
90
73
 
91
74
  .inspector .feedback-form>label,.inspector .review-summary>label{display:block;font-size:11px;margin:10px 0 5px}.inspector .frame-range input{width:100%}.inspector [role=tabpanel]{overscroll-behavior:contain}.left-panel{overscroll-behavior:contain}#stage-layout .pane-label{max-width:calc(100% - 15px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
92
75
 
93
- .rig-layer-controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:11px}.rig-layer-controls fieldset{display:flex;gap:12px;align-items:center;flex-wrap:wrap;border:0;margin:0;padding:0}.rig-layer-controls legend{float:left;margin-right:10px}.rig-layer-controls label{display:inline-flex;gap:5px;align-items:center}.rig-layer-controls input[type=checkbox]{width:auto}
94
-
95
- .numeric-view{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:#14232c;color:#c9dfe7;border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:#e0c58e}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}[data-theme=light] .numeric-view{background:#e1eaee;color:#264653}[data-theme=light] .draft-notice{color:#80521a}[data-theme=light] .outcome legend,[data-theme=light] .brand span,[data-theme=light] .toolbar{color:#496774}
76
+ .numeric-view{font:11px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:var(--code-bg);color:var(--code-fg);border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:var(--warn)}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}
96
77
 
97
78
  .camera-sync{display:inline-flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}.camera-sync input[type=checkbox]{width:auto;flex:none;margin:0}
98
79
  #stage-layout:has(>.stage-pane[hidden]){grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
99
80
 
100
81
  /* Slot explorer shares the stage row; bottom Inspector remains in the main column. */
101
- .layout{grid-template-columns:150px minmax(0,1fr) 235px!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid #49616d;background:#182d38;padding:12px;overscroll-behavior:contain}.parts-explorer h2{font-size:12px;margin:0 0 10px}.parts-explorer>label{display:block;font-size:11px;margin-top:12px}.parts-explorer input[type=search]{width:100%;min-width:0}.parts-explorer .part-solo{display:flex;gap:6px;align-items:center}.parts-explorer ul{list-style:none;margin:8px 0;padding:0;display:grid;gap:4px}.parts-explorer li{min-width:0}.parts-explorer li button{display:block;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}.parts-explorer button[aria-pressed=true]{background:#355445;border-color:#9cd3b4}.parts-explorer h2,#selected-part h3{font-weight:600}#selected-part{margin-bottom:12px}#selected-part h3{margin:0 0 8px;overflow-wrap:anywhere}#selected-part p{overflow-wrap:anywhere}[data-theme=light] .parts-explorer{background:#edf3f5;border-color:#a6bec8;color:#294552}[data-theme=light] .parts-explorer button[aria-pressed=true]{background:#c5ddcd;color:#183c2c}
102
- @media(max-width:1050px){.layout{grid-template-columns:110px minmax(0,1fr) 185px!important}.parts-explorer{padding:8px}}
103
- @media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr) 165px!important}.left-panel{display:none!important}}
104
- @media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr) 132px!important}.parts-explorer{padding:6px}.parts-explorer li button{padding:6px}.parts-explorer>.hint{font-size:10px}}
82
+ .layout{grid-template-columns:260px minmax(0,1fr)!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid var(--border);background:var(--rv-panel);padding:12px;overscroll-behavior:contain}.parts-explorer h2{font-size:12px;margin:0 0 10px}.parts-explorer>label{display:block;font-size:11px;margin-top:12px}.parts-explorer input[type=search]{width:100%;min-width:0}.parts-explorer .part-solo{display:flex;gap:6px;align-items:center}.parts-explorer ul{list-style:none;margin:8px 0;padding:0;display:grid;gap:4px}.parts-explorer li{min-width:0}.parts-explorer li button{display:block;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}.parts-explorer button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border)}.parts-explorer h2,#selected-part h3{font-weight:600}#selected-part{margin-bottom:12px}#selected-part h3{margin:0 0 8px;overflow-wrap:anywhere}#selected-part p{overflow-wrap:anywhere}
83
+ @media(max-width:1050px){.layout{grid-template-columns:200px minmax(0,1fr)!important}.parts-explorer{padding:8px}}
84
+ @media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:none!important}}
85
+ @media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr)!important}.parts-explorer{padding:6px}.parts-explorer li button{padding:6px}.parts-explorer>.hint{font-size:10px}}
105
86
 
106
87
  /* Resizable workspace: one measured shell; workspace-ui clamps the desired browser sizes. */
107
88
  .layout.workspace-layout{display:grid;grid-template-columns:var(--workspace-columns)!important;min-height:0!important;overflow:hidden}
108
89
  .workspace-layout>main{display:grid;min-width:0;min-height:0;overflow:hidden;grid-column:3;grid-row:1}
109
90
  .workspace-stage-stack{grid-row:1;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
110
91
  .workspace-stage-stack>#stage-layout{flex:1;min-height:0;height:auto;overflow:hidden}
111
- .workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid #49616d}
92
+ .workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid var(--border)}
112
93
  .workspace-sources{flex:1;min-height:0;overflow:auto;padding:10px;overscroll-behavior:contain}
113
- .workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid #49616d}
94
+ .workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid var(--border)}
114
95
  .workspace-settings h2{font-size:12px;margin:0 0 8px}
115
96
  .workspace-settings label{display:flex;align-items:center;gap:6px;font-size:11px;margin:7px 0}
116
97
  .workspace-settings select{width:auto;min-width:0;flex:1}
@@ -118,22 +99,60 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
118
99
  .workspace-settings input[type=checkbox]{width:auto;flex:none}
119
100
  .workspace-layout>.parts-explorer{display:block;grid-template-columns:none;grid-column:5;grid-row:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
120
101
  .workspace-layout .inspector{grid-row:3;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
121
- .workspace-separator{position:relative;z-index:3;background:#34515e;touch-action:none;cursor:col-resize;min-width:0;min-height:0}
102
+ .workspace-separator{position:relative;z-index:3;background:var(--border-strong);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
122
103
  .workspace-separator::after{content:'';position:absolute;inset:0 -4px}
123
- .workspace-separator:focus-visible{outline:2px solid #c8e8d6;outline-offset:-2px;background:#719581}
124
- .workspace-separator[data-resizing=true]{background:#8ac9a3}
125
- .workspace-leftWidth{grid-column:2;grid-row:1}.workspace-rightWidth{grid-column:4;grid-row:1}
104
+ .workspace-separator:focus-visible{outline:2px solid var(--rv-focus);outline-offset:-2px;background:var(--link)}
105
+ .workspace-separator[data-resizing=true]{background:var(--link-hover)}
106
+ .workspace-side-left{grid-column:2;grid-row:1}.workspace-side-right{grid-column:4;grid-row:1}
126
107
  .workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-4px 0}
127
108
  .workspace-drawer-triggers{display:flex;gap:6px;flex-wrap:wrap;flex:none;padding:4px 8px;min-height:0}
128
109
  .workspace-drawer-triggers:not(:has(button:not([hidden]))){display:none}
129
110
  .workspace-drawer-triggers button{padding:4px 7px;font-size:11px;min-height:28px}
130
- .workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid #739383;border-radius:8px;background:#182d38;color:inherit;overflow:hidden;z-index:20}
111
+ .workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-panel);color:inherit;overflow:hidden;z-index:20}
131
112
  .workspace-drawer:not([open]){display:none}.workspace-drawer[open]{display:flex;flex-direction:column;gap:8px}
132
- .workspace-drawer::backdrop{background:#07141ed0}
113
+ .workspace-drawer::backdrop{background:var(--rv-backdrop)}
133
114
  .workspace-drawer-close{align-self:flex-end;flex:none}
134
115
  .workspace-drawer>.left-panel{display:flex!important;flex-direction:column;flex:1;overflow:hidden;min-height:0;padding:0}
135
116
  .workspace-drawer>.parts-explorer{display:block;grid-template-columns:none;flex:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
136
117
  .workspace-drawer>.inspector{flex:1;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
137
- [data-theme=light] .workspace-settings,[data-theme=light] .workspace-drawer{background:#edf3f5;color:#294552;border-color:#a6bec8}
138
- [data-theme=light] .workspace-separator{background:#b2c7cf}[data-theme=light] .workspace-separator:focus-visible{outline-color:#24694d;background:#70a58c}
118
+
139
119
  @media(pointer:coarse){.workspace-separator::after{inset:0 -7px}.workspace-inspectorHeight::after{inset:-7px 0}.workspace-drawer-triggers button{min-height:38px}}
120
+
121
+ /* Workspace rework: parts explorer is the left column; checkpoints and reference fold above it. */
122
+ .package-context{border-bottom:1px solid var(--border);padding-bottom:8px;margin-bottom:10px}
123
+ .package-context>summary{cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;font-size:11px}
124
+ .package-context>summary .eyebrow{margin:0}
125
+ .package-context #checkpoint-title{color:var(--fg-soft);overflow-wrap:anywhere}
126
+ .package-context #checkpoints{max-height:30dvh;overflow:auto;margin-top:6px}
127
+ .package-context .checkpoint{padding:6px 8px;margin:2px 0}
128
+ .package-context section{margin-top:10px}
129
+ .left-panel .parts-explorer{border:0;padding:0;background:transparent;overflow:visible}
130
+ .source-files{margin-top:12px}
131
+ .source-files .eyebrow{margin-bottom:4px}
132
+
133
+ /* View bar: layer toggles keep native checkboxes; the label carries the pressed look. */
134
+ .layer-toggles{display:flex;gap:3px;padding:3px;margin:0;min-width:0;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-segmented)}
135
+ .layer-toggle{position:relative;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border:1px solid transparent;border-radius:6px;font-size:11px;color:var(--fg-soft);cursor:pointer;white-space:nowrap}
136
+ .layer-toggle input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
137
+ .layer-toggle:has(input:checked){background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border);color:var(--rv-pressed-fg)}
138
+ .layer-toggle:has(input:disabled){opacity:.45;cursor:default}
139
+ .layer-toggle:has(input:focus-visible){outline:2px solid var(--rv-focus);outline-offset:2px}
140
+ .layer-toggle kbd{font:10px var(--font-mono);padding:0 4px;border:1px solid var(--border-strong);border-radius:3px;color:var(--fg-muted)}
141
+ .view-options{display:flex;align-items:center;gap:6px 10px;flex-wrap:wrap;font-size:11px;margin-left:auto}
142
+ .view-options label{display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
143
+ .view-options select{width:auto;padding:4px;font-size:11px}
144
+ .view-options input[type=checkbox]{width:auto;margin:0}
145
+ .view-options button{padding:4px 8px;font-size:11px}
146
+
147
+ /* Footer is the one status area: message | readouts | view region and source hash. */
148
+ footer.status-bar{flex-wrap:nowrap}
149
+ .status-message{flex:1 1 180px;min-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
150
+ .status-readouts{flex:0 1 auto;max-width:60%;display:flex;align-items:center;justify-content:flex-end;gap:2px 12px;flex-wrap:wrap;min-width:0}
151
+ .status-readouts .draft-notice{padding:0}
152
+ .status-readouts .draft-notice #draft-recovery{margin-left:6px}
153
+ .status-source{display:flex;align-items:center;gap:12px;margin-left:auto;flex:none;white-space:nowrap}
154
+ .view-region{position:relative}
155
+ .view-region>summary{cursor:pointer}
156
+ .view-region-body{position:absolute;right:0;bottom:calc(100% + 10px);width:min(520px,90vw);max-height:60dvh;overflow:auto;padding:10px;z-index:10;background:var(--rv-panel);border:1px solid var(--border);border-radius:7px}
157
+ .view-region-body .numeric-view{margin:8px 0}
158
+
@@ -28,6 +28,38 @@ export function bindStageLayout(area:HTMLElement,changed:()=>void):()=>void {
28
28
  const render=()=>{if(syncStageLayout(area))changed();};
29
29
  const observer=new ResizeObserver(render);observer.observe(area);render();return ()=>observer.disconnect();
30
30
  }
31
+ /** Single-key layer toggles. Free of Space (pan), Enter (frame entry), arrows/Home/End (tabs, separators, part list). */
32
+ export const layerShortcuts=Object.freeze([
33
+ Object.freeze({key:'i',id:'layer-images',label:'Images'}),
34
+ Object.freeze({key:'m',id:'layer-mesh',label:'Mesh edges'}),
35
+ Object.freeze({key:'b',id:'layer-bones',label:'Bone points'}),
36
+ ] as const);
37
+ const nonTextInputs=new Set(['checkbox','radio','button','submit','reset','range','color','file','image']);
38
+ /** A focused text field, textarea, select or contenteditable keeps its keys. */
39
+ export function textEntryFocused(target:unknown):boolean {
40
+ const node=target as {closest?:(selector:string)=>unknown;isContentEditable?:boolean}|null;if(!node||typeof node.closest!=='function')return false;
41
+ if(node.isContentEditable||node.closest('textarea,select,[contenteditable]:not([contenteditable="false"])'))return true;
42
+ const input=node.closest('input') as {type?:string}|null;return !!input&&!nonTextInputs.has(String(input.type??'text').toLowerCase());
43
+ }
44
+ interface ShortcutInput {disabled:boolean;click():void}
45
+ /** `I`/`M`/`B` click the actual checkbox, so its change handler, checked state and draft input stay the only state. */
46
+ export function bindLayerShortcuts(root:Pick<Document,'addEventListener'|'removeEventListener'>&{getElementById(id:string):unknown}):()=>void {
47
+ const keyDown=(event:Event)=>{
48
+ const e=event as KeyboardEvent;if(e.defaultPrevented||e.repeat||e.ctrlKey||e.metaKey||e.altKey||typeof e.key!=='string'||textEntryFocused(e.target))return;
49
+ const shortcut=layerShortcuts.find(item=>item.key===e.key.toLowerCase());if(!shortcut)return;
50
+ const input=root.getElementById(shortcut.id) as ShortcutInput|null;if(!input||input.disabled)return;
51
+ e.preventDefault();input.click();
52
+ };
53
+ root.addEventListener('keydown',keyDown);return ()=>root.removeEventListener('keydown',keyDown);
54
+ }
55
+ /** Disabled toggles name their reason as a tooltip; enabled ones name their key. */
56
+ export function describeLayerToggles(root:{getElementById(id:string):unknown},reason:string):void {
57
+ for(const item of layerShortcuts){
58
+ const input=root.getElementById(item.id) as (ShortcutInput&{closest(selector:string):{title:string}|null})|null;const label=input?.closest('label');
59
+ if(input&&label)label.title=input.disabled?reason:`${item.label} · key ${item.key.toUpperCase()}`;
60
+ }
61
+ }
62
+
31
63
  // Nested or separately disposed pan binders must not restore another binder's override.
32
64
  const panVideoOverrides=new WeakMap<HTMLVideoElement,{owners:number;value:string;priority:string}>();
33
65
  export function bindPan(viewport:HTMLElement,move:(dx:number,dy:number)=>void,enabled:()=>boolean=()=>true):()=>void {
@@ -67,3 +99,4 @@ export function bindInspectorTabs(changed:()=>void,beforeChange:()=>void=()=>{})
67
99
  const activate=(tab:HTMLButtonElement)=>{beforeChange();for(const item of tabs){const active=item===tab;item.setAttribute('aria-selected',String(active));item.tabIndex=active?0:-1;}for(const panel of document.querySelectorAll<HTMLElement>('[data-inspector-panel]'))panel.hidden=panel.dataset.inspectorPanel!==tab.dataset.inspector;changed();};
68
100
  for(const [index,tab] of tabs.entries()){tab.onclick=()=>activate(tab);tab.onkeydown=event=>{const next=event.key==='ArrowRight'?(index+1)%tabs.length:event.key==='ArrowLeft'?(index+tabs.length-1)%tabs.length:event.key==='Home'?0:event.key==='End'?tabs.length-1:null;if(next===null)return;event.preventDefault();const target=tabs[next];if(target){activate(target);target.focus();}};}
69
101
  }
102
+
@@ -3,29 +3,49 @@ import {parseWorkspacePanels,panelBounds,type WorkspacePanels} from './workspace
3
3
  export type WorkspacePanel=keyof WorkspacePanels;
4
4
  export const workspaceMinimums=Object.freeze({stageWidth:240,stageHeight:180,separator:8});
5
5
  export interface SeparatorRange {min:number;max:number;value:number;enabled:boolean}
6
- export interface EffectiveWorkspaceLayout {leftWidth:number;rightWidth:number;inspectorHeight:number;stageWidth:number;stageHeight:number;leftMode:'panel'|'drawer';rightMode:'panel'|'drawer';inspectorMode:'panel'|'drawer';stageMinimumSatisfied:boolean;separators:Record<WorkspacePanel,SeparatorRange>}
6
+ export type WorkspaceColumnKey=Exclude<WorkspacePanel,'inspectorHeight'>;
7
+ /** Which saved width each side column uses; null means the page has no column on that side.
8
+ * The compare page keeps sources left and parts right; the single review page shows the parts
9
+ * explorer (its saved `rightWidth`) on the left and has no right column. */
10
+ export interface WorkspaceColumns {left:WorkspaceColumnKey|null;right:WorkspaceColumnKey|null}
11
+ export const defaultWorkspaceColumns:Readonly<WorkspaceColumns>=Object.freeze({left:'leftWidth',right:'rightWidth'});
12
+ export type WorkspacePanelMode='panel'|'drawer'|'absent';
13
+ /** Sizes and modes are keyed by saved panel, not by side; `absent` panels have width 0 and no separator. */
14
+ export interface EffectiveWorkspaceLayout {leftWidth:number;rightWidth:number;inspectorHeight:number;stageWidth:number;stageHeight:number;leftMode:WorkspacePanelMode;rightMode:WorkspacePanelMode;inspectorMode:'panel'|'drawer';stageMinimumSatisfied:boolean;separators:Record<WorkspacePanel,SeparatorRange>}
7
15
  const clamp=(value:number,min:number,max:number)=>Math.max(min,Math.min(max,value));
16
+ export function parseWorkspaceColumns(value:WorkspaceColumns):WorkspaceColumns {
17
+ const ok=(v:unknown)=>v===null||v==='leftWidth'||v==='rightWidth';
18
+ if(!value||!ok(value.left)||!ok(value.right)||value.left!==null&&value.left===value.right)throw new Error(`WORKSPACE_COLUMNS: left ${String(value?.left)} / right ${String(value?.right)}; required distinct leftWidth, rightWidth or null`);
19
+ return {left:value.left,right:value.right};
20
+ }
8
21
  /** Input rectangle excludes app chrome. Drawer triggers must remain outside collapsed panel content.
9
- * Collapse left, then right when minima cannot coexist; Inspector becomes a drawer below its minimum.
22
+ * Collapse the left column, then the right one when minima cannot coexist; Inspector becomes a drawer below its minimum.
10
23
  * Returned clamps never replace the user's desired desktop sizes. */
11
- export function allocateWorkspaceLayout(available:{width:number;height:number},desired:WorkspacePanels):EffectiveWorkspaceLayout {
12
- const panels=parseWorkspacePanels(desired),{width,height}=available,{stageWidth:minWidth,stageHeight:minHeight,separator}=workspaceMinimums;
24
+ export function allocateWorkspaceLayout(available:{width:number;height:number},desired:WorkspacePanels,columns:WorkspaceColumns=defaultWorkspaceColumns):EffectiveWorkspaceLayout {
25
+ const panels=parseWorkspacePanels(desired),{left:leftKey,right:rightKey}=parseWorkspaceColumns(columns),{width,height}=available,{stageWidth:minWidth,stageHeight:minHeight,separator}=workspaceMinimums;
13
26
  if(![width,height].every(n=>Number.isFinite(n)&&n>=0&&n<=65536))throw new Error('WORKSPACE_RECTANGLE: finite 0..65536 CSS pixels required');
14
- let left=true,right=true;if(width<panelBounds.leftWidth.min+panelBounds.rightWidth.min+minWidth+2*separator)left=false;if(width<(left?panelBounds.leftWidth.min+separator:0)+panelBounds.rightWidth.min+minWidth+separator)right=false;
15
- const count=Number(left)+Number(right),budget=Math.max(0,width-minWidth-count*separator),leftMin=left?panelBounds.leftWidth.min:0,rightMin=right?panelBounds.rightWidth.min:0,wantLeft=left?panels.leftWidth:0,wantRight=right?panels.rightWidth:0;
16
- const slack=wantLeft+wantRight-leftMin-rightMin,ratio=slack>0?Math.min(1,Math.max(0,(budget-leftMin-rightMin)/slack)):1,leftWidth=leftMin+(wantLeft-leftMin)*ratio,rightWidth=rightMin+(wantRight-rightMin)*ratio;
27
+ const boundLeft=leftKey?panelBounds[leftKey].min:0,boundRight=rightKey?panelBounds[rightKey].min:0;
28
+ let left=leftKey!==null,right=rightKey!==null;
29
+ if(left&&right&&width<boundLeft+boundRight+minWidth+2*separator)left=false;
30
+ if(right&&width<(left?boundLeft+separator:0)+boundRight+minWidth+separator)right=false;
31
+ if(left&&!right&&width<boundLeft+minWidth+separator)left=false;
32
+ const count=Number(left)+Number(right),budget=Math.max(0,width-minWidth-count*separator),leftMin=left?boundLeft:0,rightMin=right?boundRight:0,wantLeft=left?panels[leftKey!]:0,wantRight=right?panels[rightKey!]:0;
33
+ const slack=wantLeft+wantRight-leftMin-rightMin,ratio=slack>0?Math.min(1,Math.max(0,(budget-leftMin-rightMin)/slack)):1,leftSize=leftMin+(wantLeft-leftMin)*ratio,rightSize=rightMin+(wantRight-rightMin)*ratio;
17
34
  const inspector=height>=minHeight+panelBounds.inspectorHeight.min+separator,inspectorHeight=inspector?Math.min(panels.inspectorHeight,height-minHeight-separator):0;
18
- const stageWidth=Math.max(0,width-leftWidth-rightWidth-count*separator),stageHeight=Math.max(0,height-inspectorHeight-(inspector?separator:0));
35
+ const stageWidth=Math.max(0,width-leftSize-rightSize-count*separator),stageHeight=Math.max(0,height-inspectorHeight-(inspector?separator:0));
19
36
  const range=(visible:boolean,key:WorkspacePanel,value:number,max:number):SeparatorRange=>visible?{min:panelBounds[key].min,max:Math.max(panelBounds[key].min,Math.min(panelBounds[key].max,max)),value,enabled:true}:{min:0,max:0,value:0,enabled:false};
20
- return {leftWidth,rightWidth,inspectorHeight,stageWidth,stageHeight,leftMode:left?'panel':'drawer',rightMode:right?'panel':'drawer',inspectorMode:inspector?'panel':'drawer',stageMinimumSatisfied:width>=minWidth&&height>=minHeight,separators:{leftWidth:range(left,'leftWidth',leftWidth,width-rightWidth-count*separator-minWidth),rightWidth:range(right,'rightWidth',rightWidth,width-leftWidth-count*separator-minWidth),inspectorHeight:range(inspector,'inspectorHeight',inspectorHeight,height-minHeight-separator)}};
37
+ const size:Record<WorkspaceColumnKey,number>={leftWidth:0,rightWidth:0},mode:Record<WorkspaceColumnKey,WorkspacePanelMode>={leftWidth:'absent',rightWidth:'absent'},separators:Record<WorkspacePanel,SeparatorRange>={leftWidth:range(false,'leftWidth',0,0),rightWidth:range(false,'rightWidth',0,0),inspectorHeight:range(inspector,'inspectorHeight',inspectorHeight,height-minHeight-separator)};
38
+ if(leftKey){size[leftKey]=leftSize;mode[leftKey]=left?'panel':'drawer';separators[leftKey]=range(left,leftKey,leftSize,width-rightSize-count*separator-minWidth);}
39
+ if(rightKey){size[rightKey]=rightSize;mode[rightKey]=right?'panel':'drawer';separators[rightKey]=range(right,rightKey,rightSize,width-leftSize-count*separator-minWidth);}
40
+ return {leftWidth:size.leftWidth,rightWidth:size.rightWidth,inspectorHeight,stageWidth,stageHeight,leftMode:mode.leftWidth,rightMode:mode.rightWidth,inspectorMode:inspector?'panel':'drawer',stageMinimumSatisfied:width>=minWidth&&height>=minHeight,separators};
21
41
  }
22
- export interface WorkspaceSeparatorOptions {element:HTMLElement;panel:WorkspacePanel;controls:string;label:string;range:()=>SeparatorRange;resize:(desired:number)=>void;desired?:()=>number;resolveDesired?:(effective:number,context:{range:SeparatorRange;desired:number})=>number;commit?:(desired:number)=>void;events?:Pick<EventTarget,'addEventListener'|'removeEventListener'>|null;step?:number;coarseStep?:number}
42
+ export interface WorkspaceSeparatorOptions {element:HTMLElement;panel:WorkspacePanel;controls:string;label:string;range:()=>SeparatorRange;resize:(desired:number)=>void;desired?:()=>number;resolveDesired?:(effective:number,context:{range:SeparatorRange;desired:number})=>number;commit?:(desired:number)=>void;side?:'left'|'right';events?:Pick<EventTarget,'addEventListener'|'removeEventListener'>|null;step?:number;coarseStep?:number}
23
43
  /** Arrow keys act only on the separator itself. Right/bottom boundaries grow inward.
24
44
  * Pointer cancellation, capture loss, Escape and blur restore the prior desired size.
25
45
  * Coupled allocators supply their reachable effective range and resolveDesired inverse;
26
46
  * resize/commit receive desired sizes, including when effective geometry is clamped. */
27
47
  export function bindWorkspaceSeparator(options:WorkspaceSeparatorOptions):{refresh():void;dispose():void} {
28
- const {element,panel}=options,vertical=panel!=='inspectorHeight',direction=panel==='leftWidth'?1:-1,events=options.events===undefined?(typeof window==='undefined'?null:window):options.events;
48
+ const {element,panel}=options,vertical=panel!=='inspectorHeight',direction=(options.side??(panel==='leftWidth'?'left':'right'))==='left'?1:-1,events=options.events===undefined?(typeof window==='undefined'?null:window):options.events;
29
49
  if(!options.controls||!options.label||/[\x00-\x1f]/.test(options.controls+options.label))throw new Error('WORKSPACE_SEPARATOR: controls and accessible label required');
30
50
  const step=options.step??8,coarse=options.coarseStep??40;if(![step,coarse].every(n=>Number.isFinite(n)&&n>0))throw new Error('WORKSPACE_SEPARATOR: positive finite keyboard steps required');
31
51
  const attrs=['role','aria-orientation','aria-controls','aria-label','aria-valuemin','aria-valuemax','aria-valuenow','aria-valuetext','aria-disabled'],saved=new Map(attrs.map(key=>[key,element.getAttribute(key)])),tabIndex=element.tabIndex,touchAction=element.style.touchAction;
@@ -2,6 +2,9 @@
2
2
  export const workspacePreferenceKey='rig-review-preferences/1';
3
3
  export const legacyAppearanceKey='rig-review-appearance/1';
4
4
  export type WorkspaceTheme='system'|'dark'|'light';
5
+ /** Saved field names are part of the stored record and do not move with the panels.
6
+ * `rightWidth` is the parts explorer's width wherever the page shows it (the single review page shows
7
+ * it on the left); `leftWidth` is the compare page's sources column. */
5
8
  export interface WorkspacePanels {leftWidth:number;rightWidth:number;inspectorHeight:number}
6
9
  export const panelBounds=Object.freeze({leftWidth:Object.freeze({min:120,max:720}),rightWidth:Object.freeze({min:180,max:900}),inspectorHeight:Object.freeze({min:160,max:900})});
7
10
  export const defaultWorkspacePanels:Readonly<WorkspacePanels>=Object.freeze({leftWidth:170,rightWidth:260,inspectorHeight:280});
@@ -1,19 +1,19 @@
1
1
  /** Browser workspace presentation. Source selection, player clocks and review decisions stay with the page. */
2
2
  import {WorkspacePreferenceStore,panelBounds,type WorkspacePanels,type WorkspacePreferences,type WorkspaceTheme} from './workspace-preferences.ts';
3
- import {allocateWorkspaceLayout,bindWorkspaceSeparator,bindWorkspaceDrawer,InspectorScrollMemory,inspectorTabs,type InspectorTab,type WorkspacePanel,type SeparatorRange} from './workspace-layout.ts';
3
+ import {allocateWorkspaceLayout,bindWorkspaceSeparator,bindWorkspaceDrawer,defaultWorkspaceColumns,InspectorScrollMemory,inspectorTabs,type InspectorTab,type WorkspaceColumns,type WorkspacePanel,type SeparatorRange} from './workspace-layout.ts';
4
4
 
5
5
  /** Reachable effective bounds, accounting for the other panel's saved desired size. */
6
- export function workspaceResizeRange(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel):SeparatorRange {
7
- const base=allocateWorkspaceLayout(available,desired),range=base.separators[panel];if(!range.enabled)return range;
8
- const low=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].min})[panel],high=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].max})[panel];
6
+ export function workspaceResizeRange(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,columns:WorkspaceColumns=defaultWorkspaceColumns):SeparatorRange {
7
+ const base=allocateWorkspaceLayout(available,desired,columns),range=base.separators[panel];if(!range.enabled)return range;
8
+ const low=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].min},columns)[panel],high=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].max},columns)[panel];
9
9
  return {min:low,max:high,value:base[panel],enabled:high>low};
10
10
  }
11
11
  /** Invert the allocator instead of storing an already-clamped pixel value as a desktop desire. */
12
- export function workspaceResizeTarget(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,target:number):number {
13
- const range=workspaceResizeRange(available,desired,panel);if(!range.enabled)return desired[panel];
12
+ export function workspaceResizeTarget(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,target:number,columns:WorkspaceColumns=defaultWorkspaceColumns):number {
13
+ const range=workspaceResizeRange(available,desired,panel,columns);if(!range.enabled)return desired[panel];
14
14
  if(target<=range.min)return panelBounds[panel].min;if(target>=range.max)return panelBounds[panel].max;
15
15
  let low:number=panelBounds[panel].min,high:number=panelBounds[panel].max;
16
- for(let i=0;i<48;i++){const mid=(low+high)/2,value=allocateWorkspaceLayout(available,{...desired,[panel]:mid})[panel];if(value<target)low=mid;else high=mid;}
16
+ for(let i=0;i<48;i++){const mid=(low+high)/2,value=allocateWorkspaceLayout(available,{...desired,[panel]:mid},columns)[panel];if(value<target)low=mid;else high=mid;}
17
17
  return (low+high)/2;
18
18
  }
19
19
  export interface WorkspaceUI {
@@ -22,9 +22,13 @@ export interface WorkspaceUI {
22
22
  }
23
23
  export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:WorkspaceTheme)=>void}):WorkspaceUI {
24
24
  const require=<T extends HTMLElement=HTMLElement>(id:string)=>{const node=document.getElementById(id);if(!node)throw new Error('Missing workspace element: '+id);return node as T;};
25
- const layout=document.querySelector<HTMLElement>('.layout')!,main=layout.querySelector<HTMLElement>('main')!,stage=require('stage-layout'),inspector=document.querySelector<HTMLElement>('.inspector')!,right=require('parts-explorer');
25
+ const layout=document.querySelector<HTMLElement>('.layout')!,main=layout.querySelector<HTMLElement>('main')!,stage=require('stage-layout'),inspector=document.querySelector<HTMLElement>('.inspector')!,explorer=require('parts-explorer');
26
26
  layout.classList.add('workspace-layout');
27
27
  let left=layout.querySelector<HTMLElement>('.left-panel');if(!left){left=document.createElement('aside');left.className='left-panel';left.innerHTML='<h2>INPUTS</h2><p class="hint">A and B retain their fixed source bindings. The reference video and shared animation are named above the stage.</p>';layout.prepend(left);}left.id='workspace-left';inspector.id='workspace-inspector';
28
+ // A parts explorer placed inside the left column makes that column the explorer: it keeps the explorer's
29
+ // saved width and the page has no right column. Otherwise sources stay left and the explorer stays right.
30
+ const explorerLeft=left.contains(explorer),columns:WorkspaceColumns=explorerLeft?{left:'rightWidth',right:null}:defaultWorkspaceColumns;
31
+ const tabs=inspectorTabs.filter(tab=>document.getElementById('inspector-'+tab));if(!tabs.length)throw new Error('Missing workspace element: inspector-'+inspectorTabs.join('|inspector-'));
28
32
  const sourceList=document.createElement('div');sourceList.className='workspace-sources';sourceList.append(...left.childNodes);left.append(sourceList);
29
33
  const settings=document.createElement('section');settings.className='workspace-settings';settings.setAttribute('aria-label','Settings');
30
34
  settings.innerHTML='<h2>Settings</h2><label>Theme <select id="settings-theme" data-app-preference><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="settings-sync" data-app-preference type="checkbox">Sync cameras by default</label><label><input id="settings-idle" data-app-preference type="checkbox">Repeat idle by default</label><button id="settings-reset" type="button">Reset layout</button><p class="hint">Defaults apply to new inspections. Links and source drafts retain their explicit view.</p><p id="settings-state" class="hint" role="status"></p>';
@@ -33,36 +37,40 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
33
37
  const store=new WorkspacePreferenceStore({onStatus:value=>{status.textContent=value.detail;}});let desired={...store.get().panels};
34
38
  const stack=document.createElement('div');stack.className='workspace-stage-stack';for(const child of [...main.children])if(child!==inspector)stack.append(child);main.prepend(stack);
35
39
  const triggers=document.createElement('div');triggers.className='workspace-drawer-triggers';triggers.setAttribute('aria-label','Workspace panels');stack.prepend(triggers);
36
- const panels:Record<WorkspacePanel,HTMLElement>={leftWidth:left,rightWidth:right,inspectorHeight:inspector};
37
- const labels:Record<WorkspacePanel,string>={leftWidth:'Sources and settings',rightWidth:'Parts explorer',inspectorHeight:'Inspector'};
40
+ const panels:Partial<Record<WorkspacePanel,HTMLElement>>=explorerLeft?{rightWidth:left,inspectorHeight:inspector}:{leftWidth:left,rightWidth:explorer,inspectorHeight:inspector};
41
+ const sides:Partial<Record<WorkspacePanel,'left'|'right'>>=explorerLeft?{rightWidth:'left'}:{leftWidth:'left',rightWidth:'right'};
42
+ const labels:Record<WorkspacePanel,string>=explorerLeft?{leftWidth:'Sources and settings',rightWidth:'Parts and settings',inspectorHeight:'Inspector'}:{leftWidth:'Sources and settings',rightWidth:'Parts explorer',inspectorHeight:'Inspector'};
38
43
  const handles={} as Record<WorkspacePanel,HTMLElement>,dialogs={} as Record<WorkspacePanel,HTMLDialogElement>,buttons={} as Record<WorkspacePanel,HTMLButtonElement>,disposers:(()=>void)[]=[];
39
- for(const panel of Object.keys(panels) as WorkspacePanel[]){
40
- const handle=document.createElement('div');handle.className='workspace-separator workspace-'+panel;handles[panel]=handle;
41
- if(panel==='leftWidth')left.after(handle);else if(panel==='rightWidth')right.before(handle);else inspector.before(handle);
44
+ const present=Object.keys(panels) as WorkspacePanel[],node=(panel:WorkspacePanel)=>panels[panel]!;
45
+ for(const panel of present){
46
+ const handle=document.createElement('div');handle.className='workspace-separator workspace-'+panel+(sides[panel]?' workspace-side-'+sides[panel]:'');handles[panel]=handle;
47
+ if(sides[panel]==='left')node(panel).after(handle);else if(sides[panel]==='right')node(panel).before(handle);else inspector.before(handle);
42
48
  const trigger=document.createElement('button');trigger.type='button';trigger.textContent=labels[panel];trigger.hidden=true;triggers.append(trigger);buttons[panel]=trigger;
43
49
  const dialog=document.createElement('dialog');dialog.className='workspace-drawer';dialog.id='workspace-drawer-'+panel;dialog.dataset.panel=panel;const close=document.createElement('button');close.type='button';close.textContent='Close '+labels[panel];close.className='workspace-drawer-close';dialog.append(close);document.body.append(dialog);dialogs[panel]=dialog;
44
50
  disposers.push(bindWorkspaceDrawer({trigger,drawer:dialog,label:labels[panel],close}));
45
51
  }
46
- const memory=new InspectorScrollMemory();const active=():InspectorTab=>(document.querySelector<HTMLElement>('[data-inspector][aria-selected=true]')?.dataset.inspector as InspectorTab)??'view';
47
- let disposed=false,frame=0,applying=false,available={width:0,height:0},effective=allocateWorkspaceLayout(available,desired);
52
+ const memory=new InspectorScrollMemory();const active=():InspectorTab=>{const tab=document.querySelector<HTMLElement>('[data-inspector][aria-selected=true]')?.dataset.inspector as InspectorTab|undefined;return tab&&tabs.includes(tab)?tab:tabs[0]!;};
53
+ let disposed=false,frame=0,applying=false,available={width:0,height:0},effective=allocateWorkspaceLayout(available,desired,columns);
48
54
  const panelFor=(tab:InspectorTab)=>require('inspector-'+tab);
49
55
  const capture=()=>{const tab=active(),panel=panelFor(tab),saved=memory.snapshot()[tab];if(!panel.hidden&&(!saved||saved.x<=Math.max(0,panel.scrollWidth-panel.clientWidth)&&saved.y<=Math.max(0,panel.scrollHeight-panel.clientHeight)))memory.capture(tab,panel);};
50
56
  const restore=()=>{if(!disposed)memory.restore(active(),panelFor(active()));};
51
57
  const schedule=()=>{if(!disposed&&!frame)frame=requestAnimationFrame(()=>{frame=0;apply();});};
52
58
  const dock=(panel:WorkspacePanel,drawer:boolean)=>{
53
- const node=panels[panel],dialog=dialogs[panel],trigger=buttons[panel];trigger.hidden=!drawer;handles[panel].hidden=drawer;
54
- if(drawer&&node.parentElement!==dialog){const focus=node.contains(document.activeElement);dialog.append(node);if(focus)trigger.focus({preventScroll:true});}
55
- if(!drawer&&node.parentElement===dialog){const focused=node.contains(document.activeElement)||document.activeElement===trigger;if(dialog.open)dialog.close();trigger.setAttribute('aria-expanded','false');if(panel==='leftWidth')layout.prepend(node);else if(panel==='rightWidth')layout.append(node);else main.append(node);if(focused){node.tabIndex=-1;node.focus({preventScroll:true});}}
59
+ const element=node(panel),dialog=dialogs[panel],trigger=buttons[panel];trigger.hidden=!drawer;handles[panel].hidden=drawer;
60
+ if(drawer&&element.parentElement!==dialog){const focus=element.contains(document.activeElement);dialog.append(element);if(focus)trigger.focus({preventScroll:true});}
61
+ if(!drawer&&element.parentElement===dialog){const focused=element.contains(document.activeElement)||document.activeElement===trigger;if(dialog.open)dialog.close();trigger.setAttribute('aria-expanded','false');if(sides[panel]==='left')layout.prepend(element);else if(sides[panel]==='right')layout.append(element);else main.append(element);if(focused){element.tabIndex=-1;element.focus({preventScroll:true});}}
56
62
  };
57
- const bindings=(Object.keys(panels) as WorkspacePanel[]).map(panel=>bindWorkspaceSeparator({element:handles[panel],panel,controls:panels[panel].id,label:'Resize '+labels[panel],range:()=>workspaceResizeRange(available,desired,panel),resolveDesired:value=>workspaceResizeTarget(available,desired,panel,value),desired:()=>desired[panel],resize:value=>{desired={...desired,[panel]:value};apply();},commit:value=>store.update({panels:{[panel]:value}})}));
63
+ const bindings=present.map(panel=>bindWorkspaceSeparator({element:handles[panel],panel,...(sides[panel]?{side:sides[panel]}:{}),controls:node(panel).id,label:'Resize '+labels[panel],range:()=>workspaceResizeRange(available,desired,panel,columns),resolveDesired:value=>workspaceResizeTarget(available,desired,panel,value,columns),desired:()=>desired[panel],resize:value=>{desired={...desired,[panel]:value};apply();},commit:value=>store.update({panels:{[panel]:value}})}));
58
64
  function apply():void {
59
65
  if(disposed||applying)return;applying=true;
60
66
  try{
61
67
  // Chrome shares the center width. It is not charged against the actual stage minimum.
62
68
  const chrome=[...stack.children].filter(node=>node!==stage&&!((node as HTMLElement).hidden)).reduce((sum,node)=>sum+(node as HTMLElement).getBoundingClientRect().height,0);
63
- available={width:Math.max(0,layout.clientWidth),height:Math.max(0,layout.clientHeight-chrome)};effective=allocateWorkspaceLayout(available,desired);
64
- for(const panel of Object.keys(panels) as WorkspacePanel[])dock(panel,effective[panel==='leftWidth'?'leftMode':panel==='rightWidth'?'rightMode':'inspectorMode']==='drawer');
65
- layout.style.setProperty('--workspace-columns',`${effective.leftWidth}px ${effective.leftMode==='panel'?8:0}px minmax(0,1fr) ${effective.rightMode==='panel'?8:0}px ${effective.rightWidth}px`);
69
+ available={width:Math.max(0,layout.clientWidth),height:Math.max(0,layout.clientHeight-chrome)};effective=allocateWorkspaceLayout(available,desired,columns);
70
+ const mode=(panel:WorkspacePanel)=>effective[panel==='leftWidth'?'leftMode':panel==='rightWidth'?'rightMode':'inspectorMode'];
71
+ for(const panel of present)dock(panel,mode(panel)==='drawer');
72
+ const side=(key:WorkspaceColumns['left'])=>key&&mode(key)==='panel'?{width:effective[key],separator:8}:{width:0,separator:0},l=side(columns.left),r=side(columns.right);
73
+ layout.style.setProperty('--workspace-columns',`${l.width}px ${l.separator}px minmax(0,1fr) ${r.separator}px ${r.width}px`);
66
74
  main.style.gridTemplateRows=`minmax(0,1fr) ${effective.inspectorMode==='panel'?8:0}px ${effective.inspectorHeight}px`;
67
75
  layout.dataset.stageMinimum=String(effective.stageMinimumSatisfied);for(const binding of bindings)binding.refresh();options.changed();restore();
68
76
  }finally{applying=false;}
@@ -70,7 +78,7 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
70
78
  const renderSettings=()=>{const value=store.get();theme.value=value.theme;sync.checked=value.defaultCameraSync;idle.checked=value.defaultIdleRepeat;status.textContent=store.status().detail;};
71
79
  theme.onchange=()=>{store.update({theme:theme.value as WorkspaceTheme});options.defaultTheme(store.get().theme);};sync.onchange=()=>{store.update({defaultCameraSync:sync.checked});};idle.onchange=()=>{store.update({defaultIdleRepeat:idle.checked});};require<HTMLButtonElement>('settings-reset').onclick=()=>store.resetLayout();
72
80
  const unsubscribe=store.subscribe(value=>{desired={...value.panels};renderSettings();schedule();});renderSettings();
73
- for(const tab of inspectorTabs){
81
+ for(const tab of tabs){
74
82
  const panel=panelFor(tab);let user=false,timer:ReturnType<typeof setTimeout>|undefined;
75
83
  const intent=()=>{user=true;if(timer)clearTimeout(timer);timer=setTimeout(()=>{user=false;},600);};const scrolled=()=>{
76
84
  if(!user)return;const saved=memory.snapshot()[tab]??{x:0,y:0};
@@ -83,7 +91,7 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
83
91
  let extent='';const content=new MutationObserver(()=>{const next=`${panel.scrollWidth}:${panel.scrollHeight}:${panel.clientWidth}:${panel.clientHeight}`;if(next!==extent){extent=next;if(tab===active())restore();}});content.observe(panel,{childList:true,subtree:true});
84
92
  disposers.push(()=>{if(timer)clearTimeout(timer);for(const type of ['wheel','pointerdown','touchstart','keydown'])panel.removeEventListener(type,intent);panel.removeEventListener('scroll',scrolled);content.disconnect();});
85
93
  }
86
- const observer=new ResizeObserver(schedule);observer.observe(layout);for(const node of [...stack.children])if(node!==stage)observer.observe(node);for(const tab of inspectorTabs)observer.observe(panelFor(tab));
94
+ const observer=new ResizeObserver(schedule);observer.observe(layout);for(const node of [...stack.children])if(node!==stage)observer.observe(node);for(const tab of tabs)observer.observe(panelFor(tab));
87
95
  const resumed=()=>{if(!document.hidden)store.refresh();};document.addEventListener('visibilitychange',resumed);schedule();
88
96
  return {defaults:()=>store.get(),beforeTab:capture,afterTab:restore,scroll:()=>{capture();return JSON.stringify(memory.snapshot());},restoreScroll(value){if(typeof value!=='string')return;memory.replace(JSON.parse(value));restore();},revealInspector(){if(effective.inspectorMode==='drawer'&&!dialogs.inspectorHeight.open){for(const dialog of Object.values(dialogs))if(dialog.open)dialog.close();buttons.inspectorHeight.click();}},refresh:schedule,
89
97
  dispose(){if(disposed)return;capture();disposed=true;if(frame)cancelAnimationFrame(frame);observer.disconnect();document.removeEventListener('visibilitychange',resumed);unsubscribe();for(const binding of bindings)binding.dispose();for(const dispose of disposers)dispose();memory.dispose();store.dispose();}