#include "RadioSetupDialog.h"
#include "CwDecodeSettings.h"
#include "GuardedSlider.h"
#include "ComboStyle.h"
#include "SliceColorManager.h"
#include "models/RadioModel.h"
#include "models/XvtrPolicy.h"
#include "core/AppSettings.h"
#include "core/KiwiSdrManager.h"
#include "KiwiPublicReceiverPicker.h"
#include "core/LogManager.h"
#include "core/PeripheralSettings.h"
#include <QApplication>
#include <QAbstractItemView>
#include <QSysInfo>
#include "core/AudioEngine.h"
#ifdef HAVE_SERIALPORT
#include "core/SerialPortController.h"
#include "core/FlexControlManager.h"
#include <QSerialPortInfo>
#endif
#include "core/FirmwareUploader.h"
#include "core/FirmwareStager.h"
#include "core/TgxlConnection.h"
#include "core/PgxlConnection.h"
#include "core/WanConnection.h"   // PinnedCertInfo + WanCertCache (#2951)
#include "models/AntennaGeniusModel.h"

#include <QCloseEvent>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QFrame>
#include <QHeaderView>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSlider>
#include <QComboBox>
#include <QSpinBox>
#include <QDialogButtonBox>
#include <QCheckBox>
#include <QDoubleValidator>
#include <QTimer>
#include <QDesktopServices>
#include <QUrl>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QFileDialog>
#include <QStandardPaths>
#include <QFileInfo>
#include <QMessageBox>
#include <QColorDialog>
#include <QRadioButton>
#include <QProgressBar>
#include <QProcess>
#include <QListWidget>
#include <QStackedWidget>
#include <QStyle>
#include <QPlainTextEdit>
#include <QSplitter>
#include <QScrollArea>
#include <QHostAddress>
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QPainter>
#include <QPaintEvent>
#include <QPointer>
#include <QScreen>
#include <QSignalBlocker>
#include <QSizePolicy>
#include <QToolButton>

#include <algorithm>
#include <functional>
#include <memory>
#include <utility>
#include "core/ThemeManager.h"

namespace AetherSDR {

static const QString kGroupStyle =
    "QGroupBox { border: 1px solid #304050; border-radius: 4px; "
    "margin-top: 8px; padding-top: 12px; font-weight: bold; color: #8aa8c0; }"
    "QGroupBox::title { subcontrol-origin: margin; left: 10px; "
    "padding: 0 4px; }";

static const QString kLabelStyle =
    "QLabel { color: #c8d8e8; font-size: 12px; }";

static const QString kValueStyle =
    "QLabel { color: #00c8ff; font-size: 12px; font-weight: bold; }";

static const QString kEditStyle =
    "QLineEdit { background: #1a2a3a; border: 1px solid #304050; "
    "border-radius: 3px; color: #c8d8e8; font-size: 12px; padding: 2px 4px; }";

static const QString kKiwiRowStyle =
    "QFrame#kiwiAntennaRow { background: #101622; border: 1px solid #203040; "
    "border-radius: 3px; }";

static const QString kKiwiActionButtonStyle =
    "QPushButton { background: #183548; border: 1px solid #28506a; "
    "border-radius: 4px; color: #d6e7f5; font-size: 12px; "
    "font-weight: bold; padding: 4px 10px; }"
    "QPushButton:hover { background: #20465e; }"
    "QPushButton:pressed { background: #132c3d; }";

static const QString kKiwiIconButtonStyle =
    "QPushButton { background: #183548; border: 1px solid #28506a; "
    "border-radius: 4px; padding: 3px; }"
    "QPushButton:hover { background: #20465e; }"
    "QPushButton:pressed { background: #132c3d; }";

static constexpr int kInfoLeftLabelWidth = 112;
static constexpr int kInfoRightLabelWidth = 160;

// Wrap a tab page in a vertical QScrollArea so tabs whose stacked groups exceed
// the dialog's visible height (Themes, Audio, Filters, Peripherals on small or
// high-DPI displays) get a vertical scrollbar instead of forcing the dialog
// past the screen edge (#3345). setWidgetResizable(true) keeps horizontal
// expansion intact and hides the scrollbar when content already fits — users
// on wide screens see no visual change.
static QWidget* wrapTabInScrollArea(QWidget* content)
{
    auto* scroll = new QScrollArea;
    scroll->setWidgetResizable(true);
    scroll->setFrameShape(QFrame::NoFrame);
    scroll->setStyleSheet("QScrollArea { background: transparent; border: none; }");
    scroll->setWidget(content);
    return scroll;
}

static QString displayOrDash(const QString& value)
{
    const QString trimmed = value.trimmed();
    return trimmed.isEmpty() ? QStringLiteral("—") : trimmed;
}

static QString radioSerialNumber(const RadioModel* model)
{
    if (!model) {
        return QStringLiteral("—");
    }
    return displayOrDash(model->chassisSerial().isEmpty()
                             ? model->serial()
                             : model->chassisSerial());
}

static QString prefixedVersion(const QString& version)
{
    const QString trimmed = version.trimmed();
    if (trimmed.isEmpty()) {
        return QStringLiteral("—");
    }
    if (trimmed.startsWith(QLatin1Char('v'), Qt::CaseInsensitive)) {
        return trimmed;
    }
    return QStringLiteral("v%1").arg(trimmed);
}

static QString radioOptionsText(const RadioModel* model)
{
    if (!model) {
        return QStringLiteral("—");
    }
    if (!model->radioOptions().isEmpty()) {
        return model->radioOptions();
    }
    return model->hasAmplifier() ? QStringLiteral("GPS, PGXL") : QStringLiteral("GPS");
}

static void showCopiedPopup(QWidget* anchor);

class CopyValueButton final : public QToolButton {
public:
    explicit CopyValueButton(QString fieldName,
                             std::function<QString()> valueProvider,
                             QWidget* parent = nullptr)
        : QToolButton(parent),
          m_fieldName(std::move(fieldName)),
          m_valueProvider(std::move(valueProvider))
    {
        setAutoRaise(true);
        setCursor(Qt::PointingHandCursor);
        setFocusPolicy(Qt::TabFocus);
        setFixedSize(20, 20);
        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
        setStyleSheet(
            "QToolButton { background: transparent; border: 0; padding: 0; margin: 0; }"
            "QToolButton:focus { outline: none; }");
        resetToolTip();
        setAccessibleName(toolTip());

        connect(this, &QToolButton::clicked, this, [this] {
            if (!m_valueProvider) {
                return;
            }

            const QString text = m_valueProvider().trimmed();
            if (text.isEmpty() || text == QStringLiteral("—")) {
                return;
            }

            if (QClipboard* clipboard = QGuiApplication::clipboard()) {
                clipboard->setText(text);
            }

            showCopiedPopup(this);
        });
    }

protected:
    void paintEvent(QPaintEvent*) override
    {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);

        const bool hasCopyableValue = [this] {
            if (!m_valueProvider) return false;
            const QString text = m_valueProvider().trimmed();
            return !text.isEmpty() && text != QStringLiteral("—");
        }();

        if (hasCopyableValue && (underMouse() || hasFocus())) {
            painter.setPen(Qt::NoPen);
            painter.setBrush(QColor(255, 255, 255, 16));
            painter.drawRoundedRect(rect().adjusted(1, 1, -1, -1), 3, 3);
        }

        QColor stroke = QColor(QStringLiteral("#8090a0"));
        if (!hasCopyableValue) {
            stroke = QColor(QStringLiteral("#405060"));
        } else if (isDown()) {
            stroke = QColor(QStringLiteral("#00b4d8"));
        } else if (underMouse() || hasFocus()) {
            stroke = QColor(QStringLiteral("#c8d8e8"));
        }

        painter.setPen(QPen(stroke, 1.25));
        painter.setBrush(Qt::NoBrush);

        const qreal left = (width() - 16.0) / 2.0;
        const qreal top = (height() - 16.0) / 2.0;
        const QRectF back(left + 3.0, top + 1.5, 8.5, 11.0);
        const QRectF front(left + 5.5, top + 4.0, 8.5, 11.0);

        painter.drawRoundedRect(back, 1.5, 1.5);
        painter.fillRect(front.adjusted(0.8, 0.8, -0.8, -0.8), QColor(QStringLiteral("#0f0f1a")));
        painter.drawRoundedRect(front, 1.5, 1.5);
    }

private:
    void resetToolTip()
    {
        setToolTip(QStringLiteral("Copy %1").arg(m_fieldName));
    }

    QString m_fieldName;
    std::function<QString()> m_valueProvider;
};

static QWidget* makeCopyableValueLabel(const QString& fieldName, QLabel* valueLabel)
{
    auto* wrapper = new QWidget;
    auto* layout = new QHBoxLayout(wrapper);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(4);

    valueLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
    valueLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
    layout->addWidget(valueLabel);
    layout->addStretch(1);
    layout->addWidget(new CopyValueButton(fieldName, [valueLabel] {
        return valueLabel->text();
    }, wrapper));

    return wrapper;
}

static void showCopiedPopup(QWidget* anchor)
{
    if (!anchor) {
        return;
    }

    auto* popup = new QLabel(QStringLiteral("Copied"), nullptr,
                             Qt::ToolTip | Qt::FramelessWindowHint);
    popup->setAttribute(Qt::WA_DeleteOnClose);
    popup->setAttribute(Qt::WA_ShowWithoutActivating);
    AetherSDR::ThemeManager::instance().applyStyleSheet(popup, "QLabel { background: {{color.background.0}}; border: 1px solid {{color.background.2}};"
        " border-radius: 4px; color: {{color.text.primary}}; font-size: 11px;"
        " font-weight: bold; padding: 4px 8px; }");
    popup->adjustSize();

    const QPoint globalCenter = anchor->mapToGlobal(anchor->rect().center());
    const QSize popupSize = popup->sizeHint();
    QPoint pos(globalCenter.x() - popupSize.width() / 2,
               globalCenter.y() - anchor->height() / 2 - popupSize.height() - 6);

    QRect screenRect;
    if (auto* screen = anchor->screen()) {
        screenRect = screen->availableGeometry();
    } else if (auto* primary = QGuiApplication::primaryScreen()) {
        screenRect = primary->availableGeometry();
    }
    if (screenRect.isValid()) {
        pos.setX(std::clamp(pos.x(), screenRect.left(),
                            screenRect.right() - popupSize.width()));
        if (pos.y() < screenRect.top()) {
            pos.setY(globalCenter.y() + anchor->height() / 2 + 6);
        }
    }

    popup->move(pos);
    popup->show();
    QTimer::singleShot(1000, popup, &QLabel::close);
}

static QWidget* makeInfoField(const QString& labelText, QWidget* valueWidget,
                              int labelWidth = kInfoLeftLabelWidth)
{
    auto* wrapper = new QWidget;
    auto* layout = new QHBoxLayout(wrapper);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(8);

    auto* label = new QLabel(labelText);
    label->setStyleSheet(kLabelStyle);
    label->setFixedWidth(labelWidth);
    layout->addWidget(label);
    QSizePolicy policy = valueWidget->sizePolicy();
    if (policy.horizontalPolicy() != QSizePolicy::Fixed) {
        policy.setHorizontalPolicy(QSizePolicy::Expanding);
        valueWidget->setSizePolicy(policy);
    }
    layout->addWidget(valueWidget, 1);

    return wrapper;
}

static QWidget* makeCopyableInfoField(const QString& fieldName, const QString& labelText,
                                      QLabel* valueLabel, int labelWidth = kInfoLeftLabelWidth)
{
    auto* wrapper = new QWidget;
    auto* layout = new QHBoxLayout(wrapper);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(8);

    auto* label = new QLabel(labelText);
    label->setStyleSheet(kLabelStyle);
    label->setFixedWidth(labelWidth);
    layout->addWidget(label);

    valueLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
    valueLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
    layout->addWidget(valueLabel, 1);
    layout->addWidget(new CopyValueButton(fieldName, [valueLabel] {
        return valueLabel->text();
    }, wrapper));

    return wrapper;
}

static constexpr const char* kSuppressAudioDeviceNotificationsKey =
    "SuppressAudioDeviceNotifications";

static QString normalizedOscillatorValue(QString value)
{
    value = value.trimmed().toLower();
    return value == "ext" ? QStringLiteral("external") : value;
}

static QString oscillatorSourceLabel(const QString& value)
{
    const QString normalized = normalizedOscillatorValue(value);
    if (normalized == "auto") return QStringLiteral("Auto");
    if (normalized == "external") return QStringLiteral("External 10 MHz");
    if (normalized == "gpsdo") return QStringLiteral("GPSDO");
    if (normalized == "tcxo") return QStringLiteral("TCXO");
    return value.trimmed().isEmpty() ? QStringLiteral("Unknown") : value.toUpper();
}

static QString oscillatorStatusText(const RadioModel* model)
{
    const QString setting = normalizedOscillatorValue(model->oscSetting());
    const QString state = normalizedOscillatorValue(model->oscState());
    if (state.isEmpty())
        return QStringLiteral("Waiting for oscillator status");

    QString text;
    if (setting == "auto" && state != "auto") {
        text = QStringLiteral("Auto -> %1").arg(oscillatorSourceLabel(state));
    } else if (!setting.isEmpty() && setting != state && state != "auto") {
        text = QStringLiteral("%1 -> %2")
                   .arg(oscillatorSourceLabel(setting), oscillatorSourceLabel(state));
    } else {
        text = oscillatorSourceLabel(state);
    }

    text += model->oscLocked() ? QStringLiteral(" Locked")
                               : QStringLiteral(" Unlocked");
    if (state == "external" && !model->extPresent())
        text += QStringLiteral(" (not detected)");
    return text;
}

static QString oscillatorStatusColor(const RadioModel* model)
{
    if (normalizedOscillatorValue(model->oscState()).isEmpty())
        return QStringLiteral("#8aa8c0");
    return model->oscLocked() ? QStringLiteral("#00c040")
                              : QStringLiteral("#c04040");
}

static void addOscillatorChoice(QComboBox* combo, const QString& label,
                                const QString& value)
{
    if (combo->findData(value) < 0)
        combo->addItem(label, value);
}

static void refreshOscillatorSourceCombo(QComboBox* combo, const RadioModel* model,
                                         const QString& preferred = {})
{
    const QString current = normalizedOscillatorValue(
        preferred.isEmpty() ? combo->currentData().toString() : preferred);
    const QString setting = normalizedOscillatorValue(model->oscSetting());
    const QString state = normalizedOscillatorValue(model->oscState());
    const bool hasOscillatorStatus = !state.isEmpty();

    auto shouldKeep = [&](const QString& value) {
        return current == value || setting == value || state == value;
    };

    combo->clear();
    addOscillatorChoice(combo, QStringLiteral("Auto"), QStringLiteral("auto"));
    if (hasOscillatorStatus || model->tcxoPresent() || shouldKeep(QStringLiteral("tcxo")))
        addOscillatorChoice(combo, QStringLiteral("TCXO"), QStringLiteral("tcxo"));
    if (model->gpsdoPresent() || shouldKeep(QStringLiteral("gpsdo")))
        addOscillatorChoice(combo, QStringLiteral("GPSDO"), QStringLiteral("gpsdo"));
    if (hasOscillatorStatus || model->extPresent() || shouldKeep(QStringLiteral("external")))
        addOscillatorChoice(combo, QStringLiteral("External 10 MHz"),
                            QStringLiteral("external"));

    const QString desired = setting.isEmpty() ? current : setting;
    int idx = combo->findData(desired);
    if (idx < 0) idx = combo->findData(current);
    if (idx < 0) idx = combo->findData(QStringLiteral("auto"));
    if (idx >= 0) combo->setCurrentIndex(idx);
}

RadioSetupDialog::RadioSetupDialog(RadioModel* model, AudioEngine* audio,
                                   TgxlConnection* tgxl, PgxlConnection* pgxl,
                                   AntennaGeniusModel* ag,
                                   KiwiSdrManager* kiwiSdrManager,
                                   QWidget* parent)
    : PersistentDialog(QStringLiteral("Radio Setup"),
                       QStringLiteral("RadioSetupDialogGeometry"), parent),
      m_model(model), m_audio(audio),
      m_tgxl(tgxl), m_pgxl(pgxl), m_ag(ag),
      m_kiwiSdrManager(kiwiSdrManager)
{
    theme::setContainer(this, QStringLiteral("dialog/radioSetup"));
    setMinimumSize(820, 620);
    AetherSDR::ThemeManager::instance().applyStyleSheet(this, "QDialog { background: {{color.background.0}}; }");

    auto* layout = new QVBoxLayout(bodyWidget());

    auto* tabs = new QTabWidget;
    m_tabs = tabs;
    AetherSDR::ThemeManager::instance().applyStyleSheet(tabs, "QTabWidget::pane { border: 1px solid {{color.background.2}}; background: {{color.background.0}}; }"
        "QTabBar::tab { background: {{color.background.1}}; color: {{color.text.secondary}}; "
        "border: 1px solid {{color.background.2}}; padding: 4px 12px; margin-right: 2px; }"
        "QTabBar::tab:selected { background: {{color.background.0}}; color: {{color.text.primary}}; "
        "border-bottom-color: {{color.background.0}}; }");

    // Build only the default (Radio) tab eagerly; defer the rest until first
    // selected.  This avoids hardware-probing calls (QSerialPortInfo,
    // QMediaDevices) during construction, which crash on some Wayland/Qt 6.11
    // configurations (#1776).
    tabs->addTab(wrapTabInScrollArea(buildRadioTab()), "Radio");

    auto addDeferred = [&](const QString& name, std::function<QWidget*()> builder) {
        int idx = tabs->addTab(new QWidget, name);
        m_deferredBuilders[idx] = std::move(builder);
    };
    addDeferred("Network",     [this] { return buildNetworkTab(); });
    addDeferred("GPS",         [this] { return buildGpsTab(); });
    addDeferred("Audio",       [this] { return buildAudioTab(); });
    addDeferred("TX",          [this] { return buildTxTab(); });
    addDeferred("Phone/CW",   [this] { return buildPhoneCwTab(); });
    addDeferred("RX",          [this] { return buildRxTab(); });
    addDeferred("Antennas",    [this] { return buildAntennaNamesTab(); });
    addDeferred("Filters",     [this] { return buildFiltersTab(); });
    addDeferred("XVTR",        [this] { return buildXvtrTab(); });
    // External APD tab (#2186) — only present on radios that report
    // `apd configurable=1` (FLEX-8x00 series with SmartSDR 4.2.18+).
    m_apdTabIndex = tabs->addTab(new QWidget, "APD");
    m_deferredBuilders[m_apdTabIndex] = [this] { return buildApdTab(); };
    tabs->setTabVisible(m_apdTabIndex, m_model->transmitModel().apdConfigurable());
    connect(&m_model->transmitModel(), &TransmitModel::apdStateChanged,
            this, [this, tabs] {
        tabs->setTabVisible(m_apdTabIndex, m_model->transmitModel().apdConfigurable());
    });
    addDeferred("USB Cables",      [this] { return buildUsbCablesTab(); });
    addDeferred("Peripherals",     [this] { return buildPeripheralsTab(); });
    addDeferred("Themes",          [this] { return buildUiEnhancementsTab(); });
    addDeferred("SmartLink",       [this] { return buildSmartLinkTab(); });
#ifdef HAVE_SERIALPORT
    addDeferred("Serial",          [this] { return buildSerialTab(); });
#endif

    connect(tabs, &QTabWidget::currentChanged, this, &RadioSetupDialog::buildDeferredTab);

    layout->addWidget(tabs);

    auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
    AetherSDR::ThemeManager::instance().applyStyleSheet(buttons, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; padding: 4px 16px; }"
        "QPushButton:hover { background: {{color.background.1}}; }");
    connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
    layout->addWidget(buttons);
}

void RadioSetupDialog::closeEvent(QCloseEvent* event)
{
    // Persist any uncommitted "user cleared IP" edits in the Peripherals
    // tab before the base class flushes geometry to AppSettings.
    for (const auto& saver : m_peripheralRowSavers)
        saver();
    PersistentDialog::closeEvent(event);
}

// ── Radio tab ─────────────────────────────────────────────────────────────────

QWidget* RadioSetupDialog::buildRadioTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Toggle button style: green when on, red when off
    static const QString kToggleStyle =
        "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
        "padding: 3px 10px; }"
        "QPushButton:checked { background: #1a5030; color: #00e060; "
        "border: 1px solid #20a040; }";

    auto makeToggle = [](bool checked) {
        auto* btn = new QPushButton("Enabled");
        btn->setCheckable(true);
        btn->setChecked(checked);
        btn->setStyleSheet(kToggleStyle);
        return btn;
    };

    // Radio Information group
    {
        auto* group = new QGroupBox("Radio Information");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);
        grid->setColumnStretch(0, 1);
        grid->setColumnStretch(1, 1);

        m_serialLabel = new QLabel(radioSerialNumber(m_model));
        m_serialLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Radio Serial Number"),
                                              QStringLiteral("Serial:"),
                                              m_serialLabel),
                        0, 0);

        m_regionLabel = new QLabel(m_model->region().isEmpty() ? "USA" : m_model->region());
        AetherSDR::ThemeManager::instance().applyStyleSheet(m_regionLabel, "QLabel { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.accent.bright}}; font-size: 11px; font-weight: bold; "
            "padding: 3px 10px; }");
        m_regionLabel->setAlignment(Qt::AlignCenter);
        grid->addWidget(makeInfoField(QStringLiteral("Region:"), m_regionLabel,
                                      kInfoRightLabelWidth),
                        0, 1);

        m_hwVersionLabel = new QLabel(prefixedVersion(m_model->version()));
        m_hwVersionLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("HW Version"),
                                              QStringLiteral("HW Version:"),
                                              m_hwVersionLabel),
                        1, 0);

        m_remoteOnBtn = makeToggle(m_model->remoteOnEnabled());
        connect(m_remoteOnBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->setRemoteOnEnabled(on);
        });
        grid->addWidget(makeInfoField(QStringLiteral("Remote On:"), m_remoteOnBtn,
                                      kInfoRightLabelWidth),
                        1, 1);

        m_optionsLabel = new QLabel(radioOptionsText(m_model));
        m_optionsLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Options"),
                                              QStringLiteral("Options:"),
                                              m_optionsLabel),
                        2, 0);

        auto* fcBtn = makeToggle(true);
        grid->addWidget(makeInfoField(QStringLiteral("FlexControl:"), fcBtn,
                                      kInfoRightLabelWidth),
                        2, 1);

        auto* mfBtn = makeToggle(m_model->multiFlexEnabled());
        connect(mfBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->setMultiFlexEnabled(on);
        });
        grid->addWidget(makeInfoField(QStringLiteral("multiFLEX:"), mfBtn,
                                      kInfoRightLabelWidth),
                        3, 1);

        auto* rebootBtn = new QPushButton(QStringLiteral("Reboot Radio"));
        AetherSDR::ThemeManager::instance().applyStyleSheet(rebootBtn,
            "QPushButton { background: #3a1a1a; color: #ffb080; border: 1px solid #6e3030;"
            " border-radius: 3px; font-size: 11px; font-weight: bold; padding: 3px 10px; }"
            "QPushButton:hover { background: #4a2020; }"
            "QPushButton:disabled { background: {{color.button.danger.background.disabled}}; color: {{color.button.danger.foreground.disabled}}; border-color: {{color.button.danger.border.disabled}}; }");
        // Only enable when actually connected; subscribe so disconnect/reconnect
        // disables/re-enables the button without the user having to reopen the
        // dialog. rebootRadio() also early-returns on disconnected, but the
        // disabled state makes the affordance discoverable rather than silent.
        rebootBtn->setEnabled(m_model->isConnected());
        connect(m_model, &RadioModel::connectionStateChanged,
                rebootBtn, &QPushButton::setEnabled);
        connect(rebootBtn, &QPushButton::clicked, this, [this] {
            const bool wan = m_model->isWan();
            const QString body = wan
                ? QStringLiteral("Reboot the connected radio now?\n\n"
                                 "AetherSDR will disconnect. SmartLink/WAN sessions "
                                 "do not auto-reconnect today — you will need to "
                                 "reconnect manually once the radio finishes booting.")
                : QStringLiteral("Reboot the connected radio now?\n\n"
                                 "AetherSDR will disconnect and automatically reconnect "
                                 "once the radio finishes booting.");
            const auto ret = QMessageBox::warning(
                this,
                QStringLiteral("Reboot Radio"),
                body,
                QMessageBox::Ok | QMessageBox::Cancel,
                QMessageBox::Cancel);
            if (ret == QMessageBox::Ok) {
                m_model->rebootRadio();
                close();
            }
        });
        grid->addWidget(makeInfoField(QStringLiteral("Reboot:"), rebootBtn,
                                      kInfoLeftLabelWidth),
                        3, 0);

        connect(m_model, &RadioModel::infoChanged, this, [this] {
            if (m_serialLabel) {
                m_serialLabel->setText(radioSerialNumber(m_model));
            }
            if (m_hwVersionLabel) {
                m_hwVersionLabel->setText(prefixedVersion(m_model->version()));
            }
            if (m_optionsLabel) {
                m_optionsLabel->setText(radioOptionsText(m_model));
            }
        });

        for (auto* lbl : group->findChildren<QLabel*>()) {
            if (lbl->styleSheet().isEmpty())
                lbl->setStyleSheet(kLabelStyle);
        }

        vbox->addWidget(group);
    }

    // Radio Identification group
    {
        auto* group = new QGroupBox("Radio Identification");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);
        grid->setColumnStretch(0, 1);
        grid->setColumnStretch(1, 1);

        m_modelLabel = new QLabel(displayOrDash(m_model->model()));
        m_modelLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Model"),
                                              QStringLiteral("Model:"),
                                              m_modelLabel),
                        0, 0);

        m_nicknameEdit = new QLineEdit(m_model->nickname().isEmpty()
            ? m_model->name() : m_model->nickname());
        m_nicknameEdit->setStyleSheet(kEditStyle);
        grid->addWidget(makeInfoField(QStringLiteral("Nickname:"), m_nicknameEdit,
                                      kInfoRightLabelWidth),
                        0, 1);

        m_callsignEdit = new QLineEdit(m_model->callsign());
        m_callsignEdit->setStyleSheet(kEditStyle);
        grid->addWidget(makeInfoField(QStringLiteral("Callsign:"), m_callsignEdit),
                        1, 0);

        connect(m_nicknameEdit, &QLineEdit::editingFinished, this, [this] {
            m_model->sendCommand("radio name " + m_nicknameEdit->text());
        });
        connect(m_callsignEdit, &QLineEdit::editingFinished, this, [this] {
            m_model->sendCommand("radio callsign " + m_callsignEdit->text());
        });

        connect(m_model, &RadioModel::infoChanged, this, [this] {
            if (m_modelLabel) {
                m_modelLabel->setText(displayOrDash(m_model->model()));
            }
        });

        QString stationVal = AppSettings::instance().value("StationName", "").toString();
        auto* stationEdit = new QLineEdit(
            stationVal.isEmpty() ? QSysInfo::machineHostName() : stationVal);
        stationEdit->setStyleSheet(kEditStyle);
        stationEdit->setToolTip("Identifies this client to other Multi-Flex stations.\n"
                                "Defaults to OS hostname if empty.");
        grid->addWidget(makeInfoField(QStringLiteral("Station Name:"), stationEdit,
                                      kInfoRightLabelWidth),
                        1, 1);
        connect(stationEdit, &QLineEdit::editingFinished, this, [this, stationEdit] {
            auto& s = AppSettings::instance();
            s.setValue("StationName", stationEdit->text());
            s.save();
            m_model->sendCommand("client station " + stationEdit->text());
        });

        for (auto* lbl : group->findChildren<QLabel*>()) {
            if (lbl->styleSheet().isEmpty())
                lbl->setStyleSheet(kLabelStyle);
        }

        vbox->addWidget(group);
    }

    // License Info group (matches SmartSDR Radio Setup → License Info section)
    {
        auto* group = new QGroupBox("License Info");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);
        grid->setColumnStretch(0, 1);
        grid->setColumnStretch(1, 1);

        // Row 0: Subscription | Expiration
        m_licSubscriptionLabel = new QLabel(
            m_model->licenseSubscription().isEmpty() ? "—" : m_model->licenseSubscription());
        m_licSubscriptionLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Subscription"),
                                              QStringLiteral("Subscription:"),
                                              m_licSubscriptionLabel),
                        0, 0);

        m_licExpirationLabel = new QLabel(
            m_model->licenseExpirationDate().isEmpty() ? "—" : m_model->licenseExpirationDate());
        m_licExpirationLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Expiration"),
                                              QStringLiteral("Expiration:"),
                                              m_licExpirationLabel,
                                              kInfoRightLabelWidth),
                        0, 1);

        // Row 1: Radio ID | Licensed version
        m_licRadioIdLabel = new QLabel(
            m_model->licenseRadioId().isEmpty() ? "—" : m_model->licenseRadioId());
        m_licRadioIdLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Radio ID"),
                                              QStringLiteral("Radio ID:"),
                                              m_licRadioIdLabel),
                        1, 0);

        m_licMaxVersionLabel = new QLabel(
            m_model->licenseMaxVersion().isEmpty() ? "—" : m_model->licenseMaxVersion());
        m_licMaxVersionLabel->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableInfoField(QStringLiteral("Licensed version"),
                                              QStringLiteral("Licensed version:"),
                                              m_licMaxVersionLabel,
                                              kInfoRightLabelWidth),
                        1, 1);

        for (auto* lbl : group->findChildren<QLabel*>()) {
            if (lbl->styleSheet().isEmpty())
                lbl->setStyleSheet(kLabelStyle);
        }

        // Update labels live if license status arrives after dialog opens
        connect(m_model, &RadioModel::infoChanged, this, [this] {
            if (!m_model->licenseSubscription().isEmpty())
                m_licSubscriptionLabel->setText(m_model->licenseSubscription());
            if (!m_model->licenseExpirationDate().isEmpty())
                m_licExpirationLabel->setText(m_model->licenseExpirationDate());
            if (!m_model->licenseRadioId().isEmpty())
                m_licRadioIdLabel->setText(m_model->licenseRadioId());
            if (!m_model->licenseMaxVersion().isEmpty())
                m_licMaxVersionLabel->setText(m_model->licenseMaxVersion());
        });

        vbox->addWidget(group);
    }

    // Firmware Update group
    {
        auto* group = new QGroupBox("Firmware Update");
        group->setStyleSheet(kGroupStyle);
        auto* vlay = new QVBoxLayout(group);
        vlay->setSpacing(6);

        // Current version row
        auto* infoRow = new QHBoxLayout;
        infoRow->addWidget(new QLabel("FW Version:"));
        auto* curFw = new QLabel(displayOrDash(m_model->softwareVersion()));
        curFw->setStyleSheet(kValueStyle);
        infoRow->addWidget(makeCopyableValueLabel(QStringLiteral("FW Version"), curFw), 1);
        vlay->addLayout(infoRow);

        connect(m_model, &RadioModel::infoChanged, this, [this, curFw] {
            curFw->setText(displayOrDash(m_model->softwareVersion()));
        });

        // Progress bar
        m_fwProgress = new QProgressBar;
        m_fwProgress->setRange(0, 100);
        m_fwProgress->setValue(0);
        m_fwProgress->setTextVisible(true);
        m_fwProgress->setFixedHeight(20);
        AetherSDR::ThemeManager::instance().applyStyleSheet(m_fwProgress, "QProgressBar { text-align: center; font-size: 11px; color: {{color.text.primary}};"
            " background: {{color.background.1}}; border: 1px solid #2e4e6e; border-radius: 3px; }"
            "QProgressBar::chunk { background: {{color.accent}}; }");
        m_fwProgress->hide();
        vlay->addWidget(m_fwProgress);

        // Status label (multi-line)
        m_fwStatusLabel = new QLabel("");
        m_fwStatusLabel->setStyleSheet("QLabel { color: #6888a0; font-size: 10px; }");
        m_fwStatusLabel->setWordWrap(true);
        vlay->addWidget(m_fwStatusLabel);

        // Button row
        auto* btnRow = new QHBoxLayout;
        auto* checkBtn = new QPushButton("Check for Update");
        AetherSDR::ThemeManager::instance().applyStyleSheet(checkBtn, "QPushButton { background: {{color.background.1}}; color: {{color.text.primary}}; border: 1px solid #2e4e6e;"
            " border-radius: 3px; padding: 4px 8px; }"
            "QPushButton:hover { background: {{color.background.1}}; }");
        btnRow->addWidget(checkBtn);

        auto* browseBtn = new QPushButton("Select Installer...");
        browseBtn->setStyleSheet(checkBtn->styleSheet());
        btnRow->addWidget(browseBtn);

        m_fwUploadBtn = new QPushButton("Upload Firmware");
        m_fwUploadBtn->setEnabled(false);
        AetherSDR::ThemeManager::instance().applyStyleSheet(m_fwUploadBtn, "QPushButton { background: #1a3a1a; color: #80e080; border: 1px solid #2e6e2e;"
            " border-radius: 3px; padding: 4px 8px; }"
            "QPushButton:hover { background: #2a4a2a; }"
            "QPushButton:disabled { background: {{color.button.background.disabled}}; color: {{color.button.foreground.disabled}}; border-color: {{color.button.border.disabled}}; }");
        btnRow->addWidget(m_fwUploadBtn);
        vlay->addLayout(btnRow);

        // ── Stager wiring ─────────────────────────────────────────────
        m_stager = new FirmwareStager(this);

        connect(checkBtn, &QPushButton::clicked, this, [this, checkBtn] {
            checkBtn->setEnabled(false);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #6888a0; font-size: 10px; }");
            m_stager->checkForUpdate(m_model->softwareVersion());
        });

        connect(m_stager, &FirmwareStager::updateCheckComplete, this,
            [this, checkBtn](const QString& latest, bool avail) {
            checkBtn->setEnabled(true);
            if (avail) {
                m_fwStatusLabel->setStyleSheet("QLabel { color: #f0c040; font-size: 10px; }");
                m_fwStatusLabel->setText(QString(
                    "Update available: v%1\n"
                    "Download the SmartSDR installer from flexradio.com,\n"
                    "then click 'Select Installer...' to stage it.").arg(latest));
            } else {
                m_fwStatusLabel->setStyleSheet("QLabel { color: #80e080; font-size: 10px; }");
                m_fwStatusLabel->setText("Firmware is up to date (v" + latest + ").");
            }
        });

        connect(m_stager, &FirmwareStager::updateCheckFailed, this,
            [this, checkBtn](const QString& err) {
            checkBtn->setEnabled(true);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #e08080; font-size: 10px; }");
            m_fwStatusLabel->setText(err);
        });

        connect(m_stager, &FirmwareStager::stageProgress, this,
            [this](int pct, const QString& status) {
            m_fwProgress->setValue(pct);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #6888a0; font-size: 10px; }");
            m_fwStatusLabel->setText(status);
        });

        connect(m_stager, &FirmwareStager::stageComplete, this,
            [this](const QString& path, const QString&) {
            m_fwFilePath = path;
            m_fwUploadBtn->setEnabled(true);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #80e080; font-size: 10px; }");
        });

        connect(m_stager, &FirmwareStager::stageFailed, this,
            [this](const QString& err) {
            m_fwProgress->hide();
            m_fwStatusLabel->setStyleSheet("QLabel { color: #e08080; font-size: 10px; }");
            m_fwStatusLabel->setText(err);
        });

        // ── Browse / select installer manually ────────────────────────
        // Accepts the SmartSDR installer the user has already downloaded
        // from FlexRadio (.msi for v4.2+, .exe for older releases) or a
        // pre-extracted .ssdr file. The stager auto-detects which.
        connect(browseBtn, &QPushButton::clicked, this, [this] {
            const QString path = QFileDialog::getOpenFileName(
                this, "Select SmartSDR Installer or Firmware File", QString(),
                "SmartSDR installer or firmware (*.msi *.exe *.ssdr);;"
                "MSI installer (*.msi);;"
                "EXE installer (*.exe);;"
                "Extracted firmware (*.ssdr);;"
                "All files (*)");
            if (path.isEmpty()) return;

            m_fwFilePath.clear();
            m_fwUploadBtn->setEnabled(false);
            m_fwProgress->show();
            m_fwProgress->setValue(0);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #6888a0; font-size: 10px; }");
            m_fwStatusLabel->setText("Preparing firmware from " + QFileInfo(path).fileName() + "...");

            // The stager emits stageProgress / stageComplete / stageFailed.
            // Existing slots wire to those (set above) so we just kick it off.
            m_stager->stageFromLocalFile(path,
                FirmwareStager::modelToFamily(m_model->model()));
        });

        // ── Upload ────────────────────────────────────────────────────
        connect(m_fwUploadBtn, &QPushButton::clicked, this, [this] {
            if (m_fwFilePath.isEmpty()) return;

            const auto reply = QMessageBox::warning(this, "Firmware Update",
                QString("Upload %1 to %2?\n\n"
                        "The radio will reboot after the update.\n"
                        "Do not disconnect during the upload.")
                    .arg(QFileInfo(m_fwFilePath).fileName(), m_model->model()),
                QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
            if (reply != QMessageBox::Ok) return;

            if (!m_uploader)
                m_uploader = new FirmwareUploader(m_model, this);

            m_fwProgress->show();
            m_fwProgress->setValue(0);
            m_fwUploadBtn->setEnabled(false);
            m_fwStatusLabel->setStyleSheet("QLabel { color: #6888a0; font-size: 10px; }");

            connect(m_uploader, &FirmwareUploader::progressChanged, this,
                [this](int pct, const QString& status) {
                    m_fwProgress->setValue(pct);
                    m_fwStatusLabel->setText(status);
                });
            connect(m_uploader, &FirmwareUploader::finished, this,
                [this](bool ok, const QString& msg) {
                    m_fwStatusLabel->setText(msg);
                    m_fwUploadBtn->setEnabled(!ok);
                    if (ok) {
                        m_fwProgress->setValue(100);
                        m_fwStatusLabel->setStyleSheet("QLabel { color: #80e080; font-size: 10px; }");
                    } else {
                        m_fwProgress->hide();
                        m_fwStatusLabel->setStyleSheet("QLabel { color: #e08080; font-size: 10px; }");
                    }
                });

            m_uploader->upload(m_fwFilePath);
        });

        for (auto* lbl : group->findChildren<QLabel*>()) {
            if (lbl->styleSheet().isEmpty())
                lbl->setStyleSheet(kLabelStyle);
        }

        vbox->addWidget(group);
    }

    // Firmware disclaimer
    auto* disclaimer = new QLabel(
        "⚠ CAUTION: Firmware update is currently a highly experimental feature. "
        "Use at your own risk. At this time we still recommend updating "
        "via the SmartSDR Windows application.");
    disclaimer->setWordWrap(true);
    disclaimer->setStyleSheet(
        "QLabel { color: #c08040; font-size: 11px; font-style: italic;"
        " padding: 4px 8px; }");
    vbox->addWidget(disclaimer);

    vbox->addStretch(1);
    return page;
}


