@rig-c/review 0.4.0 → 0.6.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/web/styles.css CHANGED
@@ -1,11 +1,11 @@
1
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
+ :root{--rv-region-a:#ff9f43;--rv-region-a-fill:rgba(255,159,67,.18);--rv-region-b:#4dd6ff;--rv-region-b-fill:rgba(77,214,255,.18);--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;--rv-scrollbar-thumb:#2a3657;--rv-scrollbar-thumb-hover:#3d4c74;--rv-scrollbar-track:transparent;--rv-separator:#1a2747;--rv-separator-hover:#3f7fb8}
3
+ :root[data-theme=light]{--rv-region-a:#ff9f43;--rv-region-a-fill:rgba(255,159,67,.18);--rv-region-b:#4dd6ff;--rv-region-b-fill:rgba(77,214,255,.18);--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;--rv-scrollbar-thumb:#b3bed3;--rv-scrollbar-thumb-hover:#8e9bb5;--rv-scrollbar-track:transparent;--rv-separator:#c9d6ec;--rv-separator-hover:#5b93cf}
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)}.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:nowrap;height:40px;box-sizing:border-box;overflow:hidden;padding:0 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%)}
5
5
  @media(min-width:1500px){.layout{grid-template-columns:218px minmax(250px,1fr) 310px}.stages{min-height:600px}}
6
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}}
7
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}}
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:0 12px}}
9
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}}
10
10
 
11
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}
@@ -37,6 +37,7 @@
37
37
  input::placeholder,textarea::placeholder{color:var(--fg-muted)}
38
38
 
39
39
  :focus-visible{outline-color:var(--rv-focus)}
40
+ .chest-regions{margin-top:22px;padding-top:15px;border-top:1px solid var(--border)}.chest-regions code{font:11px var(--font-mono)}.chest-grid,.chest-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 10px}.chest-row{border:1px solid var(--border);border-radius:6px;padding:9px 10px;margin:10px 0 0;min-width:0}.chest-row legend{font-size:11px;color:var(--fg-muted);padding:0 4px}.chest-grid label,.chest-row label{display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--fg-muted);min-width:0}.chest-row input,.chest-row select,.chest-grid input{width:100%;padding:5px 6px;font-size:11px}.chest-row .chest-check{flex-direction:row;align-items:center;gap:6px}.chest-row .chest-check input{width:auto}.chest-more{margin-top:10px;font-size:11px;color:var(--fg-muted)}.chest-more summary{cursor:pointer}#chest-status.error{color:var(--fail)}
40
41
  /* Source pixels retain their original color; the neutral stage stays dark in both themes. */
41
42
 
42
43
  /* The application shell never scrolls; each Inspector tab and the source list owns overflow. */
@@ -48,11 +49,11 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
48
49
  .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
50
  .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}
50
51
  .inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
51
- .inspector-heading [role=tablist]{display:flex;gap:4px}
52
- .inspector-heading button{padding:4px 9px;font-size:11px}
53
- .inspector-heading button[aria-selected=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg);border-color:var(--rv-pressed-border)}
52
+
53
+
54
+
54
55
  .inspector-body{flex:1;min-height:0;overflow:hidden}
55
- .inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
56
+ .inspector-body>[data-inspector-panel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
56
57
  .inspector .inspection-controls{padding:0;border:0;background:transparent}
57
58
  .inspector .provenance{margin-top:10px}
58
59
  .inspector .transport{padding:0 0 10px;border:0;background:transparent;flex-wrap:wrap}
@@ -71,7 +72,7 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
71
72
  @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}}
72
73
  @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}}
73
74
 
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}
75
+ .inspector .feedback-form>label,.inspector .review-summary>label{display:block;font-size:11px;margin:10px 0 5px}.inspector .frame-range input{width:100%}.inspector [data-inspector-panel]{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}
75
76
 
76
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}
77
78
 
@@ -79,7 +80,12 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
79
80
  #stage-layout:has(>.stage-pane[hidden]){grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
80
81
 
81
82
  /* Slot explorer shares the stage row; bottom Inspector remains in the main column. */
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
+ .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:flex;align-items:center;gap:6px;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}
84
+ /* A slot's thumbnail: its declared page region in a 24px box, or the type icon when it has none. */
85
+ .part-thumb{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;overflow:hidden;border-radius:3px}
86
+ .part-thumb canvas{display:block;width:24px;height:24px}
87
+ .part-thumb[data-thumbnail=image]{background:var(--rv-field)}
88
+ .part-label{min-width:0;flex:1 1 auto}.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
89
  @media(max-width:1050px){.layout{grid-template-columns:200px minmax(0,1fr)!important}.parts-explorer{padding:8px}}
84
90
  @media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:none!important}}
85
91
  @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}}
@@ -91,7 +97,7 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
91
97
  .workspace-stage-stack>#stage-layout{flex:1;min-height:0;height:auto;overflow:hidden}
92
98
  .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)}
93
99
  .workspace-sources{flex:1;min-height:0;overflow:auto;padding:10px;overscroll-behavior:contain}
94
- .workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid var(--border)}
100
+ .workspace-settings{padding:10px;margin:12px -10px 0;border-top:1px solid var(--border)}
95
101
  .workspace-settings h2{font-size:12px;margin:0 0 8px}
96
102
  .workspace-settings label{display:flex;align-items:center;gap:6px;font-size:11px;margin:7px 0}
97
103
  .workspace-settings select{width:auto;min-width:0;flex:1}
@@ -99,12 +105,14 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
99
105
  .workspace-settings input[type=checkbox]{width:auto;flex:none}
