benpeloquin7 commited on
Commit
424a971
·
verified ·
1 Parent(s): 3353de8

Viewer: Combined/Source groups with a clear break, recording specs, current-call colour, sibling-set link

Browse files
Files changed (3) hide show
  1. app.js +58 -37
  2. index.html +10 -6
  3. style.css +16 -5
app.js CHANGED
@@ -21,8 +21,9 @@
21
  title: $("call-title"), meta: $("call-meta"), desc: $("call-desc"),
22
  play: $("play"), clock: $("clock"), muteAgent: $("mute-agent"), muteCustomer: $("mute-customer"), muteNoise: $("mute-noise"),
23
  listenSources: $("listen-sources"), listenCombined: $("listen-combined"), groupSources: $("group-sources"), groupCombined: $("group-combined"),
24
- combinedName: $("combined-name"), combinedNote: $("combined-note"), sourcesNote: $("sources-note"), keyNoise: $("key-noise"),
25
- badgeSources: $("badge-sources"), badgeCombined: $("badge-combined"), recipe: $("recipe"), laneL: $("lane-l-note"), laneR: $("lane-r-note"), calllines: $("calllines"),
 
26
  stage: $("stage"), waveAgent: $("wave-agent"), waveCustomer: $("wave-customer"), waveNoise: $("wave-noise"), rowNoise: $("row-noise"),
27
  waveCombL: $("wave-combined-l"), waveCombR: $("wave-combined-r"), markers: $("markers"), axis: $("axis"),
28
  playhead: $("playhead"), tip: $("tip"), nowBody: $("now-body"), logBody: $("log").querySelector("tbody"), logCount: $("log-count"),
@@ -85,14 +86,20 @@
85
  const title = PINNED ? set.title : (sets.title || document.body.dataset.title);
86
  el.setTitle.textContent = title; document.title = title;
87
  el.setBlurb.textContent = PINNED ? (set.blurb || "") : (set.blurb ? `${set.label}: ${set.blurb}` : set.label);
88
- el.siblings.replaceChildren();
89
- for (const other of sets.sets) {
90
- if (other.name === set.name) continue;
91
- const a = text("a", `Also: ${other.label}`);
92
- a.href = PINNED && other.space_url ? other.space_url : `?set=${encodeURIComponent(other.name)}`;
93
- if (PINNED && other.space_url) { a.target = "_blank"; a.rel = "noopener"; }
94
- a.title = other.title;
95
- el.siblings.append(a);
 
 
 
 
 
 
96
  }
