From 316da0009a6222127503e0a13c7fe3d83e4163cb Mon Sep 17 00:00:00 2001 From: Obvious Date: Sun, 11 Oct 2026 22:42:51 +0000 Subject: [PATCH 1/3] =?UTF-8?q?chore(copy):=20finish=20the=20sandbox=20?= =?UTF-8?q?=E2=86=92=20Explore=20label=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling PRs renamed the surfaces in the lessons themselves (0.2, 0.3, 4.8, 7.1, 8.1) but left three "sandbox" stragglers in web/: two comments (figures.js preset note, guide.js lab-help note) and one user-facing string (the agent's protocol-accept confirmation). Part of the V9 final sweep; no behavior change. Co-authored-by: Tapan Ganatma Nakkina --- web/agent.js | 2 +- web/figures.js | 2 +- web/guide.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/agent.js b/web/agent.js index 952a908..dcf2af3 100644 --- a/web/agent.js +++ b/web/agent.js @@ -486,7 +486,7 @@ const j = await r.json().catch(() => null); if (!r.ok) { status.textContent = (j && j.detail) || `accept failed (${r.status})`; status.className = 'small agent-line-no'; btn.disabled = false; return; } host.innerHTML = ''; - status.textContent = `Accepted — saved to data/protocols/${j.protocol_id || pid}.json. An accepted protocol fills the sandbox form step by step, each step still confirmed.`; + status.textContent = `Accepted — saved to data/protocols/${j.protocol_id || pid}.json. An accepted protocol fills the Explore form step by step, each step still confirmed.`; status.className = 'small agent-line-ok'; } catch (e) { status.textContent = 'the accept request failed: ' + (e && e.message || e); diff --git a/web/figures.js b/web/figures.js index 3c96394..6d1d504 100644 --- a/web/figures.js +++ b/web/figures.js @@ -68,7 +68,7 @@ } // ---------------------------------------------------------------- n · T · tau_E - const PRESET = { // ITER: this app's sandbox baseline (physics.simulate). MAST: shot #30166 at t = 0.27 s (n̄e, Te0, W/P_loss). + const PRESET = { // ITER: this app's Explore baseline (physics.simulate). MAST: shot #30166 at t = 0.27 s (n̄e, Te0, W/P_loss). iter: { n: 1.015, T: 7.6, tau: 3.81, label: 'ITER: this app’s model, volume-average T' }, mast: { n: 0.386, T: 1.22, tau: 0.049, label: 'MAST #30166 at 0.27 s: measured, core electron T' }, }; diff --git a/web/guide.js b/web/guide.js index 6caa016..5d2b6fa 100644 --- a/web/guide.js +++ b/web/guide.js @@ -72,7 +72,7 @@ ['mouseover', 'focusin'].forEach((ev) => document.addEventListener(ev, (e) => { const t = e.target.closest && e.target.closest('.term'); if (t) showTip(t); })); ['mouseout', 'focusout'].forEach((ev) => document.addEventListener(ev, (e) => { if (e.target.closest && e.target.closest('.term')) tip.hidden = true; })); - // Lab-mode help, from the same glossary: tooltips on panel labels, one line under each sandbox slider, "?" back to a lesson. + // Lab-mode help, from the same glossary: tooltips on panel labels, one line under each Explore slider, "?" back to a lesson. function labHelp() { Object.values(G).forEach((g) => (g.on || []).forEach((name) => { const el = target(name); if (el && !el.title) el.title = `${g.name}: ${g.def}`; })); const KNOB = { Ip: 'ip', B: 'bt', n: 'density', P_aux: 'heating', H: 'h-factor', Zeff: 'zeff' }; From 2c9c631dbefdc9e9dac855d3fdcc6b16e1496057 Mon Sep 17 00:00:00 2001 From: Obvious Date: Sun, 11 Oct 2026 22:43:38 +0000 Subject: [PATCH 2/3] feat(a11y): one focus token, reduced-motion collapses panel motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - style.css: repair the tail left by a sibling merge (a stray "auto; }" fragment and duplicated .srch-rows/.bad/Explore/mobile-header rules); drop the one hardcoded :focus-visible override so every interactive element shares the --focus-ring token; add @media (prefers-reduced-motion: reduce) collapsing the two orientation tweens (gauge bars, spotlight dim). - guide.js, replay.js: the two smooth scrolls (spotlight, catalog panel jump) jump instantly under the same query — the preference otherwise could not reach them from CSS. Co-authored-by: Tapan Ganatma Nakkina --- web/guide.js | 2 +- web/replay.js | 2 +- web/style.css | 23 ++++++----------------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/web/guide.js b/web/guide.js index 5d2b6fa..9a88d75 100644 --- a/web/guide.js +++ b/web/guide.js @@ -107,7 +107,7 @@ const keep = new Set(); // the lit elements and every ancestor up to the tab inTab.forEach((el) => { for (let n = el; n && n !== section.parentNode; n = n.parentNode) keep.add(n); }); const dock = parseFloat(getComputedStyle(document.body).getPropertyValue('--dock')) || 0, r = inTab[0].getBoundingClientRect(); - if (r.top < 0 || r.top > innerHeight - dock - 80) inTab[0].scrollIntoView({ block: 'start', behavior: 'smooth' }); // its top is off screen (pages that scroll) + if (r.top < 0 || r.top > innerHeight - dock - 80) inTab[0].scrollIntoView({ block: 'start', behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); // its top is off screen (pages that scroll) keep.forEach((node) => { if (inTab.includes(node)) return; // a lit element keeps all of its children [...node.children].forEach((c) => { diff --git a/web/replay.js b/web/replay.js index 4543a19..e963143 100644 --- a/web/replay.js +++ b/web/replay.js @@ -1286,7 +1286,7 @@ if (!b) return; if (b.dataset.act === 'cancel') return SL.cancelAll(); // fetched shots stay plotted if (b.dataset.act === 'exit') return exitCompare(); - if (b.dataset.act === 'search') return $('rp-srch-panel').scrollIntoView({ behavior: 'smooth', block: 'start' }); + if (b.dataset.act === 'search') return $('rp-srch-panel').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start' }); if (b.dataset.act === 'share') return void shareCompare(b); if (b.dataset.act === 'retry') // "Retry failed": every failed chip refetches under the shared machine return void cmp.ids.filter(x => SL.get(x) === 'error').forEach(id => cmpFetch(id, id === cmp.ref ? p => renderShot(p) : null)); diff --git a/web/style.css b/web/style.css index ea31ff3..6e140ed 100644 --- a/web/style.css +++ b/web/style.css @@ -301,7 +301,6 @@ body.guided header { margin-left:calc(-1 * var(--rail)); } .entry-action { margin:14px 0 0; } .entry-action a { color:var(--accent); font:600 13px/1.4 system-ui, sans-serif; text-decoration:underline; } .entry-action a:hover { color:var(--fg); } -.entry-action a:focus-visible { outline:1px solid var(--accent); outline-offset:2px; } /* command palette (web/palette.js): native in the top layer — no z-index, no overlay div. Shares the control-room look: panel surface, hairlines, mono hints. */ @@ -321,23 +320,12 @@ dialog.palette::backdrop { background:rgba(8,10,13,.72); backdrop-filter:blur(2p .pal-status { margin:6px 2px 0; min-height:1em; color:var(--accent); font:12px/1.5 var(--mono); } @media (max-width:760px) { dialog.palette { margin-top:6vh; } #pal-list { max-height:60vh; } } -/* Mobile header: the legend (its tags repeat on every panel) and the subtitle step aside, and the - tabs take their own row, so the title never clips (audit defect at 375 px, screenshot fl_AmO2E2fS). - Last in the file: it must out-order the base .prov/.tabs rules it overrides (equal specificity). */ -@media (max-width:760px) { - header { flex-wrap:wrap; height:auto; min-height:var(--head); padding:8px 16px; row-gap:2px; } - .prov, header h1 .dim { display:none; } - header .tabs { order:9; flex:1 1 100%; align-self:auto; height:34px; } +/* Reduced motion: the gauge bars' width tween and the guided-study spotlight dim are orientation + aids only — under the user's preference they apply instantly. The two JS smooth scrolls + (guide.js spotlight, the catalog panel jump) gate on the same query at their call sites. */ +@media (prefers-reduced-motion: reduce) { + .gauge > .bar, .g-dim { transition: none; } } -auto; } -.srch-rows td { white-space:nowrap; } -.bad { color:var(--bad); } - -/* Explore workspace: the visible path into the anchored editor (Replay, edit intent in the URL) */ -.entry-action { margin:14px 0 0; } -.entry-action a { color:var(--accent); font:600 13px/1.4 system-ui, sans-serif; text-decoration:underline; } -.entry-action a:hover { color:var(--fg); } -.entry-action a:focus-visible { outline:1px solid var(--accent); outline-offset:2px; } /* Mobile header: the legend (its tags repeat on every panel) and the subtitle step aside, and the tabs take their own row, so the title never clips (audit defect at 375 px, screenshot fl_AmO2E2fS). @@ -347,3 +335,4 @@ auto; } .prov, header h1 .dim { display:none; } header .tabs { order:9; flex:1 1 100%; align-self:auto; height:34px; } } + From 9cef3e044ca91fe135292f689f589559ce591518 Mon Sep 17 00:00:00 2001 From: Obvious Date: Sun, 11 Oct 2026 22:45:40 +0000 Subject: [PATCH 3/3] fix(redundancy): the insight paragraph references the limits, not their numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Limit proximity was rendered four times on one screen: the three gauges, the limits strip, the strip value's color, and the insight prose — the prose being the third restatement of the same numbers. Keep the gauges (the instant) and the limits strip (the run); the prose now names which limit is closest and points at those two. Lesson 5.5, which described the panel's prose, drops "and when" to match. Co-authored-by: Tapan Ganatma Nakkina --- web/lessons.json | 2 +- web/replay.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/lessons.json b/web/lessons.json index 33afc8a..20560af 100644 --- a/web/lessons.json +++ b/web/lessons.json @@ -185,7 +185,7 @@ { "id": "5.5", "ch": 5, "title": "What the comparison shows", "tab": "replay", "shot": 30166, "slice": { "t": 0.27 }, "focus": ["insight", "logbook"], - "body": "Two readings of the same shot, side by side. On the left, what the twin works out from the data: confinement relative to each law, the closest approach to a limit and when, and how the learned correction did on this shot. On the right, what the physicist in the control room wrote. When they agree, you can trust both a little more. When they differ, you have found something worth a closer look." }, + "body": "Two readings of the same shot, side by side. On the left, what the twin works out from the data: confinement relative to each law, which limit the shot came closest to, and how the learned correction did on this shot. On the right, what the physicist in the control room wrote. When they agree, you can trust both a little more. When they differ, you have found something worth a closer look." }, { "id": "6.1", "ch": 6, "title": "Same recipe, more power", "tab": "replay", "shot": 30192, "slice": 0, "view": "cutaway", "highlight": null, "focus": ["bar", "strip-p", "strip-w", "strip-lim"], diff --git a/web/replay.js b/web/replay.js index e963143..8552f51 100644 --- a/web/replay.js +++ b/web/replay.js @@ -583,9 +583,11 @@ if (q.H98_median !== null) out.push(`Over ${q.n_steady} near-steady slices the measured confinement time is ${fmt(q.H98_median)}× the IPB98(y,2) H-mode law and ${fmt(q.H89_median)}× the ITER89-P L-mode law.`); const name = LIMIT_LABEL[q.peak_limit] || q.peak_limit; + // Limit proximity is drawn twice already — gauges for the instant, the limits strip for the run — + // so the prose names the limit and points at them instead of restating the same numbers a third time. out.push(q.peak_limit_fraction >= 1 - ? `${name} limit exceeded: ${fmt(q.peak_limit_fraction)}× at t = ${fmt(q.peak_limit_t_s, 3)} s.` - : `Closest approach to a limit: ${name} at ${fmt(q.peak_limit_fraction)}× (t = ${fmt(q.peak_limit_t_s, 3)} s).`); + ? `The ${name} limit is crossed during this shot — its gauge reads red at the crossing, and the limits strip shows when.` + : `Closest approach to a limit: ${name}. Watch its gauge as you scrub, and read the approach off the limits strip.`); // Only claimed for ohmic shots: with beams the energy holds fast ions and the late phase may well be H-mode. const ohmic = (s.meta.heating || '').toLowerCase() === 'ohmic'; if (ohmic && q.P_loss_median_MW !== null && q.P_loss_median_MW > 2 * q.P_LH_median_MW && Math.abs(q.H89_median - 1) < 0.25)