100
106
  .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}
101
107
  .workspace-layout .inspector{grid-row:3;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
102
- .workspace-separator{position:relative;z-index:3;background:var(--border-strong);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
103
- .workspace-separator::after{content:'';position:absolute;inset:0 -4px}
108
+ .workspace-separator{position:relative;z-index:3;background:var(--rv-separator);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
109
+ .workspace-separator:hover{background:var(--rv-separator-hover)}
110
+ /* Drawn 4px (workspaceMinimums.separator); the transparent hit area stays 16px, 22px on coarse pointers. */
111
+ .workspace-separator::after{content:'';position:absolute;inset:0 -6px}
104
112
  .workspace-separator:focus-visible{outline:2px solid var(--rv-focus);outline-offset:-2px;background:var(--link)}
105
113
  .workspace-separator[data-resizing=true]{background:var(--link-hover)}
106
114
  .workspace-side-left{grid-column:2;grid-row:1}.workspace-side-right{grid-column:4;grid-row:1}
107
- .workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-4px 0}
115
+ .workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-6px 0}
108
116
  .workspace-drawer-triggers{display:flex;gap:6px;flex-wrap:wrap;flex:none;padding:4px 8px;min-height:0}
109
117
  .workspace-drawer-triggers:not(:has(button:not([hidden]))){display:none}
110
118
  .workspace-drawer-triggers button{padding:4px 7px;font-size:11px;min-height:28px}
@@ -116,14 +124,14 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
116
124
  .workspace-drawer>.parts-explorer{display:block;grid-template-columns:none;flex:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
117
125
  .workspace-drawer>.inspector{flex:1;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
118
126
 
119
- @media(pointer:coarse){.workspace-separator::after{inset:0 -7px}.workspace-inspectorHeight::after{inset:-7px 0}.workspace-drawer-triggers button{min-height:38px}}
127
+ @media(pointer:coarse){.workspace-separator::after{inset:0 -9px}.workspace-inspectorHeight::after{inset:-9px 0}.workspace-drawer-triggers button{min-height:38px}}
120
128
 
121
129
  /* Workspace rework: parts explorer is the left column; checkpoints and reference fold above it. */
122
130
  .package-context{border-bottom:1px solid var(--border);padding-bottom:8px;margin-bottom:10px}
123
131
  .package-context>summary{cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;font-size:11px}
124
132
  .package-context>summary .eyebrow{margin:0}
125
133
  .package-context #checkpoint-title{color:var(--fg-soft);overflow-wrap:anywhere}
126
- .package-context #checkpoints{max-height:30dvh;overflow:auto;margin-top:6px}
134
+ .package-context #checkpoints{margin-top:6px}
127
135
  .package-context .checkpoint{padding:6px 8px;margin:2px 0}
128
136
  .package-context section{margin-top:10px}
129
137
  .left-panel .parts-explorer{border:0;padding:0;background:transparent;overflow:visible}
@@ -138,21 +146,94 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
138
146
  .layer-toggle:has(input:disabled){opacity:.45;cursor:default}
139
147
  .layer-toggle:has(input:focus-visible){outline:2px solid var(--rv-focus);outline-offset:2px}
140
148
  .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}
149
+ /* View bar is one row: layers, camera sync and zoom, then the View menu at the right end. */
150
+ .viewbar{flex-wrap:nowrap;justify-content:flex-start;gap:8px;padding:8px 10px;min-width:0}
151
+ .viewbar>.layer-toggles{flex:0 1 auto;overflow:hidden}
152
+ .viewbar .zoom-controls{flex:none}
153
+ .view-menu{position:relative;margin-left:auto;flex:none;font-size:11px}
154
+ .view-menu>summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border:1px solid var(--border-strong);border-radius:6px;background:var(--rv-control);white-space:nowrap}
155
+ .view-menu>summary::-webkit-details-marker{display:none}
156
+ .view-menu>summary::after{content:'\25BE';color:var(--fg-muted)}
157
+ .view-menu>summary:hover{background:var(--rv-control-hover)}
158
+ .view-menu-list{position:absolute;right:0;top:calc(100% + 4px);z-index:12;display:flex;flex-direction:column;gap:3px;min-width:150px;padding:4px;background:var(--rv-panel);border:1px solid var(--border-strong);border-radius:8px}
159
+ .view-menu-list button{text-align:left;font-size:11px;padding:5px 9px;border-color:transparent;background:transparent}
160
+ .view-menu-list button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border);color:var(--rv-pressed-fg)}
161
+ .transport .idle-repeat{display:inline-flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap;flex:none}
162
+ .transport .idle-repeat input{flex:none;width:auto;margin:0}
146
163
 
147
164
  /* Footer is the one status area: message | readouts | view region and source hash. */
148
165
  footer.status-bar{flex-wrap:nowrap}
149
166
  .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}
167
+ .status-readouts{flex:0 1 auto;max-width:60%;display:flex;align-items:center;justify-content:flex-end;gap:2px 12px;flex-wrap:nowrap;min-width:0;overflow:hidden}.status-readouts>*{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
151
168
  .status-readouts .draft-notice{padding:0}
152
169
  .status-readouts .draft-notice #draft-recovery{margin-left:6px}
153
170
  .status-source{display:flex;align-items:center;gap:12px;margin-left:auto;flex:none;white-space:nowrap}
154
171
  .view-region{position:relative}
155
172
  .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}