QWidget* RadioSetupDialog::buildNetworkTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    // Network group
    {
        auto* group = new QGroupBox("Network");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);
        grid->setColumnStretch(1, 1);
        grid->setColumnStretch(3, 1);

        grid->addWidget(new QLabel("IP Address:"), 0, 0);
        auto* ipLbl = new QLabel(displayOrDash(m_model->ip()));
        ipLbl->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableValueLabel(QStringLiteral("IP Address"), ipLbl), 0, 1);

        grid->addWidget(new QLabel("Subnet Mask:"), 0, 2);
        auto* maskLbl = new QLabel(displayOrDash(m_model->netmask()));
        maskLbl->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableValueLabel(QStringLiteral("Subnet Mask"), maskLbl), 0, 3);

        grid->addWidget(new QLabel("MAC Address:"), 1, 0);
        auto* macLbl = new QLabel(displayOrDash(m_model->mac()));
        macLbl->setStyleSheet(kValueStyle);
        grid->addWidget(makeCopyableValueLabel(QStringLiteral("MAC Address"), macLbl), 1, 1);

        connect(m_model, &RadioModel::infoChanged, this, [this, ipLbl, maskLbl, macLbl] {
            ipLbl->setText(displayOrDash(m_model->ip()));
            maskLbl->setText(displayOrDash(m_model->netmask()));
            macLbl->setText(displayOrDash(m_model->mac()));
        });

        for (auto* lbl : group->findChildren<QLabel*>())
            if (lbl->styleSheet().isEmpty()) lbl->setStyleSheet(kLabelStyle);

        vbox->addWidget(group);
    }

    // Advanced group
    {
        auto* group = new QGroupBox("Advanced");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        grid->addWidget(new QLabel("Enforce Private IP Connections:"), 0, 0);
        auto* enforceBtn = new QPushButton("Enabled");
        enforceBtn->setCheckable(true);
        enforceBtn->setChecked(m_model->enforcePrivateIp());
        AetherSDR::ThemeManager::instance().applyStyleSheet(enforceBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
            "padding: 3px 10px; }"
            "QPushButton:checked { background: #1a5030; color: {{color.accent.success}}; "
            "border: 1px solid #20a040; }");
        connect(enforceBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(
                QString("radio set enforce_private_ip_connections=%1").arg(on ? 1 : 0));
        });
        grid->addWidget(enforceBtn, 0, 1);

        grid->addWidget(new QLabel("Network MTU:"), 1, 0);
        auto* mtuSpin = new QSpinBox;
        mtuSpin->setRange(576, 9000);
        mtuSpin->setValue(AppSettings::instance().value("NetworkMtu", "1450").toInt());
        mtuSpin->setSuffix(" bytes");
        mtuSpin->setToolTip("Maximum Transmission Unit for VITA-49 UDP packets.\nDefault: 1450 (compatible with most VPN/SD-WAN tunnels).");
        connect(mtuSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int val) {
            m_model->sendCommand(
                QString("client set enforce_network_mtu=1 network_mtu=%1").arg(val));
            AppSettings::instance().setValue("NetworkMtu", QString::number(val));
            AppSettings::instance().save();
        });
        grid->addWidget(mtuSpin, 1, 1);

        for (auto* lbl : group->findChildren<QLabel*>())
            if (lbl->styleSheet().isEmpty()) lbl->setStyleSheet(kLabelStyle);

        vbox->addWidget(group);
    }

    // DHCP / Static IP group
    vbox->addWidget(buildIpConfigGroup());

    vbox->addStretch(1);
    return page;
}

// Extracted to reduce lambda nesting depth in buildNetworkTab()
// (avoids GCC 13 internal compiler error on Ubuntu 24.04)
QGroupBox* RadioSetupDialog::buildIpConfigGroup()
{
    auto* group = new QGroupBox("IP Configuration");
    group->setStyleSheet(kGroupStyle);
    auto* gvbox = new QVBoxLayout(group);
    gvbox->setSpacing(6);

    auto* btnRow = new QHBoxLayout;
    btnRow->setSpacing(4);

    const bool isStatic = m_model->hasStaticIp();

    auto* dhcpBtn = new QPushButton("DHCP");
    dhcpBtn->setCheckable(true);
    dhcpBtn->setChecked(!isStatic);
    AetherSDR::ThemeManager::instance().applyStyleSheet(dhcpBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
        "padding: 4px 16px; }"
        "QPushButton:checked { background: {{color.background.2}}; color: {{color.text.primary}}; "
        "border: 1px solid {{color.accent.dim}}; }");
    btnRow->addWidget(dhcpBtn);

    auto* staticBtn = new QPushButton("Static");
    staticBtn->setCheckable(true);
    staticBtn->setChecked(isStatic);
    staticBtn->setStyleSheet(dhcpBtn->styleSheet());
    btnRow->addWidget(staticBtn);

    btnRow->addStretch(1);
    gvbox->addLayout(btnRow);

    auto* fieldsGrid = new QGridLayout;
    fieldsGrid->setSpacing(4);

    fieldsGrid->addWidget(new QLabel("IP Address:"), 0, 0);
    auto* staticIp = new QLineEdit(isStatic ? m_model->staticIp() : m_model->ip());
    staticIp->setStyleSheet(kEditStyle);
    staticIp->setEnabled(isStatic);
    fieldsGrid->addWidget(staticIp, 0, 1);

    fieldsGrid->addWidget(new QLabel("Mask:"), 1, 0);
    auto* staticMask = new QLineEdit(isStatic ? m_model->staticNetmask() : m_model->netmask());
    staticMask->setStyleSheet(kEditStyle);
    staticMask->setEnabled(isStatic);
    fieldsGrid->addWidget(staticMask, 1, 1);

    fieldsGrid->addWidget(new QLabel("Gateway:"), 2, 0);
    auto* staticGw = new QLineEdit(isStatic ? m_model->staticGateway() : m_model->gateway());
    staticGw->setStyleSheet(kEditStyle);
    staticGw->setEnabled(isStatic);
    fieldsGrid->addWidget(staticGw, 2, 1);

    for (auto* lbl : group->findChildren<QLabel*>())
        if (lbl->styleSheet().isEmpty()) lbl->setStyleSheet(kLabelStyle);

    gvbox->addLayout(fieldsGrid);

    auto* applyBtn = new QPushButton("Apply");
    applyBtn->setEnabled(false);
    AetherSDR::ThemeManager::instance().applyStyleSheet(applyBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
        "padding: 4px 16px; }"
        "QPushButton:hover { background: {{color.background.1}}; }");
    gvbox->addWidget(applyBtn, 0, Qt::AlignLeft);

    connect(dhcpBtn, &QPushButton::clicked, this,
            [dhcpBtn, staticBtn, staticIp, staticMask, staticGw, applyBtn] {
        dhcpBtn->setChecked(true);
        staticBtn->setChecked(false);
        staticIp->setEnabled(false);
        staticMask->setEnabled(false);
        staticGw->setEnabled(false);
        applyBtn->setEnabled(true);
    });
    connect(staticBtn, &QPushButton::clicked, this,
            [dhcpBtn, staticBtn, staticIp, staticMask, staticGw, applyBtn] {
        dhcpBtn->setChecked(false);
        staticBtn->setChecked(true);
        staticIp->setEnabled(true);
        staticMask->setEnabled(true);
        staticGw->setEnabled(true);
        applyBtn->setEnabled(true);
    });
    connect(applyBtn, &QPushButton::clicked, this,
            [this, dhcpBtn, staticIp, staticMask, staticGw, applyBtn] {
        if (dhcpBtn->isChecked()) {
            m_model->sendCommand("radio static_net_params reset");
            qDebug() << "RadioSetupDialog: network set to DHCP";
        } else {
            const QString cmd = QString("radio static_net_params ip=%1 gateway=%2 netmask=%3")
                .arg(staticIp->text(), staticGw->text(), staticMask->text());
            m_model->sendCommand(cmd);
            qDebug() << "RadioSetupDialog: static IP applied" << cmd;
        }
        applyBtn->setEnabled(false);
    });

    return group;
}
QWidget* RadioSetupDialog::buildGpsTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    // GPS installed status
    {
        const bool installed = (m_model->gpsStatus() != "Not Present"
                                && !m_model->gpsStatus().isEmpty());
        auto* statusLbl = new QLabel(installed ? "GPS is installed" : "GPS is not installed");
        statusLbl->setStyleSheet(installed
            ? "QLabel { color: #00c040; font-size: 16px; font-weight: bold; }"
            : "QLabel { color: #c04040; font-size: 16px; font-weight: bold; }");
        statusLbl->setAlignment(Qt::AlignCenter);
        vbox->addWidget(statusLbl);
        vbox->addSpacing(16);
    }

    // GPS data grid
    {
        auto* grid = new QGridLayout;
        grid->setSpacing(8);

        auto addField = [&](int row, int col, const QString& label, const QString& value) {
            auto* lbl = new QLabel(label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, col * 2);
            auto* val = new QLabel(value);
            val->setStyleSheet(kValueStyle);
            grid->addWidget(val, row, col * 2 + 1);
        };

        addField(0, 0, "Latitude:",     m_model->gpsLat());
        addField(0, 1, "Longitude:",    m_model->gpsLon());
        addField(1, 0, "Grid Square:",  m_model->gpsGrid());
        addField(1, 1, "Altitude:",     m_model->gpsAltitude());
        addField(2, 0, "Sat Tracked:",  QString::number(m_model->gpsTracked()));
        addField(2, 1, "Sat Visible:",  QString::number(m_model->gpsVisible()));
        addField(3, 0, "Speed:",        m_model->gpsSpeed());
        addField(3, 1, "Freq Error:",   m_model->gpsFreqError());
        addField(4, 0, "Status:",       m_model->gpsStatus());
        addField(4, 1, "UTC Time:",     m_model->gpsTime());

        vbox->addLayout(grid);
    }

    vbox->addStretch(1);
    return page;
}
QWidget* RadioSetupDialog::buildTxTab()
{
    auto& tx = m_model->transmitModel();
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    // Timings group
    {
        auto* group = new QGroupBox("Timings (in ms)");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto addTimingField = [&](int row, int col, const QString& label, int value) {
            auto* lbl = new QLabel(label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, col * 2);
            auto* edit = new QLineEdit(QString::number(value));
            edit->setStyleSheet(kEditStyle);
            edit->setFixedWidth(60);
            grid->addWidget(edit, row, col * 2 + 1);
            return edit;
        };

        // Scale factor lets the same helper drive both 1:1 ms fields and
        // the seconds-displayed timeout field which the radio still
        // expects in ms (FlexLib Radio.cs:7463 — "in milliseconds").
        auto connectTimingField = [&](QLineEdit* edit, const QString& key, int scale = 1) {
            connect(edit, &QLineEdit::editingFinished, this, [this, edit, key, scale] {
                int val = qMax(0, edit->text().toInt());
                edit->setText(QString::number(val));
                m_model->sendCommand(QString("interlock set %1=%2").arg(key).arg(val * scale));
            });
        };

        auto* accTxEdit   = addTimingField(0, 0, "ACC TX:",       tx.accTxDelay());
        auto* txDelayEdit = addTimingField(0, 1, "TX Delay:",      tx.txDelay());
        auto* tx1Edit     = addTimingField(1, 0, "RCA TX1:",       tx.tx1Delay());
        // Timeout stored on the radio in milliseconds (FlexLib Radio.cs:7463);
        // display in whole seconds for readability — minutes lose too much
        // resolution for short-cycle TOT settings.
        auto* timeoutEdit = addTimingField(1, 1, "Timeout (sec):", tx.interlockTimeout() / 1000);
        auto* tx2Edit     = addTimingField(2, 0, "RCA TX2:",       tx.tx2Delay());

        connectTimingField(accTxEdit,   "acc_tx_delay");
        connectTimingField(txDelayEdit, "tx_delay");
        connectTimingField(tx1Edit,     "tx1_delay");
        connectTimingField(timeoutEdit, "timeout", 1000);
        connectTimingField(tx2Edit,     "tx2_delay");

        // TX Profile dropdown (below Timeout, right column)
        auto* profCmb = new QComboBox;
        profCmb->addItems(tx.profileList());
        profCmb->setCurrentText(tx.activeProfile());
        AetherSDR::applyComboStyle(profCmb);
        grid->addWidget(profCmb, 2, 2, 1, 2);
        connect(profCmb, &QComboBox::currentTextChanged, this, [this](const QString& name) {
            m_model->transmitModel().loadProfile(name);
        });

        auto* tx3Edit = addTimingField(3, 0, "RCA TX3:", tx.tx3Delay());
        connectTimingField(tx3Edit, "tx3_delay");

        // TX Band Settings button
        auto* bandSetBtn = new QPushButton("TX Band Settings");
        AetherSDR::ThemeManager::instance().applyStyleSheet(bandSetBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
            "padding: 4px 12px; }"
            "QPushButton:hover { background: {{color.background.1}}; }");
        connect(bandSetBtn, &QPushButton::clicked, this, [this] {
            emit txBandSettingsRequested();
        });
        grid->addWidget(bandSetBtn, 3, 2, 1, 2);

        for (auto* lbl : group->findChildren<QLabel*>())
            if (lbl->styleSheet().isEmpty()) lbl->setStyleSheet(kLabelStyle);

        vbox->addWidget(group);
    }

    // Interlocks group
    {
        auto* group = new QGroupBox("Interlocks - TX REQ");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* rcaLbl = new QLabel("RCA:");
        rcaLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(rcaLbl, 0, 0);
        auto* rcaCmb = new QComboBox;
        rcaCmb->addItems({"Active Low", "Active High"});
        rcaCmb->setCurrentIndex(tx.rcaTxReqPolarity());
        AetherSDR::applyComboStyle(rcaCmb);
        grid->addWidget(rcaCmb, 0, 1);

        auto* accLbl = new QLabel("Accessory:");
        accLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(accLbl, 0, 2);
        auto* accCmb = new QComboBox;
        accCmb->addItems({"Active Low", "Active High"});
        accCmb->setCurrentIndex(tx.accTxReqPolarity());
        AetherSDR::applyComboStyle(accCmb);
        grid->addWidget(accCmb, 0, 3);

        vbox->addWidget(group);
    }

    // Max Power / Show TX in Waterfall / Slice-TX Follow
    //
    // Tune Mode (single_tone / two_tone) used to live here too but was
    // removed — it persisted "Two Tone" across restarts as if it were a
    // normal operating mode, which surprised users who hit the regular
    // Tune button later and got an unexpected 2-tone test.  Tune Mode is
    // now a transient one-shot, surfaced via the TUNE button's right-
    // click menu in TxApplet ("Mono Tone" / "Two Tone").  Picking either
    // sets the radio's tune_mode for the next tune cycle only; nothing
    // is written to AppSettings.
    {
        auto* grid = new QGridLayout;
        grid->setSpacing(6);

        auto* mpLbl = new QLabel("Max Power:");
        mpLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(mpLbl, 0, 0);
        auto* mpRow = new QHBoxLayout;
        auto* mpEdit = new QLineEdit(QString::number(tx.maxPowerLevel()));
        mpEdit->setStyleSheet(kEditStyle);
        mpEdit->setFixedWidth(50);
        mpRow->addWidget(mpEdit);
        auto* mpUnit = new QLabel("%");
        mpUnit->setStyleSheet(kLabelStyle);
        mpRow->addWidget(mpUnit);
        mpRow->addStretch(1);
        grid->addLayout(mpRow, 0, 1);

        connect(mpEdit, &QLineEdit::editingFinished, this, [this, mpEdit] {
            int val = qBound(0, mpEdit->text().toInt(), 100);
            mpEdit->setText(QString::number(val));
            m_model->sendCommand(
                QString("transmit set max_power_level=%1").arg(val));
        });

        auto* swLbl = new QLabel("Show TX in Waterfall:");
        swLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(swLbl, 1, 0);
        auto* swBtn = new QPushButton("Enabled");
        swBtn->setCheckable(true);
        swBtn->setChecked(tx.showTxInWaterfall());
        AetherSDR::ThemeManager::instance().applyStyleSheet(swBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
            "padding: 3px 10px; }"
            "QPushButton:checked { background: #1a5030; color: {{color.accent.success}}; "
            "border: 1px solid #20a040; }");
        connect(swBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(
                QString("transmit set show_tx_in_waterfall=%1").arg(on ? 1 : 0));
        });
        grid->addWidget(swBtn, 1, 1);

        // Slice–TX Follow Mode (#441, #1351) — mutually exclusive toggles
        auto* followLbl = new QLabel("Slice/TX Follow:");
        followLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(followLbl, 2, 0);

        const QString kFollowBtnStyle =
            "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
            "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
            "padding: 3px 8px; }"
            "QPushButton:checked { background: #1a5030; color: #00e060; "
            "border: 1px solid #20a040; }";

        bool txFollows = AppSettings::instance().value("TxFollowsActiveSlice", "False").toString() == "True";
        auto* tfBtn = new QPushButton("TX Follows Active Slice");
        tfBtn->setCheckable(true);
        tfBtn->setChecked(txFollows);
        tfBtn->setToolTip("TX follows the active slice.\nDisabled during Split operation.");
        tfBtn->setStyleSheet(kFollowBtnStyle);

        bool activeFollows = AppSettings::instance().value("ActiveFollowsTxSlice", "False").toString() == "True";
        auto* afBtn = new QPushButton("Active Slice Follows TX");
        afBtn->setCheckable(true);
        afBtn->setChecked(activeFollows);
        afBtn->setToolTip("Switch active slice when TX moves externally\n(e.g. WSJT-X or CAT command).");
        afBtn->setStyleSheet(kFollowBtnStyle);

        auto* followRow = new QHBoxLayout;
        followRow->setSpacing(6);
        followRow->addWidget(tfBtn);
        followRow->addWidget(afBtn);
        followRow->addStretch(1);
        grid->addLayout(followRow, 2, 1);

        // Mutual exclusion: enabling one disables the other
        connect(tfBtn, &QPushButton::toggled, this, [tfBtn, afBtn](bool on) {
            Q_UNUSED(tfBtn);
            auto& s = AppSettings::instance();
            s.setValue("TxFollowsActiveSlice", on ? "True" : "False");
            if (on && afBtn->isChecked()) {
                afBtn->setChecked(false);
            }
            s.save();
        });
        connect(afBtn, &QPushButton::toggled, this, [tfBtn, afBtn](bool on) {
            Q_UNUSED(afBtn);
            auto& s = AppSettings::instance();
            s.setValue("ActiveFollowsTxSlice", on ? "True" : "False");
            if (on && tfBtn->isChecked()) {
                tfBtn->setChecked(false);
            }
            s.save();
        });

        vbox->addLayout(grid);
    }

    vbox->addStretch(1);
    return page;
}
QWidget* RadioSetupDialog::buildPhoneCwTab()
{
    auto& tx = m_model->transmitModel();
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    static const QString kTogBtnStyle =
        "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
        "padding: 3px 10px; }"
        "QPushButton:checked { background: #0070c0; color: #ffffff; "
        "border: 1px solid #0090e0; }";

    auto mkTogBtn = [&](const QString& text, bool checked) {
        auto* btn = new QPushButton(text);
        btn->setCheckable(true);
        btn->setChecked(checked);
        btn->setStyleSheet(kTogBtnStyle);
        return btn;
    };

    // Microphone group
    {
        auto* group = new QGroupBox("Microphone");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setHorizontalSpacing(8);
        grid->setVerticalSpacing(6);
        grid->setColumnStretch(2, 1);

        constexpr int kMicControlButtonWidth = 104;
        auto addMicRow = [&](int row, const QString& labelText, QPushButton* button) {
            auto* label = new QLabel(labelText);
            label->setStyleSheet(kLabelStyle);
            label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
            button->setMinimumWidth(kMicControlButtonWidth);
            button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
            grid->addWidget(label, row, 0);
            grid->addWidget(button, row, 1);
        };

        auto* biasBtn = mkTogBtn("BIAS", tx.micBias());
        connect(biasBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->transmitModel().setMicBias(on);
        });
        auto* boostBtn = mkTogBtn("+20dB", tx.micBoost());
        connect(boostBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->transmitModel().setMicBoost(on);
        });
        auto* metBtn = mkTogBtn("Enabled", tx.metInRx());
        connect(metBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(QString("transmit set met_in_rx=%1").arg(on ? 1 : 0));
        });

        addMicRow(0, "Mic Bias Voltage:", biasBtn);
        addMicRow(1, "Mic +20 dB Boost:", boostBtn);
        addMicRow(2, "Level Meter During Receive:", metBtn);

        vbox->addWidget(group);
    }

    // CW group
    {
        auto* group = new QGroupBox("CW");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        // Iambic: Enabled | A | B
        auto* iamLbl = new QLabel("Iambic:");
        iamLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(iamLbl, 0, 0);
        auto* iamBtn = mkTogBtn("Enabled", tx.cwIambic());
        connect(iamBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(QString("cw iambic %1").arg(on ? 1 : 0));
        });
        grid->addWidget(iamBtn, 0, 1);
        auto* modeA = mkTogBtn("A", tx.cwIambicMode() == 0);
        auto* modeB = mkTogBtn("B", tx.cwIambicMode() == 1);
        connect(modeA, &QPushButton::clicked, this, [this, modeA, modeB] {
            modeA->setChecked(true); modeB->setChecked(false);
            m_model->sendCommand("cw mode 0");
        });
        connect(modeB, &QPushButton::clicked, this, [this, modeA, modeB] {
            modeA->setChecked(false); modeB->setChecked(true);
            m_model->sendCommand("cw mode 1");
        });
        grid->addWidget(modeA, 0, 2);
        grid->addWidget(modeB, 0, 3);

        // Swap: Dot/Dash button
        auto* swapLbl = new QLabel("Swap:");
        swapLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(swapLbl, 0, 4);
        auto* swapBtn = mkTogBtn("Dot/Dash", tx.cwSwapPaddles());
        connect(swapBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(QString("cw swap %1").arg(on ? 1 : 0));
        });
        grid->addWidget(swapBtn, 0, 5);

        // Sideband: CWU | CWL
        auto* sbLbl = new QLabel("Sideband:");
        sbLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(sbLbl, 1, 0);
        auto* cwuBtn = mkTogBtn("CWU", !tx.cwlEnabled());
        auto* cwlBtn = mkTogBtn("CWL", tx.cwlEnabled());
        connect(cwuBtn, &QPushButton::clicked, this, [this, cwuBtn, cwlBtn] {
            cwuBtn->setChecked(true); cwlBtn->setChecked(false);
            m_model->sendCommand("cw cwl_enabled 0");
        });
        connect(cwlBtn, &QPushButton::clicked, this, [this, cwuBtn, cwlBtn] {
            cwuBtn->setChecked(false); cwlBtn->setChecked(true);
            m_model->sendCommand("cw cwl_enabled 1");
        });
        grid->addWidget(cwuBtn, 1, 1);
        grid->addWidget(cwlBtn, 1, 2);

        // CWX: Sync
        auto* cwxLbl = new QLabel("CWX:");
        cwxLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(cwxLbl, 1, 4);
        auto* syncBtn = mkTogBtn("Sync", tx.syncCwx());
        connect(syncBtn, &QPushButton::toggled, this, [this](bool on) {
            m_model->sendCommand(QString("cw synccwx %1").arg(on ? 1 : 0));
        });
        grid->addWidget(syncBtn, 1, 5);

        // CW Decode — independent RX / TX toggles (#2417).  RX keeps the
        // legacy behaviour of decoding the received CW slice; TX decodes
        // the operator's own keying via the client-side sidetone, useful
        // as a self-training tool for paddle / bug timing.  MainWindow
        // re-evaluates run state and the AudioEngine TX-decode tap on
        // dialog close via refreshCwDecodeState().
        auto* decodeLbl = new QLabel("Decode:");
        decodeLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(decodeLbl, 2, 4);
        auto* rxDecodeBtn = mkTogBtn("RX", CwDecodeSettings::rxEnabled());
        auto* txDecodeBtn = mkTogBtn("TX", CwDecodeSettings::txEnabled());
        connect(rxDecodeBtn, &QPushButton::toggled, this, [](bool on) {
            CwDecodeSettings::setRxEnabled(on);
        });
        connect(txDecodeBtn, &QPushButton::toggled, this, [](bool on) {
            CwDecodeSettings::setTxEnabled(on);
        });
        grid->addWidget(rxDecodeBtn, 2, 5);
        grid->addWidget(txDecodeBtn, 2, 6);



        vbox->addWidget(group);
    }

    // Digital group
    {
        auto* group = new QGroupBox("Digital");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* markLbl = new QLabel("RTTY Mark Default:");
        markLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(markLbl, 0, 0);
        auto* markEdit = new QLineEdit(QString::number(m_model->rttyMarkDefault()));
        markEdit->setStyleSheet(kEditStyle);
        markEdit->setFixedWidth(60);
        connect(markEdit, &QLineEdit::editingFinished, this, [this, markEdit] {
            m_model->sendCommand(
                "radio set rtty_mark_default=" + markEdit->text());
        });
        grid->addWidget(markEdit, 0, 1);

        vbox->addWidget(group);
    }

    vbox->addStretch(1);
    return page;
}
QWidget* RadioSetupDialog::buildRxTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    static const QString kTogStyle =
        "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
        "padding: 3px 10px; }"
        "QPushButton:checked { background: #1a5030; color: #00e060; "
        "border: 1px solid #20a040; }";

    // Frequency Offset group
    {
        auto* group = new QGroupBox("Frequency Offset");
        group->setStyleSheet(kGroupStyle);
        auto* gvb = new QVBoxLayout(group);
        gvb->setSpacing(4);

        auto* lbl = new QLabel(m_model->gpsdoPresent()
            ? "GPSDO installed. Manual frequency offset calibration available."
            : "Manual frequency offset calibration available.");
        lbl->setStyleSheet(m_model->gpsdoPresent()
            ? "QLabel { color: #00c040; font-size: 12px; }"
            : "QLabel { color: #c0a000; font-size: 12px; }");
        lbl->setWordWrap(true);
        gvb->addWidget(lbl);

        auto* offsetGrid = new QGridLayout;
        offsetGrid->setSpacing(6);

        // Cal Frequency row
        auto* calLbl = new QLabel("Cal Frequency (MHz):");
        calLbl->setStyleSheet(kLabelStyle);
        offsetGrid->addWidget(calLbl, 0, 0);
        auto* calEdit = new QLineEdit(QString::number(m_model->calFreqMhz(), 'f', 6));
        calEdit->setStyleSheet(kEditStyle);
        calEdit->setFixedWidth(100);
        connect(calEdit, &QLineEdit::editingFinished, this, [this, calEdit] {
            m_model->sendCommand(
                "radio set cal_freq=" + calEdit->text());
        });
        offsetGrid->addWidget(calEdit, 0, 1);

        auto* startBtn = new QPushButton("Start");
        startBtn->setStyleSheet(kTogStyle);
        startBtn->setFixedWidth(60);
        auto* calStatus = new QLabel;
        AetherSDR::ThemeManager::instance().applyStyleSheet(calStatus, "QLabel { color: {{color.text.secondary}}; font-size: 11px; }");
        calStatus->setMinimumWidth(130);

        auto calibrationActive = std::make_shared<bool>(false);
        auto pllRunningSeen = std::make_shared<bool>(false);
        auto calibrationRun = std::make_shared<int>(0);
        auto setCalStatus = [calStatus](const QString& text, const QString& color) {
            calStatus->setText(text);
            calStatus->setStyleSheet(
                QStringLiteral("QLabel { color: %1; font-size: 11px; }").arg(color));
        };

        connect(startBtn, &QPushButton::clicked, this,
                [this, calEdit, startBtn, calStatus, calibrationActive, pllRunningSeen,
                 calibrationRun, setCalStatus] {
            const QString calFreq = calEdit->text().trimmed();
            if (calFreq.isEmpty()) {
                setCalStatus("Enter cal frequency", "#e0a050");
                return;
            }

            const int runId = ++(*calibrationRun);
            *calibrationActive = true;
            *pllRunningSeen = false;
            startBtn->setEnabled(false);
            startBtn->setText("Busy");
            setCalStatus("Starting...", "#8aa8c0");

            qCDebug(lcProtocol) << "RadioSetupDialog: frequency calibration requested"
                                 << "cal_freq=" << calFreq
                                 << "reset_freq_error_ppb=0"
                                 << "run_id=" << runId;
            m_model->sendCommand("radio set cal_freq=" + calFreq);
            m_model->sendCommand("radio set freq_error_ppb=0");

            QPointer<RadioSetupDialog> dialog(this);
            QPointer<QPushButton> startGuard(startBtn);
            QPointer<QLabel> statusGuard(calStatus);
            // FlexLib's StartOffsetEnabled=false path starts calibration with radio pll_start.
            m_model->sendCmdPublic("radio pll_start",
                [dialog, startGuard, statusGuard, calibrationActive, calibrationRun, runId](
                    int code, const QString& body) {
                if (!dialog)
                    return;
                QMetaObject::invokeMethod(dialog, [dialog, startGuard, statusGuard,
                                                   calibrationActive, calibrationRun, runId,
                                                   code, body] {
                    if (!dialog || !startGuard || !statusGuard)
                        return;
                    if (!*calibrationActive || *calibrationRun != runId) {
                        qCDebug(lcProtocol)
                            << "RadioSetupDialog: ignoring radio pll_start response for inactive calibration"
                            << "run_id=" << runId
                            << "current_run_id=" << *calibrationRun
                            << "code" << code << "body:" << body;
                        return;
                    }
                    if (code == 0) {
                        statusGuard->setText("Calibrating...");
                        AetherSDR::ThemeManager::instance().applyStyleSheet(statusGuard, "QLabel { color: {{color.accent.bright}}; font-size: 11px; }");
                        qCDebug(lcProtocol)
                            << "RadioSetupDialog: radio pll_start accepted";
                        return;
                    }

                    *calibrationActive = false;
                    startGuard->setEnabled(true);
                    startGuard->setText("Start");
                    statusGuard->setText(QString("Error 0x%1")
                        .arg(code, 0, 16).toUpper());
                    statusGuard->setStyleSheet(
                        "QLabel { color: #ff7070; font-size: 11px; }");
                    qCWarning(lcProtocol)
                        << "RadioSetupDialog: radio pll_start failed: code"
                        << Qt::hex << code << "body:" << body;
                });
            });

            QTimer::singleShot(20000, this,
                [startGuard, statusGuard, calibrationActive, calibrationRun, runId] {
                if (!startGuard || !statusGuard || !*calibrationActive
                    || *calibrationRun != runId)
                    return;
                *calibrationActive = false;
                startGuard->setEnabled(true);
                startGuard->setText("Start");
                statusGuard->setText("No response");
                statusGuard->setStyleSheet(
                    "QLabel { color: #ff7070; font-size: 11px; }");
                qCWarning(lcProtocol)
                    << "RadioSetupDialog: frequency calibration timed out waiting for pll_done=1"
                    << "run_id=" << runId;
            });
        });
        offsetGrid->addWidget(startBtn, 0, 2);
        offsetGrid->addWidget(calStatus, 0, 3);

        // Freq Error PPB row
        auto* ppbLbl = new QLabel("Freq Offset (ppb):");
        ppbLbl->setStyleSheet(kLabelStyle);
        offsetGrid->addWidget(ppbLbl, 1, 0);
        auto* ppbEdit = new QLineEdit(QString::number(m_model->freqErrorPpb()));
        ppbEdit->setStyleSheet(kEditStyle);
        ppbEdit->setFixedWidth(80);
        connect(ppbEdit, &QLineEdit::editingFinished, this, [this, ppbEdit] {
            m_model->sendCommand(
                "radio set freq_error_ppb=" + ppbEdit->text());
        });
        offsetGrid->addWidget(ppbEdit, 1, 1);
        auto* ppbUnitLbl = new QLabel("ppb");
        ppbUnitLbl->setStyleSheet(kLabelStyle);
        offsetGrid->addWidget(ppbUnitLbl, 1, 2);
        offsetGrid->setColumnStretch(3, 1);
        gvb->addLayout(offsetGrid);

        connect(m_model, &RadioModel::infoChanged, this, [this, calEdit, ppbEdit] {
            if (!calEdit->hasFocus())
                calEdit->setText(QString::number(m_model->calFreqMhz(), 'f', 6));
            if (!ppbEdit->hasFocus())
                ppbEdit->setText(QString::number(m_model->freqErrorPpb()));
        });

        connect(m_model, &RadioModel::statusReceived, this,
                [startBtn, calStatus, calibrationActive, pllRunningSeen](
                    const QString& object, const QMap<QString, QString>& kvs) {
            if (object != QStringLiteral("radio") || !kvs.contains(QStringLiteral("pll_done")))
                return;

            const QString pllDone = kvs.value(QStringLiteral("pll_done"));
            const QString freqError = kvs.value(QStringLiteral("freq_error_ppb"));
            const QString calFreq = kvs.value(QStringLiteral("cal_freq"));
            qCDebug(lcProtocol)
                << "RadioSetupDialog: frequency calibration status"
                << "pll_done=" << pllDone
                << "freq_error_ppb=" << freqError
                << "cal_freq=" << calFreq
                << "active=" << *calibrationActive
                << "running_seen=" << *pllRunningSeen;

            if (!*calibrationActive)
                return;

            if (pllDone == QStringLiteral("0")) {
                *pllRunningSeen = true;
                calStatus->setText("Calibrating...");
                AetherSDR::ThemeManager::instance().applyStyleSheet(calStatus, "QLabel { color: {{color.accent.bright}}; font-size: 11px; }");
                return;
            }

            if (pllDone == QStringLiteral("1") && !*pllRunningSeen) {
                qCDebug(lcProtocol)
                    << "RadioSetupDialog: ignoring pll_done=1 before pll_done=0 for active calibration"
                    << "freq_error_ppb=" << freqError
                    << "cal_freq=" << calFreq;
                return;
            }

            if (pllDone == QStringLiteral("1")) {
                *calibrationActive = false;
                startBtn->setEnabled(true);
                startBtn->setText("Start");
                calStatus->setText(freqError.isEmpty()
                    ? QStringLiteral("Complete")
                    : QStringLiteral("Complete (%1 ppb)").arg(freqError));
                AetherSDR::ThemeManager::instance().applyStyleSheet(calStatus, "QLabel { color: {{color.accent.success}}; font-size: 11px; }");
                qCDebug(lcProtocol)
                    << "RadioSetupDialog: frequency calibration completed"
                    << "freq_error_ppb=" << freqError
                    << "cal_freq=" << calFreq;
            }
        });

        vbox->addWidget(group);
    }

    // 10 MHz Reference group
    {
        auto* group = new QGroupBox("10 MHz Reference");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* srcLbl = new QLabel("Source:");
        srcLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(srcLbl, 0, 0);

        auto* srcCmb = new QComboBox;
        AetherSDR::applyComboStyle(srcCmb);
        refreshOscillatorSourceCombo(srcCmb, m_model);
        connect(srcCmb, &QComboBox::currentIndexChanged, this, [this, srcCmb](int i) {
            m_model->sendCommand(
                "radio oscillator " + srcCmb->itemData(i).toString());
        });
        grid->addWidget(srcCmb, 0, 1);

        // Lock status
        auto* lockLbl = new QLabel(oscillatorStatusText(m_model));
        lockLbl->setStyleSheet(QStringLiteral(
            "QLabel { color: %1; font-size: 12px; font-weight: bold; }")
            .arg(oscillatorStatusColor(m_model)));
        grid->addWidget(lockLbl, 0, 2);

        // Live-update oscillator status when radio state changes (#967)
        connect(m_model, &RadioModel::oscillatorChanged, this, [this, srcCmb, lockLbl] {
            lockLbl->setText(oscillatorStatusText(m_model));
            lockLbl->setStyleSheet(QStringLiteral(
                "QLabel { color: %1; font-size: 12px; font-weight: bold; }")
                .arg(oscillatorStatusColor(m_model)));

            const QString current = normalizedOscillatorValue(srcCmb->currentData().toString());
            QSignalBlocker blocker(srcCmb);
            refreshOscillatorSourceCombo(srcCmb, m_model, current);
        });

        vbox->addWidget(group);
    }

    // General RX settings
    {
        auto* grid = new QGridLayout;
        grid->setSpacing(6);

        auto addToggle = [&](int row, const QString& label, bool checked,
                              const QString& cmd) {
            auto* lbl = new QLabel(label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, 0);
            auto* btn = new QPushButton("Enabled");
            btn->setCheckable(true);
            btn->setChecked(checked);
            btn->setStyleSheet(kTogStyle);
            connect(btn, &QPushButton::toggled, this, [this, cmd](bool on) {
                m_model->sendCommand(
                    QString("%1=%2").arg(cmd).arg(on ? 1 : 0));
            });
            grid->addWidget(btn, row, 1);
        };

        addToggle(0, "Mute local audio when remote:", m_model->muteLocalWhenRemote(),
                  "radio set mute_local_audio_when_remote");
        addToggle(1, "Binaural audio:", m_model->binauralRx(),
                  "radio set binaural_rx");

        vbox->addLayout(grid);
    }

    vbox->addStretch(1);
    return page;
}
// ── Audio tab ────────────────────────────────────────────────────────────────

