diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
index 7b78e7c0..ec469f9b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
@@ -6201,6 +6201,7 @@ var wfmCciValEl = document.getElementById("wfm-cci-val");
var wfmAciFillEl = document.getElementById("wfm-aci-fill");
var wfmAciValEl = document.getElementById("wfm-aci-val");
var samControlsCol = document.getElementById("sam-controls-col");
+var modeControlsRow = document.getElementById("mode-controls-row");
var samStereoWidthEl = document.getElementById("sam-stereo-width");
var samCarrierSyncEl = document.getElementById("sam-carrier-sync");
var sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
@@ -6508,6 +6509,7 @@ function updateWfmControls() {
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
if (wfmControlsCol) wfmControlsCol.style.display = mode === "WFM" ? "" : "none";
if (samControlsCol) samControlsCol.style.display = mode === "SAM" ? "" : "none";
+ if (modeControlsRow) modeControlsRow.style.display = mode === "WFM" || mode === "SAM" ? "" : "none";
}
if (!hasWebCodecs) {
rxAudioBtn.disabled = true;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index a543fa80..5f8728b6 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -256,6 +256,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
VFO
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index a23c0342..0e6904b3 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -278,9 +278,17 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
.controls-row {
display: flex;
flex-wrap: wrap;
+ justify-content: center;
gap: 1rem 1.25rem;
align-items: flex-start;
}
+/* Whatever the current mode adds — WFM's deemphasis and interference meters,
+ SAM's carrier sync — sits on its own line under the controls every rig has,
+ instead of stretching the row sideways when one mode happens to be active. */
+.controls-row-mode {
+ padding-top: 0.85rem;
+ border-top: 1px solid color-mix(in srgb, var(--border-light) 45%, transparent);
+}
.controls-col {
min-width: 0;
display: flex;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
index 0e42fe1c..fa1c9020 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
@@ -5145,6 +5145,7 @@ const wfmCciValEl = document.getElementById("wfm-cci-val");
const wfmAciFillEl = document.getElementById("wfm-aci-fill");
const wfmAciValEl = document.getElementById("wfm-aci-val");
const samControlsCol = document.getElementById("sam-controls-col");
+const modeControlsRow = document.getElementById("mode-controls-row");
const samStereoWidthEl = document.getElementById("sam-stereo-width") as HTMLInputElement | null;
const samCarrierSyncEl = document.getElementById("sam-carrier-sync") as HTMLSelectElement | null;
const sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
@@ -5463,6 +5464,9 @@ function updateWfmControls() {
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
if (wfmControlsCol) wfmControlsCol.style.display = mode === "WFM" ? "" : "none";
if (samControlsCol) samControlsCol.style.display = mode === "SAM" ? "" : "none";
+ // The row holds only these two, so it goes with them — an empty one would
+ // still take a track and a gap in the tray, and draw its divider.
+ if (modeControlsRow) modeControlsRow.style.display = (mode === "WFM" || mode === "SAM") ? "" : "none";
}
// Show compatibility warning for non-Chromium browsers
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs
index d118cbb8..f7e0d727 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs
@@ -49,6 +49,23 @@ try {
assert.equal(modeAfter.active, target, "the clicked mode is not the marked one");
assert.ok(modeAfter.selectWidth <= 2, `the hidden select still occupies ${modeAfter.selectWidth}px`);
+ // Mode-specific controls live on their own row, which has to leave with them:
+ // an empty one would still take a track and a gap in the tray and draw its
+ // divider under the controls every rig has.
+ const modeRowState = async () => page.evaluate(() => {
+ const row = document.getElementById("mode-controls-row");
+ return { display: getComputedStyle(row).display, height: Math.round(row.getBoundingClientRect().height) };
+ });
+ await page.locator('#mode-picker button[data-mode="WFM"]').click();
+ await page.waitForTimeout(250);
+ const withWfm = await modeRowState();
+ assert.notEqual(withWfm.display, "none", "WFM controls did not bring their row up");
+ assert.ok(withWfm.height > 0, `WFM row has no height (${withWfm.height}px)`);
+ await page.locator('#mode-picker button[data-mode="FM"]').click();
+ await page.waitForTimeout(250);
+ const withoutWfm = await modeRowState();
+ assert.equal(withoutWfm.display, "none", "the mode row stayed behind with nothing in it");
+
// Scheduler controls read left to right: step, hand back, then the entry on
// air. The separator is drawn by the current-entry block, so it can only sit
// in the right place if that block is last.