173
+ /* A popover above the 40px footer, clamped to the viewport: fixed so the footer's overflow never clips it
174
+ and opening it never scrolls the page; its own body scrolls. */
175
+ .view-region-body{position:fixed;right:8px;bottom:calc(40px + 6px);width:min(520px,calc(100vw - 16px));max-width:calc(100vw - 16px);max-height:calc(100dvh - 40px - 16px);box-sizing:border-box;overflow:auto;overscroll-behavior:contain;padding:10px;z-index:10;background:var(--rv-panel);border:1px solid var(--border);border-radius:7px}
157
176
  .view-region-body .numeric-view{margin:8px 0}
158
177
 
178
+
179
+ /* Header mode switch: Setup and Animate reflect the Inspector's selected section. */
180
+ .mode-switch{display:flex;gap:3px;padding:3px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-segmented)}
181
+ .mode-switch button{border:0;background:transparent;font-size:12px;padding:5px 12px}
182
+ .mode-switch button[aria-pressed=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg)}
183
+
184
+ /* Panel headers stay put while the panel body scrolls beneath them. */
185
+ .sticky-header{position:sticky;top:0;z-index:2;background:var(--rv-panel)}
186
+ /* The left column's fixed header row: title and search never scroll and nothing sits above them. */
187
+ .left-header{flex:none;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--rv-panel)}
188
+ .left-header h2{font-size:12px;font-weight:600;margin:0 0 6px}
189
+ .left-header input[type=search]{width:100%;min-width:0}
190
+ /* Parts header is one row: the title left, the search filling the rest. */
191
+ .parts-header{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center;gap:8px}
192
+ .parts-header h2{flex:none;margin:0}
193
+ .parts-header input[type=search]{flex:1 1 auto;width:auto;min-width:0}
194
+ .workspace-sources{padding-top:0}
195
+ .workspace-sources>.package-context{padding-top:10px}
196
+ .workspace-settings>.sticky-header{margin:-10px -10px 8px;padding:10px 10px 6px}
197
+ .workspace-settings[data-collapsed=true]>.sticky-header{margin-bottom:-10px}
198
+ .settings-toggle{display:flex;align-items:center;gap:6px;width:100%;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
199
+ .settings-toggle::before{content:'▸';flex:none;color:var(--fg-muted)}
200
+ .settings-toggle[aria-expanded=true]::before{content:'▾'}
201
+ .workspace-drawer-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none;font-size:12px;font-weight:600}
202
+
203
+ /* One thin scrollbar for every scrolling region, from the theme tokens: the standard properties for
204
+ Firefox, a 6px ::-webkit-scrollbar for Chromium/WebKit (Chromium ignores it once scrollbar-width is set). */
205
+ @supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:var(--rv-scrollbar-thumb) var(--rv-scrollbar-track)}}
206
+ *::-webkit-scrollbar{width:6px;height:6px}
207
+ *::-webkit-scrollbar-track{background:var(--rv-scrollbar-track)}
208
+ *::-webkit-scrollbar-thumb{background:var(--rv-scrollbar-thumb);border-radius:3px}
209
+ *::-webkit-scrollbar-thumb:hover{background:var(--rv-scrollbar-thumb-hover)}
210
+ *::-webkit-scrollbar-corner{background:transparent}
211
+
212
+ /* Review dialog: today's Feedback section in a native modal opened from the top bar's Write review. */
213
+ .review-dialog{position:fixed;inset:0;width:min(94vw,1100px);max-width:94vw;max-height:90dvh;margin:auto;padding:0;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-panel);color:inherit;overflow:hidden}
214
+ .review-dialog:not([open]){display:none}
215
+ .review-dialog[open]{display:flex;flex-direction:column}
216
+ .review-dialog::backdrop{background:var(--rv-backdrop)}
217
+ .review-dialog-header{flex:none;display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid var(--border)}
218
+ .review-dialog-header h2{margin:0;font-size:14px;font-weight:600;color:var(--fg-strong)}
219
+ .review-dialog-header .hint{margin:0;flex:1;min-width:0}
220
+ .review-dialog-body{flex:1;min-height:0;overflow:auto;padding:14px 16px;overscroll-behavior:contain}
221
+ .review-dialog-actions{flex:none;display:flex;align-items:center;gap:12px;padding:10px 14px;border-top:1px solid var(--border)}
222
+ .review-message{flex:1;min-width:0;font-size:11px;color:var(--fg-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
223
+ .review-message.error{color:var(--fail)}
224
+ .review-draft-state{font-size:11px;color:var(--warn)}
225
+ .review-draft-state:empty{display:none}
226
+ .review-dialog .provenance{margin-top:10px}
227
+ .review-dialog .review-summary>label{display:block;font-size:11px;margin:10px 0 5px}
228
+ .review-dialog .report-panel{padding:10px 0}
229
+ @media(max-width:540px){.review-dialog-header .hint{display:none}}
230
+ :root{--rv-bust-r:#00e5ff;--rv-bust-r-fill:rgba(0,229,255,.13);--rv-bust-l:#52ff88;--rv-bust-l-fill:rgba(82,255,136,.13);--rv-bust-overlap:#ff5252;--rv-bust-overlap-fill:rgba(255,82,82,.13);--rv-bust-handle:#ffffff;--rv-bust-ink:#111318;--rv-bust-label-bg:rgba(12,14,18,.82);--rv-bust-flash:#ffcc00}
231
+ :root[data-theme=light]{--rv-bust-r:#00e5ff;--rv-bust-r-fill:rgba(0,229,255,.13);--rv-bust-l:#52ff88;--rv-bust-l-fill:rgba(82,255,136,.13);--rv-bust-overlap:#ff5252;--rv-bust-overlap-fill:rgba(255,82,82,.13);--rv-bust-handle:#ffffff;--rv-bust-ink:#111318;--rv-bust-label-bg:rgba(12,14,18,.82);--rv-bust-flash:#ffcc00}
232
+ .view-tools{display:flex;gap:4px;flex:none}.view-tool{padding:5px 9px;font-size:11px;white-space:nowrap}
233
+ .chest-editor-bar{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;padding:6px 10px;border-bottom:1px solid var(--border);background:var(--rv-bar);font-size:11px}.chest-editor-bar[hidden]{display:none}.chest-editor-bar .bust-group{display:flex;align-items:center;gap:6px;padding-left:10px;border-left:1px solid var(--border)}.chest-editor-bar .bust-group:first-child{padding-left:0;border-left:0}.chest-editor-bar label{display:flex;align-items:center;gap:4px;color:var(--fg-muted)}.chest-editor-bar input[type=number]{width:58px;padding:3px 5px;font-size:11px}.chest-editor-bar button{padding:4px 8px;font-size:11px}.chest-editor-bar button[aria-disabled=true]{opacity:.6}.bust-tag{font-weight:700}.bust-tag-r{color:var(--rv-bust-r)}.bust-tag-l{color:var(--rv-bust-l)}#chest-editor-status{flex:1 1 220px;min-width:0}
234
+ :root{--rv-mesh-veil:rgba(8,10,14,.55);--rv-mesh-edge:#69f1ff;--rv-mesh-hull:#ffb020;--rv-mesh-constraint:#ff4fd8;--rv-mesh-removed:#ff5252;--rv-mesh-vertex:#ffffff;--rv-mesh-ink:#111318;--rv-mesh-selected:#ffe14d;--rv-mesh-added:#52ff88;--rv-mesh-hover:#ffffff;--rv-mesh-box:#ffe14d;--rv-mesh-box-fill:rgba(255,225,77,.12)}
235
+ :root[data-theme=light]{--rv-mesh-veil:rgba(245,247,251,.6);--rv-mesh-edge:#69f1ff;--rv-mesh-hull:#ffb020;--rv-mesh-constraint:#ff4fd8;--rv-mesh-removed:#ff5252;--rv-mesh-vertex:#ffffff;--rv-mesh-ink:#111318;--rv-mesh-selected:#ffe14d;--rv-mesh-added:#52ff88;--rv-mesh-hover:#ffffff;--rv-mesh-box:#ffe14d;--rv-mesh-box-fill:rgba(255,225,77,.12)}
236
+ .mesh-editor-bar kbd{margin-left:5px;font-size:10px;opacity:.75}#mesh-editor-status{flex:1 1 260px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
237
+ #rig-pane[data-mesh-mode] .viewport{cursor:crosshair}#rig-pane[data-mesh-mode=modify][data-mesh-hover] .viewport{cursor:move}#rig-pane[data-mesh-mode=create] .viewport{cursor:copy}#rig-pane[data-mesh-mode=delete][data-mesh-hover] .viewport{cursor:not-allowed}#rig-pane[data-mesh-mode=edges][data-mesh-hover=vertex] .viewport{cursor:cell}
238
+ #rig-pane[data-chest-hover=move] .viewport{cursor:move}#rig-pane[data-chest-hover=r] .viewport,#rig-pane[data-chest-hover=band] .viewport{cursor:ew-resize}#rig-pane[data-chest-hover=band] .viewport{cursor:ns-resize}
239
+ .environment-panel{padding:15px;border-top:1px solid var(--border);background:var(--rv-panel);font-size:11px}.environment-panel .row{margin-bottom:9px}.environment-panel .row label{display:flex;align-items:center;gap:4px;color:var(--fg-muted)}.environment-panel input[type=range]{flex:1;max-width:180px}.environment-panel input[type=number]{width:72px;padding:4px 6px;font-variant-numeric:tabular-nums}.environment-panel button{padding:4px 9px;font-size:11px}.environment-panel output{font:11px var(--font-mono);color:var(--fg-muted)}.environment-panel .hint{margin:0 0 9px}.environment-readout{display:block;margin:0 0 9px;color:var(--fg-strong)!important;overflow-wrap:anywhere}.physics-constraints summary{cursor:pointer;color:var(--fg-soft)}.physics-table-wrap{overflow-x:auto;max-height:260px;overflow-y:auto}.physics-table{width:100%;border-collapse:collapse;font:11px var(--font-mono);font-variant-numeric:tabular-nums}.physics-table th,.physics-table td{padding:3px 6px;border-top:1px solid var(--border);text-align:right;white-space:nowrap}.physics-table th{color:var(--fg-muted);font-weight:500}.physics-table th:first-child,.physics-table td:first-child,.physics-table th:nth-child(2),.physics-table td:nth-child(2){text-align:left}.physics-table .absent{color:var(--fg-muted)}
@@ -93,10 +93,16 @@ export function bindPan(viewport:HTMLElement,move:(dx:number,dy:number)=>void,en
93
93
  return ()=>{blur();window.removeEventListener('blur',blur);document.removeEventListener('keydown',keyDown,true);window.removeEventListener('keyup',keyUp,true);viewport.removeEventListener('pointerdown',start);viewport.removeEventListener('pointermove',drag);viewport.removeEventListener('pointerup',end);viewport.removeEventListener('pointercancel',end);viewport.removeEventListener('lostpointercapture',end);viewport.removeEventListener('dragstart',native);};
94
94
  }
95
95
 
96
- /** beforeChange runs while the old panel is still visible; changed runs after the new one is shown. */
96
+ /** beforeChange runs while the old panel is still visible; changed runs after the new one is shown.
97
+ * The selected section is the one source of the mode. A `[data-inspector]` element is a section selector:
98
+ * a `role=tab` in an Inspector tab row (aria-selected, roving tabindex, arrow keys) or a header `[data-mode]`
99
+ * toggle that is itself the selector (aria-pressed). Mode buttons without `data-inspector` select the matching
100
+ * tab and only reflect it; a tab with no mode button (compare's Feedback) leaves every mode button unpressed. */
97
101
  export function bindInspectorTabs(changed:()=>void,beforeChange:()=>void=()=>{}):void {
98
- const tabs=[...document.querySelectorAll<HTMLButtonElement>('[data-inspector]')];
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();};
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();}};}
102
+ const tabs=[...document.querySelectorAll<HTMLButtonElement>('[data-inspector]')],modes=[...document.querySelectorAll<HTMLButtonElement>('[data-mode]')].filter(mode=>!tabs.includes(mode));
103
+ const toggle=(item:HTMLButtonElement)=>item.dataset.mode!==undefined,state=(item:HTMLButtonElement)=>toggle(item)?'aria-pressed':'aria-selected';
104
+ const reflect=()=>{if(!modes.length)return;const selected=tabs.find(item=>item.getAttribute(state(item))==='true')?.dataset.inspector;for(const mode of modes)mode.setAttribute('aria-pressed',String(mode.dataset.mode===selected));};
105
+ const activate=(tab:HTMLButtonElement)=>{beforeChange();for(const item of tabs){const active=item===tab;item.setAttribute(state(item),String(active));if(!toggle(item))item.tabIndex=active?0:-1;}for(const panel of document.querySelectorAll<HTMLElement>('[data-inspector-panel]'))panel.hidden=panel.dataset.inspectorPanel!==tab.dataset.inspector;reflect();changed();};
106
+ for(const mode of modes)mode.onclick=()=>{const tab=tabs.find(item=>item.dataset.inspector===mode.dataset.mode);if(tab)activate(tab);};reflect();
107
+ for(const [index,tab] of tabs.entries()){tab.onclick=()=>activate(tab);if(toggle(tab))continue;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();}};}
101
108
  }