QWidget* RadioSetupDialog::buildAudioTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);

    // ── Radio Audio Outputs ──────────────────────────────────────────────
    auto* outGroup = new QGroupBox("Radio Audio Outputs");
    outGroup->setStyleSheet(kGroupStyle);
    auto* outLayout = new QVBoxLayout(outGroup);

    // Line Out
    auto* lineoutRow = new QHBoxLayout;
    auto* lineoutLabel = new QLabel("Line Out:");
    lineoutLabel->setStyleSheet(kLabelStyle);
    lineoutLabel->setFixedWidth(90);
    auto* lineoutSlider = new GuardedSlider(Qt::Horizontal);
    lineoutSlider->setRange(0, 100);
    lineoutSlider->setValue(m_model->lineoutGain());
    auto* lineoutValue = new QLabel(QString::number(m_model->lineoutGain()));
    lineoutValue->setStyleSheet(kValueStyle);
    lineoutValue->setFixedWidth(30);
    auto* lineoutMute = new QPushButton("Mute");
    lineoutMute->setCheckable(true);
    lineoutMute->setChecked(m_model->lineoutMute());
    lineoutMute->setFixedWidth(50);
    AetherSDR::ThemeManager::instance().applyStyleSheet(lineoutMute, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px; }"
        "QPushButton:checked { background: #8b0000; color: {{color.text.primary}}; }");
    lineoutRow->addWidget(lineoutLabel);
    lineoutRow->addWidget(lineoutSlider, 1);
    lineoutRow->addWidget(lineoutValue);
    lineoutRow->addWidget(lineoutMute);
    outLayout->addLayout(lineoutRow);

    connect(lineoutSlider, &QSlider::valueChanged, this, [this, lineoutValue](int v) {
        lineoutValue->setText(QString::number(v));
        m_model->setLineoutGain(v);
    });
    connect(lineoutMute, &QPushButton::toggled, m_model, &RadioModel::setLineoutMute);

    // Headphone
    auto* hpRow = new QHBoxLayout;
    auto* hpLabel = new QLabel("Headphone:");
    hpLabel->setStyleSheet(kLabelStyle);
    hpLabel->setFixedWidth(90);
    auto* hpSlider = new GuardedSlider(Qt::Horizontal);
    hpSlider->setRange(0, 100);
    hpSlider->setValue(m_model->headphoneGain());
    auto* hpValue = new QLabel(QString::number(m_model->headphoneGain()));
    hpValue->setStyleSheet(kValueStyle);
    hpValue->setFixedWidth(30);
    auto* hpMute = new QPushButton("Mute");
    hpMute->setCheckable(true);
    hpMute->setChecked(m_model->headphoneMute());
    hpMute->setFixedWidth(50);
    AetherSDR::ThemeManager::instance().applyStyleSheet(hpMute, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px; }"
        "QPushButton:checked { background: #8b0000; color: {{color.text.primary}}; }");
    hpRow->addWidget(hpLabel);
    hpRow->addWidget(hpSlider, 1);
    hpRow->addWidget(hpValue);
    hpRow->addWidget(hpMute);
    outLayout->addLayout(hpRow);

    connect(hpSlider, &QSlider::valueChanged, this, [this, hpValue](int v) {
        hpValue->setText(QString::number(v));
        m_model->setHeadphoneGain(v);
    });
    connect(hpMute, &QPushButton::toggled, m_model, &RadioModel::setHeadphoneMute);

    // Front Speaker (mute only) — only on M-suffix models with built-in speaker
    // M-suffix models have a built-in front speaker (6400M, 6600M, 8400M, 8600M, AU-510M, AU-520M)
    bool hasFrontSpeaker = m_model->model().endsWith("M", Qt::CaseInsensitive);
    if (hasFrontSpeaker) {
        auto* spkRow = new QHBoxLayout;
        auto* spkLabel = new QLabel("Front Speaker:");
        spkLabel->setStyleSheet(kLabelStyle);
        spkLabel->setFixedWidth(90);
        auto* spkMute = new QPushButton("Mute");
        spkMute->setCheckable(true);
        spkMute->setChecked(m_model->frontSpeakerMute());
        spkMute->setFixedWidth(50);
        AetherSDR::ThemeManager::instance().applyStyleSheet(spkMute, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px; }"
            "QPushButton:checked { background: #8b0000; color: {{color.text.primary}}; }");
        spkRow->addWidget(spkLabel);
        spkRow->addStretch(1);
        spkRow->addWidget(spkMute);
        outLayout->addLayout(spkRow);
        connect(spkMute, &QPushButton::toggled, m_model, &RadioModel::setFrontSpeakerMute);
    }

    // Update from radio status
    connect(m_model, &RadioModel::audioOutputChanged, this,
            [this, lineoutSlider, lineoutValue, lineoutMute,
             hpSlider, hpValue, hpMute] {
        QSignalBlocker b1(lineoutSlider), b2(lineoutMute),
                       b3(hpSlider), b4(hpMute);
        lineoutSlider->setValue(m_model->lineoutGain());
        lineoutValue->setText(QString::number(m_model->lineoutGain()));
        lineoutMute->setChecked(m_model->lineoutMute());
        hpSlider->setValue(m_model->headphoneGain());
        hpValue->setText(QString::number(m_model->headphoneGain()));
        hpMute->setChecked(m_model->headphoneMute());
    });

    vbox->addWidget(outGroup);

    // ── Audio Compression ────────────────────────────────────────────────
    {
        auto* compGroup = new QGroupBox("Audio Compression (SmartLink)");
        compGroup->setStyleSheet(kGroupStyle);
        auto* compLayout = new QHBoxLayout(compGroup);
        compLayout->setSpacing(4);

        QString current = AppSettings::instance().value("AudioCompression", "None").toString();

        const QString btnStyle =
            "QPushButton { background: #1a2a3a; color: #c8d8e8; border: 1px solid #304050; "
            "border-radius: 3px; padding: 2px 10px; font-size: 11px; }"
            "QPushButton:checked { background: #00607a; color: #e0f0ff; border-color: #00b4d8; }";

        auto* autoBtn = new QPushButton("Auto");
        autoBtn->setCheckable(true); autoBtn->setChecked(current == "Auto");
        autoBtn->setStyleSheet(btnStyle);
        auto* noneBtn = new QPushButton("Uncompressed");
        noneBtn->setCheckable(true); noneBtn->setChecked(current == "None");
        noneBtn->setStyleSheet(btnStyle);
        auto* opusBtn = new QPushButton("Opus");
        opusBtn->setCheckable(true); opusBtn->setChecked(current == "Opus");
        opusBtn->setStyleSheet(btnStyle);

        auto setComp = [autoBtn, noneBtn, opusBtn](const QString& val) {
            QSignalBlocker b1(autoBtn), b2(noneBtn), b3(opusBtn);
            autoBtn->setChecked(val == "Auto");
            noneBtn->setChecked(val == "None");
            opusBtn->setChecked(val == "Opus");
            auto& s = AppSettings::instance();
            s.setValue("AudioCompression", val);
            s.save();
        };

        connect(autoBtn, &QPushButton::clicked, this, [setComp]() { setComp("Auto"); });
        connect(noneBtn, &QPushButton::clicked, this, [setComp]() { setComp("None"); });
        connect(opusBtn, &QPushButton::clicked, this, [setComp]() { setComp("Opus"); });

        compLayout->addWidget(autoBtn);
        compLayout->addWidget(noneBtn);
        compLayout->addWidget(opusBtn);
        compLayout->addStretch();

        auto* hint = new QLabel("Auto = Opus on SmartLink, uncompressed on LAN");
        AetherSDR::ThemeManager::instance().applyStyleSheet(hint, "QLabel { color: {{color.text.label}}; font-size: 10px; }");
        compLayout->addWidget(hint);

        vbox->addWidget(compGroup);
    }

    // ── Packet-Loss Concealment ─────────────────────────────────────────
    // Fades dropped VITA-49 audio packets to silence (uncompressed) or
    // calls libopus native PLC (Opus) instead of splicing the next packet
    // directly. Cuts the broadband click on lossy WAN/SmartLink. (#2731)
    {
        auto* plcCheck = new QCheckBox(
            "Smooth packet loss (conceal dropped audio packets)");
        AetherSDR::ThemeManager::instance().applyStyleSheet(plcCheck, "QCheckBox { color: {{color.text.primary}}; font-size: 11px; }");
        plcCheck->setToolTip(
            "When the radio's audio stream loses a UDP packet, fade the gap\n"
            "to silence (uncompressed) or synthesize a perceptually smooth\n"
            "fill with libopus PLC (Opus) instead of splicing the next packet\n"
            "directly. Reduces the high-pitch click that the splice produces\n"
            "on lossy WAN/SmartLink links. Capped at ~80 ms before the audio\n"
            "drops to clean silence.");
        plcCheck->setChecked(
            AppSettings::instance()
                .value("AudioPacketLossConcealment", "True").toString() == "True");
        connect(plcCheck, &QCheckBox::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("AudioPacketLossConcealment", on ? "True" : "False");
            s.save();
            // PanadapterStream lives on the network worker thread (#502);
            // route the toggle through QueuedConnection so the atomic and
            // map mutations happen on the owning thread.
            if (m_model && m_model->panStream()) {
                QMetaObject::invokeMethod(
                    m_model->panStream(),
                    [stream = m_model->panStream(), on]() {
                        stream->setPacketLossConcealment(on);
                    },
                    Qt::QueuedConnection);
            }
        });
        vbox->addWidget(plcCheck);
    }

    // ── Prevent Sleep ───────────────────────────────────────────────────
    {
        auto* sleepCheck = new QCheckBox("Prevent system sleep while connected");
        AetherSDR::ThemeManager::instance().applyStyleSheet(sleepCheck, "QCheckBox { color: {{color.text.primary}}; font-size: 11px; }");
        sleepCheck->setToolTip("Hold a system power assertion to prevent idle sleep\n"
                               "while connected to a radio. Keeps TCP/UDP/audio\n"
                               "streams alive during long sessions.");
        sleepCheck->setChecked(
            AppSettings::instance().value("InhibitSleepWhileConnected", "False").toString() == "True");
        connect(sleepCheck, &QCheckBox::toggled, this, [](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("InhibitSleepWhileConnected", on ? "True" : "False");
            s.save();
        });
        vbox->addWidget(sleepCheck);
    }

    // ── PC Audio Devices ────────────────────────────────────────────────
    auto* pcGroup = new QGroupBox("PC Audio Devices");
    pcGroup->setStyleSheet(kGroupStyle);
    auto* pcLayout = new QVBoxLayout(pcGroup);

    // Input device
    auto* inRow = new QHBoxLayout;
    auto* inLabel = new QLabel("Input:");
    inLabel->setStyleSheet(kLabelStyle);
    inLabel->setFixedWidth(90);
    auto* inCombo = new QComboBox;
    AetherSDR::applyComboStyle(inCombo);
    const auto inDevices = QMediaDevices::audioInputs();
    for (const auto& dev : inDevices)
        inCombo->addItem(dev.description(), dev.id());
    const auto curIn = m_audio ? m_audio->inputDevice() : QAudioDevice();
    const auto selIn = curIn.isNull() ? QMediaDevices::defaultAudioInput() : curIn;
    int inIdx = inCombo->findData(selIn.id());
    if (inIdx >= 0) inCombo->setCurrentIndex(inIdx);
    inRow->addWidget(inLabel);
    inRow->addWidget(inCombo, 1);
    pcLayout->addLayout(inRow);

    // Output device
    auto* outRow = new QHBoxLayout;
    auto* outLabel = new QLabel("Output:");
    outLabel->setStyleSheet(kLabelStyle);
    outLabel->setFixedWidth(90);
    auto* outCombo = new QComboBox;
    AetherSDR::applyComboStyle(outCombo);
    const auto outDevices = QMediaDevices::audioOutputs();
    for (const auto& dev : outDevices)
        outCombo->addItem(dev.description(), dev.id());
    // Select current device (or system default)
    const auto curOut = m_audio ? m_audio->outputDevice() : QAudioDevice();
    const auto selOut = curOut.isNull() ? QMediaDevices::defaultAudioOutput() : curOut;
    int outIdx = outCombo->findData(selOut.id());
    if (outIdx >= 0) outCombo->setCurrentIndex(outIdx);
    outRow->addWidget(outLabel);
    outRow->addWidget(outCombo, 1);
    pcLayout->addLayout(outRow);

    auto* promptCheck = new QCheckBox("Prompt on Audio Device Changes");
    AetherSDR::ThemeManager::instance().applyStyleSheet(promptCheck, "QCheckBox { color: {{color.text.primary}}; font-size: 11px; }");
    promptCheck->setToolTip("Show the Audio Device Detected dialog when a new PC audio device appears.");
    const bool suppressAudioDeviceNotifications =
        AppSettings::instance()
            .value(kSuppressAudioDeviceNotificationsKey, "False")
            .toString() == "True";
    promptCheck->setChecked(!suppressAudioDeviceNotifications);
    connect(promptCheck, &QCheckBox::toggled, this, [](bool on) {
        auto& s = AppSettings::instance();
        s.setValue(kSuppressAudioDeviceNotificationsKey, on ? "False" : "True");
        s.save();
    });
    pcLayout->addWidget(promptCheck);

    // Wire device changes to AudioEngine
    if (m_audio) {
        // Route through QueuedConnection so setInputDevice/setOutputDevice
        // execute on the audio worker thread, preventing use-after-free on
        // macOS CoreAudio when switching devices from the GUI thread (#1114).
        connect(inCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                this, [this, inDevices](int idx) {
            if (idx >= 0 && idx < inDevices.size()) {
                const QAudioDevice dev = inDevices[idx];
                QMetaObject::invokeMethod(m_audio, [this, dev]() {
                    m_audio->setInputDevice(dev);
                }, Qt::QueuedConnection);
            }
        });
        connect(outCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                this, [this, outDevices](int idx) {
            if (idx >= 0 && idx < outDevices.size()) {
                const QAudioDevice dev = outDevices[idx];
                QMetaObject::invokeMethod(m_audio, [this, dev]() {
                    m_audio->setOutputDevice(dev);
                }, Qt::QueuedConnection);
            }
        });
    }

    // Audio Boost toggle
    {
        auto* boostRow = new QHBoxLayout;
        auto* boostLabel = new QLabel("Audio Boost:");
        boostLabel->setStyleSheet(kLabelStyle);
        boostLabel->setFixedWidth(90);
        bool boostOn = AppSettings::instance().value("AudioBoost", "False").toString() == "True";
        auto* boostBtn = new QPushButton("Enabled");
        boostBtn->setCheckable(true);
        boostBtn->setChecked(boostOn);
        boostBtn->setToolTip("Apply 50% software gain boost to PC audio output.\n"
                             "Compensates for lower levels with AGC-controlled audio.");
        AetherSDR::ThemeManager::instance().applyStyleSheet(boostBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
            "padding: 3px 10px; }"
            "QPushButton:checked { background: #1a5030; color: {{color.accent.success}}; "
            "border: 1px solid #20a040; }");
        connect(boostBtn, &QPushButton::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("AudioBoost", on ? "True" : "False");
            s.save();
            if (m_audio) {
                QMetaObject::invokeMethod(m_audio, [this, on]() {
                    m_audio->setRxBoost(on);
                }, Qt::QueuedConnection);
            }
        });
        boostRow->addWidget(boostLabel);
        boostRow->addWidget(boostBtn);
        boostRow->addStretch(1);
        pcLayout->addLayout(boostRow);
    }

    // Audio Buffer (ms)
    {
        auto* bufRow = new QHBoxLayout;
        auto* bufLabel = new QLabel("Audio Buffer:");
        bufLabel->setStyleSheet(kLabelStyle);
        bufLabel->setFixedWidth(90);
        int bufMs = AppSettings::instance().value("AudioBufferMs", "200").toInt();
        auto* bufEdit = new QLineEdit(QString::number(bufMs));
        bufEdit->setStyleSheet(kEditStyle);
        bufEdit->setFixedWidth(50);
        auto* bufUnit = new QLabel("ms");
        bufUnit->setStyleSheet(kLabelStyle);
        auto* bufHint = new QLabel("(50–1000, increase for VPN/SmartLink jitter)");
        AetherSDR::ThemeManager::instance().applyStyleSheet(bufHint, "QLabel { color: {{color.background.3}}; font-size: 10px; }");
        connect(bufEdit, &QLineEdit::editingFinished, this, [this, bufEdit] {
            int val = qBound(50, bufEdit->text().toInt(), 1000);
            bufEdit->setText(QString::number(val));
            auto& s = AppSettings::instance();
            s.setValue("AudioBufferMs", QString::number(val));
            s.save();
            if (m_audio) {
                QMetaObject::invokeMethod(m_audio, [this, val]() {
                    m_audio->setRxBufferCapMs(val);
                }, Qt::QueuedConnection);
            }
        });
        bufRow->addWidget(bufLabel);
        bufRow->addWidget(bufEdit);
        bufRow->addWidget(bufUnit);
        bufRow->addWidget(bufHint);
        bufRow->addStretch(1);
        pcLayout->addLayout(bufRow);
    }

    vbox->addWidget(pcGroup);

    // ── Recording ───────────────────────────────────────────────────────
    {
        auto* recGroup = new QGroupBox("Recording");
        recGroup->setStyleSheet(kGroupStyle);
        auto* recLayout = new QVBoxLayout(recGroup);

        auto& settings = AppSettings::instance();

        // Mode: Radio Side vs Client Side
        auto* modeRow = new QHBoxLayout;
        auto* modeLabel = new QLabel("Record Mode:");
        modeLabel->setStyleSheet(kLabelStyle);
        modeLabel->setFixedWidth(90);
        modeRow->addWidget(modeLabel);

        const QString modeBtnStyle =
            "QPushButton { background: #1a2a3a; color: #c8d8e8; border: 1px solid #304050; "
            "border-radius: 3px; padding: 2px 10px; font-size: 11px; }"
            "QPushButton:checked { background: #00607a; color: #e0f0ff; border-color: #00b4d8; }";

        auto* radioSideBtn = new QPushButton("Radio Side");
        radioSideBtn->setCheckable(true);
        radioSideBtn->setStyleSheet(modeBtnStyle);
        auto* clientSideBtn = new QPushButton("Client Side");
        clientSideBtn->setCheckable(true);
        clientSideBtn->setStyleSheet(modeBtnStyle);

        bool clientSide = settings.value("RecordingMode", "Radio").toString() == "Client";
        radioSideBtn->setChecked(!clientSide);
        clientSideBtn->setChecked(clientSide);

        connect(radioSideBtn, &QPushButton::clicked, this, [radioSideBtn, clientSideBtn]() {
            QSignalBlocker b(clientSideBtn);
            radioSideBtn->setChecked(true);
            clientSideBtn->setChecked(false);
            auto& s = AppSettings::instance();
            s.setValue("RecordingMode", "Radio");
            s.save();
        });
        connect(clientSideBtn, &QPushButton::clicked, this, [radioSideBtn, clientSideBtn]() {
            QSignalBlocker b(radioSideBtn);
            clientSideBtn->setChecked(true);
            radioSideBtn->setChecked(false);
            auto& s = AppSettings::instance();
            s.setValue("RecordingMode", "Client");
            s.save();
        });

        modeRow->addWidget(radioSideBtn);
        modeRow->addWidget(clientSideBtn);
        modeRow->addStretch();
        recLayout->addLayout(modeRow);

        // Recording directory (client-side only)
        auto* dirRow = new QHBoxLayout;
        auto* dirLabel = new QLabel("Save to:");
        dirLabel->setStyleSheet(kLabelStyle);
        dirLabel->setFixedWidth(90);
        auto* dirEdit = new QLineEdit;
        dirEdit->setText(settings.value("QsoRecordingDir",
            QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
            + "/AetherSDR/Recordings").toString());
        AetherSDR::ThemeManager::instance().applyStyleSheet(dirEdit, "QLineEdit { background: {{color.background.1}}; color: {{color.text.primary}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; padding: 2px 4px; font-size: 11px; }");
        auto* browseBtn = new QPushButton("...");
        browseBtn->setFixedWidth(30);
        browseBtn->setStyleSheet(modeBtnStyle);
        connect(browseBtn, &QPushButton::clicked, this, [this, dirEdit]() {
            QString dir = QFileDialog::getExistingDirectory(this, "Select Recording Directory",
                                                            dirEdit->text());
            if (!dir.isEmpty()) {
                dirEdit->setText(dir);
                auto& s = AppSettings::instance();
                s.setValue("QsoRecordingDir", dir);
                s.save();
            }
        });
        connect(dirEdit, &QLineEdit::editingFinished, this, [dirEdit]() {
            auto& s = AppSettings::instance();
            s.setValue("QsoRecordingDir", dirEdit->text());
            s.save();
        });
        dirRow->addWidget(dirLabel);
        dirRow->addWidget(dirEdit, 1);
        dirRow->addWidget(browseBtn);
        recLayout->addLayout(dirRow);

        // Auto-record on TX
        auto* autoRow = new QHBoxLayout;
        auto* autoCheck = new QCheckBox("Auto-record on TX");
        AetherSDR::ThemeManager::instance().applyStyleSheet(autoCheck, "QCheckBox { color: {{color.text.primary}}; }");
        autoCheck->setChecked(settings.value("QsoRecordingAutoRecord", "False").toString() == "True");
        connect(autoCheck, &QCheckBox::toggled, this, [](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("QsoRecordingAutoRecord", on ? "True" : "False");
            s.save();
        });
        autoRow->addWidget(autoCheck);

        // Idle timeout
        auto* timeoutLabel = new QLabel("Idle timeout:");
        AetherSDR::ThemeManager::instance().applyStyleSheet(timeoutLabel, "QLabel { color: {{color.text.secondary}}; font-size: 11px; }");
        auto* timeoutSpin = new QSpinBox;
        timeoutSpin->setRange(10, 3600);
        timeoutSpin->setSuffix(" sec");
        timeoutSpin->setValue(settings.value("QsoRecordingIdleTimeout", "120").toInt());
        AetherSDR::ThemeManager::instance().applyStyleSheet(timeoutSpin, "QSpinBox { background: {{color.background.1}}; color: {{color.text.primary}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; padding: 2px; font-size: 11px; }");
        connect(timeoutSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [](int v) {
            auto& s = AppSettings::instance();
            s.setValue("QsoRecordingIdleTimeout", QString::number(v));
            s.save();
        });
        autoRow->addStretch();
        autoRow->addWidget(timeoutLabel);
        autoRow->addWidget(timeoutSpin);
        recLayout->addLayout(autoRow);

        vbox->addWidget(recGroup);
    }

    // ── NVIDIA BNR (GPU Noise Removal) ──────────────────────────────────