97
  if (!PINNED && sets.sets.length > 1) {
98
  el.setSwitch.hidden = false;
@@ -142,11 +149,12 @@
142
  if (state.hasNoise) setSource(el.aNoise, paths.proxy(row.take_id, "noise"), paths.wav(row.noise_file));
143
  else { el.aNoise.removeAttribute("src"); el.aNoise.load(); }
144
  setSource(el.aCombined, paths.proxy(row.take_id, "combined"), paths.wav(row.file_name));
145
- const renoised = row.stereo_content === "renoised";
146
- el.combinedName.textContent = renoised ? "Processed call" : "Combined call";
147
- el.combinedNote.textContent = renoised ? "what an agent-side system would receive, one stereo file" : "the two studio tracks side by side in one stereo file, no processing";
148
- el.sourcesNote.textContent = `what was captured, ${state.hasNoise ? "three" : "two"} separate tracks; switch each on or off`;
149
- describeCombined(row);
 
150
  applyListen();
151
  renderLog(); setNow(-1); drawAll(); frame();
152
  try {
@@ -219,25 +227,31 @@
219
  el.groupSources.classList.toggle("inactive", !src); el.groupCombined.classList.toggle("inactive", src);
220
  el.badgeSources.hidden = !src; el.badgeCombined.hidden = src;
221
  }
222
- function describeCombined(row) {
 
 
 
 
223
  let r = {};
224
  try { r = JSON.parse(row.renoised || "{}") || {}; } catch { r = {}; }
225
- if (row.stereo_content !== "renoised") {
226
- el.laneL.textContent = "customer, clean"; el.laneR.textContent = "agent, clean";
227
- el.recipe.textContent = "Combined call: the two clean tracks side by side, customer left, agent right; no processing. " +
228
- "Studio recordings play at their recorded levels.";
229
- return;
 
 
 
 
 
 
 
 
230
  }
231
- const snr = (r.noise_mix || {}).snr || {}, room = r.room_simulation || {}, loud = r.loudness_normalisation || {};
232
- const bed = ((r.noise_mix || {}).bed || row.noise_bed || "ambient").toLowerCase();
233
- el.laneL.textContent = `customer, cabin + ${bed} bed`; el.laneR.textContent = "agent, normalised";
234
- el.recipe.textContent =
235
- `Combined call: customer channel through a simulated car cabin${room.rt60_s != null ? ` (RT60 ${Number(room.rt60_s).toFixed(2)} s)` : ""}, ` +
236
- `mixed with the ${bed} bed at ${snr.target_db_a ?? "?"} dB(A) signal-to-noise ratio over the customer's speech` +
237
- `${snr.measured_db_a != null ? ` (measured ${snr.measured_db_a})` : ""}; agent channel clean; both channels normalised to ` +
238
- `${loud.target_lufs ?? "?"} LUFS under a ${loud.true_peak_ceiling_dbtp ?? -1} dBTP ceiling; 24-bit. ` +
239
- "Studio recordings play at their recorded levels, so loudness differs between the two groups by design. " +
240
- "The combined lanes reuse the customer and agent colours: same people, processed.";
241
  }
242
  document.addEventListener("keydown", (e) => {
243
  if (!state.take || e.target.closest("input, textarea, select")) return;
@@ -267,17 +281,24 @@
267
  const r = el.waveAgent.getBoundingClientRect(), s = el.stage.getBoundingClientRect();
268
  el.playhead.hidden = false; el.playhead.style.left = `${r.left - s.left + xOf(t)}px`;
269
  }
270
- /* One hairline per tool call through every waveform row, from the first row to the marker strip. */
 
271
  function drawCallLines() {
272
  const s = el.stage.getBoundingClientRect();
273
- const first = el.stage.querySelector(".group .row canvas").getBoundingClientRect(), m = el.markers.getBoundingClientRect();
 
 
274
  Object.assign(el.calllines.style, { left: `${first.left - s.left}px`, width: `${first.width}px`, top: `${first.top - s.top}px`, height: `${m.top - first.top}px` });
 
275
  el.calllines.replaceChildren();
276
  state.events.forEach((ev, i) => {
277
- const line = document.createElement("div");
278
- line.className = "callline" + (ev.is_error ? " err" : "") + (i === state.current ? " current" : "");
279
- line.dataset.idx = i; line.style.left = `${xOf(ev.onset_time)}px`;
280
- el.calllines.append(line);
 
 
 
281
  });
282
  }
283
  for (const c of WAVES()) {
 
21
  title: $("call-title"), meta: $("call-meta"), desc: $("call-desc"),
22
  play: $("play"), clock: $("clock"), muteAgent: $("mute-agent"), muteCustomer: $("mute-customer"), muteNoise: $("mute-noise"),
23
  listenSources: $("listen-sources"), listenCombined: $("listen-combined"), groupSources: $("group-sources"), groupCombined: $("group-combined"),
24
+ combinedName: $("combined-name"), combinedNote: $("combined-note"), sourcesName: $("sources-name"), sourcesNote: $("sources-note"), keyNoise: $("key-noise"),
25
+ specs: $("specs"), emptySibling: $("empty-sibling"),
26
+ badgeSources: $("badge-sources"), badgeCombined: $("badge-combined"), laneL: $("lane-l-note"), laneR: $("lane-r-note"), calllines: $("calllines"),
27
  stage: $("stage"), waveAgent: $("wave-agent"), waveCustomer: $("wave-customer"), waveNoise: $("wave-noise"), rowNoise: $("row-noise"),
28
  waveCombL: $("wave-combined-l"), waveCombR: $("wave-combined-r"), markers: $("markers"), axis: $("axis"),
29
  playhead: $("playhead"), tip: $("tip"), nowBody: $("now-body"), logBody: $("log").querySelector("tbody"), logCount: $("log-count"),
 
86
  const title = PINNED ? set.title : (sets.title || document.body.dataset.title);
87
  el.setTitle.textContent = title; document.title = title;
88
  el.setBlurb.textContent = PINNED ? (set.blurb || "") : (set.blurb ? `${set.label}: ${set.blurb}` : set.label);
89
+ el.siblings.replaceChildren(); el.emptySibling.replaceChildren();
90
+ const others = sets.sets.filter((o) => o.name !== set.name);
91
+ if (others.length) el.siblings.append(text("span", "Other set:", "muted"));
92
+ el.emptySibling.append(document.createTextNode(`This page is the ${set.label} set (${set.n_takes} calls). `));
93
+ for (const other of others) {
94
+ const link = () => {
95
+ const a = text("a", `${other.label} set ${PINNED && other.space_url ? "↗" : "→"}`, "sibling");
96
+ a.href = PINNED && other.space_url ? other.space_url : `?set=${encodeURIComponent(other.name)}`;
97
+ if (PINNED && other.space_url) { a.target = "_blank"; a.rel = "noopener"; }
98
+ a.title = other.title;
99
+ return a;
100
+ };
101
+ el.siblings.append(link());
102
+ el.emptySibling.append(document.createTextNode(`The ${other.label} set (${other.n_takes} calls) has its own page: `), link(), document.createTextNode(". "));
103
  }
104
  if (!PINNED && sets.sets.length > 1) {
105
  el.setSwitch.hidden = false;
 
149
  if (state.hasNoise) setSource(el.aNoise, paths.proxy(row.take_id, "noise"), paths.wav(row.noise_file));
150
  else { el.aNoise.removeAttribute("src"); el.aNoise.load(); }
151
  setSource(el.aCombined, paths.proxy(row.take_id, "combined"), paths.wav(row.file_name));
152
+ const renoised = row.stereo_content === "renoised", f = formatsOf(row);
153
+ el.combinedName.textContent = "Combined";
154
+ el.combinedNote.textContent = renoised ? "single post-processed stereo file" : "single stereo file, the two source tracks side by side, unprocessed";
155
+ el.sourcesName.textContent = "Source";
156
+ el.sourcesNote.textContent = `studio recordings · ${fmtSpec(f.agent_audio)} speaker-isolated recordings${state.hasNoise ? " and ambient noise" : ""}; switch each on or off`;
157
+ renderSpecs(row, f);
158
  applyListen();
159
  renderLog(); setNow(-1); drawAll(); frame();
160
  try {
 
227
  el.groupSources.classList.toggle("inactive", !src); el.groupCombined.classList.toggle("inactive", src);
228
  el.badgeSources.hidden = !src; el.badgeCombined.hidden = src;
229
  }
230
+ const formatsOf = (row) => { try { return JSON.parse(row.formats || "{}") || {}; } catch { return {}; } };
231
+ const fmtSpec = (f) => f ? `${Math.round((f.sample_rate_hz || 48000) / 1000)} kHz / ${f.bit_depth || "?"}-bit` : "48 kHz / 32-bit";
232
+ /* The "Recording specs" block under the waveforms: what each group is, from the row's format blocks and
233
+ the renoise recipe, so the numbers are the take's own. */
234
+ function renderSpecs(row, f) {
235
  let r = {};
236
  try { r = JSON.parse(row.renoised || "{}") || {}; } catch { r = {}; }
237
+ const renoised = row.stereo_content === "renoised";
238
+ const bed = (row.noise_bed || (r.noise_mix || {}).bed || "").toLowerCase() || "ambient";
239
+ const rows = [["Source", `Agent and customer: ${fmtSpec(f.agent_audio)} mono WAV, one microphone each, unprocessed.`
240
+ + (state.hasNoise ? ` Ambient: the ${bed} bed played into both talents' headphones, ${fmtSpec(f.noise_audio)} mono WAV, studio playback level.` : "")]];
241
+ if (renoised) {
242
+ const snr = (r.noise_mix || {}).snr || {}, room = r.room_simulation || {}, loud = r.loudness_normalisation || {};
243
+ rows.push(["Combined", `${fmtSpec(f.stereo)} stereo WAV. Customer LEFT: the source track through a simulated car cabin`
244
+ + `${room.rt60_s != null ? ` (RT60 ${Number(room.rt60_s).toFixed(2)} s)` : ""}, mixed with the ${bed} bed at ${snr.target_db_a ?? "?"} dB(A) `
245
+ + `signal-to-noise ratio over the customer's speech${snr.measured_db_a != null ? ` (measured ${snr.measured_db_a})` : ""}. Agent RIGHT: the source track, clean. `
246
+ + `Both channels normalised to ${loud.target_lufs ?? "?"} LUFS under a ${loud.true_peak_ceiling_dbtp ?? -1} dBTP ceiling.`]);
247
+ rows.push(["Levels", "Source tracks play at their recorded levels; the combined file is loudness-normalised, so the two groups differ in loudness by design."]);
248
+ } else {
249
+ rows.push(["Combined", `${fmtSpec(f.stereo)} stereo WAV: customer LEFT, agent RIGHT, the two source tracks side by side, unprocessed.`]);
250
  }
251
+ rows.push(["Timeline", "Every file of the take has the same length and starts on the same instant as the tool-call clock."]);
252
+ el.specs.replaceChildren(...rows.flatMap(([k, v]) => [text("dt", k), text("dd", v)]));
253
+ el.laneL.textContent = renoised ? `customer, cabin + ${bed} bed` : "customer, clean";
254
+ el.laneR.textContent = renoised ? "agent, normalised" : "agent, clean";
 
 
 
 
 
 
255
  }
256
  document.addEventListener("keydown", (e) => {
257
  if (!state.take || e.target.closest("input, textarea, select")) return;
 
281
  const r = el.waveAgent.getBoundingClientRect(), s = el.stage.getBoundingClientRect();
282
  el.playhead.hidden = false; el.playhead.style.left = `${r.left - s.left + xOf(t)}px`;
283
  }
284
+ /* One hairline per tool call through the waveform rows of each group: a segment per group, so the
285
+ lines stop at the gap between Combined and Source and resume below it. */
286
  function drawCallLines() {
287
  const s = el.stage.getBoundingClientRect();
288
+ const visible = (g) => [...g.querySelectorAll(".row canvas")].filter((c) => !c.closest(".row").hidden).map((c) => c.getBoundingClientRect());
289
+ const groups = [...el.stage.querySelectorAll(".group")].map(visible).filter((cs) => cs.length);
290
+ const first = groups[0][0], m = el.markers.getBoundingClientRect();
291
  Object.assign(el.calllines.style, { left: `${first.left - s.left}px`, width: `${first.width}px`, top: `${first.top - s.top}px`, height: `${m.top - first.top}px` });
292
+ const segments = groups.map((cs) => [cs[0].top - first.top, cs[cs.length - 1].bottom - first.top]);
293
  el.calllines.replaceChildren();
294
  state.events.forEach((ev, i) => {
295
+ for (const [top, bottom] of segments) {
296
+ const line = document.createElement("div");
297
+ line.className = "callline" + (ev.is_error ? " err" : "") + (i === state.current ? " current" : "");
298
+ line.dataset.idx = i;
299
+ Object.assign(line.style, { left: `${xOf(ev.onset_time)}px`, top: `${top}px`, height: `${bottom - top}px` });
300
+ el.calllines.append(line);
301
+ }
302
  });
303
  }
304
  for (const c of WAVES()) {
index.html CHANGED
@@ -35,8 +35,8 @@
35
  <span id="clock" class="clock">0:00 / 0:00</span>
36
  <div class="listen" role="radiogroup" aria-label="Listen to">
37
  <span class="muted">Listen to</span>
38
- <button class="seg on" id="listen-combined" role="radio" aria-checked="true" title="The combined call, one stereo file (C toggles)">Combined</button>
39
- <button class="seg" id="listen-sources" role="radio" aria-checked="false" title="The studio tracks, each with its own on/off (C toggles)">Sources</button>
40
  </div>
41
  <div class="legend" role="group" aria-label="Colours">
42
  <span class="key static"><span class="swatch s1"></span>Agent</span>
@@ -45,18 +45,18 @@
45
  <span class="key static"><span class="swatch dot s3"></span>Tool call</span>
46
  <span class="key static"><span class="swatch dot err">!</span>Error</span>
47
  </div>
48
- <span class="hint muted">space play · ← → 5 s · [ ] tool calls · C combined/sources</span>
49
  </div>
50
  <div class="work">
51
  <div class="left">
52
  <div class="stage" id="stage">
53
  <div class="group" id="group-combined">
54
- <div class="group-head"><span class="group-name" id="combined-name">Processed call</span><span class="muted" id="combined-note">what an agent-side system would receive, one stereo file</span><span class="badge listening" id="badge-combined">listening</span></div>
55
  <div class="row"><div class="row-label">Combined L<small id="lane-l-note">customer + bed</small></div><canvas id="wave-combined-l" class="wave lane"></canvas></div>
56
  <div class="row"><div class="row-label">Combined R<small id="lane-r-note">agent</small></div><canvas id="wave-combined-r" class="wave lane"></canvas></div>
57
  </div>
58
  <div class="group" id="group-sources">
59
- <div class="group-head"><span class="group-name">Studio recordings</span><span class="muted" id="sources-note">what was captured, three separate tracks; switch each on or off</span><span class="badge listening" id="badge-sources" hidden>listening</span></div>
60
  <div class="row"><div class="row-label">Agent<small>clean microphone</small><button class="row-mute" id="mute-agent" aria-pressed="false" title="Switch the agent track off or on">on</button></div><canvas id="wave-agent" class="wave"></canvas></div>
61
  <div class="row"><div class="row-label">Customer<small>clean microphone</small><button class="row-mute" id="mute-customer" aria-pressed="false" title="Switch the customer track off or on">on</button></div><canvas id="wave-customer" class="wave"></canvas></div>
62
  <div class="row" id="row-noise"><div class="row-label">Ambient<small>noise bed in headphones</small><button class="row-mute" id="mute-noise" aria-pressed="false" title="Switch the noise bed off or on">on</button></div><canvas id="wave-noise" class="wave"></canvas></div>
@@ -67,7 +67,10 @@
67
  <div id="playhead" class="playhead" hidden></div>
68
  <div id="tip" class="tip" role="tooltip" hidden></div>
69
  </div>
70
- <p id="recipe" class="recipe"></p>
 
 
 
71
  </div>
72
  <div class="right">
73
  <section class="now" aria-live="polite">
@@ -97,6 +100,7 @@
97
  </section>
98
  <section class="empty" id="empty">
99
  <p class="muted">Select a call on the left.</p>
 
100
  <p class="muted small">Each call plays as one timeline: the waveforms of every recorded form on the left, the agent's tool calls as markers through them, and on the right the current call's function signature and result, with the full log below it.</p>
101
  <p id="load-error" class="error" hidden></p>
102
  </section>
 
35
  <span id="clock" class="clock">0:00 / 0:00</span>
36
  <div class="listen" role="radiogroup" aria-label="Listen to">
37
  <span class="muted">Listen to</span>
38
+ <button class="seg on" id="listen-combined" role="radio" aria-checked="true" title="The Combined group: one stereo file (C toggles)">Combined</button>
39
+ <button class="seg" id="listen-sources" role="radio" aria-checked="false" title="The Source group: the studio tracks, each with its own on/off (C toggles)">Source</button>
40
  </div>
41
  <div class="legend" role="group" aria-label="Colours">
42
  <span class="key static"><span class="swatch s1"></span>Agent</span>
 
45
  <span class="key static"><span class="swatch dot s3"></span>Tool call</span>
46
  <span class="key static"><span class="swatch dot err">!</span>Error</span>
47
  </div>
48
+ <span class="hint muted">space play · ← → 5 s · [ ] tool calls · C combined/source</span>
49
  </div>
50
  <div class="work">
51
  <div class="left">
52
  <div class="stage" id="stage">
53
  <div class="group" id="group-combined">
54
+ <div class="group-head"><span class="group-name" id="combined-name">Combined</span><span class="muted" id="combined-note">single post-processed stereo file</span><span class="badge listening" id="badge-combined">listening</span></div>
55
  <div class="row"><div class="row-label">Combined L<small id="lane-l-note">customer + bed</small></div><canvas id="wave-combined-l" class="wave lane"></canvas></div>
56
  <div class="row"><div class="row-label">Combined R<small id="lane-r-note">agent</small></div><canvas id="wave-combined-r" class="wave lane"></canvas></div>
57
  </div>
58
  <div class="group" id="group-sources">
59
+ <div class="group-head"><span class="group-name" id="sources-name">Source</span><span class="muted" id="sources-note">studio recordings · 48 kHz / 32-bit speaker-isolated recordings and ambient noise; switch each on or off</span><span class="badge listening" id="badge-sources" hidden>listening</span></div>
60
  <div class="row"><div class="row-label">Agent<small>clean microphone</small><button class="row-mute" id="mute-agent" aria-pressed="false" title="Switch the agent track off or on">on</button></div><canvas id="wave-agent" class="wave"></canvas></div>
61
  <div class="row"><div class="row-label">Customer<small>clean microphone</small><button class="row-mute" id="mute-customer" aria-pressed="false" title="Switch the customer track off or on">on</button></div><canvas id="wave-customer" class="wave"></canvas></div>
62
  <div class="row" id="row-noise"><div class="row-label">Ambient<small>noise bed in headphones</small><button class="row-mute" id="mute-noise" aria-pressed="false" title="Switch the noise bed off or on">on</button></div><canvas id="wave-noise" class="wave"></canvas></div>
 
67
  <div id="playhead" class="playhead" hidden></div>
68
  <div id="tip" class="tip" role="tooltip" hidden></div>
69
  </div>
70
+ <section class="specs" aria-label="Recording specs">
71
+ <div class="panel-head">Recording specs</div>
72
+ <dl id="specs"></dl>
73
+ </section>
74
  </div>
75
  <div class="right">
76
  <section class="now" aria-live="polite">
 
100
  </section>
101
  <section class="empty" id="empty">
102
  <p class="muted">Select a call on the left.</p>
103
+ <p id="empty-sibling" class="muted small"></p>
104
  <p class="muted small">Each call plays as one timeline: the waveforms of every recorded form on the left, the agent's tool calls as markers through them, and on the right the current call's function signature and result, with the full log below it.</p>
105
  <p id="load-error" class="error" hidden></p>
106
  </section>
style.css CHANGED
@@ -10,6 +10,7 @@
10
  --series-noise: #898781;
11
  --status-critical: #d03b3b;
12
  --wash-1: rgba(42,120,214,0.10);
 
13
  }
14
  @media (prefers-color-scheme: dark) {
15
  :root:where(:not([data-theme="light"])) {
@@ -21,6 +22,7 @@
21
  --series-noise: #898781;
22
  --status-critical: #d03b3b;
23
  --wash-1: rgba(57,135,229,0.14);
 
24
  }
25
  }
26
  * { box-sizing: border-box; }
@@ -43,7 +45,8 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
43
  .links { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 6px 16px; font-size: 13px; color: var(--text-secondary); white-space: nowrap; padding-top: 2px; }
44
  .links a { text-decoration: none; }
45
  .links a:hover { text-decoration: underline; }
46
- .siblings a { padding: 3px 9px; border: 1px solid var(--border); border-radius: 12px; }
 
47
  .set-switch select { font: inherit; font-size: 13px; padding: 2px 6px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary); }
48
 
49
  .layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); min-height: calc(100% - 46px); }
@@ -81,7 +84,10 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
81
  .seg { font: inherit; font-size: 13px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary); padding: 4px 10px; cursor: pointer; }
82
  .seg:first-of-type { border-radius: 6px 0 0 6px; } .seg:last-of-type { border-radius: 0 6px 6px 0; margin-left: -1px; }
83
  .seg.on { background: var(--text-primary); color: var(--surface-1); border-color: var(--text-primary); }
84
- .recipe { font-size: 12px; color: var(--text-secondary); margin: 8px 4px 0; max-width: 130ch; }
 
 
 
85
 
86
  /* waveforms left, tool-call panels right when the window is wide enough; stacked otherwise */
87
  .work { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
@@ -95,7 +101,7 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
95
  @media (max-width: 1100px) { .right { grid-template-columns: 1fr; } .layout { grid-template-columns: 240px minmax(0, 1fr); } }
96
 
97
  .stage { position: relative; background: var(--surface-1); border: 1px solid var(--gridline); border-radius: 8px; padding: 4px 12px 6px 0; }
98
- .group + .group { border-top: 1px solid var(--gridline); margin-top: 6px; }
99
  .group-head { display: flex; align-items: baseline; gap: 10px; padding: 6px 12px 2px; font-size: 12px; }
100
  .group-head .group-name { text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); font-weight: 600; }
101
  .group.inactive .row { opacity: .5; }
@@ -124,7 +130,7 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
124
  .markers g:hover .dot, .markers g.current .dot { r: 7; }
125
  .markers g:hover .tick, .markers g.current .tick { stroke-width: 3; }
126
  .calllines { position: absolute; pointer-events: none; }
127
- .callline { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--series-3); opacity: .4; }
128
  .callline.err { background: var(--status-critical); }