102
-
@@ -1,7 +1,8 @@
1
1
  /** Effective workspace geometry and local DOM bindings. Never stores source or global preferences. */
2
2
  import {parseWorkspacePanels,panelBounds,type WorkspacePanels} from './workspace-preferences.ts';
3
3
  export type WorkspacePanel=keyof WorkspacePanels;
4
- export const workspaceMinimums=Object.freeze({stageWidth:240,stageHeight:180,separator:8});
4
+ /** `separator` is the drawn thickness of every resize handle in CSS pixels; its hit area is wider (styles.css `::after`). */
5
+ export const workspaceMinimums=Object.freeze({stageWidth:240,stageHeight:180,separator:4});
5
6
  export interface SeparatorRange {min:number;max:number;value:number;enabled:boolean}
6
7
  export type WorkspaceColumnKey=Exclude<WorkspacePanel,'inspectorHeight'>;
7
8
  /** Which saved width each side column uses; null means the page has no column on that side.
@@ -8,11 +8,16 @@ export type WorkspaceTheme='system'|'dark'|'light';
8
8
  export interface WorkspacePanels {leftWidth:number;rightWidth:number;inspectorHeight:number}
9
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})});
10
10
  export const defaultWorkspacePanels:Readonly<WorkspacePanels>=Object.freeze({leftWidth:170,rightWidth:260,inspectorHeight:280});
11
- type Field='theme'|'defaultCameraSync'|'defaultIdleRepeat'|keyof WorkspacePanels;
12
- const fields:readonly Field[]=['theme','defaultCameraSync','defaultIdleRepeat','leftWidth','rightWidth','inspectorHeight'];
11
+ type Flag='defaultCameraSync'|'defaultIdleRepeat'|'settingsCollapsed';
12
+ type Field='theme'|Flag|keyof WorkspacePanels;
13
+ const fields:readonly Field[]=['theme','defaultCameraSync','defaultIdleRepeat','settingsCollapsed','leftWidth','rightWidth','inspectorHeight'];
14
+ const flags:readonly Flag[]=['defaultCameraSync','defaultIdleRepeat','settingsCollapsed'];
15
+ /** Fields added after the record shipped. A stored record that predates one has neither the value nor its
16
+ * stamp: it loads with the default value at revision 0, exactly as if that field had never been edited. */
17
+ const addedFields:ReadonlySet<Field>=new Set(['settingsCollapsed']);
13
18
  interface FieldStamp {revision:number;writer:string}