#ifdef HAVE_BNR
    {
        auto* bnrGroup = new QGroupBox("NVIDIA BNR (GPU Noise Removal)");
        bnrGroup->setStyleSheet(kGroupStyle);
        auto* bnrLayout = new QVBoxLayout(bnrGroup);

        // Autostart checkbox
        auto* autoRow = new QHBoxLayout;
        auto* autoStart = new QPushButton("Autostart Container");
        autoStart->setCheckable(true);
        autoStart->setChecked(
            AppSettings::instance().value("BnrAutostart", "False").toString() == "True");
        AetherSDR::ThemeManager::instance().applyStyleSheet(autoStart, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px 10px; }"
            "QPushButton:checked { background: #00607a; color: {{color.text.primary}}; border-color: {{color.accent}}; }");
        autoRow->addWidget(autoStart);

        // Container name
        auto* nameLbl = new QLabel("Container:");
        nameLbl->setStyleSheet(kLabelStyle);
        auto* nameEdit = new QLineEdit(
            AppSettings::instance().value("BnrContainerName", "maxine-bnr").toString());
        nameEdit->setFixedWidth(120);
        AetherSDR::ThemeManager::instance().applyStyleSheet(nameEdit, "QLineEdit { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px 4px; }");
        autoRow->addWidget(nameLbl);
        autoRow->addWidget(nameEdit);
        autoRow->addStretch(1);
        bnrLayout->addLayout(autoRow);

        // Status row
        auto* statusRow = new QHBoxLayout;
        auto* statusDot = new QLabel("\u2B24");  // filled circle
        statusDot->setStyleSheet("QLabel { color: #404040; font-size: 10px; }");
        auto* statusLbl = new QLabel("Unknown");
        statusLbl->setStyleSheet(kLabelStyle);
        statusRow->addWidget(statusDot);
        statusRow->addWidget(statusLbl);

        auto* checkBtn = new QPushButton("Check Status");
        checkBtn->setFixedWidth(90);
        AetherSDR::ThemeManager::instance().applyStyleSheet(checkBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 2px; }"
            "QPushButton:hover { background: {{color.background.1}}; }");
        statusRow->addWidget(checkBtn);

        auto* startBtn = new QPushButton("Start");
        startBtn->setFixedWidth(50);
        startBtn->setStyleSheet(checkBtn->styleSheet());
        statusRow->addWidget(startBtn);

        auto* stopBtn = new QPushButton("Stop");
        stopBtn->setFixedWidth(50);
        stopBtn->setStyleSheet(checkBtn->styleSheet());
        statusRow->addWidget(stopBtn);
        statusRow->addStretch(1);
        bnrLayout->addLayout(statusRow);

        // Check container status
        auto checkStatus = [statusDot, statusLbl, nameEdit]() {
            QProcess proc;
            proc.start("docker", {"inspect", "-f", "{{.State.Status}}", nameEdit->text()});
            proc.waitForFinished(3000);
            QString out = proc.readAllStandardOutput().trimmed();
            QString err = proc.readAllStandardError().trimmed();
            qDebug() << "BNR checkStatus:" << out << err << "exit:" << proc.exitCode();
            if (out == "running") {
                statusDot->setStyleSheet("QLabel { color: #00d860; font-size: 10px; }");
                statusLbl->setText("Running");
            } else if (!out.isEmpty()) {
                statusDot->setStyleSheet("QLabel { color: #d8a000; font-size: 10px; }");
                statusLbl->setText("Stopped (" + out + ")");
            } else {
                statusDot->setStyleSheet("QLabel { color: #d83030; font-size: 10px; }");
                statusLbl->setText("Not found");
            }
        };

        connect(checkBtn, &QPushButton::clicked, this, checkStatus);

        connect(startBtn, &QPushButton::clicked, this,
                [nameEdit, statusLbl, checkStatus]() {
            auto* proc = new QProcess;
            connect(proc, &QProcess::finished, statusLbl, [proc, checkStatus]() {
                proc->deleteLater();
                checkStatus();
            });
            proc->start("docker", {"start", nameEdit->text()});
        });

        connect(stopBtn, &QPushButton::clicked, this,
                [nameEdit, statusLbl, checkStatus]() {
            auto* proc = new QProcess;
            connect(proc, &QProcess::finished, statusLbl, [proc, checkStatus]() {
                proc->deleteLater();
                checkStatus();
            });
            proc->start("docker", {"stop", nameEdit->text()});
        });

        connect(autoStart, &QPushButton::toggled, this, [](bool on) {
            AppSettings::instance().setValue("BnrAutostart", on ? "True" : "False");
        });
        connect(nameEdit, &QLineEdit::textChanged, this, [](const QString& name) {
            AppSettings::instance().setValue("BnrContainerName", name);
        });

        vbox->addWidget(bnrGroup);

        // Check on dialog open (context object ensures timer is cancelled if dialog closes)
        QTimer::singleShot(0, statusLbl, checkStatus);
    }
#endif

    vbox->addStretch(1);
    return page;
}

// ── Filters tab ─────────────────────────────────────────────────────────────

QWidget* RadioSetupDialog::buildFiltersTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    static const QString kAutoBtn =
        "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
        "padding: 3px 10px; }"
        "QPushButton:checked { background: #0070c0; color: #ffffff; "
        "border: 1px solid #0090e0; }";

    // Filter sharpness sliders — taller groove + wider handle than the
    // canonical slider for the radio-setup dialog's heavier visual style.
    // Sizes kept site-local (deliberate emphasis); colours routed through
    // color.slider.* so live theme switching works and per-applet
    // overrides could retint these in future without per-call-site work.
    static const QString kFilterSlider = QStringLiteral(
        "QSlider::groove:horizontal { background: {{color.slider.background}}; height: 6px; border-radius: 3px; }"
        "QSlider::handle:horizontal { background: {{color.slider.handle}}; width: 14px; "
        "margin: -5px 0; border-radius: 7px; }");

    // Filter Options group
    {
        auto* group = new QGroupBox("Filter Options");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(8);

        // Column headers
        auto* lowLbl = new QLabel("Low Latency");
        lowLbl->setStyleSheet(kLabelStyle);
        lowLbl->setAlignment(Qt::AlignCenter);
        grid->addWidget(lowLbl, 0, 1);
        auto* sharpLbl = new QLabel("Sharp Filters");
        sharpLbl->setStyleSheet(kLabelStyle);
        sharpLbl->setAlignment(Qt::AlignCenter);
        grid->addWidget(sharpLbl, 0, 2);

        struct FilterRow {
            const char* label;
            const char* modeCmd;   // voice, cw, digital
            int level;
            bool autoOn;
        };
        FilterRow rows[] = {
            {"Voice:",   "voice",   m_model->filterSharpnessVoice(),   m_model->filterSharpnessVoiceAuto()},
            {"CW:",      "cw",      m_model->filterSharpnessCw(),      m_model->filterSharpnessCwAuto()},
            {"Digital:", "digital", m_model->filterSharpnessDigital(), m_model->filterSharpnessDigitalAuto()},
        };

        for (int i = 0; i < 3; ++i) {
            auto& r = rows[i];
            int row = i + 1;

            auto* lbl = new QLabel(r.label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, 0);

            auto* slider = new GuardedSlider(Qt::Horizontal);
            slider->setRange(0, 3);
            slider->setValue(r.level);
            AetherSDR::ThemeManager::instance().applyStyleSheet(slider, kFilterSlider);
            slider->setEnabled(!r.autoOn);
            grid->addWidget(slider, row, 1, 1, 2);

            auto* autoBtn = new QPushButton("Auto");
            autoBtn->setCheckable(true);
            autoBtn->setChecked(r.autoOn);
            autoBtn->setStyleSheet(kAutoBtn);
            grid->addWidget(autoBtn, row, 3);

            QString mode = QString::fromLatin1(r.modeCmd);
            connect(slider, &QSlider::valueChanged, this, [this, mode](int v) {
                m_model->sendCommand(
                    QString("radio filter_sharpness %1 level=%2").arg(mode).arg(v));
            });
            connect(autoBtn, &QPushButton::toggled, this, [this, slider, mode](bool on) {
                slider->setEnabled(!on);
                m_model->sendCommand(
                    QString("radio filter_sharpness %1 auto_level=%2").arg(mode).arg(on ? 1 : 0));
            });
        }

        vbox->addWidget(group);
    }

    // Low Latency Digital checkbox
    {
        auto* group = new QGroupBox;
        group->setStyleSheet(kGroupStyle);
        auto* hb = new QHBoxLayout(group);

        auto* chk = new QCheckBox("Use Low Latency Filters for Digital Modes");
        chk->setChecked(m_model->lowLatencyDigital());
        AetherSDR::ThemeManager::instance().applyStyleSheet(chk, "QCheckBox { color: {{color.text.primary}}; font-size: 12px; spacing: 8px; }"
            "QCheckBox::indicator { width: 16px; height: 16px; "
            "border: 2px solid {{color.background.3}}; border-radius: 3px; background: {{color.background.0}}; }"
            "QCheckBox::indicator:checked { background: {{color.background.2}}; border: 2px solid #00a0e0; }");
        connect(chk, &QCheckBox::toggled, this, [this](bool on) {
            m_model->sendCommand(
                QString("radio set low_latency_digital_modes=%1").arg(on ? 1 : 0));
        });
        hb->addWidget(chk);

        vbox->addWidget(group);
    }

    vbox->addStretch(1);
    return page;
}
QWidget* RadioSetupDialog::buildXvtrTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    // Sub-tabs: one per XVTR + a "+" tab to add new
    auto* xvtrTabs = new QTabWidget;
    AetherSDR::ThemeManager::instance().applyStyleSheet(xvtrTabs, "QTabWidget::pane { border: 1px solid {{color.background.2}}; background: {{color.background.0}}; }"
        "QTabBar::tab { background: {{color.background.1}}; color: {{color.text.secondary}}; "
        "border: 1px solid {{color.background.2}}; padding: 3px 10px; margin-right: 2px; }"
        "QTabBar::tab:selected { background: {{color.background.0}}; color: {{color.text.primary}}; "
        "border-bottom-color: {{color.background.0}}; }");

    auto buildXvtrPage = [this, xvtrTabs](int idx, const RadioModel::XvtrInfo& x) {
        auto* pg = new QWidget;
        auto* grid = new QGridLayout(pg);
        grid->setSpacing(6);

        auto addField = [&](int row, int col, const QString& label, const QString& value,
                             bool editable = true) -> QLineEdit* {
            auto* lbl = new QLabel(label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, col * 2);
            auto* edit = new QLineEdit(value);
            edit->setStyleSheet(kEditStyle);
            edit->setFixedWidth(100);
            edit->setReadOnly(!editable);
            grid->addWidget(edit, row, col * 2 + 1);
            return edit;
        };

        auto* nameEdit   = addField(0, 0, "Name:", x.name);
        auto* validLbl   = new QLabel(x.isValid ? "Valid" : "Invalid");
        validLbl->setStyleSheet(x.isValid
            ? "QLabel { color: #00c040; font-size: 12px; font-weight: bold; }"
            : "QLabel { color: #c04040; font-size: 12px; font-weight: bold; }");
        grid->addWidget(validLbl, 0, 3);

        auto* rfEdit     = addField(1, 0, "RF Freq (MHz):", QString::number(x.rfFreq, 'f', 3));
        auto* ifEdit     = addField(1, 1, "IF Freq (MHz):", QString::number(x.ifFreq, 'f', 3));
        auto* loEdit     = addField(2, 0, "LO Freq (MHz):", QString::number(x.rfFreq - x.ifFreq, 'f', 3), false);
        auto* errEdit    = addField(2, 1, "LO Error (MHz):", QString::number(x.loError, 'f', 6));
        auto* rxGainEdit = addField(3, 0, "RX Gain (dB):", QString::number(x.rxGain, 'f', 1));

        // RX Only toggle
        auto* rxOnlyLbl = new QLabel("RX Only:");
        rxOnlyLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(rxOnlyLbl, 3, 2);
        auto* rxOnlyBtn = new QPushButton("Enabled");
        rxOnlyBtn->setCheckable(true);
        rxOnlyBtn->setChecked(x.rxOnly);
        AetherSDR::ThemeManager::instance().applyStyleSheet(rxOnlyBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; font-weight: bold; "
            "padding: 3px 10px; }"
            "QPushButton:checked { background: #1a5030; color: {{color.accent.success}}; "
            "border: 1px solid #20a040; }");
        connect(rxOnlyBtn, &QPushButton::toggled, this, [this, idx](bool on) {
            m_model->sendCommand(
                QString("xvtr set %1 rx_only=%2").arg(idx).arg(on ? 1 : 0));
        });
        grid->addWidget(rxOnlyBtn, 3, 3);

        auto* maxPwrEdit = addField(4, 0, "Max Power (dBm):", QString::number(x.maxPower, 'f', 1));

        // Remove button
        auto* removeBtn = new QPushButton("Remove");
        removeBtn->setStyleSheet(
            "QPushButton { background: #3a1a1a; border: 1px solid #504040; "
            "border-radius: 3px; color: #ff6060; font-size: 11px; font-weight: bold; "
            "padding: 4px 16px; }"
            "QPushButton:hover { background: #502020; }");
        connect(removeBtn, &QPushButton::clicked, pg, [this, idx, xvtrTabs, pg] {
            m_model->sendCommand(QString("xvtr remove %1").arg(idx));
            int tabIdx = xvtrTabs->indexOf(pg);
            if (tabIdx >= 0) xvtrTabs->removeTab(tabIdx);
        });
        grid->addWidget(removeBtn, 4, 3);

        auto maxPowerRange = [this, ifEdit] {
            return XvtrPolicy::maxPowerRangeFor(ifEdit->text().toDouble(), m_model->model());
        };
        auto* maxPwrValidator = new QDoubleValidator(maxPwrEdit);
        maxPwrValidator->setDecimals(2);
        maxPwrValidator->setNotation(QDoubleValidator::StandardNotation);
        auto updateMaxPowerValidator = [maxPwrValidator, maxPowerRange] {
            const XvtrPolicy::MaxPowerRange range = maxPowerRange();
            maxPwrValidator->setRange(range.minimumDbm, range.maximumDbm, 2);
        };
        updateMaxPowerValidator();
        maxPwrEdit->setValidator(maxPwrValidator);
        auto submitMaxPower = [this, maxPwrEdit, ifEdit, idx](bool sendWhenUnchanged) {
            bool ok = false;
            const double requested = maxPwrEdit->text().toDouble(&ok);
            if (!ok) {
                return;
            }

            const double clamped = XvtrPolicy::clampMaxPowerDbm(
                requested, ifEdit->text().toDouble(), m_model->model());
            maxPwrEdit->setText(QString::number(clamped, 'f', 2));
            if (!sendWhenUnchanged && qFuzzyCompare(requested + 1.0, clamped + 1.0)) {
                return;
            }

            m_model->sendCommand(
                QString("xvtr set %1 max_power=%2")
                    .arg(idx)
                    .arg(QString::number(clamped, 'f', 2)));
        };

        // Wire editable fields
        connect(nameEdit, &QLineEdit::editingFinished, this, [this, nameEdit, idx] {
            m_model->sendCommand(
                QString("xvtr set %1 name=%2").arg(idx).arg(nameEdit->text()));
        });
        auto updateLo = [rfEdit, ifEdit, loEdit] {
            double rf = rfEdit->text().toDouble();
            double ifF = ifEdit->text().toDouble();
            loEdit->setText(QString::number(rf - ifF, 'f', 3));
        };
        connect(rfEdit, &QLineEdit::editingFinished, this, [this, rfEdit, idx, updateLo] {
            m_model->sendCommand(
                QString("xvtr set %1 rf_freq=%2").arg(idx).arg(rfEdit->text()));
            updateLo();
        });
        connect(ifEdit, &QLineEdit::editingFinished, this,
                [this, ifEdit, idx, updateLo, updateMaxPowerValidator, submitMaxPower] {
            m_model->sendCommand(
                QString("xvtr set %1 if_freq=%2").arg(idx).arg(ifEdit->text()));
            updateLo();
            updateMaxPowerValidator();
            submitMaxPower(false);
        });
        connect(errEdit, &QLineEdit::editingFinished, this, [this, errEdit, idx] {
            m_model->sendCommand(
                QString("xvtr set %1 lo_error=%2").arg(idx).arg(errEdit->text()));
        });
        connect(rxGainEdit, &QLineEdit::editingFinished, this, [this, rxGainEdit, idx] {
            m_model->sendCommand(
                QString("xvtr set %1 rx_gain=%2").arg(idx).arg(rxGainEdit->text()));
        });
        connect(maxPwrEdit, &QLineEdit::editingFinished, this, [submitMaxPower] {
            submitMaxPower(true);
        });

        return pg;
    };

    // Add existing XVTR pages
    const auto& xvtrs = m_model->xvtrList();
    for (auto it = xvtrs.constBegin(); it != xvtrs.constEnd(); ++it) {
        xvtrTabs->addTab(buildXvtrPage(it.key(), it.value()),
                          it.value().name.isEmpty() ? QString::number(it.key()) : it.value().name);
    }

    // "+" tab to add new
    auto* addPage = new QWidget;
    auto* addVb = new QVBoxLayout(addPage);
    auto* addBtn = new QPushButton("Create New Transverter");
    AetherSDR::ThemeManager::instance().applyStyleSheet(addBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 12px; font-weight: bold; "
        "padding: 8px 20px; }"
        "QPushButton:hover { background: {{color.background.1}}; }");
    connect(addBtn, &QPushButton::clicked, this, [this, xvtrTabs, buildXvtrPage] {
        m_model->sendCmdPublic("xvtr create",
            [this, xvtrTabs, buildXvtrPage](int code, const QString& body) {
                if (code != 0) return;
                // Wait briefly for the radio's status update to arrive
                QTimer::singleShot(300, this, [this, xvtrTabs, buildXvtrPage] {
                    // Find the newest XVTR that doesn't have a tab yet
                    const auto& xvtrs = m_model->xvtrList();
                    for (auto it = xvtrs.constBegin(); it != xvtrs.constEnd(); ++it) {
                        // Check if we already have a tab for this index
                        bool found = false;
                        for (int t = 0; t < xvtrTabs->count() - 1; ++t) {
                            if (xvtrTabs->tabText(t) == it.value().name ||
                                xvtrTabs->tabText(t) == QString::number(it.key()))
                                found = true;
                        }
                        if (!found) {
                            QString tabName = it.value().name.isEmpty()
                                ? QString("New") : it.value().name;
                            int insertIdx = xvtrTabs->count() - 1; // before "+"
                            xvtrTabs->insertTab(insertIdx,
                                buildXvtrPage(it.key(), it.value()), tabName);
                            xvtrTabs->setCurrentIndex(insertIdx);
                        }
                    }
                });
            });
    });
    addVb->addWidget(addBtn, 0, Qt::AlignCenter);
    addVb->addStretch(1);
    xvtrTabs->addTab(addPage, "+");

    vbox->addWidget(xvtrTabs);
    vbox->addStretch(1);
    return page;
}

