privateboard 0.1.11 → 0.1.13
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/dist/cli.js +4260 -1660
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/public/adjourn-overlay.css +8 -4
- package/public/agent-profile.css +345 -0
- package/public/agent-profile.js +222 -3
- package/public/app.js +4990 -149
- package/public/home.html +609 -1
- package/public/i18n.js +49 -385
- package/public/index.html +9549 -5314
- package/public/new-agent.js +148 -37
- package/public/quote-cta.css +7 -3
- package/public/quote-cta.js +21 -0
- package/public/report.html +109 -12
- package/public/room-settings.css +67 -0
- package/public/room-settings.js +72 -6
- package/public/themes.css +38 -0
- package/public/typing-sfx.js +87 -1
- package/public/user-settings.js +3 -1
- package/public/voice-replay.css +70 -1
- package/public/voice-replay.js +265 -2
package/public/room-settings.js
CHANGED
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
intensity: "sharp",
|
|
167
167
|
style: "auto",
|
|
168
168
|
incognito: false,
|
|
169
|
+
voteTrigger: "auto",
|
|
169
170
|
history: [
|
|
170
171
|
{ ts: "Apr 28 · 21:08", who: "system", kind: "open", label: "room opened" }
|
|
171
172
|
]
|
|
@@ -173,7 +174,7 @@
|
|
|
173
174
|
|
|
174
175
|
// Staged changes layered on top of ROOM_STATE — committed only when
|
|
175
176
|
// the user clicks Confirm. The shape mirrors the room config keys.
|
|
176
|
-
let STAGED = { mode: null, intensity: null, incognito: null };
|
|
177
|
+
let STAGED = { mode: null, intensity: null, incognito: null, voteTrigger: null };
|
|
177
178
|
// Snapshot of ROOM_STATE.members at overlay-open time. The members
|
|
178
179
|
// array itself is mutated optimistically by add/removeMember; this
|
|
179
180
|
// baseline lets us detect "dirty" by diffing and lets us roll back
|
|
@@ -196,11 +197,12 @@
|
|
|
196
197
|
STAGED.mode !== null ||
|
|
197
198
|
STAGED.intensity !== null ||
|
|
198
199
|
STAGED.incognito !== null ||
|
|
200
|
+
STAGED.voteTrigger !== null ||
|
|
199
201
|
membersDirty()
|
|
200
202
|
);
|
|
201
203
|
}
|
|
202
204
|
function resetStaged() {
|
|
203
|
-
STAGED = { mode: null, intensity: null, incognito: null };
|
|
205
|
+
STAGED = { mode: null, intensity: null, incognito: null, voteTrigger: null };
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
const MODES = [
|
|
@@ -362,6 +364,29 @@
|
|
|
362
364
|
</label>
|
|
363
365
|
</div>
|
|
364
366
|
|
|
367
|
+
<div class="rs-config-row">
|
|
368
|
+
<div class="rs-config-row-label">
|
|
369
|
+
<span class="rs-config-row-name">Vote phase</span>
|
|
370
|
+
<span class="rs-config-row-hint">when the chair opens the round-end vote</span>
|
|
371
|
+
</div>
|
|
372
|
+
<div class="rs-vote-trigger-grp" role="radiogroup" aria-label="Vote phase trigger">
|
|
373
|
+
<label class="rs-radio-row" data-rs-vote-trigger-row="auto">
|
|
374
|
+
<input type="radio" name="rs-vote-trigger" value="auto" data-rs-vote-trigger-input>
|
|
375
|
+
<span class="rs-radio-label">
|
|
376
|
+
<span class="rs-radio-name">Auto</span>
|
|
377
|
+
<span class="rs-radio-desc">chair drops the vote prompt at every round wrap</span>
|
|
378
|
+
</span>
|
|
379
|
+
</label>
|
|
380
|
+
<label class="rs-radio-row" data-rs-vote-trigger-row="manual">
|
|
381
|
+
<input type="radio" name="rs-vote-trigger" value="manual" data-rs-vote-trigger-input>
|
|
382
|
+
<span class="rs-radio-label">
|
|
383
|
+
<span class="rs-radio-name">Manual</span>
|
|
384
|
+
<span class="rs-radio-desc">you trigger the vote with the bottom-bar button</span>
|
|
385
|
+
</span>
|
|
386
|
+
</label>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
365
390
|
</div>
|
|
366
391
|
</div>
|
|
367
392
|
|
|
@@ -544,6 +569,16 @@
|
|
|
544
569
|
checkbox.checked = !!effective;
|
|
545
570
|
}
|
|
546
571
|
|
|
572
|
+
function renderVoteTrigger() {
|
|
573
|
+
const eff = STAGED.voteTrigger !== null ? STAGED.voteTrigger : ROOM_STATE.voteTrigger;
|
|
574
|
+
modal.querySelectorAll("[data-rs-vote-trigger-input]").forEach((input) => {
|
|
575
|
+
input.checked = input.value === eff;
|
|
576
|
+
});
|
|
577
|
+
modal.querySelectorAll("[data-rs-vote-trigger-row]").forEach((row) => {
|
|
578
|
+
row.classList.toggle("active", row.dataset.rsVoteTriggerRow === eff);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
547
582
|
function renderIntensity() {
|
|
548
583
|
// Intensity is now a 3-chip row (Calm / Sharp / Terse) instead of
|
|
549
584
|
// a slider · highlight the active chip. The hint line above shows
|
|
@@ -602,6 +637,7 @@
|
|
|
602
637
|
ROOM_STATE.mode = room.mode || "constructive";
|
|
603
638
|
ROOM_STATE.intensity = room.intensity || "sharp";
|
|
604
639
|
ROOM_STATE.incognito = room.incognito === true;
|
|
640
|
+
ROOM_STATE.voteTrigger = room.voteTrigger === "manual" ? "manual" : "auto";
|
|
605
641
|
if (Array.isArray(app.currentMembers) && app.currentMembers.length) {
|
|
606
642
|
ROOM_STATE.members = app.currentMembers.map((a) => a.id);
|
|
607
643
|
}
|
|
@@ -627,6 +663,7 @@
|
|
|
627
663
|
renderModes();
|
|
628
664
|
renderIntensity();
|
|
629
665
|
renderIncognito();
|
|
666
|
+
renderVoteTrigger();
|
|
630
667
|
renderConfirmState();
|
|
631
668
|
closeCastPicker();
|
|
632
669
|
overlay.classList.add("open");
|
|
@@ -738,16 +775,23 @@
|
|
|
738
775
|
renderIncognito();
|
|
739
776
|
renderConfirmState();
|
|
740
777
|
}
|
|
778
|
+
function stageVoteTrigger(next) {
|
|
779
|
+
const v = next === "manual" ? "manual" : "auto";
|
|
780
|
+
STAGED.voteTrigger = v === ROOM_STATE.voteTrigger ? null : v;
|
|
781
|
+
renderVoteTrigger();
|
|
782
|
+
renderConfirmState();
|
|
783
|
+
}
|
|
741
784
|
|
|
742
785
|
/** Push staged config + member changes to the backend. */
|
|
743
786
|
async function commit() {
|
|
744
787
|
if (!isDirty()) { close(); return; }
|
|
745
788
|
|
|
746
|
-
// Build the settings patch (mode / intensity / incognito).
|
|
789
|
+
// Build the settings patch (mode / intensity / incognito / voteTrigger).
|
|
747
790
|
const patch = {};
|
|
748
|
-
if (STAGED.mode !== null)
|
|
749
|
-
if (STAGED.intensity !== null)
|
|
750
|
-
if (STAGED.incognito !== null)
|
|
791
|
+
if (STAGED.mode !== null) patch.mode = STAGED.mode;
|
|
792
|
+
if (STAGED.intensity !== null) patch.intensity = STAGED.intensity;
|
|
793
|
+
if (STAGED.incognito !== null) patch.incognito = STAGED.incognito;
|
|
794
|
+
if (STAGED.voteTrigger !== null) patch.voteTrigger = STAGED.voteTrigger;
|
|
751
795
|
|
|
752
796
|
const btn = modal.querySelector("[data-rs-confirm]");
|
|
753
797
|
const status = modal.querySelector("[data-rs-status]");
|
|
@@ -793,11 +837,13 @@
|
|
|
793
837
|
mode: ROOM_STATE.mode,
|
|
794
838
|
intensity: ROOM_STATE.intensity,
|
|
795
839
|
incognito: ROOM_STATE.incognito,
|
|
840
|
+
voteTrigger: ROOM_STATE.voteTrigger,
|
|
796
841
|
};
|
|
797
842
|
const after = {
|
|
798
843
|
mode: patch.mode ?? ROOM_STATE.mode,
|
|
799
844
|
intensity: patch.intensity ?? ROOM_STATE.intensity,
|
|
800
845
|
incognito: typeof patch.incognito === "boolean" ? patch.incognito : ROOM_STATE.incognito,
|
|
846
|
+
voteTrigger: patch.voteTrigger ?? ROOM_STATE.voteTrigger,
|
|
801
847
|
};
|
|
802
848
|
Object.assign(ROOM_STATE, after);
|
|
803
849
|
if (STAGED.mode !== null) {
|
|
@@ -812,6 +858,19 @@
|
|
|
812
858
|
logEvent({ kind: "incognito", before: before.incognito, after: after.incognito,
|
|
813
859
|
label: `memory: ${before.incognito ? "incognito" : "default"} → ${after.incognito ? "incognito" : "default"}` });
|
|
814
860
|
}
|
|
861
|
+
if (STAGED.voteTrigger !== null) {
|
|
862
|
+
logEvent({ kind: "voteTrigger", before: before.voteTrigger, after: after.voteTrigger,
|
|
863
|
+
label: `vote phase: ${before.voteTrigger} → ${after.voteTrigger}` });
|
|
864
|
+
}
|
|
865
|
+
// Mirror the new voteTrigger onto the live app.currentRoom so the
|
|
866
|
+
// bottom-bar manual button shows/hides immediately without
|
|
867
|
+
// waiting for an SSE round-trip.
|
|
868
|
+
if (STAGED.voteTrigger !== null && window.app && window.app.currentRoom) {
|
|
869
|
+
window.app.currentRoom.voteTrigger = after.voteTrigger;
|
|
870
|
+
if (typeof window.app.refreshManualVoteButton === "function") {
|
|
871
|
+
window.app.refreshManualVoteButton();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
815
874
|
resetStaged();
|
|
816
875
|
if (btn) { btn.disabled = false; btn.textContent = "[ Confirm ]"; }
|
|
817
876
|
close();
|
|
@@ -1040,6 +1099,13 @@
|
|
|
1040
1099
|
setTimeout(() => stageIncognito(incBox.checked), 0);
|
|
1041
1100
|
return;
|
|
1042
1101
|
}
|
|
1102
|
+
// Vote trigger radio · same defer-and-read-state pattern as
|
|
1103
|
+
// incognito so the native input flip resolves before we stage.
|
|
1104
|
+
const vtInput = e.target.closest("[data-rs-vote-trigger-input]");
|
|
1105
|
+
if (vtInput) {
|
|
1106
|
+
setTimeout(() => stageVoteTrigger(vtInput.value), 0);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1043
1109
|
}, true);
|
|
1044
1110
|
|
|
1045
1111
|
// Pointer-driven drag for the intensity slider (mouse + touch + stylus).
|
package/public/themes.css
CHANGED
|
@@ -315,6 +315,44 @@
|
|
|
315
315
|
--magenta: #C5839E;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/* ─── NINTENDO · 8-bit NES palette · Mario red on dark sky ───
|
|
319
|
+
Built from the classic NES title-screen vocabulary: deep
|
|
320
|
+
night-sky background, pure-white NES title text, Mario red
|
|
321
|
+
as the primary accent (the slot called `--lime*` in this
|
|
322
|
+
codebase), coin-block gold for amber, fire-flower red for
|
|
323
|
+
warnings, and sky-cloud cyan for the moderator accent. Every
|
|
324
|
+
swatch is high-saturation / high-contrast in the NES family —
|
|
325
|
+
no muted desaturated tones (those don't belong in 8-bit). */
|
|
326
|
+
:root[data-theme="nintendo"] {
|
|
327
|
+
--bg: #181820;
|
|
328
|
+
--panel: #22222C;
|
|
329
|
+
--panel-2: #2A2A38;
|
|
330
|
+
--panel-3: #34344A;
|
|
331
|
+
--hi: #44445A;
|
|
332
|
+
|
|
333
|
+
--line: #2C2C3A;
|
|
334
|
+
--line-bright: #44445A;
|
|
335
|
+
--line-strong: #5C5C72;
|
|
336
|
+
|
|
337
|
+
--text: #FCFCFC;
|
|
338
|
+
--text-soft: #C8C8D8;
|
|
339
|
+
--text-dim: #8E8EA0;
|
|
340
|
+
--text-faint: #5C5C70;
|
|
341
|
+
|
|
342
|
+
/* Primary accent · Mario red. Token names stay `--lime*` so every
|
|
343
|
+
callsite that already references the primary keeps working. */
|
|
344
|
+
--lime: #E60012;
|
|
345
|
+
--lime-deep: #A40009;
|
|
346
|
+
--lime-dim: #4F0007;
|
|
347
|
+
|
|
348
|
+
--amber: #FBC000; /* coin / question block */
|
|
349
|
+
--amber-dim: #6B4D00;
|
|
350
|
+
--red: #FC4438; /* fire flower */
|
|
351
|
+
--red-dim: #6E1A14;
|
|
352
|
+
--cyan: #5BC0EB; /* Mario sky / chair moderator */
|
|
353
|
+
--magenta: #FC74FC; /* princess pink / power-up */
|
|
354
|
+
}
|
|
355
|
+
|
|
318
356
|
/* ─── REGENT · gold-on-warm-dark · the eastwood twin
|
|
319
357
|
The same warm dark palette as the default eastwood, with the
|
|
320
358
|
primary forest-green accent swapped for the warm gold used by
|
package/public/typing-sfx.js
CHANGED
|
@@ -142,6 +142,92 @@
|
|
|
142
142
|
noise.stop(t0 + 0.06);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/** Speaker-change cue · fires once when the round-table stage flips
|
|
146
|
+
* to a new speaker (idle → A, A → B). A short triangle-wave swoop
|
|
147
|
+
* at ~660 → 990 Hz, decaying over ~220ms — distinct from the
|
|
148
|
+
* keyboard-click texture of `tick()` so the ear reads it as a
|
|
149
|
+
* scene transition rather than typing. Same enabled-flag and
|
|
150
|
+
* AudioContext as tick · the user-settings toggle controls both. */
|
|
151
|
+
function speakerChange() {
|
|
152
|
+
if (!_enabled) return;
|
|
153
|
+
if (document.visibilityState !== "visible") return;
|
|
154
|
+
const ctx = ensureContext();
|
|
155
|
+
if (!ctx) return;
|
|
156
|
+
const t0 = ctx.currentTime;
|
|
157
|
+
const dur = 0.22;
|
|
158
|
+
const osc = ctx.createOscillator();
|
|
159
|
+
osc.type = "triangle";
|
|
160
|
+
// Brief upward sweep · 660Hz → 990Hz across the first 70ms, then
|
|
161
|
+
// hold while the gain envelope fades. Triangle waveform reads as
|
|
162
|
+
// softer / more "chime"-like than sine for this register.
|
|
163
|
+
osc.frequency.setValueAtTime(660, t0);
|
|
164
|
+
osc.frequency.exponentialRampToValueAtTime(990, t0 + 0.07);
|
|
165
|
+
const gain = ctx.createGain();
|
|
166
|
+
gain.gain.setValueAtTime(0.0001, t0);
|
|
167
|
+
gain.gain.exponentialRampToValueAtTime(0.085, t0 + 0.01);
|
|
168
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
|
169
|
+
osc.connect(gain).connect(ctx.destination);
|
|
170
|
+
osc.start(t0);
|
|
171
|
+
osc.stop(t0 + dur);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Chair gavel cue · fires before chair voice playback begins in
|
|
175
|
+
* voice mode. Two-strike wooden knock that reads as "court is in
|
|
176
|
+
* session — listen up." Designed by ear:
|
|
177
|
+
* · Layer 1 · low sine ~190 Hz, 1ms attack + 220ms decay,
|
|
178
|
+
* gives the bass "thunk" of wood on wood.
|
|
179
|
+
* · Layer 2 · filtered noise burst at ~3.2 kHz, 30ms total,
|
|
180
|
+
* provides the sharp percussive transient.
|
|
181
|
+
* · Two strikes 160ms apart so the ear hears a deliberate
|
|
182
|
+
* "knock-knock" pattern (single strike read as a glitch /
|
|
183
|
+
* typewriter chime in early prototyping). */
|
|
184
|
+
function gavel() {
|
|
185
|
+
if (!_enabled) return;
|
|
186
|
+
if (document.visibilityState !== "visible") return;
|
|
187
|
+
const ctx = ensureContext();
|
|
188
|
+
if (!ctx) return;
|
|
189
|
+
const t0 = ctx.currentTime;
|
|
190
|
+
const strikes = [0, 0.16];
|
|
191
|
+
for (const offset of strikes) {
|
|
192
|
+
const t = t0 + offset;
|
|
193
|
+
// Body resonance · low sine, fat envelope.
|
|
194
|
+
const body = ctx.createOscillator();
|
|
195
|
+
body.type = "sine";
|
|
196
|
+
body.frequency.setValueAtTime(220, t);
|
|
197
|
+
body.frequency.exponentialRampToValueAtTime(140, t + 0.18);
|
|
198
|
+
const bodyGain = ctx.createGain();
|
|
199
|
+
bodyGain.gain.setValueAtTime(0.0001, t);
|
|
200
|
+
bodyGain.gain.exponentialRampToValueAtTime(0.18, t + 0.005);
|
|
201
|
+
bodyGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
|
|
202
|
+
body.connect(bodyGain).connect(ctx.destination);
|
|
203
|
+
body.start(t);
|
|
204
|
+
body.stop(t + 0.25);
|
|
205
|
+
// Transient click · 30ms filtered noise burst.
|
|
206
|
+
const noiseBuf = ctx.createBuffer(
|
|
207
|
+
1,
|
|
208
|
+
Math.max(1, Math.floor(ctx.sampleRate * 0.03)),
|
|
209
|
+
ctx.sampleRate,
|
|
210
|
+
);
|
|
211
|
+
const noiseData = noiseBuf.getChannelData(0);
|
|
212
|
+
for (let i = 0; i < noiseData.length; i++) {
|
|
213
|
+
noiseData[i] = (Math.random() * 2 - 1) * (1 - i / noiseData.length);
|
|
214
|
+
}
|
|
215
|
+
const noise = ctx.createBufferSource();
|
|
216
|
+
noise.buffer = noiseBuf;
|
|
217
|
+
const bp = ctx.createBiquadFilter();
|
|
218
|
+
bp.type = "bandpass";
|
|
219
|
+
bp.frequency.value = 3200;
|
|
220
|
+
bp.Q.value = 0.8;
|
|
221
|
+
const noiseGain = ctx.createGain();
|
|
222
|
+
noiseGain.gain.setValueAtTime(0.0001, t);
|
|
223
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.10, t + 0.003);
|
|
224
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.035);
|
|
225
|
+
noise.connect(bp).connect(noiseGain).connect(ctx.destination);
|
|
226
|
+
noise.start(t);
|
|
227
|
+
noise.stop(t + 0.05);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
145
231
|
function setEnabled(on) {
|
|
146
232
|
_enabled = !!on;
|
|
147
233
|
writeEnabled(_enabled);
|
|
@@ -154,5 +240,5 @@
|
|
|
154
240
|
|
|
155
241
|
// Public surface · attached to window so app.js (and the
|
|
156
242
|
// user-settings toggle) can reach it without an import.
|
|
157
|
-
window.boardroomTypingSfx = { tick, setEnabled, isEnabled };
|
|
243
|
+
window.boardroomTypingSfx = { tick, speakerChange, gavel, setEnabled, isEnabled };
|
|
158
244
|
})();
|
package/public/user-settings.js
CHANGED
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
{ slug: "nebirhos", name: "Nebirhos", desc: "teal · warm orange highlights",
|
|
41
41
|
swatches: ["#0A1414","#11201F","#5EB1A6","#357770","#DD9258","#D87060","#6FBEC2","#B8D4D0"] },
|
|
42
42
|
{ slug: "wedisagree", name: "We Disagree", desc: "argumentative orange · subtle green",
|
|
43
|
-
swatches: ["#14110E","#1F1A14","#DD7B40","#A8521E","#E6B872","#E26060","#6FB28A","#D8CBBC"] }
|
|
43
|
+
swatches: ["#14110E","#1F1A14","#DD7B40","#A8521E","#E6B872","#E26060","#6FB28A","#D8CBBC"] },
|
|
44
|
+
{ slug: "nintendo", name: "8-bit", desc: "NES palette · Mario red · coin gold · sky cyan",
|
|
45
|
+
swatches: ["#181820","#22222C","#E60012","#A40009","#FBC000","#FC4438","#5BC0EB","#FCFCFC"] }
|
|
44
46
|
];
|
|
45
47
|
|
|
46
48
|
const PROVIDERS = [
|
package/public/voice-replay.css
CHANGED
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
.voice-replay-overlay {
|
|
18
18
|
position: fixed;
|
|
19
19
|
right: 24px;
|
|
20
|
-
|
|
20
|
+
/* The adjourned-bar (where Voice Replay is launched from) is
|
|
21
|
+
`min-height: 44px` and pinned to the bottom of the room view.
|
|
22
|
+
`bottom: 24px` would overlap it; lift the panel to clear the
|
|
23
|
+
bar with a small gap so the user always reads the bar's
|
|
24
|
+
[Export / Voice Replay / Convene Follow-up] actions even while
|
|
25
|
+
the player is open. Same offset for expanded + collapsed so
|
|
26
|
+
the title bar sits flush above the bottom bar when shrunk. */
|
|
27
|
+
bottom: 56px;
|
|
21
28
|
z-index: 9300;
|
|
22
29
|
width: 380px;
|
|
23
30
|
max-width: calc(100vw - 32px);
|
|
@@ -56,6 +63,12 @@
|
|
|
56
63
|
font-size: 13px;
|
|
57
64
|
letter-spacing: 0;
|
|
58
65
|
}
|
|
66
|
+
.vr-head-actions {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 4px;
|
|
70
|
+
}
|
|
71
|
+
.vr-collapse,
|
|
59
72
|
.vr-close {
|
|
60
73
|
width: 22px;
|
|
61
74
|
height: 22px;
|
|
@@ -68,13 +81,69 @@
|
|
|
68
81
|
font-size: 11px;
|
|
69
82
|
cursor: pointer;
|
|
70
83
|
transition: border-color 0.12s, color 0.12s;
|
|
84
|
+
font-family: var(--mono);
|
|
85
|
+
line-height: 1;
|
|
71
86
|
}
|
|
87
|
+
.vr-collapse { font-size: 14px; padding-bottom: 4px; }
|
|
88
|
+
.vr-collapse:hover,
|
|
72
89
|
.vr-close:hover { border-color: var(--lime, #6FB572); color: var(--lime, #6FB572); }
|
|
73
90
|
|
|
74
91
|
.vr-body {
|
|
75
92
|
padding: 14px 14px 14px;
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
/* Collapsed posture · the entire floating panel hides, freeing
|
|
96
|
+
the chat / round-table stage of any overlap. The user re-opens
|
|
97
|
+
by clicking the inline `.vr-inline-expand` pill that gets
|
|
98
|
+
slotted into the adjourned-bar's action group right next to the
|
|
99
|
+
Voice Replay button. Audio keeps playing in the background. */
|
|
100
|
+
.voice-replay-overlay.is-collapsed {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Inline replay control group · three buttons (Next / Pause /
|
|
105
|
+
Expand) mounted into the adjourned-bar's action group right
|
|
106
|
+
after Voice Replay when the floating panel is collapsed. Each
|
|
107
|
+
button inherits `.ghost-btn`'s panel + hairline chrome so the
|
|
108
|
+
group reads as a sibling of Export / Voice Replay / Convene
|
|
109
|
+
Follow-up. The Expand button carries a small lime pulsing dot
|
|
110
|
+
so the user can locate "what's still active" at a glance. */
|
|
111
|
+
.vr-inline-group {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 6px;
|
|
115
|
+
}
|
|
116
|
+
/* Icon-only ghost-btns · all the visual chrome (panel bg, hairline
|
|
117
|
+
border, mono font, uppercase, hover lime) comes from `.ghost-btn`
|
|
118
|
+
in index.html. We don't override font-size / color / padding so
|
|
119
|
+
the inline group reads as a true sibling of Export / Voice
|
|
120
|
+
Replay / Convene Follow-up — just with bracket-wrapped glyphs
|
|
121
|
+
instead of trailing labels. The Expand button gets a small lime
|
|
122
|
+
pulsing dot tagged on the end so the user can locate "what's
|
|
123
|
+
still active" at a glance. */
|
|
124
|
+
.vr-inline-btn .vib-icon {
|
|
125
|
+
display: block; /* drop the inline baseline gap so the SVG centres */
|
|
126
|
+
}
|
|
127
|
+
.vr-inline-expand {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 6px;
|
|
131
|
+
}
|
|
132
|
+
.vr-inline-expand .vie-pulse {
|
|
133
|
+
width: 5px;
|
|
134
|
+
height: 5px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
background: var(--lime, #6FB572);
|
|
137
|
+
animation: vr-inline-pulse 1.6s ease-in-out infinite;
|
|
138
|
+
}
|
|
139
|
+
@keyframes vr-inline-pulse {
|
|
140
|
+
0%, 100% { opacity: 1; }
|
|
141
|
+
50% { opacity: 0.35; }
|
|
142
|
+
}
|
|
143
|
+
@media (prefers-reduced-motion: reduce) {
|
|
144
|
+
.vr-inline-expand .vie-pulse { animation: none; opacity: 0.7; }
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
/* ─── Loading row ───
|
|
79
148
|
Centered both axes so the loading state reads as a focused
|
|
80
149
|
placeholder, not a left-pinned debug log. The dots row sits
|