14
- export interface WorkspacePreferences {schema:'rig-review-preferences/1';revision:number;theme:WorkspaceTheme;defaultCameraSync:boolean;defaultIdleRepeat:boolean;panels:WorkspacePanels;stamps:Record<Field,FieldStamp>}
15
- export interface WorkspacePreferencePatch {theme?:WorkspaceTheme;defaultCameraSync?:boolean;defaultIdleRepeat?:boolean;panels?:Partial<WorkspacePanels>}
19
+ export interface WorkspacePreferences {schema:'rig-review-preferences/1';revision:number;theme:WorkspaceTheme;defaultCameraSync:boolean;defaultIdleRepeat:boolean;settingsCollapsed:boolean;panels:WorkspacePanels;stamps:Record<Field,FieldStamp>}
20
+ export interface WorkspacePreferencePatch {theme?:WorkspaceTheme;defaultCameraSync?:boolean;defaultIdleRepeat?:boolean;settingsCollapsed?:boolean;panels?:Partial<WorkspacePanels>}
16
21
  export interface PreferenceStorage {getItem(key:string):string|null;setItem(key:string,value:string):void}
17
22
  export interface PreferenceStatus {state:'available'|'memory-only'|'corrupt';detail:string}
18
23
  export interface PreferenceEventSource {addEventListener(type:string,listener:EventListener):void;removeEventListener(type:string,listener:EventListener):void}