QWidget* RadioSetupDialog::buildAntennaNamesTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    auto* group = new QGroupBox("Antenna Names");
    group->setStyleSheet(kGroupStyle);
    auto* groupLayout = new QVBoxLayout(group);
    groupLayout->setSpacing(6);

    auto* rowsWidget = new QWidget;
    rowsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
    auto* grid = new QGridLayout(rowsWidget);
    grid->setContentsMargins(8, 2, 8, 2);
    grid->setHorizontalSpacing(8);
    grid->setVerticalSpacing(4);
    grid->setAlignment(Qt::AlignTop);
    grid->setColumnStretch(1, 1);
    grid->setColumnStretch(2, 1);

    auto* scroll = new QScrollArea;
    scroll->setWidgetResizable(true);
    scroll->setFrameShape(QFrame::NoFrame);
    scroll->setStyleSheet("QScrollArea { background: transparent; border: none; }");
    scroll->setWidget(rowsWidget);
    groupLayout->addWidget(scroll);
    vbox->addWidget(group, 1);

    QVBoxLayout* kiwiRowsLayout = nullptr;

    if (m_kiwiSdrManager) {
        auto* kiwiGroup = new QGroupBox("KiwiSDR RX Antennas");
        kiwiGroup->setStyleSheet(kGroupStyle);
        auto* kiwiLayout = new QVBoxLayout(kiwiGroup);
        kiwiLayout->setSpacing(6);

        auto* kiwiScroll = new QScrollArea;
        kiwiScroll->setWidgetResizable(true);
        kiwiScroll->setFrameShape(QFrame::NoFrame);
        kiwiScroll->setMinimumHeight(190);
        kiwiScroll->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
        kiwiScroll->setStyleSheet("QScrollArea { background: transparent; border: none; }");
        kiwiScroll->setAccessibleName("KiwiSDR RX antennas");
        kiwiScroll->setAccessibleDescription(
            "Configured KiwiSDR receive-only antenna endpoints.");

        auto* kiwiRowsWidget = new QWidget;
        kiwiRowsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
        kiwiRowsLayout = new QVBoxLayout(kiwiRowsWidget);
        kiwiRowsLayout->setContentsMargins(8, 4, 8, 4);
        kiwiRowsLayout->setSpacing(6);
        kiwiRowsLayout->setAlignment(Qt::AlignTop);
        kiwiScroll->setWidget(kiwiRowsWidget);
        kiwiLayout->addWidget(kiwiScroll);
        vbox->addWidget(kiwiGroup, 0);
    }

    // Antenna display names are intentionally local to AetherSDR. FlexLib exposes
    // canonical RX/TX antenna lists and rxant/txant setters, but no verified
    // writable display-name API; radio commands must keep using ANT1/XVTR/etc.
    auto refresh = std::make_shared<std::function<void()>>();
    auto scheduleRefresh = [this, refresh] {
        QTimer::singleShot(0, this, [refresh] {
            if (*refresh)
                (*refresh)();
        });
    };

    auto wireSlice = [this, scheduleRefresh](SliceModel* slice) {
        if (!slice)
            return;
        connect(slice, &SliceModel::rxAntennaChanged, this,
                [scheduleRefresh](const QString&) { scheduleRefresh(); });
        connect(slice, &SliceModel::txAntennaChanged, this,
                [scheduleRefresh](const QString&) { scheduleRefresh(); });
        connect(slice, &SliceModel::rxAntennaListChanged, this,
                [scheduleRefresh](const QStringList&) { scheduleRefresh(); });
        connect(slice, &SliceModel::txAntennaListChanged, this,
                [scheduleRefresh](const QStringList&) { scheduleRefresh(); });
    };

    *refresh = [this, grid] {
        while (QLayoutItem* item = grid->takeAt(0)) {
            if (QWidget* w = item->widget())
                w->deleteLater();
            delete item;
        }

        auto addHeader = [grid](const QString& text, int col) {
            auto* lbl = new QLabel(text);
            AetherSDR::ThemeManager::instance().applyStyleSheet(lbl, "QLabel { color: {{color.text.secondary}}; font-size: 11px; font-weight: bold; }");
            grid->addWidget(lbl, 0, col);
        };
        addHeader("Port", 0);
        addHeader("Custom name", 1);
        addHeader("Preview", 2);

        const QStringList tokens = m_model->knownAntennaTokens();
        if (tokens.isEmpty()) {
            auto* empty = new QLabel("Waiting for antenna ports from the radio.");
            AetherSDR::ThemeManager::instance().applyStyleSheet(empty, "QLabel { color: {{color.text.label}}; font-size: 12px; padding: 8px; }");
            grid->addWidget(empty, 1, 0, 1, 4);
            return;
        }

        int row = 1;
        for (const QString& token : tokens) {
            auto* port = new QLabel(token);
            port->setStyleSheet(kValueStyle);
            grid->addWidget(port, row, 0);

            auto* edit = new QLineEdit(m_model->antennaAlias(token));
            edit->setMaxLength(16);
            edit->setStyleSheet(kEditStyle);
            edit->setPlaceholderText(token);
            grid->addWidget(edit, row, 1);

            const bool disambiguate =
                m_model->antennaAliasNeedsDisambiguation(token, tokens);
            auto* preview = new QLabel(m_model->antennaDisplayName(token, disambiguate));
            preview->setStyleSheet(kLabelStyle);
            grid->addWidget(preview, row, 2);

            auto* clearBtn = new QPushButton("Clear");
            AetherSDR::ThemeManager::instance().applyStyleSheet(clearBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
                "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; "
                "font-weight: bold; padding: 3px 10px; }"
                "QPushButton:hover { background: {{color.background.1}}; }");
            grid->addWidget(clearBtn, row, 3);

            connect(edit, &QLineEdit::textChanged, this,
                    [preview, token](const QString& text) {
                const QString alias = text.trimmed();
                preview->setText(alias.isEmpty() ? token : alias);
            });
            connect(edit, &QLineEdit::editingFinished, this, [this, edit, token] {
                m_model->setAntennaAlias(token, edit->text());
            });
            connect(clearBtn, &QPushButton::clicked, this, [this, edit, token] {
                edit->clear();
                m_model->clearAntennaAlias(token);
            });
            ++row;
        }
    };

    for (SliceModel* slice : m_model->slices())
        wireSlice(slice);
    connect(m_model, &RadioModel::sliceAdded, this,
            [wireSlice, scheduleRefresh](SliceModel* slice) {
        wireSlice(slice);
        scheduleRefresh();
    });
    connect(m_model, &RadioModel::antListChanged, this,
            [scheduleRefresh](const QStringList&) { scheduleRefresh(); });
    connect(m_model, &RadioModel::antennaAliasesChanged, this, scheduleRefresh);

    auto refreshKiwi = std::make_shared<std::function<void()>>();
    if (m_kiwiSdrManager && kiwiRowsLayout) {
        auto stateText = [this](const QString& id) {
            const KiwiSdrClient::State state = m_kiwiSdrManager->state(id);
            QString base;
            switch (state) {
            case KiwiSdrClient::State::Disconnected:
                base = QStringLiteral("Disconnected");
                break;
            case KiwiSdrClient::State::Connecting:
                base = QStringLiteral("Connecting");
                break;
            case KiwiSdrClient::State::Connected:
                base = QStringLiteral("Connected");
                break;
            case KiwiSdrClient::State::Error:
                base = QStringLiteral("Error");
                break;
            }
            const QString detail = m_kiwiSdrManager->stateDetail(id).trimmed();
            if (!detail.isEmpty()
                && state != KiwiSdrClient::State::Connected
                && state != KiwiSdrClient::State::Connecting) {
                return QStringLiteral("%1\n%2").arg(base, detail);
            }
            return base.isEmpty() ? QStringLiteral("Disconnected") : base;
        };

        auto styleKiwiEdit = [](QLineEdit* edit) {
            edit->setStyleSheet(kEditStyle);
            edit->setMinimumHeight(24);
        };

        auto styleKiwiButton = [](QPushButton* button) {
            button->setStyleSheet(kKiwiActionButtonStyle);
            button->setMinimumWidth(96);
            button->setAutoDefault(false);
        };

        auto styleKiwiIconButton = [](QPushButton* button) {
            button->setStyleSheet(kKiwiIconButtonStyle);
            button->setFixedSize(30, 26);
            button->setAutoDefault(false);
        };

        *refreshKiwi = [this, kiwiRowsLayout, stateText, styleKiwiEdit,
                        styleKiwiButton, styleKiwiIconButton] {
            while (QLayoutItem* item = kiwiRowsLayout->takeAt(0)) {
                if (QWidget* widget = item->widget()) {
                    widget->deleteLater();
                }
                delete item;
            }

            const QVector<KiwiSdrAntennaProfile> profiles =
                m_kiwiSdrManager->profiles();
            for (int row = 0; row < profiles.size(); ++row) {
                const KiwiSdrAntennaProfile profile = profiles[row];

                auto* rowFrame = new QFrame;
                rowFrame->setObjectName("kiwiAntennaRow");
                rowFrame->setStyleSheet(kKiwiRowStyle);
                auto* rowLayout = new QGridLayout(rowFrame);
                rowLayout->setContentsMargins(6, 5, 6, 5);
                rowLayout->setHorizontalSpacing(6);
                rowLayout->setVerticalSpacing(3);
                rowLayout->setColumnStretch(0, 1);

                auto* nameEdit = new QLineEdit(profile.name);
                nameEdit->setMaxLength(16);
                nameEdit->setPlaceholderText("Custom Name");
                nameEdit->setAccessibleName("KiwiSDR antenna name");
                nameEdit->setAccessibleDescription(
                    "Required display name for this KiwiSDR receive antenna.");
                styleKiwiEdit(nameEdit);
                rowLayout->addWidget(nameEdit, 0, 0);

                auto* status = new QLabel(stateText(profile.id));
                status->setAccessibleName("KiwiSDR antenna status");
                status->setStyleSheet(
                    "QLabel { color: #c8d8e8; font-size: 12px; padding-left: 6px; }");
                status->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
                status->setWordWrap(true);
                status->setToolTip(status->text());
                rowLayout->addWidget(status, 0, 1, 1, 3);

                auto* endpointEdit = new QLineEdit(profile.endpoint);
                endpointEdit->setAccessibleName("KiwiSDR server");
                endpointEdit->setAccessibleDescription(
                    "Hostname or hostname:port for this KiwiSDR endpoint.");
                styleKiwiEdit(endpointEdit);
                rowLayout->addWidget(endpointEdit, 1, 0);

                auto* autoCheck = new QCheckBox;
                autoCheck->setText("Auto");
                autoCheck->setChecked(profile.autoConnect);
                autoCheck->setAccessibleName("Auto connect KiwiSDR antenna");
                autoCheck->setStyleSheet(
                    "QCheckBox { color: #c8d8e8; font-size: 12px; spacing: 4px; }");
                rowLayout->addWidget(autoCheck, 1, 1, Qt::AlignCenter);

                const bool connected =
                    m_kiwiSdrManager->state(profile.id)
                    == KiwiSdrClient::State::Connected;
                auto* connectButton =
                    new QPushButton(connected ? "Disconnect" : "Connect");
                connectButton->setAccessibleName(
                    connected ? "Disconnect KiwiSDR antenna"
                              : "Connect KiwiSDR antenna");
                styleKiwiButton(connectButton);
                rowLayout->addWidget(connectButton, 1, 2);

                auto* removeButton = new QPushButton;
                removeButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
                removeButton->setToolTip("Remove");
                removeButton->setAccessibleName("Remove KiwiSDR antenna");
                styleKiwiIconButton(removeButton);
                rowLayout->addWidget(removeButton, 1, 3);
                kiwiRowsLayout->addWidget(rowFrame);

                auto updateProfile = [this, profile, nameEdit, endpointEdit,
                                      autoCheck] {
                    const QString name = nameEdit->text().trimmed();
                    const QString endpoint =
                        KiwiSdrClient::normalizeEndpoint(endpointEdit->text());
                    if (name.isEmpty()) {
                        QSignalBlocker blocker(nameEdit);
                        nameEdit->setText(profile.name);
                        return;
                    }
                    if (endpoint.isEmpty()) {
                        QSignalBlocker blocker(endpointEdit);
                        endpointEdit->setText(profile.endpoint);
                        return;
                    }
                    if (endpointEdit->text() != endpoint) {
                        QSignalBlocker blocker(endpointEdit);
                        endpointEdit->setText(endpoint);
                    }
                    KiwiSdrAntennaProfile updated = profile;
                    updated.name = name;
                    updated.endpoint = endpoint;
                    updated.autoConnect = autoCheck->isChecked();
                    m_kiwiSdrManager->updateProfile(updated);
                };
                connect(nameEdit, &QLineEdit::editingFinished,
                        this, updateProfile);
                connect(nameEdit, &QLineEdit::returnPressed,
                        this, updateProfile);
                connect(endpointEdit, &QLineEdit::editingFinished,
                        this, updateProfile);
                connect(endpointEdit, &QLineEdit::returnPressed,
                        this, updateProfile);
                connect(autoCheck, &QCheckBox::toggled,
                        this, [updateProfile](bool) { updateProfile(); });
                connect(connectButton, &QPushButton::clicked,
                        this, [this, profile, connected] {
                    if (connected) {
                        m_kiwiSdrManager->disconnectProfile(profile.id);
                    } else {
                        m_kiwiSdrManager->connectProfile(profile.id);
                    }
                });
                connect(removeButton, &QPushButton::clicked,
                        this, [this, profile] {
                    m_kiwiSdrManager->removeProfile(profile.id);
                });
            }

            auto* rowFrame = new QFrame;
            rowFrame->setObjectName("kiwiAntennaRow");
            rowFrame->setStyleSheet(kKiwiRowStyle);
            auto* rowLayout = new QGridLayout(rowFrame);
            rowLayout->setContentsMargins(6, 5, 6, 5);
            rowLayout->setHorizontalSpacing(6);
            rowLayout->setVerticalSpacing(3);
            rowLayout->setColumnStretch(0, 1);

            auto* nameEdit = new QLineEdit;
            nameEdit->setMaxLength(16);
            nameEdit->setPlaceholderText("Custom Name");
            nameEdit->setAccessibleName("New KiwiSDR antenna name");
            nameEdit->setAccessibleDescription(
                "Required display name for the new KiwiSDR receive antenna.");
            styleKiwiEdit(nameEdit);
            rowLayout->addWidget(nameEdit, 0, 0);

            auto* endpointEdit = new QLineEdit;
            endpointEdit->setPlaceholderText("host:8073");
            endpointEdit->setAccessibleName("New KiwiSDR server");
            endpointEdit->setAccessibleDescription(
                "Hostname or hostname:port for the new KiwiSDR receive antenna.");
            styleKiwiEdit(endpointEdit);
            rowLayout->addWidget(endpointEdit, 1, 0);

            auto* autoCheck = new QCheckBox;
            autoCheck->setText("Auto");
            autoCheck->setAccessibleName("Auto connect new KiwiSDR antenna");
            autoCheck->setStyleSheet(
                "QCheckBox { color: #c8d8e8; font-size: 12px; spacing: 4px; }");
            rowLayout->addWidget(autoCheck, 1, 1, Qt::AlignCenter);

            auto committed = std::make_shared<bool>(false);
            auto commitNewRow = [this, nameEdit, endpointEdit, autoCheck,
                                 committed] {
                if (*committed) {
                    return;
                }
                const QString name = nameEdit->text().trimmed();
                const QString endpoint =
                    KiwiSdrClient::normalizeEndpoint(endpointEdit->text());
                if (name.isEmpty() || endpoint.isEmpty()) {
                    return;
                }
                *committed = true;
                const QString id = m_kiwiSdrManager->addProfile(name, endpoint);
                if (autoCheck->isChecked()) {
                    KiwiSdrAntennaProfile profile = m_kiwiSdrManager->profile(id);
                    profile.autoConnect = true;
                    m_kiwiSdrManager->updateProfile(profile);
                }
            };

            // Browse the public KiwiSDR directory to fill in a receiver. Only
            // API-permitting receivers are listed (web-only operators honored).
            // Picking one adds the profile immediately — no extra Tab/confirm.
            auto* browseButton = new QPushButton("Browse public…");
            browseButton->setAccessibleName("Browse public KiwiSDR receivers");
            browseButton->setAccessibleDescription(
                "Choose from the public KiwiSDR directory; receivers whose "
                "operator disabled the external API are not shown.");
            rowLayout->addWidget(browseButton, 0, 1);
            connect(browseButton, &QPushButton::clicked, this,
                    [this, nameEdit, endpointEdit, commitNewRow] {
                KiwiPublicReceiverPicker picker(this);
                if (picker.exec() == QDialog::Accepted
                    && !picker.selectedEndpoint().isEmpty()) {
                    endpointEdit->setText(picker.selectedEndpoint());
                    if (nameEdit->text().trimmed().isEmpty())
                        nameEdit->setText(picker.selectedName());
                    commitNewRow();  // add directly; no Tab-out needed
                }
            });
            kiwiRowsLayout->addWidget(rowFrame);

            connect(nameEdit, &QLineEdit::editingFinished,
                    this, commitNewRow);
            connect(nameEdit, &QLineEdit::returnPressed,
                    this, commitNewRow);
            connect(endpointEdit, &QLineEdit::editingFinished,
                    this, commitNewRow);
            connect(endpointEdit, &QLineEdit::returnPressed,
                    this, commitNewRow);
            connect(autoCheck, &QCheckBox::toggled,
                    this, [commitNewRow](bool) { commitNewRow(); });

            kiwiRowsLayout->addStretch(1);
        };

        connect(m_kiwiSdrManager, &KiwiSdrManager::profilesChanged,
                this, [refreshKiwi] {
            if (*refreshKiwi) {
                (*refreshKiwi)();
            }
        });
        connect(m_kiwiSdrManager, &KiwiSdrManager::profileStateChanged,
                this, [refreshKiwi](const QString&, KiwiSdrClient::State,
                                    const QString&) {
            if (*refreshKiwi) {
                (*refreshKiwi)();
            }
        });
    }

    (*refresh)();
    if (refreshKiwi && *refreshKiwi) {
        (*refreshKiwi)();
    }
    return page;
}

// ── APD tab (External Adaptive Pre-Distortion) ──────────────────────────────
//
// Per-TX-antenna selection of the sample port the radio uses for APD
// adaptation.  INTERNAL samples inside the radio (legacy behaviour);
// RX_A/RX_B/XVTA/XVTB take a coupled feedback signal from one of the
// receive or transverter inputs — required to train APD against the
// real RF when transmitting through an external linear amplifier.
//
// Tab is added eagerly but kept hidden until the radio reports
// `apd configurable=1`.  Only the FLEX-8x00 series on SmartSDR 4.2.18+
// reports this; older firmware and 6000-series radios stay hidden.

QWidget* RadioSetupDialog::buildApdTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    // Model header — matches other tabs (e.g. Filters, TX)
    {
        auto* hdr = new QHBoxLayout;
        hdr->addStretch(1);
        auto* modelLbl = new QLabel(m_model->model());
        AetherSDR::ThemeManager::instance().applyStyleSheet(modelLbl, "QLabel { color: {{color.accent.bright}}; font-size: 20px; font-weight: bold; }");
        hdr->addWidget(modelLbl);
        vbox->addLayout(hdr);
    }

    auto& tx = m_model->transmitModel();

    // External Sampler group — 2-column grid: ANT1/XVTA on the top row,
    // ANT2/XVTB on the bottom row, then the Reset button on its own row.
    {
        auto* group = new QGroupBox("External Sampler (per TX ANT)");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(8);

        // Row, col-pair, antenna name → builds label + combo, hooks signals.
        auto buildRow = [&](int row, int colBase, const QString& ant) {
            auto* lbl = new QLabel(ant + ":");
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, colBase);

            auto* combo = new QComboBox;
            const auto s = tx.apdSampler(ant);
            combo->addItems(s.available);
            combo->setCurrentText(s.selected);
            AetherSDR::applyComboStyle(combo);
            grid->addWidget(combo, row, colBase + 1);

            m_apdSamplerCombos.insert(ant, combo);

            connect(combo, &QComboBox::currentTextChanged, this,
                    [this, ant](const QString& port) {
                if (port.isEmpty()) return;
                m_model->transmitModel().setApdSamplerPort(ant, port);
            });
        };

        buildRow(0, 0, "ANT1");
        buildRow(0, 2, "XVTA");
        buildRow(1, 0, "ANT2");
        buildRow(1, 2, "XVTB");

        // Equalizer Reset button (row 2) — clears all per-antenna training.
        auto* resetLbl = new QLabel("Equalizer Reset:");
        resetLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(resetLbl, 2, 0);

        auto* resetBtn = new QPushButton("Reset");
        AetherSDR::ThemeManager::instance().applyStyleSheet(resetBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; "
            "font-weight: bold; padding: 3px 16px; }"
            "QPushButton:hover { background: {{color.background.1}}; }");
        connect(resetBtn, &QPushButton::clicked, this, [this] {
            m_model->transmitModel().resetApdEqualizer();
        });
        grid->addWidget(resetBtn, 2, 1, Qt::AlignLeft);

        vbox->addWidget(group);
    }

    // Live updates: when sampler status arrives later (e.g. ANT changes,
    // first-connection populate), refresh the relevant combo's options
    // without firing change-signals back to the radio.
    connect(&tx, &TransmitModel::apdSamplerChanged, this,
            &RadioSetupDialog::refreshApdSamplerCombo);

    vbox->addStretch(1);
    return page;
}

void RadioSetupDialog::refreshApdSamplerCombo(const QString& txAnt)
{
    auto* combo = m_apdSamplerCombos.value(txAnt);
    if (!combo) return;

    const auto s = m_model->transmitModel().apdSampler(txAnt);
    QSignalBlocker b(combo);
    combo->clear();
    combo->addItems(s.available);
    combo->setCurrentText(s.selected);
}

// ── USB Cables tab ───────────────────────────────────────────────────────────