129
  .callline.current { opacity: .9; width: 2px; margin-left: -0.5px; }
130
  .playhead { position: absolute; top: 4px; bottom: 28px; width: 2px; margin-left: -1px; background: var(--text-primary); pointer-events: none; opacity: .9; }
@@ -139,6 +145,9 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
139
  .now-body .when { color: var(--text-secondary); font-variant-numeric: tabular-nums; font-size: 12px; display: block; margin-bottom: 6px; }
140
  .sig { padding: 8px 10px; border-radius: 6px; background: var(--page); border: 1px solid var(--gridline); font-size: 12.5px; }
141
  .sig .fn { font-weight: 700; color: var(--text-primary); }
 
 
 
142
  .now-body .result { margin-top: 8px; padding: 8px; border-radius: 6px; background: var(--page); max-height: 220px; overflow: auto; }
143
  .now-body .sub { font-size: 12px; color: var(--text-secondary); margin: 8px 0 2px; }
144
  .now-body.changed { animation: lift .6s ease-out; }
@@ -151,7 +160,9 @@ th { text-align: left; font-weight: 600; color: var(--text-secondary); font-size
151
  td { padding: 6px 10px; border-bottom: 1px solid var(--gridline); vertical-align: top; }
152
  tbody tr { cursor: pointer; }
153
  tbody tr:hover { background: var(--wash-1); }
154
- tbody tr[aria-current="true"] { background: var(--wash-1); box-shadow: inset 3px 0 0 var(--series-3); }
 
 
155
  td.num, td.time { font-variant-numeric: tabular-nums; color: var(--text-secondary); white-space: nowrap; }
156
  td.call { max-width: 0; width: 100%; }
157
  td.call code { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
10
  --series-noise: #898781;
11
  --status-critical: #d03b3b;
12
  --wash-1: rgba(42,120,214,0.10);
13
+ --wash-3: rgba(27,175,122,0.14);
14
  }
15
  @media (prefers-color-scheme: dark) {
16
  :root:where(:not([data-theme="light"])) {
 
22
  --series-noise: #898781;
23
  --status-critical: #d03b3b;
24
  --wash-1: rgba(57,135,229,0.14);
25
+ --wash-3: rgba(25,158,112,0.22);
26
  }
27
  }
28
  * { box-sizing: border-box; }
 
45
  .links { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 6px 16px; font-size: 13px; color: var(--text-secondary); white-space: nowrap; padding-top: 2px; }
46
  .links a { text-decoration: none; }
47
  .links a:hover { text-decoration: underline; }
48
+ .siblings a.sibling { padding: 4px 12px; border: 1px solid var(--series-1); color: var(--series-1); border-radius: 14px; font-weight: 600; margin-left: 6px; }
49
+ .siblings a.sibling:hover { background: var(--wash-1); text-decoration: none; }
50
  .set-switch select { font: inherit; font-size: 13px; padding: 2px 6px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary); }