@@ -23,21 +28,21 @@ const theme=(v:unknown):WorkspaceTheme=>{if(v!=='system'&&v!=='dark'&&v!=='light
23
28
  const boolean=(v:unknown,label:string):boolean=>{if(typeof v!=='boolean')throw new Error(`${label}: expected boolean`);return v;};
24
29
  const panel=(v:unknown,key:keyof WorkspacePanels):number=>{const {min,max}=panelBounds[key];if(typeof v!=='number'||!Number.isFinite(v)||v<min||v>max)throw new Error(`PREFERENCES_PANEL: ${key} requires finite ${min}..${max} CSS pixels`);return v;};
25
30
  export function parseWorkspacePanels(value:unknown):WorkspacePanels {const o=object(value,'panels');closed(o,Object.keys(panelBounds),'panels');return {leftWidth:panel(o.leftWidth,'leftWidth'),rightWidth:panel(o.rightWidth,'rightWidth'),inspectorHeight:panel(o.inspectorHeight,'inspectorHeight')};}
26
- export function defaultWorkspacePreferences():WorkspacePreferences {return {schema:workspacePreferenceKey,revision:0,theme:'system',defaultCameraSync:true,defaultIdleRepeat:true,panels:{...defaultWorkspacePanels},stamps:Object.fromEntries(fields.map(field=>[field,{revision:0,writer:''}])) as Record<Field,FieldStamp>};}
31
+ export function defaultWorkspacePreferences():WorkspacePreferences {return {schema:workspacePreferenceKey,revision:0,theme:'system',defaultCameraSync:true,defaultIdleRepeat:true,settingsCollapsed:true,panels:{...defaultWorkspacePanels},stamps:Object.fromEntries(fields.map(field=>[field,{revision:0,writer:''}])) as Record<Field,FieldStamp>};}
27
32
  const clone=(v:WorkspacePreferences):WorkspacePreferences=>({...v,panels:{...v.panels},stamps:Object.fromEntries(fields.map(field=>[field,{...v.stamps[field]}])) as Record<Field,FieldStamp>});
28
- function leaf(value:WorkspacePreferences,field:Field):WorkspaceTheme|boolean|number {return field in panelBounds?value.panels[field as keyof WorkspacePanels]:value[field as 'theme'|'defaultCameraSync'|'defaultIdleRepeat'];}
29
- function put(value:WorkspacePreferences,field:Field,v:WorkspaceTheme|boolean|number):void {if(field==='theme')value.theme=theme(v);else if(field==='defaultCameraSync'||field==='defaultIdleRepeat')value[field]=boolean(v,field);else value.panels[field]=panel(v,field);}
33
+ function leaf(value:WorkspacePreferences,field:Field):WorkspaceTheme|boolean|number {return field in panelBounds?value.panels[field as keyof WorkspacePanels]:value[field as 'theme'|Flag];}
34
+ function put(value:WorkspacePreferences,field:Field,v:WorkspaceTheme|boolean|number):void {if(field==='theme')value.theme=theme(v);else if(flags.includes(field as Flag))value[field as Flag]=boolean(v,field);else value.panels[field as keyof WorkspacePanels]=panel(v,field as keyof WorkspacePanels);}
30
35
  export function parseWorkspacePreferences(value:unknown):WorkspacePreferences {
31
- const o=object(value,'preferences');closed(o,['schema','revision','theme','defaultCameraSync','defaultIdleRepeat','panels','stamps'],'preferences');if(o.schema!==workspacePreferenceKey)throw new Error('PREFERENCES_VERSION: unsupported schema');
32
- const result=defaultWorkspacePreferences();result.theme=theme(o.theme);result.defaultCameraSync=boolean(o.defaultCameraSync,'defaultCameraSync');result.defaultIdleRepeat=boolean(o.defaultIdleRepeat,'defaultIdleRepeat');result.panels=parseWorkspacePanels(o.panels);
36
+ const o=object(value,'preferences');closed(o,['schema','revision','theme','defaultCameraSync','defaultIdleRepeat','settingsCollapsed','panels','stamps'],'preferences');if(o.schema!==workspacePreferenceKey)throw new Error('PREFERENCES_VERSION: unsupported schema');
37
+ const result=defaultWorkspacePreferences();result.theme=theme(o.theme);result.defaultCameraSync=boolean(o.defaultCameraSync,'defaultCameraSync');result.defaultIdleRepeat=boolean(o.defaultIdleRepeat,'defaultIdleRepeat');if('settingsCollapsed'in o)result.settingsCollapsed=boolean(o.settingsCollapsed,'settingsCollapsed');result.panels=parseWorkspacePanels(o.panels);
33
38
  const stamps=object(o.stamps,'stamps');closed(stamps,fields,'stamps');
34
- for(const field of fields){const stamp=object(stamps[field],`stamps.${field}`);closed(stamp,['revision','writer'],`stamps.${field}`);if(typeof stamp.revision!=='number'||!Number.isSafeInteger(stamp.revision)||stamp.revision<0||typeof stamp.writer!=='string'||!(/^[A-Za-z0-9_-]{1,80}$/.test(stamp.writer)||stamp.revision===0&&stamp.writer===''))throw new Error(`PREFERENCES_STAMP: invalid ${field} revision/writer`);if(stamp.revision===0&&(stamp.writer!==''||leaf(result,field)!==leaf(defaultWorkspacePreferences(),field)))throw new Error(`PREFERENCES_STAMP: unstamped ${field} differs from defaults`);result.stamps[field]={revision:stamp.revision,writer:stamp.writer};}
39
+ for(const field of fields){const stamp=stamps[field]===undefined&&addedFields.has(field)?{revision:0,writer:''}:object(stamps[field],`stamps.${field}`);closed(stamp,['revision','writer'],`stamps.${field}`);if(typeof stamp.revision!=='number'||!Number.isSafeInteger(stamp.revision)||stamp.revision<0||typeof stamp.writer!=='string'||!(/^[A-Za-z0-9_-]{1,80}$/.test(stamp.writer)||stamp.revision===0&&stamp.writer===''))throw new Error(`PREFERENCES_STAMP: invalid ${field} revision/writer`);if(stamp.revision===0&&(stamp.writer!==''||leaf(result,field)!==leaf(defaultWorkspacePreferences(),field)))throw new Error(`PREFERENCES_STAMP: unstamped ${field} differs from defaults`);result.stamps[field]={revision:stamp.revision,writer:stamp.writer};}
35
40
  const revision=Math.max(...fields.map(field=>result.stamps[field].revision));if(o.revision!==revision)throw new Error('PREFERENCES_REVISION: record revision must equal maximum field revision');result.revision=revision;return result;
36
41
  }
37
42
  function parseText(raw:string):WorkspacePreferences {if(raw.length>8192)throw new Error('PREFERENCES_SIZE: record exceeds 8192 characters');return parseWorkspacePreferences(JSON.parse(raw));}
38
43
  function patches(value:unknown):Map<Field,WorkspaceTheme|boolean|number> {
39
- const o=object(value,'preference patch');closed(o,['theme','defaultCameraSync','defaultIdleRepeat','panels'],'preference patch');const patch=new Map<Field,WorkspaceTheme|boolean|number>();
40
- if('theme'in o)patch.set('theme',theme(o.theme));for(const field of ['defaultCameraSync','defaultIdleRepeat'] as const)if(field in o)patch.set(field,boolean(o[field],field));
44
+ const o=object(value,'preference patch');closed(o,['theme',...flags,'panels'],'preference patch');const patch=new Map<Field,WorkspaceTheme|boolean|number>();
45
+ if('theme'in o)patch.set('theme',theme(o.theme));for(const field of flags)if(field in o)patch.set(field,boolean(o[field],field));
41
46
  if('panels'in o){const p=object(o.panels,'panel patch');closed(p,Object.keys(panelBounds),'panel patch');for(const key of Object.keys(p) as (keyof WorkspacePanels)[])patch.set(key,panel(p[key],key));}return patch;
42
47
  }
43
48
  /** Per-field Lamport stamps converge even when independent tabs wrote stale whole records.
@@ -1,6 +1,6 @@
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,defaultWorkspaceColumns,InspectorScrollMemory,inspectorTabs,type InspectorTab,type WorkspaceColumns,type WorkspacePanel,type SeparatorRange} from './workspace-layout.ts';
3
+ import {allocateWorkspaceLayout,bindWorkspaceSeparator,bindWorkspaceDrawer,defaultWorkspaceColumns,InspectorScrollMemory,inspectorTabs,workspaceMinimums,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
6
  export function workspaceResizeRange(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,columns:WorkspaceColumns=defaultWorkspaceColumns):SeparatorRange {
@@ -16,11 +16,16 @@ export function workspaceResizeTarget(available:{width:number;height:number},des
16
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
+ /** The selected Inspector section name, from either a selected tab or a pressed header selector. */
20
+ export function selectedInspector(root:Pick<Document,'querySelector'>=document):string|undefined {
21
+ return root.querySelector<HTMLElement>('[data-inspector][aria-selected=true],[data-inspector][aria-pressed=true]')?.dataset.inspector;
22
+ }
19
23
  export interface WorkspaceUI {
20
24
  defaults:()=>WorkspacePreferences;beforeTab:()=>void;afterTab:()=>void;scroll:()=>string;restoreScroll:(value:unknown)=>void;
21
25
  revealInspector:()=>void;refresh:()=>void;dispose:()=>void;
22
26
  }
23
- export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:WorkspaceTheme)=>void}):WorkspaceUI {
27
+ /** `currentTheme`, when given, is the appearance the Settings theme select shows; otherwise it shows the saved default. */
28
+ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:WorkspaceTheme)=>void;currentTheme?:()=>WorkspaceTheme|undefined}):WorkspaceUI {
24
29
  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
30
  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
31
  layout.classList.add('workspace-layout');
@@ -28,11 +33,18 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
28
33
  // A parts explorer placed inside the left column makes that column the explorer: it keeps the explorer's
29
34
  // saved width and the page has no right column. Otherwise sources stay left and the explorer stays right.
30
35
  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-'));
32
- const sourceList=document.createElement('div');sourceList.className='workspace-sources';sourceList.append(...left.childNodes);left.append(sourceList);
36
+ // A section is an Inspector panel only while it carries data-inspector-panel; the review page's Feedback
37
+ // section lives in the review dialog instead and keeps its id without being a panel.
38
+ const tabs=inspectorTabs.filter(tab=>document.getElementById('inspector-'+tab)?.dataset.inspectorPanel===tab);if(!tabs.length)throw new Error('Missing workspace element: inspector-'+inspectorTabs.join('|inspector-'));
39
+ // The left column is a fixed header row (a leading `.left-header`, when the page has one) and one scrolling
40
+ // body holding everything else, Settings included: the body is the column's only scroll container.
41
+ const header=[...left.children].find(node=>node.className.split(' ').includes('left-header'))??null;
42
+ const sourceList=document.createElement('div');sourceList.className='workspace-sources';sourceList.append(...[...left.childNodes].filter(node=>node!==header));left.append(sourceList);
33
43
  const settings=document.createElement('section');settings.className='workspace-settings';settings.setAttribute('aria-label','Settings');
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>';
35
- left.append(settings);
44
+ // The header holds the expand/collapse control; the body is hidden while collapsed (a workspace preference, default collapsed).
45
+ const settingsHeader=document.createElement('h2'),settingsToggle=document.createElement('button'),settingsBody=document.createElement('div');settingsHeader.className='sticky-header settings-header';settingsToggle.type='button';settingsToggle.id='settings-toggle';settingsToggle.className='settings-toggle';settingsToggle.textContent='Settings';settingsToggle.setAttribute('aria-controls','settings-body');settingsBody.id='settings-body';settingsBody.className='settings-body';settingsHeader.append(settingsToggle);settings.append(settingsHeader,settingsBody);
46
+ settingsBody.innerHTML='<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>';
47
+ sourceList.append(settings);
36
48
  const theme=require<HTMLSelectElement>('settings-theme'),sync=require<HTMLInputElement>('settings-sync'),idle=require<HTMLInputElement>('settings-idle'),status=require('settings-state');
37
49
  const store=new WorkspacePreferenceStore({onStatus:value=>{status.textContent=value.detail;}});let desired={...store.get().panels};
38
50
  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);