QWidget* RadioSetupDialog::buildUsbCablesTab()
{
    auto* page = new QWidget;
    auto* hbox = new QHBoxLayout(page);
    hbox->setSpacing(6);

    auto* cableModel = &m_model->usbCableModel();

    // Style constants
    static const QString kCombo =
        "QComboBox { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; padding: 2px 4px; }"
        "QComboBox::drop-down { border: none; }"
        "QComboBox QAbstractItemView { background: #1a2a3a; color: #c8d8e8; "
        "selection-background-color: #00b4d8; }";
    static const QString kEdit =
        "QLineEdit { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; padding: 2px 4px; }";
    static const QString kSpin =
        "QSpinBox { background: #1a2a3a; border: 1px solid #304050; "
        "color: #c8d8e8; font-size: 11px; padding: 2px; }";
    static const QString kCheck =
        "QCheckBox { color: #c8d8e8; font-size: 11px; }";

    // ── Left: cable list ────────────────────────────────────────────────
    auto* listGroup = new QGroupBox("Cables");
    listGroup->setStyleSheet(kGroupStyle);
    listGroup->setFixedWidth(180);
    auto* listLayout = new QVBoxLayout(listGroup);

    auto* cableList = new QListWidget;
    AetherSDR::ThemeManager::instance().applyStyleSheet(cableList, "QListWidget { background: {{color.background.0}}; color: {{color.text.primary}}; border: 1px solid {{color.background.1}}; "
        "font-size: 11px; }"
        "QListWidget::item { padding: 4px; }"
        "QListWidget::item:selected { background: {{color.accent}}; color: {{color.background.0}}; }");
    listLayout->addWidget(cableList);
    hbox->addWidget(listGroup);

    // ── Right: stacked property panels ──────────────────────────────────
    auto* stack = new QStackedWidget;

    // Page 0: No cable selected
    {
        auto* empty = new QWidget;
        auto* emptyLayout = new QVBoxLayout(empty);
        auto* lbl = new QLabel("No USB cables detected.\n\nPlug a USB-serial adapter\n"
                               "into the radio's rear USB port.");
        lbl->setAlignment(Qt::AlignCenter);
        lbl->setStyleSheet("QLabel { color: #606880; font-size: 12px; }");
        emptyLayout->addWidget(lbl);
        stack->addWidget(empty);  // index 0
    }

    // Helper: create source combo (shared across CAT, BCD, Bit)
    auto makeSourceCombo = []() {
        auto* combo = new QComboBox;
        combo->addItems({"None", "TX Pan", "TX Slice", "Active Slice",
                         "TX Ant", "RX Ant", "Ordinal Slice"});
        combo->setStyleSheet(kCombo);
        return combo;
    };
    // Map source display name → protocol value
    auto sourceToProto = [](const QString& display) -> QString {
        if (display == "TX Pan")        return "tx_pan";
        if (display == "TX Slice")      return "tx_slice";
        if (display == "Active Slice")  return "active_slice";
        if (display == "TX Ant")        return "tx_ant";
        if (display == "RX Ant")        return "rx_ant";
        if (display == "Ordinal Slice") return "ordinal_slice";
        return "None";
    };
    auto protoToSource = [](const QString& proto) -> int {
        if (proto == "tx_pan")        return 1;
        if (proto == "tx_slice")      return 2;
        if (proto == "active_slice")  return 3;
        if (proto == "tx_ant")        return 4;
        if (proto == "rx_ant")        return 5;
        if (proto == "ordinal_slice") return 6;
        return 0;  // None
    };

    // Helper: serial parameter group (shared by CAT and Passthrough)
    auto makeSerialGroup = [](const QString& title) {
        auto* group = new QGroupBox(title);
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(4);

        auto* speedCombo = new QComboBox;
        for (int s : {300,600,1200,2400,4800,9600,14400,19200,38400,57600,115200,230400,460800,921600})
            speedCombo->addItem(QString::number(s));
        speedCombo->setCurrentText("9600");
        speedCombo->setStyleSheet(kCombo);
        grid->addWidget(new QLabel("Speed:"), 0, 0);
        grid->addWidget(speedCombo, 0, 1);

        auto* dataCombo = new QComboBox;
        dataCombo->addItems({"7", "8"});
        dataCombo->setCurrentText("8");
        dataCombo->setStyleSheet(kCombo);
        grid->addWidget(new QLabel("Data Bits:"), 1, 0);
        grid->addWidget(dataCombo, 1, 1);

        auto* parityCombo = new QComboBox;
        parityCombo->addItems({"none", "odd", "even", "mark", "space"});
        parityCombo->setStyleSheet(kCombo);
        grid->addWidget(new QLabel("Parity:"), 2, 0);
        grid->addWidget(parityCombo, 2, 1);

        auto* stopCombo = new QComboBox;
        stopCombo->addItems({"1", "2"});
        stopCombo->setStyleSheet(kCombo);
        grid->addWidget(new QLabel("Stop Bits:"), 3, 0);
        grid->addWidget(stopCombo, 3, 1);

        auto* flowCombo = new QComboBox;
        flowCombo->addItems({"none", "rts_cts", "dtr_dsr", "xon_xoff"});
        flowCombo->setStyleSheet(kCombo);
        grid->addWidget(new QLabel("Flow:"), 4, 0);
        grid->addWidget(flowCombo, 4, 1);

        struct SerialWidgets { QComboBox *speed, *data, *parity, *stop, *flow; QGroupBox* group; };
        auto* w = new SerialWidgets{speedCombo, dataCombo, parityCombo, stopCombo, flowCombo, group};
        group->setProperty("_widgets", QVariant::fromValue(static_cast<void*>(w)));
        return group;
    };

    // Page 1: CAT cable
    QWidget* catPage;
    QLineEdit* catNameEdit;
    QCheckBox* catEnabledCheck;
    QLabel*    catStatusLabel;
    QComboBox* catSourceCombo;
    QCheckBox* catAutoReportCheck;
    QGroupBox* catSerialGroup;
    {
        catPage = new QWidget;
        auto* vbox = new QVBoxLayout(catPage);
        vbox->setSpacing(6);

        // Common header
        auto* headerGroup = new QGroupBox("Cable Settings");
        headerGroup->setStyleSheet(kGroupStyle);
        auto* hg = new QGridLayout(headerGroup);
        hg->setSpacing(4);
        hg->addWidget(new QLabel("Name:"), 0, 0);
        catNameEdit = new QLineEdit;
        catNameEdit->setStyleSheet(kEdit);
        hg->addWidget(catNameEdit, 0, 1);
        catEnabledCheck = new QCheckBox("Enabled");
        catEnabledCheck->setStyleSheet(kCheck);
        hg->addWidget(catEnabledCheck, 1, 0, 1, 2);
        catStatusLabel = new QLabel("Unplugged");
        AetherSDR::ThemeManager::instance().applyStyleSheet(catStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        hg->addWidget(new QLabel("Status:"), 2, 0);
        hg->addWidget(catStatusLabel, 2, 1);
        vbox->addWidget(headerGroup);

        // Serial params
        catSerialGroup = makeSerialGroup("Serial Parameters");
        vbox->addWidget(catSerialGroup);

        // CAT source
        auto* srcGroup = new QGroupBox("CAT Source");
        srcGroup->setStyleSheet(kGroupStyle);
        auto* sg = new QGridLayout(srcGroup);
        sg->setSpacing(4);
        sg->addWidget(new QLabel("Source:"), 0, 0);
        catSourceCombo = makeSourceCombo();
        sg->addWidget(catSourceCombo, 0, 1);
        catAutoReportCheck = new QCheckBox("Auto Report");
        catAutoReportCheck->setStyleSheet(kCheck);
        sg->addWidget(catAutoReportCheck, 1, 0, 1, 2);
        vbox->addWidget(srcGroup);

        vbox->addStretch();
        stack->addWidget(catPage);  // index 1
    }

    // Page 2: BCD cable
    QWidget* bcdPage;
    QLineEdit* bcdNameEdit;
    QCheckBox* bcdEnabledCheck;
    QLabel*    bcdStatusLabel;
    QComboBox* bcdSourceCombo;
    QComboBox* bcdTypeCombo;
    QComboBox* bcdPolarityCombo;
    {
        bcdPage = new QWidget;
        auto* vbox = new QVBoxLayout(bcdPage);
        vbox->setSpacing(6);

        auto* headerGroup = new QGroupBox("Cable Settings");
        headerGroup->setStyleSheet(kGroupStyle);
        auto* hg = new QGridLayout(headerGroup);
        hg->setSpacing(4);
        hg->addWidget(new QLabel("Name:"), 0, 0);
        bcdNameEdit = new QLineEdit;
        bcdNameEdit->setStyleSheet(kEdit);
        hg->addWidget(bcdNameEdit, 0, 1);
        bcdEnabledCheck = new QCheckBox("Enabled");
        bcdEnabledCheck->setStyleSheet(kCheck);
        hg->addWidget(bcdEnabledCheck, 1, 0, 1, 2);
        bcdStatusLabel = new QLabel("Unplugged");
        AetherSDR::ThemeManager::instance().applyStyleSheet(bcdStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        hg->addWidget(new QLabel("Status:"), 2, 0);
        hg->addWidget(bcdStatusLabel, 2, 1);
        vbox->addWidget(headerGroup);

        auto* bcdGroup = new QGroupBox("BCD Settings");
        bcdGroup->setStyleSheet(kGroupStyle);
        auto* bg = new QGridLayout(bcdGroup);
        bg->setSpacing(4);
        bg->addWidget(new QLabel("BCD Type:"), 0, 0);
        bcdTypeCombo = new QComboBox;
        bcdTypeCombo->addItems({"HF (bcd)", "VHF (vbcd)", "HF+VHF (bcd_vbcd)"});
        bcdTypeCombo->setStyleSheet(kCombo);
        bg->addWidget(bcdTypeCombo, 0, 1);
        bg->addWidget(new QLabel("Polarity:"), 1, 0);
        bcdPolarityCombo = new QComboBox;
        bcdPolarityCombo->addItems({"Active High", "Active Low"});
        bcdPolarityCombo->setStyleSheet(kCombo);
        bg->addWidget(bcdPolarityCombo, 1, 1);
        bg->addWidget(new QLabel("Source:"), 2, 0);
        bcdSourceCombo = makeSourceCombo();
        bg->addWidget(bcdSourceCombo, 2, 1);
        vbox->addWidget(bcdGroup);

        vbox->addStretch();
        stack->addWidget(bcdPage);  // index 2
    }

    // Page 3: Bit cable
    QWidget* bitPage;
    QLineEdit* bitNameEdit;
    QCheckBox* bitEnabledCheck;
    QLabel*    bitStatusLabel;
    {
        bitPage = new QWidget;
        auto* vbox = new QVBoxLayout(bitPage);
        vbox->setSpacing(6);

        auto* headerGroup = new QGroupBox("Cable Settings");
        headerGroup->setStyleSheet(kGroupStyle);
        auto* hg = new QGridLayout(headerGroup);
        hg->setSpacing(4);
        hg->addWidget(new QLabel("Name:"), 0, 0);
        bitNameEdit = new QLineEdit;
        bitNameEdit->setStyleSheet(kEdit);
        hg->addWidget(bitNameEdit, 0, 1);
        bitEnabledCheck = new QCheckBox("Enabled");
        bitEnabledCheck->setStyleSheet(kCheck);
        hg->addWidget(bitEnabledCheck, 1, 0, 1, 2);
        bitStatusLabel = new QLabel("Unplugged");
        AetherSDR::ThemeManager::instance().applyStyleSheet(bitStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        hg->addWidget(new QLabel("Status:"), 2, 0);
        hg->addWidget(bitStatusLabel, 2, 1);
        vbox->addWidget(headerGroup);

        // 8-row bit grid
        auto* bitGroup = new QGroupBox("Bit Configuration (0-7)");
        bitGroup->setStyleSheet(kGroupStyle);
        auto* bitGrid = new QGridLayout(bitGroup);
        bitGrid->setSpacing(2);

        // Header row
        int col = 0;
        for (const auto& h : {"Bit", "En", "Source", "Output", "Polarity", "Band"}) {
            auto* lbl = new QLabel(h);
            AetherSDR::ThemeManager::instance().applyStyleSheet(lbl, "QLabel { color: {{color.text.secondary}}; font-size: 10px; font-weight: bold; }");
            lbl->setAlignment(Qt::AlignCenter);
            bitGrid->addWidget(lbl, 0, col++);
        }

        for (int b = 0; b < 8; ++b) {
            int row = b + 1;
            auto* bitLabel = new QLabel(QString::number(b));
            bitLabel->setAlignment(Qt::AlignCenter);
            AetherSDR::ThemeManager::instance().applyStyleSheet(bitLabel, "QLabel { color: {{color.text.primary}}; font-size: 10px; }");
            bitGrid->addWidget(bitLabel, row, 0);

            auto* enCheck = new QCheckBox;
            bitGrid->addWidget(enCheck, row, 1, Qt::AlignCenter);

            auto* srcCombo = new QComboBox;
            srcCombo->addItems({"None", "Active Slice", "TX Slice"});
            srcCombo->setStyleSheet(kCombo + "QComboBox { font-size: 9px; }");
            srcCombo->setFixedWidth(90);
            bitGrid->addWidget(srcCombo, row, 2);

            auto* outCombo = new QComboBox;
            outCombo->addItems({"band", "freq_range"});
            outCombo->setStyleSheet(kCombo + "QComboBox { font-size: 9px; }");
            outCombo->setFixedWidth(80);
            bitGrid->addWidget(outCombo, row, 3);

            auto* polCombo = new QComboBox;
            polCombo->addItems({"High", "Low"});
            polCombo->setStyleSheet(kCombo + "QComboBox { font-size: 9px; }");
            polCombo->setFixedWidth(50);
            bitGrid->addWidget(polCombo, row, 4);

            auto* bandEdit = new QLineEdit;
            bandEdit->setPlaceholderText("e.g. 20");
            bandEdit->setFixedWidth(50);
            bandEdit->setStyleSheet(kEdit + "QLineEdit { font-size: 9px; }");
            bitGrid->addWidget(bandEdit, row, 5);

            // Wire signals to send commands
            connect(enCheck, &QCheckBox::toggled, this, [cableModel, cableList, b](bool on) {
                auto* item = cableList->currentItem();
                if (!item) return;
                cableModel->sendSetBit(item->data(Qt::UserRole).toString(), b,
                                       "enable", on ? "1" : "0");
            });
            connect(outCombo, &QComboBox::currentTextChanged, this,
                    [cableModel, cableList, b](const QString& text) {
                auto* item = cableList->currentItem();
                if (!item) return;
                cableModel->sendSetBit(item->data(Qt::UserRole).toString(), b, "output", text);
            });
            connect(polCombo, &QComboBox::currentTextChanged, this,
                    [cableModel, cableList, b](const QString& text) {
                auto* item = cableList->currentItem();
                if (!item) return;
                cableModel->sendSetBit(item->data(Qt::UserRole).toString(), b,
                                       "polarity", text == "High" ? "active_high" : "active_low");
            });
            connect(bandEdit, &QLineEdit::editingFinished, this,
                    [cableModel, cableList, b, bandEdit]() {
                auto* item = cableList->currentItem();
                if (!item) return;
                cableModel->sendSetBit(item->data(Qt::UserRole).toString(), b,
                                       "band", bandEdit->text());
            });
        }

        vbox->addWidget(bitGroup);
        vbox->addStretch();
        stack->addWidget(bitPage);  // index 3
    }

    // Page 4: Passthrough cable
    QWidget* ptPage;
    QLineEdit* ptNameEdit;
    QCheckBox* ptEnabledCheck;
    QLabel*    ptStatusLabel;
    QGroupBox* ptSerialGroup;
    {
        ptPage = new QWidget;
        auto* vbox = new QVBoxLayout(ptPage);
        vbox->setSpacing(6);

        auto* headerGroup = new QGroupBox("Cable Settings");
        headerGroup->setStyleSheet(kGroupStyle);
        auto* hg = new QGridLayout(headerGroup);
        hg->setSpacing(4);
        hg->addWidget(new QLabel("Name:"), 0, 0);
        ptNameEdit = new QLineEdit;
        ptNameEdit->setStyleSheet(kEdit);
        hg->addWidget(ptNameEdit, 0, 1);
        ptEnabledCheck = new QCheckBox("Enabled");
        ptEnabledCheck->setStyleSheet(kCheck);
        hg->addWidget(ptEnabledCheck, 1, 0, 1, 2);
        ptStatusLabel = new QLabel("Unplugged");
        AetherSDR::ThemeManager::instance().applyStyleSheet(ptStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        hg->addWidget(new QLabel("Status:"), 2, 0);
        hg->addWidget(ptStatusLabel, 2, 1);
        vbox->addWidget(headerGroup);

        ptSerialGroup = makeSerialGroup("Serial Parameters");
        vbox->addWidget(ptSerialGroup);

        vbox->addStretch();
        stack->addWidget(ptPage);  // index 4
    }

    hbox->addWidget(stack, 1);

    // ── Populate cable list from model ──────────────────────────────────
    auto refreshList = [cableList, cableModel]() {
        QString prevSn;
        if (cableList->currentItem())
            prevSn = cableList->currentItem()->data(Qt::UserRole).toString();
        cableList->clear();
        for (auto it = cableModel->cables().begin(); it != cableModel->cables().end(); ++it) {
            const auto& cable = it.value();
            QString label = cable.name.isEmpty() ? cable.serialNumber : cable.name;
            label += QString(" [%1]").arg(cable.type.toUpper());
            if (!cable.present)
                label += " (unplugged)";
            auto* item = new QListWidgetItem(label);
            item->setData(Qt::UserRole, cable.serialNumber);
            if (cable.enabled && cable.present)
                item->setForeground(QColor("#30d050"));
            else if (cable.enabled)
                item->setForeground(QColor("#d0d030"));
            else
                item->setForeground(QColor("#808080"));
            cableList->addItem(item);
            if (cable.serialNumber == prevSn)
                cableList->setCurrentItem(item);
        }
    };

    // ── Select cable → show properties ──────────────────────────────────
    auto showCableProps = [=](const QString& sn) {
        if (sn.isEmpty() || !cableModel->cables().contains(sn)) {
            stack->setCurrentIndex(0);
            return;
        }
        const auto& cable = cableModel->cables()[sn];
        const QString& t = cable.type;

        if (t == "cat") {
            stack->setCurrentIndex(1);
            QSignalBlocker b1(catNameEdit), b2(catEnabledCheck), b3(catSourceCombo), b4(catAutoReportCheck);
            catNameEdit->setText(cable.name);
            catEnabledCheck->setChecked(cable.enabled);
            catStatusLabel->setText(cable.present ? "Plugged In" : "Unplugged");
            catStatusLabel->setStyleSheet(cable.present
                ? "QLabel { color: #30d050; font-size: 11px; }"
                : "QLabel { color: #808080; font-size: 11px; }");
            catSourceCombo->setCurrentIndex(protoToSource(cable.source));
            catAutoReportCheck->setChecked(cable.autoReport);
        } else if (t == "bcd" || t == "vbcd" || t == "bcd_vbcd") {
            stack->setCurrentIndex(2);
            QSignalBlocker b1(bcdNameEdit), b2(bcdEnabledCheck), b3(bcdSourceCombo),
                           b4(bcdTypeCombo), b5(bcdPolarityCombo);
            bcdNameEdit->setText(cable.name);
            bcdEnabledCheck->setChecked(cable.enabled);
            bcdStatusLabel->setText(cable.present ? "Plugged In" : "Unplugged");
            bcdStatusLabel->setStyleSheet(cable.present
                ? "QLabel { color: #30d050; font-size: 11px; }"
                : "QLabel { color: #808080; font-size: 11px; }");
            bcdSourceCombo->setCurrentIndex(protoToSource(cable.source));
            if (t == "vbcd") bcdTypeCombo->setCurrentIndex(1);
            else if (t == "bcd_vbcd") bcdTypeCombo->setCurrentIndex(2);
            else bcdTypeCombo->setCurrentIndex(0);
            bcdPolarityCombo->setCurrentIndex(cable.activeHigh ? 0 : 1);
        } else if (t == "bit") {
            stack->setCurrentIndex(3);
            QSignalBlocker b1(bitNameEdit), b2(bitEnabledCheck);
            bitNameEdit->setText(cable.name);
            bitEnabledCheck->setChecked(cable.enabled);
            bitStatusLabel->setText(cable.present ? "Plugged In" : "Unplugged");
            bitStatusLabel->setStyleSheet(cable.present
                ? "QLabel { color: #30d050; font-size: 11px; }"
                : "QLabel { color: #808080; font-size: 11px; }");
            // Update bit grid rows
            auto* bitGroup = bitPage->findChild<QGroupBox*>("Bit Configuration (0-7)");
            // Bit grid cells are updated by index in the grid layout — skip for now,
            // per-bit UI refresh would iterate the grid children
        } else if (t == "passthrough") {
            stack->setCurrentIndex(4);
            QSignalBlocker b1(ptNameEdit), b2(ptEnabledCheck);
            ptNameEdit->setText(cable.name);
            ptEnabledCheck->setChecked(cable.enabled);
            ptStatusLabel->setText(cable.present ? "Plugged In" : "Unplugged");
            ptStatusLabel->setStyleSheet(cable.present
                ? "QLabel { color: #30d050; font-size: 11px; }"
                : "QLabel { color: #808080; font-size: 11px; }");
        } else {
            stack->setCurrentIndex(0);
        }
    };

    connect(cableList, &QListWidget::currentItemChanged, this,
            [showCableProps](QListWidgetItem* current, QListWidgetItem*) {
        if (current)
            showCableProps(current->data(Qt::UserRole).toString());
    });

    // ── Wire model signals ──────────────────────────────────────────────
    connect(cableModel, &UsbCableModel::cableAdded, this, [refreshList](const QString&) {
        refreshList();
    });
    connect(cableModel, &UsbCableModel::cableRemoved, this, [refreshList, stack](const QString&) {
        refreshList();
        stack->setCurrentIndex(0);
    });
    connect(cableModel, &UsbCableModel::cableChanged, this,
            [refreshList, cableList, showCableProps](const QString& sn) {
        refreshList();
        if (cableList->currentItem() &&
            cableList->currentItem()->data(Qt::UserRole).toString() == sn)
            showCableProps(sn);
    });

    // ── Wire property edits → commands ──────────────────────────────────
    // CAT
    auto sendCatProp = [cableModel, cableList](const QString& key, const QString& val) {
        auto* item = cableList->currentItem();
        if (!item) return;
        cableModel->sendSet(item->data(Qt::UserRole).toString(), key, val);
    };
    connect(catNameEdit, &QLineEdit::editingFinished, this, [catNameEdit, sendCatProp]() {
        sendCatProp("name", QString(catNameEdit->text()).replace(' ', QChar(0x7F)));
    });
    connect(catEnabledCheck, &QCheckBox::toggled, this, [sendCatProp](bool on) {
        sendCatProp("enable", on ? "1" : "0");
    });
    connect(catSourceCombo, &QComboBox::currentTextChanged, this,
            [sendCatProp, sourceToProto](const QString& text) {
        sendCatProp("source", sourceToProto(text));
    });
    connect(catAutoReportCheck, &QCheckBox::toggled, this, [sendCatProp](bool on) {
        sendCatProp("auto_report", on ? "1" : "0");
    });

    // BCD
    auto sendBcdProp = [cableModel, cableList](const QString& key, const QString& val) {
        auto* item = cableList->currentItem();
        if (!item) return;
        cableModel->sendSet(item->data(Qt::UserRole).toString(), key, val);
    };
    connect(bcdNameEdit, &QLineEdit::editingFinished, this, [bcdNameEdit, sendBcdProp]() {
        sendBcdProp("name", QString(bcdNameEdit->text()).replace(' ', QChar(0x7F)));
    });
    connect(bcdEnabledCheck, &QCheckBox::toggled, this, [sendBcdProp](bool on) {
        sendBcdProp("enable", on ? "1" : "0");
    });
    connect(bcdTypeCombo, &QComboBox::currentIndexChanged, this,
            [sendBcdProp](int idx) {
        static const char* types[] = {"bcd", "vbcd", "bcd_vbcd"};
        if (idx >= 0 && idx < 3) sendBcdProp("type", types[idx]);
    });
    connect(bcdPolarityCombo, &QComboBox::currentIndexChanged, this,
            [sendBcdProp](int idx) {
        sendBcdProp("polarity", idx == 0 ? "active_high" : "active_low");
    });
    connect(bcdSourceCombo, &QComboBox::currentTextChanged, this,
            [sendBcdProp, sourceToProto](const QString& text) {
        sendBcdProp("source", sourceToProto(text));
    });

    // Bit cable header
    auto sendBitProp = [cableModel, cableList](const QString& key, const QString& val) {
        auto* item = cableList->currentItem();
        if (!item) return;
        cableModel->sendSet(item->data(Qt::UserRole).toString(), key, val);
    };
    connect(bitNameEdit, &QLineEdit::editingFinished, this, [bitNameEdit, sendBitProp]() {
        sendBitProp("name", QString(bitNameEdit->text()).replace(' ', QChar(0x7F)));
    });
    connect(bitEnabledCheck, &QCheckBox::toggled, this, [sendBitProp](bool on) {
        sendBitProp("enable", on ? "1" : "0");
    });

    // Passthrough
    auto sendPtProp = [cableModel, cableList](const QString& key, const QString& val) {
        auto* item = cableList->currentItem();
        if (!item) return;
        cableModel->sendSet(item->data(Qt::UserRole).toString(), key, val);
    };
    connect(ptNameEdit, &QLineEdit::editingFinished, this, [ptNameEdit, sendPtProp]() {
        sendPtProp("name", QString(ptNameEdit->text()).replace(' ', QChar(0x7F)));
    });
    connect(ptEnabledCheck, &QCheckBox::toggled, this, [sendPtProp](bool on) {
        sendPtProp("enable", on ? "1" : "0");
    });

    // Initial populate
    refreshList();

    return page;
}

#ifdef HAVE_SERIALPORT
QWidget* RadioSetupDialog::buildSerialTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);
    vbox->setContentsMargins(8, 8, 8, 8);

    const QString kLabelStyle = "QLabel { color: #8898a8; font-size: 11px; }";
    const QString kGroupStyle = "QGroupBox { color: #00b4d8; font-size: 12px; border: 1px solid #203040; "
                                "border-radius: 4px; margin-top: 6px; padding-top: 14px; } "
                                "QGroupBox::title { subcontrol-origin: margin; left: 8px; }";

    auto& settings = AppSettings::instance();

    // ── USB control surfaces (Ulanzi Dial, StreamDeck+) (#3257) ──────────
    // These are opt-in because the first call into each backend triggers the
    // macOS Input Monitoring permission prompt (kIOHIDOptionsTypeSeizeDevice
    // in the IOKit-direct backend, hid_open() in HIDAPI). Defaulting them off
    // means the prompt only ever fires for users who actually own and want to
    // use the hardware.
    {
        auto* group = new QGroupBox("USB Control Surfaces");
        group->setStyleSheet(kGroupStyle);
        auto* gvbox = new QVBoxLayout(group);
        gvbox->setSpacing(6);

        auto* note = new QLabel(
            "Enable only if you connect a Ulanzi Dial or Elgato Stream Deck+. "
            "On macOS, enabling will trigger an Input Monitoring permission "
            "prompt the first time AetherSDR scans for the device.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        gvbox->addWidget(note);

        auto* ulanziEnable = new QCheckBox("Enable Ulanzi Dial");
        AetherSDR::ThemeManager::instance().applyStyleSheet(
            ulanziEnable, "QCheckBox { color: {{color.text.primary}}; }");
        ulanziEnable->setChecked(
            settings.value("UlanziDialEnabled", "False").toString() == "True");
        connect(ulanziEnable, &QCheckBox::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("UlanziDialEnabled", on ? "True" : "False");
            s.save();
            emit serialSettingsChanged();
        });
        gvbox->addWidget(ulanziEnable);

#ifdef HAVE_HIDAPI
        auto* hidEnable = new QCheckBox(
            "Enable HID encoders / StreamDeck+ (RC-28, PowerMate, ShuttleXpress, …)");
        AetherSDR::ThemeManager::instance().applyStyleSheet(
            hidEnable, "QCheckBox { color: {{color.text.primary}}; }");
        hidEnable->setChecked(
            settings.value("HidEncoderEnabled", "False").toString() == "True");
        connect(hidEnable, &QCheckBox::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("HidEncoderEnabled", on ? "True" : "False");
            s.save();
            emit serialSettingsChanged();
        });
        gvbox->addWidget(hidEnable);
#endif

        vbox->addWidget(group);
    }

    // ── Port Configuration ───────────────────────────────────────────────
    {
        auto* group = new QGroupBox("Port Configuration");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(4);

        // Port selector + manual entry for non-standard TTYs (#897)
        grid->addWidget(new QLabel("Port:"), 0, 0);
        auto* portCombo = new QComboBox;
        portCombo->setMinimumWidth(200);
        for (const auto& info : QSerialPortInfo::availablePorts())
            portCombo->addItem(QString("%1 — %2").arg(info.portName(), info.description()),
                               info.portName());
        // "Custom..." sentinel triggers manual entry field
        portCombo->addItem("Custom...", QStringLiteral("__custom__"));
        QString savedPort = settings.value("SerialPortName", "").toString();
        bool isCustom = false;
        for (int i = 0; i < portCombo->count() - 1; ++i) {
            if (portCombo->itemData(i).toString() == savedPort) {
                portCombo->setCurrentIndex(i);
                break;
            }
            if (i == portCombo->count() - 2) {
                isCustom = !savedPort.isEmpty();
            }
        }
        grid->addWidget(portCombo, 0, 1);

        auto* refreshBtn = new QPushButton("Refresh");
        // Let native style (notably macOS) drive the button height; a fixed
        // 24 px clipped the macOS button bezel and label baseline.
        refreshBtn->setMinimumHeight(24);
        grid->addWidget(refreshBtn, 0, 3);

        // Custom port row — hidden unless "Custom..." selected or saved port is custom
        auto* customLabel = new QLabel("Path:");
        auto* customEdit = new QLineEdit;
        customEdit->setPlaceholderText("/dev/ttyr0");
        customLabel->setVisible(isCustom);
        customEdit->setVisible(isCustom);
        if (isCustom) {
            customEdit->setText(savedPort);
            portCombo->setCurrentIndex(portCombo->count() - 1);  // select "Custom..."
        }
        grid->addWidget(customLabel, 1, 0);
        grid->addWidget(customEdit, 1, 1, 1, 3);

        connect(portCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                this, [portCombo, customLabel, customEdit](int idx) {
            bool custom = (portCombo->itemData(idx).toString() == "__custom__");
            customLabel->setVisible(custom);
            customEdit->setVisible(custom);
        });

        connect(refreshBtn, &QPushButton::clicked, this, [portCombo, customEdit]() {
            QString customText = customEdit->text();
            int customIdx = portCombo->count() - 1;  // "Custom..." is last
            bool wasCustom = (portCombo->currentIndex() == customIdx);
            // Remove all but "Custom..."
            while (portCombo->count() > 1)
                portCombo->removeItem(0);
            for (const auto& info : QSerialPortInfo::availablePorts())
                portCombo->insertItem(portCombo->count() - 1,
                    QString("%1 — %2").arg(info.portName(), info.description()),
                    info.portName());
            if (wasCustom) {
                portCombo->setCurrentIndex(portCombo->count() - 1);
            }
        });

        // Baud rate
        grid->addWidget(new QLabel("Baud:"), 2, 0);
        auto* baudCombo = new QComboBox;
        for (int b : {9600, 19200, 38400, 57600, 115200})
            baudCombo->addItem(QString::number(b), b);
        int savedBaud = settings.value("SerialBaudRate", "9600").toInt();
        baudCombo->setCurrentIndex(baudCombo->findData(savedBaud));
        grid->addWidget(baudCombo, 2, 1);

        // Data bits
        grid->addWidget(new QLabel("Data:"), 2, 2);
        auto* dataCombo = new QComboBox;
        dataCombo->addItem("8", 8);
        dataCombo->addItem("7", 7);
        int savedData = settings.value("SerialDataBits", "8").toInt();
        dataCombo->setCurrentIndex(dataCombo->findData(savedData));
        grid->addWidget(dataCombo, 2, 3);

        // Parity
        grid->addWidget(new QLabel("Parity:"), 3, 0);
        auto* parityCombo = new QComboBox;
        parityCombo->addItem("None", 0);
        parityCombo->addItem("Even", 2);
        parityCombo->addItem("Odd", 3);
        int savedParity = settings.value("SerialParity", "0").toInt();
        parityCombo->setCurrentIndex(parityCombo->findData(savedParity));
        grid->addWidget(parityCombo, 3, 1);

        // Stop bits
        grid->addWidget(new QLabel("Stop:"), 3, 2);
        auto* stopCombo = new QComboBox;
        stopCombo->addItem("1", 1);
        stopCombo->addItem("2", 2);
        int savedStop = settings.value("SerialStopBits", "1").toInt();
        stopCombo->setCurrentIndex(stopCombo->findData(savedStop));
        grid->addWidget(stopCombo, 3, 3);

        vbox->addWidget(group);

        // Save port settings on any change
        auto savePort = [portCombo, customEdit, baudCombo, dataCombo, parityCombo, stopCombo]() {
            auto& s = AppSettings::instance();
            QString port = portCombo->currentData().toString();
            if (port == "__custom__") {
                port = customEdit->text().trimmed();
            }
            s.setValue("SerialPortName", port);
            s.setValue("SerialBaudRate", baudCombo->currentData().toString());
            s.setValue("SerialDataBits", dataCombo->currentData().toString());
            s.setValue("SerialParity", parityCombo->currentData().toString());
            s.setValue("SerialStopBits", stopCombo->currentData().toString());
            s.save();
        };
        connect(portCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, savePort);
        connect(customEdit, &QLineEdit::textChanged, this, savePort);
        connect(baudCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, savePort);
        connect(dataCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, savePort);
        connect(parityCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, savePort);
        connect(stopCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, savePort);
    }

    // ── Pin Assignment ───────────────────────────────────────────────────
    {
        auto* group = new QGroupBox("Pin Assignment");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(4);

        auto* headerPin = new QLabel("Pin");
        auto* headerFn  = new QLabel("Function");
        auto* headerPol = new QLabel("Polarity");
        headerPin->setStyleSheet(kLabelStyle);
        headerFn->setStyleSheet(kLabelStyle);
        headerPol->setStyleSheet(kLabelStyle);
        grid->addWidget(headerPin, 0, 0);
        grid->addWidget(headerFn,  0, 1);
        grid->addWidget(headerPol, 0, 2);

        auto makeFnCombo = [this](const QString& savedKey) {
            auto* combo = new QComboBox;
            combo->addItem("None",   "None");
            combo->addItem("PTT",    "PTT");
            combo->addItem("CW Key", "CwKey");
            combo->addItem("CW PTT", "CwPTT");
            QString saved = AppSettings::instance().value(savedKey, "None").toString();
            for (int i = 0; i < combo->count(); ++i)
                if (combo->itemData(i).toString() == saved) { combo->setCurrentIndex(i); break; }
            connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [savedKey, combo]() {
                auto& s = AppSettings::instance();
                s.setValue(savedKey, combo->currentData().toString());
                s.save();
            });
            return combo;
        };

        auto makePolCombo = [this](const QString& savedKey) {
            auto* combo = new QComboBox;
            combo->addItem("Active High", "ActiveHigh");
            combo->addItem("Active Low",  "ActiveLow");
            QString saved = AppSettings::instance().value(savedKey, "ActiveHigh").toString();
            combo->setCurrentIndex(saved == "ActiveLow" ? 1 : 0);
            connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [savedKey, combo]() {
                auto& s = AppSettings::instance();
                s.setValue(savedKey, combo->currentData().toString());
                s.save();
            });
            return combo;
        };

        // DTR row
        grid->addWidget(new QLabel("DTR"), 1, 0);
        grid->addWidget(makeFnCombo("SerialDtrFunction"), 1, 1);
        grid->addWidget(makePolCombo("SerialDtrPolarity"), 1, 2);

        // RTS row
        grid->addWidget(new QLabel("RTS"), 2, 0);
        grid->addWidget(makeFnCombo("SerialRtsFunction"), 2, 1);
        grid->addWidget(makePolCombo("SerialRtsPolarity"), 2, 2);

        // Input pin function combo (different options than output)
        auto makeInputFnCombo = [this](const QString& savedKey) {
            auto* combo = new QComboBox;
            combo->addItem("None",         "None");
            combo->addItem("PTT Input",    "PttInput");
            combo->addItem("CW Key Input", "CwKeyInput");
            combo->addItem("CW Dit Input", "CwDitInput");
            combo->addItem("CW Dah Input", "CwDahInput");
            QString saved = AppSettings::instance().value(savedKey, "None").toString();
            for (int i = 0; i < combo->count(); ++i)
                if (combo->itemData(i).toString() == saved) { combo->setCurrentIndex(i); break; }
            connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [savedKey, combo]() {
                auto& s = AppSettings::instance();
                s.setValue(savedKey, combo->currentData().toString());
                s.save();
            });
            return combo;
        };

        // CTS row (input)
        auto* ctsLabel = new QLabel("CTS");
        ctsLabel->setStyleSheet("QLabel { color: #60a0c0; }");
        grid->addWidget(ctsLabel, 3, 0);
        grid->addWidget(makeInputFnCombo("SerialCtsFunction"), 3, 1);
        grid->addWidget(makePolCombo("SerialCtsPolarity"), 3, 2);

        // DSR row (input)
        auto* dsrLabel = new QLabel("DSR");
        dsrLabel->setStyleSheet("QLabel { color: #60a0c0; }");
        grid->addWidget(dsrLabel, 4, 0);
        grid->addWidget(makeInputFnCombo("SerialDsrFunction"), 4, 1);
        grid->addWidget(makePolCombo("SerialDsrPolarity"), 4, 2);

        // DCD row (input) — added for accessories that wire to the FTDI
        // chip's DCD# pin (e.g. HaliKey Serial from Halibut Electronics,
        // whose TRS Ring is wired to both DSR and DCD).
        auto* dcdLabel = new QLabel("DCD");
        dcdLabel->setStyleSheet("QLabel { color: #60a0c0; }");
        grid->addWidget(dcdLabel, 5, 0);
        grid->addWidget(makeInputFnCombo("SerialDcdFunction"), 5, 1);
        grid->addWidget(makePolCombo("SerialDcdPolarity"), 5, 2);

        // Paddle swap
        auto* swapCb = new QCheckBox("Paddle Swap (swap dit/dah)");
        AetherSDR::ThemeManager::instance().applyStyleSheet(swapCb, "QCheckBox { color: {{color.text.primary}}; }");
        swapCb->setChecked(AppSettings::instance().value("SerialPaddleSwap", "False").toString() == "True");
        connect(swapCb, &QCheckBox::toggled, this, [](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("SerialPaddleSwap", on ? "True" : "False");
            s.save();
        });
        grid->addWidget(swapCb, 6, 0, 1, 3);

        vbox->addWidget(group);
    }

    // ── Open / Close / Auto-open ────────────────────────────────────────
    {
        auto* row = new QHBoxLayout;
        row->setSpacing(8);

        const QString btnStyle =
            "QPushButton { background: #00b4d8; color: #0f0f1a; font-weight: bold; "
            "border: 1px solid #008ba8; padding: 3px; border-radius: 3px; }"
            "QPushButton:hover { background: #00c8f0; }"
            "QPushButton:disabled { background: {{color.button.background.disabled}}; color: {{color.button.foreground.disabled}}; border-color: {{color.button.border.disabled}}; }";

        auto* openBtn = new QPushButton("Open");
        // Min-width rather than fixed-width: macOS native button metrics need
        // more horizontal room than the 80 px Windows/Fusion baseline.
        openBtn->setMinimumWidth(80);
        AetherSDR::ThemeManager::instance().applyStyleSheet(openBtn, btnStyle);
        auto* closeBtn = new QPushButton("Close");
        closeBtn->setMinimumWidth(80);
        AetherSDR::ThemeManager::instance().applyStyleSheet(closeBtn, btnStyle);

        auto* statusLabel = new QLabel;
        statusLabel->setStyleSheet("QLabel { font-size: 11px; }");

        row->addWidget(openBtn);
        row->addWidget(closeBtn);
        row->addWidget(statusLabel);
        row->addStretch();

        auto updatePortStatus = [openBtn, closeBtn, statusLabel]() {
            bool open = AppSettings::instance().value("SerialPortOpen", "False").toString() == "True";
            openBtn->setEnabled(!open);
            closeBtn->setEnabled(open);
            if (open) {
                statusLabel->setText("Open");
                AetherSDR::ThemeManager::instance().applyStyleSheet(statusLabel, "QLabel { color: {{color.accent.success}}; font-size: 11px; }");
            } else {
                statusLabel->setText("Closed");
                AetherSDR::ThemeManager::instance().applyStyleSheet(statusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
            }
        };

        connect(openBtn, &QPushButton::clicked, this, [this, updatePortStatus]() {
            auto& s = AppSettings::instance();
            s.setValue("SerialPortOpen", "True");
            s.save();
            updatePortStatus();
            emit serialSettingsChanged();
        });

        connect(closeBtn, &QPushButton::clicked, this, [this, updatePortStatus]() {
            auto& s = AppSettings::instance();
            s.setValue("SerialPortOpen", "False");
            s.save();
            updatePortStatus();
            emit serialSettingsChanged();
        });

        updatePortStatus();
        vbox->addLayout(row);

        auto* autoOpen = new QCheckBox("Auto-open serial port on startup");
        AetherSDR::ThemeManager::instance().applyStyleSheet(autoOpen, "QCheckBox { color: {{color.text.primary}}; }");
        autoOpen->setChecked(settings.value("SerialAutoOpen", "False").toString() == "True");
        connect(autoOpen, &QCheckBox::toggled, this, [](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("SerialAutoOpen", on ? "True" : "False");
            s.save();
        });
        vbox->addWidget(autoOpen);
    }

    // ── FlexControl tuning knob ────────────────────────────────────────
    {
        auto* group = new QGroupBox("FlexControl Tuning Knob");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        // Status
        auto* fcStatusLabel = new QLabel("Not detected");
        AetherSDR::ThemeManager::instance().applyStyleSheet(fcStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        m_flexControlStatusLabel = fcStatusLabel;
        grid->addWidget(new QLabel("Status:"), 0, 0);
        grid->addWidget(fcStatusLabel, 0, 1);

        // Detect / Close buttons
        auto* fcDetectBtn = new QPushButton("Detect");
        // Same macOS-native-metrics consideration as the Open/Close pair above.
        fcDetectBtn->setMinimumWidth(80);
        AetherSDR::ThemeManager::instance().applyStyleSheet(fcDetectBtn, "QPushButton { background: {{color.accent}}; color: {{color.background.0}}; font-weight: bold; "
            "border: 1px solid {{color.accent.dim}}; padding: 3px; border-radius: 3px; }"
            "QPushButton:hover { background: {{color.accent.bright}}; }");
        auto* fcCloseBtn = new QPushButton("Close");
        fcCloseBtn->setMinimumWidth(80);
        fcCloseBtn->setStyleSheet(fcDetectBtn->styleSheet());
        fcCloseBtn->setEnabled(false);
        m_flexControlDetectButton = fcDetectBtn;
        m_flexControlCloseButton = fcCloseBtn;

        auto* btnRow = new QHBoxLayout;
        btnRow->addWidget(fcDetectBtn);
        btnRow->addWidget(fcCloseBtn);
        btnRow->addStretch();
        grid->addLayout(btnRow, 0, 2);

        // Update status display
        connect(fcDetectBtn, &QPushButton::clicked, this, [this] {
            QString port = FlexControlManager::detectPort();
            if (port.isEmpty()) {
                setFlexControlConnectionStatus(false);
                return;
            }
            // Store port for MainWindow to open
            auto& s = AppSettings::instance();
            s.setValue("FlexControlPort", port);
            s.setValue("FlexControlOpen", "True");
            s.save();
            emit serialSettingsChanged();
        });
        connect(fcCloseBtn, &QPushButton::clicked, this, [this] {
            auto& s = AppSettings::instance();
            s.setValue("FlexControlOpen", "False");
            s.save();
            setFlexControlConnectionStatus(false);
            emit serialSettingsChanged();
        });

        // Show current state from settings
        if (settings.value("FlexControlOpen", "False").toString() == "True") {
            QString port = settings.value("FlexControlPort").toString();
            if (!port.isEmpty())
                setFlexControlConnectionStatus(true, port);
        }

        // Button action configuration
        static const QStringList actions = {
            "None", "StepUp", "StepDown", "ToggleMox",
            "ToggleTune", "ToggleMute", "ToggleLock",
            "BandZoom", "SegmentZoom",
            "NextSlice", "PrevSlice",
            "SplitActiveSlice",
            "ToggleAgc", "VolumeUp", "VolumeDown",
            "WheelFrequency", "WheelVolume", "WheelPower",
            "WheelRit", "WheelXit",
            "WheelSliceAudio",
            "WheelHeadphoneVolume",
            "WheelAgcT", "WheelApf", "WheelCwSpeed",
            "ClearRit", "ClearXit", "ToggleApf",
            "CwxF1", "CwxF2", "CwxF3", "CwxF4",
            "CwxF5", "CwxF6", "CwxF7", "CwxF8",
            "CwxF9", "CwxF10", "CwxF11", "CwxF12"
        };
        static const char* defaultActions[4][2] = {
            {"StepUp", "StepDown"},
            {"ToggleMox", "ToggleTune"},
            {"ToggleMute", "ToggleLock"},
            {"StepUp", "StepDown"},      // Knob button
        };
        static const char* btnLabels[4] = {"Button 1:", "Button 2:", "Button 3:", "Knob Button:"};
        static const char* actLabels[2] = {"Tap", "Double"};

        for (int b = 0; b < 4; ++b) {
            grid->addWidget(new QLabel(btnLabels[b]), b + 1, 0);
            auto* row = new QHBoxLayout;
            for (int a = 0; a < 2; ++a) {
                row->addWidget(new QLabel(actLabels[a]));
                auto* combo = new QComboBox;
                combo->addItems(actions);
                combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
                QString key = QString("FlexControlBtn%1Action%2").arg(b + 1).arg(a);
                QString current = settings.value(key, defaultActions[b][a]).toString();
                int idx = actions.indexOf(current);
                if (idx >= 0) combo->setCurrentIndex(idx);
                m_flexControlActionCombos.insert(key, combo);
                m_flexControlActionDefaults.insert(key, QString::fromLatin1(defaultActions[b][a]));
                connect(combo, &QComboBox::currentTextChanged, this, [this, key](const QString& text) {
                    auto& s = AppSettings::instance();
                    s.setValue(key, text);
                    s.save();
                    emit serialSettingsChanged();
                });
                row->addWidget(combo);
            }
            row->addStretch();
            grid->addLayout(row, b + 1, 1, 1, 2);
        }

        // Auto-detect checkbox
        auto* autoDetect = new QCheckBox("Auto-detect on startup");
        AetherSDR::ThemeManager::instance().applyStyleSheet(autoDetect, "QCheckBox { color: {{color.text.primary}}; }");
        autoDetect->setChecked(settings.value("FlexControlAutoDetect", "True").toString() == "True");
        connect(autoDetect, &QCheckBox::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("FlexControlAutoDetect", on ? "True" : "False");
            s.save();
            emit serialSettingsChanged();
        });
        grid->addWidget(autoDetect, 5, 0, 1, 3);

        auto* invertDir = new QCheckBox("Invert tuning direction");
        AetherSDR::ThemeManager::instance().applyStyleSheet(invertDir, "QCheckBox { color: {{color.text.primary}}; }");
        invertDir->setChecked(settings.value("FlexControlInvertDir", "False").toString() == "True");
        m_flexControlInvertCheck = invertDir;
        connect(invertDir, &QCheckBox::toggled, this, [this](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("FlexControlInvertDir", on ? "True" : "False");
            s.save();
            emit serialSettingsChanged();
        });
        grid->addWidget(invertDir, 6, 0, 1, 3);

        vbox->addWidget(group);
    }

    // ── HID / StreamDeck+ LCD button action mapping (#1510) ──────────────────
#ifdef HAVE_HIDAPI
    {
        auto* group = new QGroupBox("StreamDeck+ LCD Button Actions");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel(
            "Assign an action to each of the 8 LCD buttons. "
            "The button label updates on the device to match.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 4);

        static const struct { const char* id; const char* label; } kKeyActions[] = {
            {"None",             "None"},
            {"ToggleMox",        "Toggle TX (MOX)"},
            {"ToggleTune",       "Toggle Tune"},
            {"ToggleRit",        "Toggle RIT on/off"},
            {"ToggleXit",        "Toggle XIT on/off"},
            {"ClearRit",         "Clear RIT offset"},
            {"ClearXit",         "Clear XIT offset"},
            {"StepUp",           "Step Size Up"},
            {"StepDown",         "Step Size Down"},
            {"ToggleMute",       "Toggle Mute"},
            {"ToggleLock",       "Toggle Slice Lock"},
            {"ToggleApf",        "Toggle APF"},
            {"ToggleAgc",        "Cycle AGC Mode"},
            {"BandZoom",         "Toggle Band Zoom"},
            {"SegmentZoom",      "Toggle Segment Zoom"},
            {"NextSlice",        "Next Slice"},
            {"PrevSlice",        "Previous Slice"},
            {"VolumeUp",         "Volume Up (+5)"},
            {"VolumeDown",       "Volume Down (-5)"},
            {"SplitActiveSlice", "Toggle Split"},
        };

        // 8 keys laid out as 2 columns of 4
        for (int i = 0; i < 8; ++i) {
            const int row = (i % 4) + 1;
            const int col = (i / 4) * 2;

            grid->addWidget(new QLabel(QString("Key %1:").arg(i + 1)), row, col);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kKeyActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key    = QString("HidKeyAction%1").arg(i);
            const QString saved  = settings.value(key, QStringLiteral("None")).toString();
            const int     selIdx = combo->findData(saved);
            combo->setCurrentIndex(selIdx >= 0 ? selIdx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_hidKeyActionCombos[i] = combo;
            grid->addWidget(combo, row, col + 1);
        }
        grid->setColumnStretch(1, 1);
        grid->setColumnStretch(3, 1);

        vbox->addWidget(group);
    }

    // ── HID Encoder — per-encoder action mapping (#1510) ─────────────────────
    {
        auto* group = new QGroupBox("HID Encoder / StreamDeck+ Encoders");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel(
            "Assign an action to each encoder dial. "
            "Single-encoder devices (RC-28, PowerMate, ShuttleXpress) use Encoder 1 only.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 3);

        static const struct { const char* id; const char* label; } kEncoderActions[] = {
            {"WheelFrequency",      "Tune Slice"},
            {"WheelRit",            "RIT (Receive Incremental Tuning)"},
            {"WheelXit",            "XIT (Transmit Incremental Tuning)"},
            {"WheelVolume",         "Master Volume"},
            {"WheelSliceAudio",     "Slice Audio Volume"},
            {"WheelHeadphoneVolume","Headphone Volume"},
            {"WheelAgcT",           "AGC Threshold"},
            {"WheelApf",            "APF Level"},
            {"WheelCwSpeed",        "CW Speed"},
            {"WheelPower",          "RF Power"},
            {"None",                "None"},
        };

        static const char* kEncoderDefaults[4] = {
            "WheelFrequency", "WheelRit", "WheelXit", "WheelVolume"
        };

        for (int i = 0; i < 4; ++i) {
            grid->addWidget(new QLabel(QString("Encoder %1:").arg(i + 1)), i + 1, 0);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kEncoderActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key = QString("HidEncoderAction%1").arg(i);
            const QString saved = settings.value(key, QString::fromLatin1(kEncoderDefaults[i])).toString();
            const int idx = combo->findData(saved);
            combo->setCurrentIndex(idx >= 0 ? idx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_hidEncoderActionCombos[i] = combo;
            grid->addWidget(combo, i + 1, 1, 1, 2);
            grid->setColumnStretch(1, 1);
        }

        vbox->addWidget(group);
    }

    // ── HID Encoder — per-encoder push-button action mapping (#1510) ─────────
    {
        auto* group = new QGroupBox("StreamDeck+ Encoder Push Actions");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel(
            "Action when each encoder dial is pressed. "
            "Defaults: Enc 1 = Cycle Tuning Step, Enc 2 = Toggle RIT, Enc 3 = Toggle XIT.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 3);

        static const struct { const char* id; const char* label; } kPushActions[] = {
            {"StepCycle",  "Cycle Tuning Step"},
            {"ToggleRit",  "Toggle RIT on/off"},
            {"ToggleXit",  "Toggle XIT on/off"},
            {"ToggleMox",  "Toggle TX (MOX)"},
            {"ToggleMute", "Toggle Mute"},
            {"ToggleLock", "Lock Slice"},
            {"None",       "None"},
        };

        static const char* kPushDefaults[4] = {
            "StepCycle", "ToggleRit", "ToggleXit", "None"
        };

        for (int i = 0; i < 4; ++i) {
            grid->addWidget(new QLabel(QString("Encoder %1 push:").arg(i + 1)), i + 1, 0);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kPushActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key = QString("HidEncoderPushAction%1").arg(i);
            const QString saved = settings.value(key, QString::fromLatin1(kPushDefaults[i])).toString();
            const int idx = combo->findData(saved);
            combo->setCurrentIndex(idx >= 0 ? idx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_hidEncoderPushActionCombos[i] = combo;
            grid->addWidget(combo, i + 1, 1, 1, 2);
            grid->setColumnStretch(1, 1);
        }

        vbox->addWidget(group);
    }

    // --- TMate 2 device actions, encoders, and backlight ---------------------
    {
        auto* group = new QGroupBox("TMate 2 Key Actions");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel("Assign actions to the six TMate 2 function keys.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 4);

        static const struct { const char* id; const char* label; } kTMate2KeyActions[] = {
            {"None",             "None"},
            {"ToggleMox",        "Toggle TX (MOX)"},
            {"ToggleTune",       "Toggle Tune"},
            {"ToggleRit",        "Toggle RIT on/off"},
            {"ToggleXit",        "Toggle XIT on/off"},
            {"ClearRit",         "Clear RIT offset"},
            {"ClearXit",         "Clear XIT offset"},
            {"StepUp",           "Step Size Up"},
            {"StepDown",         "Step Size Down"},
            {"ToggleMute",       "Toggle Mute"},
            {"ToggleLock",       "Toggle Slice Lock"},
            {"ToggleApf",        "Toggle APF"},
            {"ToggleAgc",        "Cycle AGC Mode"},
            {"BandZoom",         "Toggle Band Zoom"},
            {"SegmentZoom",      "Toggle Segment Zoom"},
            {"NextSlice",        "Next Slice"},
            {"PrevSlice",        "Previous Slice"},
            {"VolumeUp",         "Volume Up (+5)"},
            {"VolumeDown",       "Volume Down (-5)"},
            {"SplitActiveSlice", "Toggle Split"},
        };

        static const char* kTMate2KeyDefaults[6] = {
            "ToggleMox", "ToggleAgc", "BandZoom", "ToggleApf", "ToggleMute", "ToggleRit"
        };

        for (int i = 0; i < 6; ++i) {
            const int row = (i % 3) + 1;
            const int col = (i / 3) * 2;
            grid->addWidget(new QLabel(QString("F%1:").arg(i + 1)), row, col);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kTMate2KeyActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key = QString("TMate2KeyAction%1").arg(i);
            const QString saved = settings.value(
                key, QString::fromLatin1(kTMate2KeyDefaults[i])).toString();
            const int idx = combo->findData(saved);
            combo->setCurrentIndex(idx >= 0 ? idx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_tmate2KeyActionCombos[i] = combo;
            grid->addWidget(combo, row, col + 1);
        }
        grid->setColumnStretch(1, 1);
        grid->setColumnStretch(3, 1);

        vbox->addWidget(group);
    }

    {
        auto* group = new QGroupBox("TMate 2 Encoder Actions");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel("Assign actions to the three TMate 2 encoder dials.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 3);

        static const struct { const char* id; const char* label; } kTMate2EncoderActions[] = {
            {"WheelFrequency",      "Tune Slice"},
            {"WheelRit",            "RIT (Receive Incremental Tuning)"},
            {"WheelXit",            "XIT (Transmit Incremental Tuning)"},
            {"WheelVolume",         "Master Volume"},
            {"WheelSliceAudio",     "Slice Audio Volume"},
            {"WheelHeadphoneVolume","Headphone Volume"},
            {"WheelAgcT",           "AGC Threshold"},
            {"WheelApf",            "APF Level"},
            {"WheelCwSpeed",        "CW Speed"},
            {"WheelPower",          "RF Power"},
            {"None",                "None"},
        };

        static const char* kTMate2EncoderDefaults[3] = {
            "WheelFrequency", "WheelRit", "WheelXit"
        };

        for (int i = 0; i < 3; ++i) {
            grid->addWidget(new QLabel(QString("Encoder %1:").arg(i + 1)), i + 1, 0);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kTMate2EncoderActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key = QString("TMate2EncoderAction%1").arg(i);
            const QString saved = settings.value(
                key, QString::fromLatin1(kTMate2EncoderDefaults[i])).toString();
            const int idx = combo->findData(saved);
            combo->setCurrentIndex(idx >= 0 ? idx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_tmate2EncoderActionCombos[i] = combo;
            grid->addWidget(combo, i + 1, 1, 1, 2);
            grid->setColumnStretch(1, 1);
        }

        vbox->addWidget(group);
    }

    {
        auto* group = new QGroupBox("TMate 2 Encoder Push Actions");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel("Action when each TMate 2 encoder is pressed.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 3);

        static const struct { const char* id; const char* label; } kTMate2PushActions[] = {
            {"StepCycle",  "Cycle Tuning Step"},
            {"ToggleRit",  "Toggle RIT on/off"},
            {"ToggleXit",  "Toggle XIT on/off"},
            {"ToggleMox",  "Toggle TX (MOX)"},
            {"ToggleMute", "Toggle Mute"},
            {"ToggleLock", "Lock Slice"},
            {"None",       "None"},
        };

        static const char* kTMate2PushDefaults[3] = {
            "StepCycle", "ToggleRit", "ToggleXit"
        };

        for (int i = 0; i < 3; ++i) {
            grid->addWidget(new QLabel(QString("Encoder %1 push:").arg(i + 1)), i + 1, 0);

            auto* combo = new QComboBox;
            combo->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QComboBox"));
            for (const auto& act : kTMate2PushActions)
                combo->addItem(QString::fromLatin1(act.label), QString::fromLatin1(act.id));

            const QString key = QString("TMate2PushAction%1").arg(i);
            const QString saved = settings.value(
                key, QString::fromLatin1(kTMate2PushDefaults[i])).toString();
            const int idx = combo->findData(saved);
            combo->setCurrentIndex(idx >= 0 ? idx : 0);

            connect(combo, &QComboBox::currentIndexChanged, this, [combo, key, this](int) {
                auto& s = AppSettings::instance();
                s.setValue(key, combo->currentData().toString());
                s.save();
                emit serialSettingsChanged();
            });

            m_tmate2EncoderPushActionCombos[i] = combo;
            grid->addWidget(combo, i + 1, 1, 1, 2);
            grid->setColumnStretch(1, 1);
        }

        vbox->addWidget(group);
    }

    {
        auto* group = new QGroupBox("TMate 2 Display");
        group->setStyleSheet(kGroupStyle);
        auto* grid = new QGridLayout(group);
        grid->setSpacing(6);

        auto* note = new QLabel(
            "Backlight colours and temporary display feedback. "
            "Overlay duration controls how long changed values are shown on the TMate 2 LCD.");
        note->setWordWrap(true);
        note->setStyleSheet(kLabelStyle);
        grid->addWidget(note, 0, 0, 1, 7);

        static const struct {
            const char* rowLabel;
            const char* rKey; const char* gKey; const char* bKey;
            const char* rDflt; const char* gDflt; const char* bDflt;
            int spinOffset;
        } kRows[2] = {
            {"RX backlight:", "TMate2BacklightR",   "TMate2BacklightG",   "TMate2BacklightB",
                              "0",                  "50",                 "255", 0},
            {"TX backlight:", "TMate2TxBacklightR", "TMate2TxBacklightG", "TMate2TxBacklightB",
                              "255",                "30",                 "0",   3},
        };

        static const char* kLabels[3] = {"R:", "G:", "B:"};
        for (int row = 0; row < 2; ++row) {
            auto* rowLbl = new QLabel(QString::fromLatin1(kRows[row].rowLabel));
            rowLbl->setStyleSheet(kLabelStyle);
            grid->addWidget(rowLbl, row + 1, 0);
            const char* keys[3] = {kRows[row].rKey, kRows[row].gKey, kRows[row].bKey};
            const char* dflts[3] = {kRows[row].rDflt, kRows[row].gDflt, kRows[row].bDflt};
            for (int ch = 0; ch < 3; ++ch) {
                auto* lbl = new QLabel(QString::fromLatin1(kLabels[ch]));
                lbl->setStyleSheet(kLabelStyle);
                grid->addWidget(lbl, row + 1, 1 + ch * 2);

                auto* spin = new QSpinBox;
                spin->setRange(0, 255);
                spin->setValue(settings.value(keys[ch], dflts[ch]).toInt());
                spin->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QSpinBox"));
                grid->addWidget(spin, row + 1, 2 + ch * 2);
                m_tmate2BacklightSpins[kRows[row].spinOffset + ch] = spin;

                const QString key = QString::fromLatin1(keys[ch]);
                connect(spin, QOverload<int>::of(&QSpinBox::valueChanged),
                        this, [this, key](int val) {
                    auto& s = AppSettings::instance();
                    s.setValue(key, QString::number(val));
                    s.save();
                    emit serialSettingsChanged();
                });
            }
        }

        auto addTimingSpin = [&](int row, const QString& label, const QString& key,
                                 int dflt, int min, int max, int step) -> QSpinBox* {
            auto* lbl = new QLabel(label);
            lbl->setStyleSheet(kLabelStyle);
            grid->addWidget(lbl, row, 0, 1, 2);

            auto* spin = new QSpinBox;
            spin->setRange(min, max);
            spin->setSingleStep(step);
            spin->setSuffix(" ms");
            spin->setValue(settings.value(key, QString::number(dflt)).toInt());
            spin->setStyleSheet(QString(kEditStyle).replace("QLineEdit", "QSpinBox"));
            grid->addWidget(spin, row, 2, 1, 2);

            connect(spin, QOverload<int>::of(&QSpinBox::valueChanged),
                    this, [this, key](int val) {
                auto& s = AppSettings::instance();
                s.setValue(key, QString::number(val));
                s.save();
                emit serialSettingsChanged();
            });
            return spin;
        };

        m_tmate2OverlayDurationSpin = addTimingSpin(
            3, "Overlay duration:", "TMate2OverlayDurationMs", 1500, 100, 10000, 100);
        m_tmate2UserInteractionTimeoutSpin = addTimingSpin(
            4, "User interaction timeout:", "TMate2UserInteractionTimeoutMs", 2000, 0, 60000, 100);

        for (int col = 2; col <= 6; col += 2)
            grid->setColumnStretch(col, 1);

        vbox->addWidget(group);
    }

#endif

    vbox->addStretch();
    return page;
}
#endif



// ─── Peripherals tab — manual IP connect for TGXL, PGXL, AG (#914) ───────────

QWidget* RadioSetupDialog::buildPeripheralsTab()
{
    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(8);

    auto* group = new QGroupBox("External Devices — Manual IP Connection");
    group->setStyleSheet(kGroupStyle);
    auto* grid = new QGridLayout(group);
    grid->setSpacing(6);

    // Column headers
    auto addHeader = [&](int col, const QString& text) {
        auto* lbl = new QLabel(text);
        AetherSDR::ThemeManager::instance().applyStyleSheet(lbl, "QLabel { color: {{color.text.secondary}}; font-size: 11px; font-weight: bold; }");
        grid->addWidget(lbl, 0, col);
    };
    addHeader(0, "Device");
    addHeader(1, "IP Address");
    addHeader(2, "Port");
    addHeader(3, "");
    addHeader(4, "Status");

    auto& settings = AppSettings::instance();

    static const QString kBtnStyle =
        "QPushButton { background: #1a2a3a; border: 1px solid #304050; "
        "border-radius: 3px; color: #c8d8e8; font-size: 11px; font-weight: bold; "
        "padding: 3px 10px; }"
        "QPushButton:hover { background: #203040; }";

    // Helper to build one peripheral row
    auto buildRow = [&](int row, const QString& label, const QString& ipKey,
                        const QString& portKey, int defaultPort,
                        auto connectFn, auto disconnectFn, auto isConnectedFn,
                        auto peerAddressFn, auto peerPortFn) {
        // Device label
        auto* devLbl = new QLabel(label);
        devLbl->setStyleSheet(kLabelStyle);
        grid->addWidget(devLbl, row, 0);

        // IP field — pre-fill from settings, or from live connection if discovered
        auto* ipEdit = new QLineEdit;
        ipEdit->setPlaceholderText("e.g. 192.168.1.100");
        ipEdit->setStyleSheet(kEditStyle);
        ipEdit->setMinimumWidth(140);
        QString savedIp = settings.value(ipKey, "").toString();
        if (!savedIp.isEmpty()) {
            ipEdit->setText(savedIp);
        } else if (isConnectedFn()) {
            ipEdit->setText(peerAddressFn());
        }
        grid->addWidget(ipEdit, row, 1);

        // Port field — pre-fill from settings, or from live connection
        auto* portSpin = new QSpinBox;
        portSpin->setRange(1, 65535);
        int savedPort = settings.value(portKey, "0").toInt();
        if (savedPort > 0) {
            portSpin->setValue(savedPort);
        } else if (isConnectedFn() && peerPortFn() > 0) {
            portSpin->setValue(peerPortFn());
        } else {
            portSpin->setValue(defaultPort);
        }
        AetherSDR::ThemeManager::instance().applyStyleSheet(portSpin, "QSpinBox { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
            "border-radius: 3px; color: {{color.text.primary}}; font-size: 12px; padding: 2px; }");
        grid->addWidget(portSpin, row, 2);

        // Status label
        auto* statusLbl = new QLabel(isConnectedFn() ? "Connected" : "Not connected");
        statusLbl->setStyleSheet(isConnectedFn()
            ? "QLabel { color: #00e060; font-size: 11px; }"
            : "QLabel { color: #8aa8c0; font-size: 11px; }");
        grid->addWidget(statusLbl, row, 4);

        // Connect/Disconnect button
        auto* btn = new QPushButton(isConnectedFn() ? "Disconnect" : "Connect");
        btn->setStyleSheet(kBtnStyle);
        grid->addWidget(btn, row, 3);

        connect(btn, &QPushButton::clicked, this,
                [=, &settings]() {
            QString ip = ipEdit->text().trimmed();
            if (isConnectedFn()) {
                // If the user cleared the IP field before clicking, wipe
                // the saved manual IP/port FIRST — the disconnect signal
                // fires synchronously and downstream handlers (e.g. SS
                // button visibility) read these settings to decide
                // whether to keep showing the device. Clearing after the
                // disconnect would leave the button visible.
                if (ip.isEmpty()) {
                    settings.remove(ipKey);
                    settings.remove(portKey);
                    settings.save();
                }
                disconnectFn();
            } else {
                if (ip.isEmpty()) {
                    // Empty IP while disconnected: if a manual IP was saved
                    // previously, treat this click as "save back to default"
                    // — clear the persisted manual IP/port so the device
                    // stops auto-connecting.
                    if (!settings.value(ipKey, "").toString().isEmpty()) {
                        settings.remove(ipKey);
                        settings.remove(portKey);
                        settings.save();
                    }
                    return;
                }
                int port = portSpin->value();
                // Save to settings
                settings.setValue(ipKey, ip);
                settings.setValue(portKey, QString::number(port));
                settings.save();
                connectFn(ip, static_cast<quint16>(port));
            }
        });

        // Update UI on connection state changes
        auto updateState = [btn, statusLbl, isConnectedFn]() {
            bool conn = isConnectedFn();
            btn->setText(conn ? "Disconnect" : "Connect");
            statusLbl->setText(conn ? "Connected" : "Not connected");
            statusLbl->setStyleSheet(conn
                ? "QLabel { color: #00e060; font-size: 11px; }"
                : "QLabel { color: #8aa8c0; font-size: 11px; }");
        };

        // Save-on-close: if the user has cleared the IP field and closes
        // the dialog without clicking Connect/Disconnect, treat that as
        // "wipe the saved manual IP/port". A non-empty edit still requires
        // an explicit Connect click so a partially-typed IP cannot leak in.
        m_peripheralRowSavers.append([ipEdit, ipKey, portKey, &settings,
                                      isConnectedFn, disconnectFn]() {
            if (!ipEdit) return;
            const QString ip = ipEdit->text().trimmed();
            if (!ip.isEmpty()) return;
            const QString savedIp = settings.value(ipKey, "").toString();
            if (savedIp.isEmpty()) return;
            // The user cleared a previously-saved IP. If still connected
            // (e.g. auto-connect ran at startup), disconnect first so
            // downstream visibility handlers see the cleared settings.
            settings.remove(ipKey);
            settings.remove(portKey);
            settings.save();
            if (isConnectedFn()) disconnectFn();
        });

        return updateState;
    };

    // Row 1: Tuner Genius XL (TGXL)
    if (m_tgxl) {
        auto updateTgxl = buildRow(1, "Tuner Genius XL (TGXL)", "TGXL_ManualIp", "TGXL_ManualPort", 9010,
            [this](const QString& ip, quint16 port) { m_tgxl->connectToTgxl(ip, port); },
            [this]() { m_tgxl->disconnect(); },
            [this]() { return m_tgxl->isConnected(); },
            [this]() { return m_tgxl->peerAddress(); },
            [this]() { return m_tgxl->peerPort(); });
        connect(m_tgxl, &TgxlConnection::connected, this, updateTgxl);
        connect(m_tgxl, &TgxlConnection::disconnected, this, updateTgxl);
        // Pre-fill radio-discovered TGXL IP when no saved IP and not connected (#1039)
        auto* tgxlIpEdit = qobject_cast<QLineEdit*>(grid->itemAtPosition(1, 1)->widget());
        if (tgxlIpEdit && tgxlIpEdit->text().isEmpty()) {
            QString discovered = m_model->tunerModel().tgxlIp();
            if (!discovered.isEmpty()) {
                tgxlIpEdit->setText(discovered);
            }
        }
        // Show TCP error reason in status column (#1039)
        auto* tgxlStatus = qobject_cast<QLabel*>(grid->itemAtPosition(1, 4)->widget());
        if (tgxlStatus) {
            connect(m_tgxl, &TgxlConnection::connectionFailed, this,
                    [tgxlStatus](const QString& err) {
                tgxlStatus->setText("Error: " + err);
                tgxlStatus->setStyleSheet("QLabel { color: #e06060; font-size: 11px; }");
            });
        }
    }

    // Row 2: Power Genius XL (PGXL)
    if (m_pgxl) {
        auto updatePgxl = buildRow(2, "Power Genius XL (PGXL)", "PGXL_ManualIp", "PGXL_ManualPort", 9008,
            [this](const QString& ip, quint16 port) { m_pgxl->connectToPgxl(ip, port); },
            [this]() { m_pgxl->disconnect(); },
            [this]() { return m_pgxl->isConnected(); },
            [this]() { return m_pgxl->peerAddress(); },
            [this]() { return m_pgxl->peerPort(); });
        connect(m_pgxl, &PgxlConnection::connected, this, updatePgxl);
        connect(m_pgxl, &PgxlConnection::disconnected, this, updatePgxl);
    }

    // Row 3: Antenna Genius (AG) — hide "Connected" when ShackSwitch is using the model
    if (m_ag) {
        auto isRealAg = [this]() {
            if (!m_ag->isConnected()) return false;
            return !AntennaGeniusModel::isShackSwitch(m_ag->connectedDevice());
        };
        auto updateAg = buildRow(3, "Antenna Genius (AG)", "AG_ManualIp", "AG_ManualPort", 9007,
            [this](const QString& ip, quint16 port) {
                m_ag->connectToAddress(QHostAddress(ip), port);
            },
            [this]() { m_ag->disconnectFromDevice(); },
            isRealAg,
            [this]() { return m_ag->peerAddress(); },
            [this]() { return m_ag->peerPort(); });
        connect(m_ag, &AntennaGeniusModel::connected,    this, updateAg);
        connect(m_ag, &AntennaGeniusModel::disconnected, this, updateAg);
    }

    // Row 4: ShackSwitch — Connect/Disconnect + status (same pattern as AG)
    //         plus a small "⚙ Web UI" button that opens the device's web interface.
    if (m_ag) {
        auto isSsConnected = [this]() {
            return m_ag->isConnected() &&
                   AntennaGeniusModel::isShackSwitch(m_ag->connectedDevice());
        };
        auto updateSs = buildRow(4, "ShackSwitch", "SS_ManualIp", "SS_ControlPort", 9007,
            [this](const QString& ip, quint16 /*port*/) {
                // Always connect on port 9007 (AG control protocol)
                AgDeviceInfo info;
                info.ip     = QHostAddress(ip);
                info.port   = 9007;
                info.serial = QStringLiteral("ShackSwitch-manual");
                info.name   = QStringLiteral("ShackSwitch");
                m_ag->connectToDevice(info);
            },
            [this]() { m_ag->disconnectFromDevice(); },
            isSsConnected,
            [this]() { return m_ag->peerAddress(); },
            [this]() { return (quint16)9007; });
        connect(m_ag, &AntennaGeniusModel::connected,    this, updateSs);
        connect(m_ag, &AntennaGeniusModel::disconnected, this, updateSs);

        // "⚙ Web UI" button — opens ShackSwitch web interface in a compact app window
        auto* webBtn = new QPushButton("⚙ Web UI");
        webBtn->setStyleSheet(kBtnStyle);
        webBtn->setToolTip("Open ShackSwitch web interface");
        grid->addWidget(webBtn, 4, 5);
        connect(webBtn, &QPushButton::clicked, this, [this]() {
            auto& s = AppSettings::instance();
            QString ip = s.value("SS_ManualIp", "").toString();
            // Only use live address if the connected device is actually the ShackSwitch
            if (ip.isEmpty() && m_ag->isConnected()) {
                if (AntennaGeniusModel::isShackSwitch(m_ag->connectedDevice()))
                    ip = m_ag->peerAddress();
            }
            if (ip.isEmpty()) return;
            // Use beacon webPort only when advertising a valid port (>1024).
            int port = 0;
            if (m_ag->isConnected()) {
                const auto& dev = m_ag->connectedDevice();
                if (AntennaGeniusModel::isShackSwitch(dev) && dev.webPort > 1024)
                    port = dev.webPort;
            }
            if (port <= 1024)
                port = s.value("SS_WebPort", "5000").toInt();
            if (port <= 1024) port = 5000;
            QDesktopServices::openUrl(QUrl("http://" + ip + ":" + QString::number(port) + "/"));
        });
    }

    for (auto* lbl : group->findChildren<QLabel*>())
        if (lbl->styleSheet().isEmpty()) lbl->setStyleSheet(kLabelStyle);

    vbox->addWidget(group);

    // Auto-reconnect checkbox
    auto* reconnectCheck = new QCheckBox("Auto-reconnect to peripherals on connection drop");
    AetherSDR::ThemeManager::instance().applyStyleSheet(reconnectCheck,
        "QCheckBox { color: {{color.text.primary}}; font-size: 11px; }");
    const bool autoReconnect = PeripheralSettings::autoReconnect();
    reconnectCheck->setChecked(autoReconnect);
    connect(reconnectCheck, &QCheckBox::toggled, this, [this](bool on) {
        PeripheralSettings::setAutoReconnect(on);
        // Propagate immediately to live connection objects
        if (m_tgxl) {
            m_tgxl->setAutoReconnect(on);
        }
        if (m_pgxl) {
            m_pgxl->setAutoReconnect(on);
        }
        if (m_ag) {
            m_ag->setAutoReconnect(on);
        }
    });
    vbox->addWidget(reconnectCheck);

    // Info note
    auto* note = new QLabel(
        "Configure manual IP addresses for peripherals that cannot be discovered via UDP broadcast.\n"
        "This is needed for remote, VPN, and SmartLink connections. "
        "Configured devices auto-connect when the radio connects.");
    note->setWordWrap(true);
    AetherSDR::ThemeManager::instance().applyStyleSheet(note, "QLabel { color: {{color.text.label}}; font-size: 11px; padding: 8px; }");
    vbox->addWidget(note);

    vbox->addStretch();
    return page;
}

void RadioSetupDialog::buildDeferredTab(int index)
{
    auto it = m_deferredBuilders.find(index);
    if (it == m_deferredBuilders.end())
        return;                             // already built or out of range

    QWidget* placeholder = m_tabs->widget(index);
    QWidget* content = it.value()();        // run the real builder
    auto* lay = new QVBoxLayout(placeholder);
    lay->setContentsMargins(0, 0, 0, 0);
    // Wrap in a scroll area so tall tabs (Themes, Audio, Filters,
    // Peripherals on small / high-DPI displays) become scrollable instead
    // of forcing the dialog past the screen edge (#3345).
    lay->addWidget(wrapTabInScrollArea(content));
    m_deferredBuilders.erase(it);           // build only once
}

void RadioSetupDialog::selectTab(const QString& tabName)
{
    if (!m_tabs) return;
    for (int i = 0; i < m_tabs->count(); ++i) {
        if (m_tabs->tabText(i) == tabName) {
            m_tabs->setCurrentIndex(i);
            return;
        }
    }
}

void RadioSetupDialog::refreshFlexControlButtonActions()
{
    auto& settings = AppSettings::instance();
    for (auto it = m_flexControlActionCombos.begin();
         it != m_flexControlActionCombos.end(); ++it) {
        auto* combo = it.value();
        if (!combo)
            continue;
        const QString fallback = m_flexControlActionDefaults.value(it.key(), QStringLiteral("None"));
        const QString saved = settings.value(it.key(), fallback).toString();
        const int idx = combo->findText(saved);
        if (idx < 0)
            continue;
        const QSignalBlocker blocker(combo);
        combo->setCurrentIndex(idx);
    }
    if (m_flexControlInvertCheck) {
        const bool inverted =
            settings.value("FlexControlInvertDir", "False").toString() == "True";
        const QSignalBlocker blocker(m_flexControlInvertCheck);
        m_flexControlInvertCheck->setChecked(inverted);
    }
}

void RadioSetupDialog::setFlexControlConnectionStatus(bool connected, const QString& port)
{
    if (m_flexControlStatusLabel) {
        if (connected) {
            const QString displayPort = port.isEmpty()
                ? AppSettings::instance().value("FlexControlPort").toString()
                : port;
            m_flexControlStatusLabel->setText(QString("Connected (%1)").arg(displayPort));
            AetherSDR::ThemeManager::instance().applyStyleSheet(m_flexControlStatusLabel, "QLabel { color: {{color.accent.success}}; font-size: 11px; }");
        } else {
            m_flexControlStatusLabel->setText("Not detected");
            AetherSDR::ThemeManager::instance().applyStyleSheet(m_flexControlStatusLabel, "QLabel { color: {{color.text.label}}; font-size: 11px; }");
        }
    }
    if (m_flexControlCloseButton)
        m_flexControlCloseButton->setEnabled(connected);
    if (m_flexControlDetectButton)
        m_flexControlDetectButton->setEnabled(!connected);
}

// ── UI Enhancements tab ───────────────────────────────────────────────────────

QWidget* RadioSetupDialog::buildUiEnhancementsTab()
{
    static const QString kBtnBase =
        "QPushButton { border: 1px solid #304050; border-radius: 3px; "
        "font-weight: bold; font-size: 13px; min-width: 36px; min-height: 36px; }"
        "QPushButton:hover { border: 2px solid #60a0c0; }";

    auto* page = new QWidget;
    auto* vbox = new QVBoxLayout(page);
    vbox->setSpacing(12);
    vbox->setContentsMargins(16, 16, 16, 16);

    // ── Slice letter display ─────────────────────────────────────────────────
    // Two display modes for slice letters in the GUI (#2606):
    //   "Global"       (default) — letters track the radio's global slice
    //                  index ('A' = slot 0, 'B' = slot 1, ...) so Multi-Flex
    //                  operators can see at a glance which global slots are
    //                  in use.
    //   "RadioIndexed" — use the radio-provided per-client letter (matches
    //                  SmartSDR behaviour) with the global slot id rendered
    //                  as a subscript so slot awareness survives.
    //
    // Pure display change — slice IDs in commands, settings keys, and
    // signal routing remain global throughout.
    {
        auto* letterGrp = new QGroupBox("Slice Letter Display");
        letterGrp->setStyleSheet(kGroupStyle);
        auto* letterLayout = new QVBoxLayout(letterGrp);
        letterLayout->setSpacing(8);

        auto* radioRow = new QHBoxLayout;
        auto* globalRadio = new QRadioButton("Global slot index (A=0, B=1, …)");
        auto* radioIdxRadio =
            new QRadioButton("Radio-assigned letter with global subscript (A₂)");
        AetherSDR::ThemeManager::instance().applyStyleSheet(globalRadio, "QRadioButton { color: {{color.text.primary}}; font-size: 12px; }");
        AetherSDR::ThemeManager::instance().applyStyleSheet(radioIdxRadio, "QRadioButton { color: {{color.text.primary}}; font-size: 12px; }");
        radioRow->addWidget(globalRadio);
        radioRow->addWidget(radioIdxRadio);
        radioRow->addStretch();
        letterLayout->addLayout(radioRow);

        auto* letterDesc = new QLabel(
            "Choose how slice letters are rendered in badges, faders, and "
            "applet labels. Multi-Flex sessions with multiple clients only: "
            "Radio-assigned letters match SmartSDR's behaviour; the global "
            "subscript preserves which physical slot you're on.");
        letterDesc->setStyleSheet("QLabel { color: #7090a0; font-size: 11px; }");
        letterDesc->setWordWrap(true);
        letterLayout->addWidget(letterDesc);

        auto& s = AppSettings::instance();
        const QString current =
            s.value("SliceLetterDisplay", "Global").toString();
        (current == "RadioIndexed" ? radioIdxRadio : globalRadio)->setChecked(true);

        auto saveLetterMode = [this](const QString& mode) {
            auto& s = AppSettings::instance();
            s.setValue("SliceLetterDisplay", mode);
            s.save();
            // Push a refresh through anything that paints a slice letter
            // — the active slice path's syncFromSlice() pulls.  Cheapest
            // broad-stroke: re-emit currentSliceChanged so the model
            // listeners reapply via their existing slots.
            emit sliceLetterDisplayModeChanged();
        };
        connect(globalRadio, &QRadioButton::toggled, this, [saveLetterMode](bool on) {
            if (on) saveLetterMode("Global");
        });
        connect(radioIdxRadio, &QRadioButton::toggled, this, [saveLetterMode](bool on) {
            if (on) saveLetterMode("RadioIndexed");
        });

        vbox->addWidget(letterGrp);
    }

    // ── Slice color group ────────────────────────────────────────────────────
    auto* grp = new QGroupBox("Slice Colors");
    grp->setStyleSheet(kGroupStyle);
    auto* grpLayout = new QVBoxLayout(grp);
    grpLayout->setSpacing(10);

    auto* modeLayout = new QHBoxLayout;
    auto* defaultsRadio = new QRadioButton("Use Aether defaults");
    auto* customRadio   = new QRadioButton("Custom colors");
    AetherSDR::ThemeManager::instance().applyStyleSheet(defaultsRadio, "QRadioButton { color: {{color.text.primary}}; font-size: 12px; }");
    AetherSDR::ThemeManager::instance().applyStyleSheet(customRadio, "QRadioButton { color: {{color.text.primary}}; font-size: 12px; }");
    modeLayout->addWidget(defaultsRadio);
    modeLayout->addWidget(customRadio);
    modeLayout->addStretch();
    grpLayout->addLayout(modeLayout);

    auto* desc = new QLabel(
        "Customize the color used for each slice marker, filter band, and badge.");
    desc->setStyleSheet("QLabel { color: #7090a0; font-size: 11px; }");
    desc->setWordWrap(true);
    grpLayout->addWidget(desc);

    // Grid of 8 color buttons (A–H)
    auto* colorGrid = new QHBoxLayout;
    colorGrid->setSpacing(8);

    static const char kLetters[] = "ABCDEFGH";
    // One button per slice; holds the current color as its background.
    QVector<QPushButton*> colorBtns;
    for (int i = 0; i < AetherSDR::kSliceColorCount; ++i) {
        auto* col = new QVBoxLayout;
        col->setSpacing(4);

        auto* lbl = new QLabel(QString(kLetters[i]));
        lbl->setAlignment(Qt::AlignCenter);
        AetherSDR::ThemeManager::instance().applyStyleSheet(lbl, "QLabel { color: {{color.text.secondary}}; font-size: 11px; }");

        auto* btn = new QPushButton(QString(kLetters[i]));
        btn->setStyleSheet(kBtnBase);
        colorBtns.append(btn);

        col->addWidget(lbl);
        col->addWidget(btn);
        colorGrid->addLayout(col);
    }
    colorGrid->addStretch();
    grpLayout->addLayout(colorGrid);

    // Reset-all button
    auto* resetRow = new QHBoxLayout;
    auto* resetBtn = new QPushButton("Reset All to Defaults");
    AetherSDR::ThemeManager::instance().applyStyleSheet(resetBtn, "QPushButton { background: {{color.background.1}}; border: 1px solid {{color.background.2}}; "
        "border-radius: 3px; color: {{color.text.primary}}; font-size: 11px; padding: 3px 12px; }"
        "QPushButton:hover { background: {{color.background.1}}; }");
    resetRow->addWidget(resetBtn);
    resetRow->addStretch();
    grpLayout->addLayout(resetRow);

    vbox->addWidget(grp);
    vbox->addStretch();

    // ── Helpers (capture manager by pointer, buttons by value) ────────────────
    SliceColorManager* pMgr = &SliceColorManager::instance();

    // Updates a single button's background to reflect current color.
    auto applyBtnColor = [pMgr, colorBtns](int idx) mutable {
        QColor c = pMgr->activeColor(idx);
        bool light = (c.red() * 299 + c.green() * 587 + c.blue() * 114) > 128000;
        QString textColor = light ? "#000000" : "#ffffff";
        colorBtns[idx]->setStyleSheet(
            kBtnBase +
            QStringLiteral("QPushButton { background: %1; color: %2; }")
                .arg(c.name(), textColor));
    };

    auto refreshAllBtns = [applyBtnColor]() mutable {
        for (int i = 0; i < AetherSDR::kSliceColorCount; ++i)
            applyBtnColor(i);
    };

    auto syncModeRadios = [defaultsRadio, customRadio, colorBtns](bool custom) mutable {
        defaultsRadio->blockSignals(true);
        customRadio->blockSignals(true);
        defaultsRadio->setChecked(!custom);
        customRadio->setChecked(custom);
        defaultsRadio->blockSignals(false);
        customRadio->blockSignals(false);
        for (QPushButton* b : colorBtns)
            b->setEnabled(custom);
    };

    // ── Initial state ─────────────────────────────────────────────────────────
    syncModeRadios(pMgr->useCustomColors());
    refreshAllBtns();

    // ── Signals ───────────────────────────────────────────────────────────────
    connect(defaultsRadio, &QRadioButton::toggled, page,
            [pMgr, syncModeRadios, refreshAllBtns](bool checked) mutable {
        if (!checked) return;
        pMgr->setUseCustomColors(false);
        syncModeRadios(false);
        refreshAllBtns();
    });

    connect(customRadio, &QRadioButton::toggled, page,
            [pMgr, syncModeRadios, refreshAllBtns](bool checked) mutable {
        if (!checked) return;
        pMgr->setUseCustomColors(true);
        syncModeRadios(true);
        refreshAllBtns();
    });

    for (int i = 0; i < AetherSDR::kSliceColorCount; ++i) {
        connect(colorBtns[i], &QPushButton::clicked, page,
                [i, pMgr, applyBtnColor, page]() mutable {
            QColor initial = pMgr->customColor(i);
            QColor chosen = QColorDialog::getColor(initial, page,
                                                   QStringLiteral("Slice %1 Color")
                                                       .arg(QChar('A' + i)));
            if (!chosen.isValid()) return;
            pMgr->setCustomColor(i, chosen);
            applyBtnColor(i);
        });
    }

    connect(resetBtn, &QPushButton::clicked, page,
            [pMgr, refreshAllBtns]() mutable {
        for (int i = 0; i < AetherSDR::kSliceColorCount; ++i)
            pMgr->resetToDefault(i);
        refreshAllBtns();
    });

    // Sync button backgrounds when another widget changes a color
    connect(pMgr, &SliceColorManager::colorsChanged, page,
            [syncModeRadios, refreshAllBtns, pMgr]() mutable {
        syncModeRadios(pMgr->useCustomColors());
        refreshAllBtns();
    });

    // ── Single-click delay group (#3009) ────────────────────────────────────
    // Several widgets (slice-mute button, RX chain stages, waveform widgets)
    // defer the single-click action by this interval so a double click can
    // override it.  Default is the platform's QApplication::doubleClickInterval
    // (typically 400 ms on Linux, 500 ms on Windows).  Power users who never
    // double-click can set this to 0 for instant single-click response;
    // double-click affordances become unreachable in that case but the
    // mute-all keyboard shortcut still works for the most common use.
    {
        auto* clickGrp = new QGroupBox("Single-click delay");
        clickGrp->setStyleSheet(kGroupStyle);
        auto* clickLayout = new QVBoxLayout(clickGrp);
        clickLayout->setSpacing(8);

        auto* row = new QHBoxLayout;
        row->setSpacing(8);

        auto* clickLabel = new QLabel("Delay (ms):");
        AetherSDR::ThemeManager::instance().applyStyleSheet(clickLabel, "QLabel { color: {{color.text.primary}}; font-size: 12px; }");
        row->addWidget(clickLabel);

        auto& s = AppSettings::instance();
        const int platformDefault = QApplication::doubleClickInterval();
        const int current = s.value("ClickDiscriminationIntervalMs",
                                     platformDefault).toInt();

        auto* clickSpin = new QSpinBox;
        clickSpin->setRange(0, 1000);
        clickSpin->setSingleStep(50);
        clickSpin->setSuffix(" ms");
        clickSpin->setValue(qBound(0, current, 1000));
        clickSpin->setFixedWidth(110);
        AetherSDR::ThemeManager::instance().applyStyleSheet(clickSpin, "QSpinBox { background: {{color.background.1}}; color: {{color.text.primary}}; "
            "border: 1px solid {{color.background.2}}; border-radius: 3px; padding: 2px 4px; }");
        row->addWidget(clickSpin);

        auto* resetBtn = new QPushButton("Reset");
        AetherSDR::ThemeManager::instance().applyStyleSheet(resetBtn, "QPushButton { background: {{color.background.1}}; color: {{color.text.primary}}; "
            "border: 1px solid {{color.background.2}}; border-radius: 3px; padding: 4px 12px; }"
            "QPushButton:hover { border-color: #60a0c0; }");
        resetBtn->setToolTip(QString("Reset to platform default (%1 ms)")
                             .arg(platformDefault));
        row->addWidget(resetBtn);

        row->addStretch();
        clickLayout->addLayout(row);

        auto* clickDesc = new QLabel(QString(
            "Time AetherSDR waits after a single click on a widget that "
            "also has a double-click action (e.g. the slice mute button) "
            "before firing the single-click. The platform default is "
            "%1 ms. Set to 0 to fire single-click actions instantly — "
            "this also disables the double-click affordances on those "
            "widgets.").arg(platformDefault));
        clickDesc->setStyleSheet("QLabel { color: #7090a0; font-size: 11px; }");
        clickDesc->setWordWrap(true);
        clickLayout->addWidget(clickDesc);

        connect(clickSpin, QOverload<int>::of(&QSpinBox::valueChanged),
                this, [](int v) {
            auto& s = AppSettings::instance();
            s.setValue("ClickDiscriminationIntervalMs", QString::number(v));
            s.save();
        });
        connect(resetBtn, &QPushButton::clicked, this,
                [clickSpin, platformDefault]() {
            clickSpin->setValue(platformDefault);
            // valueChanged handler above persists the value.
        });

        vbox->addWidget(clickGrp);
    }

    // ── Mouse wheel group (#3302) ───────────────────────────────────────────
    // Trackball / inverted-scroll users (and parity with Thetis / KE9NS) get
    // a single checkbox that reverses the wheel direction for frequency
    // tuning.  Reading is per-event in the wheel handlers, so this takes
    // effect immediately with no signal plumbing.  The Ctrl+wheel bandwidth
    // zoom on the panadapter is intentionally not reversed.
    {
        auto* wheelGrp = new QGroupBox("Mouse wheel");
        wheelGrp->setStyleSheet(kGroupStyle);
        auto* wheelLayout = new QVBoxLayout(wheelGrp);
        wheelLayout->setSpacing(8);

        auto* reverseChk = new QCheckBox("Reverse mouse-wheel tuning direction");
        AetherSDR::ThemeManager::instance().applyStyleSheet(reverseChk,
            "QCheckBox { color: {{color.text.primary}}; font-size: 12px; }");
        {
            auto& s = AppSettings::instance();
            reverseChk->setChecked(s.value("ReverseMouseWheel", false).toBool());
        }
        wheelLayout->addWidget(reverseChk);

        auto* wheelDesc = new QLabel(
            "When enabled, scrolling the wheel up tunes the frequency down "
            "(and vice versa). Useful for trackballs and any pointer where "
            "the natural scroll direction feels inverted. Affects the VFO "
            "frequency display and the panadapter / waterfall; the "
            "Ctrl+wheel bandwidth zoom is not reversed.");
        wheelDesc->setStyleSheet("QLabel { color: #7090a0; font-size: 11px; }");
        wheelDesc->setWordWrap(true);
        wheelLayout->addWidget(wheelDesc);

        connect(reverseChk, &QCheckBox::toggled, this, [](bool on) {
            auto& s = AppSettings::instance();
            s.setValue("ReverseMouseWheel", on);
            s.save();
        });

        vbox->addWidget(wheelGrp);
    }

    return page;
}

QWidget* RadioSetupDialog::buildSmartLinkTab()
{
    // Phase 2 of GHSA-wfx7-w6p8-4jr2 (#2951): Pinned Certificates panel.
    // Lists every SmartLink host this client has TOFU-pinned a cert
    // fingerprint for, when it was pinned, and lets the operator
    // forget individual pins or clear the whole cache. Used when the
    // operator deliberately rotates a radio's cert (firmware update,
    // hardware replacement) and wants the next connect to re-pin
    // silently instead of triggering the mismatch dialog.

    auto* page = new QWidget;
    auto* root = new QVBoxLayout(page);
    root->setContentsMargins(16, 16, 16, 16);
    root->setSpacing(12);

    auto* grp = new QGroupBox("Pinned SmartLink Certificates");
    grp->setStyleSheet(kGroupStyle);
    auto* grpLay = new QVBoxLayout(grp);
    grpLay->setSpacing(8);

    auto* desc = new QLabel(
        "AetherSDR pins each SmartLink radio's TLS certificate the first "
        "time you connect (trust-on-first-use). Subsequent connects "
        "compare against the pin; a mismatch shows a warning dialog and "
        "pauses the connection until you accept or reject it.\n\n"
        "Forget a pin when you deliberately change a radio's certificate "
        "(firmware update, hardware replacement) so the next connect can "
        "re-pin silently. See GHSA-wfx7-w6p8-4jr2 for the threat model.");
    desc->setStyleSheet("QLabel { color: #7090a0; font-size: 11px; }");
    desc->setWordWrap(true);
    grpLay->addWidget(desc);

    auto* table = new QTableWidget(0, 3, grp);
    m_pinnedCertsTable = table;
    table->setHorizontalHeaderLabels({"Host", "SHA-256 fingerprint", "Pinned"});
    table->horizontalHeader()->setStretchLastSection(false);
    table->horizontalHeader()->setSectionResizeMode(
        0, QHeaderView::ResizeToContents);
    table->horizontalHeader()->setSectionResizeMode(
        1, QHeaderView::Stretch);
    table->horizontalHeader()->setSectionResizeMode(
        2, QHeaderView::ResizeToContents);
    table->verticalHeader()->setVisible(false);
    table->setEditTriggers(QAbstractItemView::NoEditTriggers);
    table->setSelectionBehavior(QAbstractItemView::SelectRows);
    AetherSDR::ThemeManager::instance().applyStyleSheet(table, "QTableWidget { background: {{color.background.0}}; color: {{color.text.primary}};"
        " gridline-color: {{color.background.1}}; }"
        "QHeaderView::section { background: {{color.background.1}}; color: #b0c4d6;"
        " padding: 4px; border: none; }");
    grpLay->addWidget(table);

    auto* btnRow = new QHBoxLayout;
    btnRow->setSpacing(8);
    auto* forgetSel = new QPushButton("Forget selected");
    auto* forgetAll = new QPushButton("Forget all");
    static const QString kPinnedBtnStyle =
        "QPushButton { background: #1a2230; border: 1px solid #2a3744;"
        " border-radius: 3px; color: #c8d8e8; font-size: 11px;"
        " padding: 4px 12px; }"
        "QPushButton:hover { background: #243044; color: #e8e8e8; }"
        "QPushButton:pressed { background: #2a3a52; }";
    forgetSel->setStyleSheet(kPinnedBtnStyle);
    forgetAll->setStyleSheet(kPinnedBtnStyle);
    btnRow->addWidget(forgetSel);
    btnRow->addStretch();
    btnRow->addWidget(forgetAll);
    grpLay->addLayout(btnRow);

    connect(forgetSel, &QPushButton::clicked, this, [this, table]() {
        const int row = table->currentRow();
        if (row < 0) return;
        auto* item = table->item(row, 0);
        if (!item) return;
        const QString host = item->text();
        WanCertCache::forgetPinnedCert(host);
        refreshPinnedCertsTable();
    });

    connect(forgetAll, &QPushButton::clicked, this, [this]() {
        if (QMessageBox::question(this, tr("Forget all SmartLink certificates"),
                tr("Clear every pinned SmartLink cert fingerprint?\n\n"
                   "Next connect to each radio will silently re-pin "
                   "whatever certificate it presents (no mismatch warning)."))
            != QMessageBox::Yes) {
            return;
        }
        WanCertCache::forgetAllPinnedCerts();
        refreshPinnedCertsTable();
    });

    root->addWidget(grp);
    root->addStretch();

    refreshPinnedCertsTable();
    return page;
}

void RadioSetupDialog::refreshPinnedCertsTable()
{
    if (!m_pinnedCertsTable) return;
    const QVector<PinnedCertInfo> pins = WanCertCache::listPinnedCerts();
    m_pinnedCertsTable->setRowCount(pins.size());
    for (int i = 0; i < pins.size(); ++i) {
        const PinnedCertInfo& pin = pins.at(i);
        m_pinnedCertsTable->setItem(i, 0, new QTableWidgetItem(pin.host));
        // Use a monospace cell for the fingerprint so the hex aligns.
        auto* fpItem = new QTableWidgetItem(pin.fingerprintHex);
        QFont mono("monospace");
        fpItem->setFont(mono);
        fpItem->setToolTip(pin.fingerprintHex);
        m_pinnedCertsTable->setItem(i, 1, fpItem);
        const QString when = pin.pinnedAtIso.isEmpty()
            ? QStringLiteral("(pre-phase 2)")
            : pin.pinnedAtIso.left(10);   // YYYY-MM-DD
        m_pinnedCertsTable->setItem(i, 2, new QTableWidgetItem(when));
    }
}

} // namespace AetherSDR