51
 
52
  .layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); min-height: calc(100% - 46px); }
 
84
  .seg { font: inherit; font-size: 13px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary); padding: 4px 10px; cursor: pointer; }
85
  .seg:first-of-type { border-radius: 6px 0 0 6px; } .seg:last-of-type { border-radius: 0 6px 6px 0; margin-left: -1px; }
86
  .seg.on { background: var(--text-primary); color: var(--surface-1); border-color: var(--text-primary); }
87
+ .specs { margin-top: 12px; background: var(--surface-1); border: 1px solid var(--gridline); border-radius: 8px; padding: 0 0 10px; }
88
+ .specs dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 0; padding: 2px 16px 0; font-size: 12px; }
89
+ .specs dt { color: var(--text-primary); font-weight: 600; }
90
+ .specs dd { margin: 0; color: var(--text-secondary); max-width: 130ch; }
91
 
92
  /* waveforms left, tool-call panels right when the window is wide enough; stacked otherwise */
93
  .work { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
 
101
  @media (max-width: 1100px) { .right { grid-template-columns: 1fr; } .layout { grid-template-columns: 240px minmax(0, 1fr); } }
102
 
103
  .stage { position: relative; background: var(--surface-1); border: 1px solid var(--gridline); border-radius: 8px; padding: 4px 12px 6px 0; }
104
+ .group + .group { border-top: 3px solid var(--baseline); margin-top: 14px; padding-top: 4px; }
105
  .group-head { display: flex; align-items: baseline; gap: 10px; padding: 6px 12px 2px; font-size: 12px; }
106
  .group-head .group-name { text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); font-weight: 600; }