@@ -46,10 +58,12 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
46
58
  const handle=document.createElement('div');handle.className='workspace-separator workspace-'+panel+(sides[panel]?' workspace-side-'+sides[panel]:'');handles[panel]=handle;
47
59
  if(sides[panel]==='left')node(panel).after(handle);else if(sides[panel]==='right')node(panel).before(handle);else inspector.before(handle);
48
60
  const trigger=document.createElement('button');trigger.type='button';trigger.textContent=labels[panel];trigger.hidden=true;triggers.append(trigger);buttons[panel]=trigger;
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;
61
+ 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';
62
+ // The drawer's header stays above its scrolling panel: title and the close action.
63
+ const header=document.createElement('div');header.className='sticky-header workspace-drawer-header';const title=document.createElement('span');title.textContent=labels[panel];header.append(title,close);dialog.append(header);document.body.append(dialog);dialogs[panel]=dialog;
50
64
  disposers.push(bindWorkspaceDrawer({trigger,drawer:dialog,label:labels[panel],close}));
51
65
  }
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]!;};
66
+ const memory=new InspectorScrollMemory();const active=():InspectorTab=>{const tab=selectedInspector() as InspectorTab|undefined;return tab&&tabs.includes(tab)?tab:tabs[0]!;};
53
67
  let disposed=false,frame=0,applying=false,available={width:0,height:0},effective=allocateWorkspaceLayout(available,desired,columns);
54
68
  const panelFor=(tab:InspectorTab)=>require('inspector-'+tab);
55
69
  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);};
@@ -69,14 +83,14 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
69
83
  available={width:Math.max(0,layout.clientWidth),height:Math.max(0,layout.clientHeight-chrome)};effective=allocateWorkspaceLayout(available,desired,columns);
70
84
  const mode=(panel:WorkspacePanel)=>effective[panel==='leftWidth'?'leftMode':panel==='rightWidth'?'rightMode':'inspectorMode'];
71
85
  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);
86
+ const gap=workspaceMinimums.separator,side=(key:WorkspaceColumns['left'])=>key&&mode(key)==='panel'?{width:effective[key],separator:gap}:{width:0,separator:0},l=side(columns.left),r=side(columns.right);
73
87
  layout.style.setProperty('--workspace-columns',`${l.width}px ${l.separator}px minmax(0,1fr) ${r.separator}px ${r.width}px`);
74
- main.style.gridTemplateRows=`minmax(0,1fr) ${effective.inspectorMode==='panel'?8:0}px ${effective.inspectorHeight}px`;
88
+ main.style.gridTemplateRows=`minmax(0,1fr) ${effective.inspectorMode==='panel'?gap:0}px ${effective.inspectorHeight}px`;
75
89
  layout.dataset.stageMinimum=String(effective.stageMinimumSatisfied);for(const binding of bindings)binding.refresh();options.changed();restore();
76
90
  }finally{applying=false;}
77
91
  }
78
- const renderSettings=()=>{const value=store.get();theme.value=value.theme;sync.checked=value.defaultCameraSync;idle.checked=value.defaultIdleRepeat;status.textContent=store.status().detail;};
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();
92
+ const renderSettings=()=>{const value=store.get();settingsToggle.setAttribute('aria-expanded',String(!value.settingsCollapsed));settingsBody.hidden=value.settingsCollapsed;settings.dataset.collapsed=String(value.settingsCollapsed);theme.value=options.currentTheme?.()??value.theme;sync.checked=value.defaultCameraSync;idle.checked=value.defaultIdleRepeat;status.textContent=store.status().detail;};
93
+ 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();settingsToggle.onclick=()=>{store.update({settingsCollapsed:!store.get().settingsCollapsed});};
80
94
  const unsubscribe=store.subscribe(value=>{desired={...value.panels};renderSettings();schedule();});renderSettings();
81
95
  for(const tab of tabs){
82
96
  const panel=panelFor(tab);let user=false,timer:ReturnType<typeof setTimeout>|undefined;