107
  .group.inactive .row { opacity: .5; }
 
130
  .markers g:hover .dot, .markers g.current .dot { r: 7; }
131
  .markers g:hover .tick, .markers g.current .tick { stroke-width: 3; }
132
  .calllines { position: absolute; pointer-events: none; }
133
+ .callline { position: absolute; width: 1px; background: var(--series-3); opacity: .4; }
134
  .callline.err { background: var(--status-critical); }
135
  .callline.current { opacity: .9; width: 2px; margin-left: -0.5px; }
136
  .playhead { position: absolute; top: 4px; bottom: 28px; width: 2px; margin-left: -1px; background: var(--text-primary); pointer-events: none; opacity: .9; }
 
145
  .now-body .when { color: var(--text-secondary); font-variant-numeric: tabular-nums; font-size: 12px; display: block; margin-bottom: 6px; }
146
  .sig { padding: 8px 10px; border-radius: 6px; background: var(--page); border: 1px solid var(--gridline); font-size: 12.5px; }
147
  .sig .fn { font-weight: 700; color: var(--text-primary); }
148
+ .now-body .sig { border-left: 3px solid var(--series-3); }
149
+ .now-body .sig .fn { color: var(--series-3); }
150
+ .now-body .when::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--series-3); margin-right: 6px; vertical-align: 0; }
151
  .now-body .result { margin-top: 8px; padding: 8px; border-radius: 6px; background: var(--page); max-height: 220px; overflow: auto; }
152
  .now-body .sub { font-size: 12px; color: var(--text-secondary); margin: 8px 0 2px; }
153
  .now-body.changed { animation: lift .6s ease-out; }
 
160
  td { padding: 6px 10px; border-bottom: 1px solid var(--gridline); vertical-align: top; }
161
  tbody tr { cursor: pointer; }
162
  tbody tr:hover { background: var(--wash-1); }
163
+ tbody tr[aria-current="true"] { background: var(--wash-3); box-shadow: inset 3px 0 0 var(--series-3); }
164
+ tbody tr[aria-current="true"] .fn { color: var(--series-3); }
165
+ tbody tr[aria-current="true"] td.num, tbody tr[aria-current="true"] td.time { color: var(--text-primary); font-weight: 600; }
166
  td.num, td.time { font-variant-numeric: tabular-nums; color: var(--text-secondary); white-space: nowrap; }
167
  td.call { max-width: 0; width: 100%; }
168
  td.call code { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }