import { useEffect, useMemo, useRef, useState } from 'react';
import {
  ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
  ChevronDown, ChevronRight,
  User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
  Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check, Pencil,
} from 'lucide-react';
import {
  GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
  GetListsSettings, SaveListsSettings,
  GetCATSettings, SaveCATSettings, DiscoverFlexRadios,
  ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
  GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
  GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
  GetAntGeniusSettings, SaveAntGeniusSettings,
  GetTunerGeniusSettings, SaveTunerGeniusSettings,
  GetAmplifiers, SaveAmplifiers, GetAmpStatuses, AmpOperate,
  GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
  GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
  GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
  GetClublogMostWantedInfo, SetClublogMostWantedEnabled, DownloadClublogMostWanted,
  GetSecretStatus, SetPassphrase, RemovePassphrase,
  GetEmailSettings, SaveEmailSettings, TestEmail,
  QSLGetEmailTemplates, QSLSaveEmailTemplates,
  GetDVKMessages, SetDVKLabel, DVKStartRecord, DVKStopRecord, DVKPreview, DVKStop, GetDVKStatus,
  AudioStartMonitor, AudioStopMonitor, AudioMonitorActive,
  AudioStartTX, AudioStopTX, AudioTXActive,
  ListClusterServers, SaveClusterServer, DeleteClusterServer,
  GetClusterAutoConnect, SetClusterAutoConnect,
  ConnectClusterServer, DisconnectClusterServer,
  ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
  GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
  GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase, RenameDatabase, RevealDataFolder, RenameLogbook,
  GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
  GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
  GetTelemetryEnabled, SetTelemetryEnabled,
  GetQSLDefaults, SaveQSLDefaults,
  SetWinkeyerTrace,
  SetCIVTrace,
  CIVTraceEnabled,
  WinkeyerTraceEnabled,
  GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload, TestCloudlogUpload,
  GetPOTAToken, SavePOTAToken,
  TestLoTWUpload, ListTQSLStationLocations,
  DownloadLoTWUsers, GetLoTWUsersStatus,
  GetScpStatus, SetScpEnabled, DownloadScp,
  DownloadULSCounties, ULSStatus, BackfillUSCounties,
  ComputeStationInfo,
  GetUIPref, SetUIPref,
  GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
  GetADIFMonitor, SaveADIFMonitor, PickADIFMonitorFile,
  GetRelayAuto, SaveRelayAuto, GetStationDevices,
} from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
import type { main as mainModels, cluster as clusterModels } from '../../wailsjs/go/models';
import { EventsOn } from '../../wailsjs/runtime/runtime';

import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { writeUiPref } from '@/lib/uiPref';
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme';
import { OperatingPanel } from '@/components/OperatingPanel';
import { UDPIntegrationsPanel } from '@/components/UDPIntegrationsPanel';

type LookupSettings = LookupSettingsForm;
type StationSettings = StationSettingsForm;
type ListsSettings = ListsSettingsForm;
type ModePreset = ModePresetForm;
type CATSettings = Omit<mainModels.CATSettings, 'convertValues'>;
type RotatorSettings = Omit<mainModels.RotatorSettings, 'convertValues'>;
type ClusterServer = Omit<clusterModels.ServerConfig, 'convertValues'>;
type ClusterServerStatus = Omit<clusterModels.ServerStatus, 'convertValues'>;
type Profile = Omit<profileModels.Profile, 'convertValues'>;

// Catalog of all standard ADIF bands, in natural frequency order. The user
// picks a subset on the right; everything else in the UI (entry strip,
// band-slot grid, band-map switcher) iterates that subset.
const BAND_CATALOG = [
  '2190m','630m','560m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m',
  '8m','6m','5m','4m','2m','1.25m','70cm','33cm','23cm','13cm','9cm','6cm','3cm','1.25cm',
  '6mm','4mm','2.5mm','2mm','1mm',
];

// Catalog of common ADIF modes with sensible RST defaults. When the user
// picks one on the right, the RSTs are pre-filled but stay editable.
const MODE_CATALOG: Array<{ name: string; sent: string; rcvd: string }> = [
  { name: 'SSB',          sent: '59',  rcvd: '59'  },
  { name: 'CW',           sent: '599', rcvd: '599' },
  { name: 'AM',           sent: '59',  rcvd: '59'  },
  { name: 'FM',           sent: '59',  rcvd: '59'  },
  { name: 'DIGITALVOICE', sent: '59',  rcvd: '59'  },
  { name: 'FT8',          sent: '-10', rcvd: '-10' },
  { name: 'FT4',          sent: '-10', rcvd: '-10' },
  { name: 'JS8',          sent: '-10', rcvd: '-10' },
  { name: 'MSK144',       sent: '+00', rcvd: '+00' },
  { name: 'JT65',         sent: '-15', rcvd: '-15' },
  { name: 'JT9',          sent: '-15', rcvd: '-15' },
  { name: 'Q65',          sent: '-15', rcvd: '-15' },
  { name: 'FST4',         sent: '-15', rcvd: '-15' },
  { name: 'FST4W',        sent: '-15', rcvd: '-15' },
  { name: 'WSPR',         sent: '-20', rcvd: '-20' },
  { name: 'RTTY',         sent: '599', rcvd: '599' },
  { name: 'PSK31',        sent: '599', rcvd: '599' },
  { name: 'PSK63',        sent: '599', rcvd: '599' },
  { name: 'PSK125',       sent: '599', rcvd: '599' },
  { name: 'OLIVIA',       sent: '599', rcvd: '599' },
  { name: 'CONTESTI',     sent: '599', rcvd: '599' },
  { name: 'MFSK',         sent: '599', rcvd: '599' },
  { name: 'THROB',        sent: '599', rcvd: '599' },
  { name: 'HELL',         sent: '599', rcvd: '599' },
  { name: 'PACKET',       sent: '599', rcvd: '599' },
  { name: 'PACTOR',       sent: '599', rcvd: '599' },
  { name: 'VARA',         sent: '599', rcvd: '599' },
  { name: 'VARA HF',      sent: '599', rcvd: '599' },
  { name: 'ARDOP',        sent: '599', rcvd: '599' },
  { name: 'ATV',          sent: '59',  rcvd: '59'  },
  { name: 'SSTV',         sent: '59',  rcvd: '59'  },
  { name: 'C4FM',         sent: '59',  rcvd: '59'  },
  { name: 'DSTAR',        sent: '59',  rcvd: '59'  },
  { name: 'DMR',          sent: '59',  rcvd: '59'  },
  { name: 'FUSION',       sent: '59',  rcvd: '59'  },
];

const emptyProfile = (): Profile => ({
  id: 0,
  name: '',
  callsign: '', operator: '', op_name: '', owner_callsign: '',
  my_grid: '', my_country: '',
  my_state: '', my_cnty: '',
  my_street: '', my_city: '', my_postal_code: '',
  my_sota_ref: '', my_pota_ref: '',
  my_rig: '', my_antenna: '',
  tx_pwr: undefined,
  is_active: false,
  sort_order: 0,
  db: { backend: '', host: '', port: 3306, user: '', password: '', database: '' },
  // Server-managed timestamps — send null (NOT "") so Go's time.Time unmarshal
  // leaves the zero value instead of failing to parse an empty RFC3339 string.
  created_at: null as any,
  updated_at: null as any,
});

interface Props {
  initialSection?: string;
  onClose: () => void;
  onSaved: () => void;
  onMainPaneChanged?: (side: 'left' | 'right', value: string) => void; // live Main-view layout update
  flexAvailable?: boolean; // CAT backend is FlexRadio → offer it as a Main pane
  icomAvailable?: boolean; // CAT backend is Icom → offer the Icom console as a Main pane
  yaesuAvailable?: boolean; // CAT backend is Yaesu → offer the Yaesu console as a Main pane
}

// Pretty little card showing what OpsLog will stamp on each QSO based on
// the callsign + grid in the Station Information form. Debounces the
// backend resolver so we don't fire on every keystroke; refreshes when
// inputs change. Empty card when no callsign yet.
/* ====== Tree definition ======
   Section IDs are stable strings — adding new ones means adding a panel below.
   `disabled: true` greys them out and shows the "coming soon" placeholder. */
type SectionId =
  | 'general'
  | 'email'
  | 'station'
  | 'profiles'
  | 'operating'
  | 'confirmations'
  | 'external-services'
  | 'udp'
  | 'adifmon'
  | 'lookup'
  | 'lists-bands'
  | 'lists-modes'
  | 'cluster'
  | 'backup'
  | 'database'
  | 'autostart'
  | 'uscounties'
  | 'awards'
  | 'cat'
  | 'rotator'
  | 'winkeyer'
  | 'antenna'
  | 'antgenius'
  | 'tunergenius'
  | 'pgxl'
  | 'flex'
  | 'relayauto'
  | 'audio';

type TreeNode =
  | { kind: 'group'; label: string; icon?: any; defaultOpen?: boolean; children: TreeNode[] }
  | { kind: 'item';  label: string; id: SectionId; disabled?: boolean; vendor?: 'o3a' };

// A compact 4O3A wordmark for the sidebar, drawn here rather than shipped as an
// image: an inline SVG scales with the row, follows the theme (currentColor), and
// adds no binary asset. It marks which panels drive 4O3A hardware — the same
// reason the language picker draws its flags inline.
//
// This is a plain wordmark, NOT the manufacturer's logo artwork. If you would
// rather show the official mark, drop the SVG in and swap this component out.
function VendorMark({ vendor }: { vendor: 'o3a' }) {
	if (vendor !== 'o3a') return null;
	// The 4O3A mark: "4", the green waveform, "3A". Drawn inline rather than
	// shipped as an image — it scales with the row, keeps the sidebar's own type
	// for the digits, and adds no binary asset. The waveform is the part that
	// carries the brand, so only it is SVG; the characters stay HTML text so they
	// match the surrounding menu at any theme or zoom.
	const lens = [3.2, 5.2, 7.4, 9.5, 7.4, 5.2, 3.2]; // half-heights, tallest in the middle
	return (
		<span className="ml-1.5 flex shrink-0 items-center gap-[1px] text-[10px] font-bold leading-none opacity-80" title="4O3A Signature">
			<span>4</span>
			<svg viewBox="0 0 26 22" className="h-[13px] w-[15px]" aria-hidden="true">
				{lens.map((ry, i) => (
					<ellipse key={i} cx={3 + i * 3.3} cy={11} rx={1.5} ry={ry} fill="#1f9d3a" />
				))}
			</svg>
			<span>3A</span>
		</span>
	);
}

// buildTree returns the settings sidebar. The FlexRadio item only appears when
// the active CAT backend is a Flex (per-band antenna config is Flex-specific).
function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[] {
  const hardware: TreeNode[] = [
    { kind: 'item', label: t('sec.cat'),          id: 'cat' },
    { kind: 'item', label: t('sec.rotator'),      id: 'rotator' },
    { kind: 'item', label: t('sec.winkeyer'),     id: 'winkeyer' },
    { kind: 'item', label: t('sec.antenna'),      id: 'antenna' },
    { kind: 'item', label: t('sec.antgenius'),    id: 'antgenius',   vendor: 'o3a' },
    { kind: 'item', label: t('sec.tunergenius'),  id: 'tunergenius', vendor: 'o3a' },
    { kind: 'item', label: t('sec.pgxl'),         id: 'pgxl' },
    ...(flexAvailable ? [{ kind: 'item', label: t('sec.flex'), id: 'flex' } as TreeNode] : []),
    { kind: 'item', label: t('sec.relayauto'),    id: 'relayauto' },
    { kind: 'item', label: t('sec.audio'),        id: 'audio' },
  ];
  return [
  {
    kind: 'group', label: t('nav.user'), icon: User, defaultOpen: true, children: [
      { kind: 'item', label: t('sec.station'), id: 'station' },
      { kind: 'item', label: t('sec.profiles'), id: 'profiles' },
      { kind: 'item', label: t('sec.operating'), id: 'operating' },
      { kind: 'item', label: t('sec.confirmations'), id: 'confirmations' },
      { kind: 'item', label: t('sec.external'), id: 'external-services' },
    ],
  },
  {
    kind: 'group', label: t('nav.software'), icon: Cog, defaultOpen: true, children: [
      { kind: 'item', label: t('sec.general'), id: 'general' },
      { kind: 'item', label: t('sec.email'), id: 'email' },
      { kind: 'item', label: t('sec.lookup'), id: 'lookup' },
      { kind: 'group', label: t('nav.lists'), icon: Database, defaultOpen: true, children: [
        { kind: 'item', label: t('sec.bands'),      id: 'lists-bands' },
        { kind: 'item', label: t('sec.modes'), id: 'lists-modes' },
      ]},
      { kind: 'item', label: t('sec.cluster'),     id: 'cluster' },
      { kind: 'item', label: t('sec.udp'), id: 'udp' },
      { kind: 'item', label: t('sec.adifmon'), id: 'adifmon' },
      { kind: 'item', label: t('sec.uscounties'), id: 'uscounties' },
      { kind: 'item', label: t('sec.database'), id: 'database' },
      { kind: 'item', label: t('sec.autostart'), id: 'autostart' },
    ],
  },
  {
    kind: 'group', label: t('nav.hardware'), icon: Server, defaultOpen: true, children: hardware,
  },
  ];
}

// Map section id → i18n key (breadcrumb / placeholders).
const SECTION_KEY: Partial<Record<SectionId, string>> = {
  station: 'sec.station', profiles: 'sec.profiles', operating: 'sec.operating', confirmations: 'sec.confirmations',
  'external-services': 'sec.external', lookup: 'sec.lookup', 'lists-bands': 'sec.bands', 'lists-modes': 'sec.modes',
  cluster: 'sec.cluster', backup: 'sec.backup', database: 'sec.database', autostart: 'sec.autostart', udp: 'sec.udp',
  adifmon: 'sec.adifmon',
  uscounties: 'sec.uscounties',
  awards: 'sec.awards', cat: 'sec.cat', rotator: 'sec.rotator', winkeyer: 'sec.winkeyer', antenna: 'sec.antenna',
  antgenius: 'sec.antgenius', tunergenius: 'sec.tunergenius', pgxl: 'sec.pgxl', flex: 'sec.flex', audio: 'sec.audio', general: 'sec.general', email: 'sec.email',
  relayauto: 'sec.relayauto',
};

// Map section id → friendly name (used in breadcrumb / placeholders).
const SECTION_LABELS: Partial<Record<SectionId, string>> = {
  station: 'Station Information',
  profiles: 'Profiles',
  operating: 'Operating conditions',
  confirmations: 'Confirmations',
  'external-services': 'External services',
  lookup: 'Callsign Lookup',
  'lists-bands': 'Bands',
  'lists-modes': 'Modes & default RST',
  cluster: 'DX Cluster',
  backup: 'Database backup',
  database: 'Database',
  autostart: 'Autostart',
  udp: 'UDP integrations',
  adifmon: 'ADIF monitor',
  awards: 'Awards',
  cat: 'CAT interface',
  rotator: 'Rotator',
  winkeyer: 'CW Keyer',
  antenna: 'Ultrabeam / Steppir',
  antgenius: 'Antenna Genius',
  tunergenius: 'Tuner Genius',
  pgxl: 'Amplifier',
  flex: 'FlexRadio',
  relayauto: 'Relay auto-control',
  audio: 'Audio devices',
};

// ===== Tree component =====

interface TreeProps {
  selected: SectionId;
  onSelect: (id: SectionId) => void;
  flexAvailable?: boolean;
}

function Tree({ selected, onSelect, flexAvailable }: TreeProps) {
  const { t } = useI18n();
  return (
    <nav className="text-sm">
      {buildTree(!!flexAvailable, t).map((node, i) => (
        <TreeNodeView key={i} node={node} depth={0} selected={selected} onSelect={onSelect} />
      ))}
    </nav>
  );
}

function TreeNodeView({
  node, depth, selected, onSelect,
}: { node: TreeNode; depth: number; selected: SectionId; onSelect: (id: SectionId) => void }) {
  if (node.kind === 'item') {
    const isActive = selected === node.id;
    return (
      <button
        onClick={() => { if (!node.disabled) onSelect(node.id); }}
        disabled={node.disabled}
        className={cn(
          'w-full text-left px-2 py-1.5 rounded-md text-[12.5px] transition-colors flex items-center',
          isActive ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted/60',
          node.disabled && 'opacity-50 cursor-not-allowed italic',
        )}
        style={{ paddingLeft: 8 + depth * 14 }}
      >
        <span className="truncate">{node.label}</span>
        {node.vendor && !node.disabled && <VendorMark vendor={node.vendor} />}
        {node.disabled && (
          <Construction className="ml-auto size-3 shrink-0 opacity-60" />
        )}
      </button>
    );
  }
  // group
  const [open, setOpen] = useState(node.defaultOpen ?? false);
  const Icon = node.icon;
  return (
    <div>
      <button
        onClick={() => setOpen((v) => !v)}
        className="w-full text-left px-2 py-1.5 rounded-md text-[12px] uppercase tracking-wider text-muted-foreground hover:text-foreground flex items-center gap-1.5 font-semibold"
        style={{ paddingLeft: 8 + depth * 14 }}
      >
        {open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
        {Icon && <Icon className="size-3.5 opacity-70" />}
        <span>{node.label}</span>
      </button>
      {open && (
        <div>
          {node.children.map((c, i) => (
            <TreeNodeView key={i} node={c} depth={depth + 1} selected={selected} onSelect={onSelect} />
          ))}
        </div>
      )}
    </div>
  );
}

// ===== Section content panels =====

// Representative surface/card/accent swatch for each theme (picker preview —
// the theme is not applied until selected). Module-scope so their component
// identity is STABLE across SettingsModal re-renders; defining them inside the
// component would give each render a fresh function, remounting the Radix
// Select and slamming the open dropdown shut on any ambient re-render.
const THEME_SWATCH: Record<Exclude<ThemeChoice, 'auto'>, { bg: string; card: string; accent: string }> = {
  'light-warm':    { bg: '#e8dfc9', card: '#faf6ea', accent: '#b8410c' },
  'light-cool':    { bg: '#f4f6f8', card: '#ffffff', accent: '#2563eb' },
  'light-sage':    { bg: '#eef1ec', card: '#f8faf6', accent: '#2f855a' },
  'dim-slate':     { bg: '#343b47', card: '#3d4552', accent: '#fb923c' },
  'dark-warm':     { bg: '#221d18', card: '#2e2820', accent: '#e07a2e' },
  'dark-graphite': { bg: '#16181d', card: '#1f232b', accent: '#f97316' },
  'high-contrast': { bg: '#000000', card: '#0d0d0d', accent: '#ff7a1a' },
};

function ThemeSwatch({ theme }: { theme: Exclude<ThemeChoice, 'auto'> }) {
  const s = THEME_SWATCH[theme];
  return (
    <span className="flex h-5 w-8 overflow-hidden rounded-[3px] border border-black/20 shrink-0" style={{ background: s.bg }}>
      <span className="m-0.5 flex flex-1 items-center justify-center rounded-[2px]" style={{ background: s.card }}>
        <span className="h-2 w-2 rounded-full" style={{ background: s.accent }} />
      </span>
    </span>
  );
}

// Auto swatch = split light/dark, since it follows the OS preference.
function ThemeOptionSwatch({ theme }: { theme: ThemeChoice }) {
  if (theme === 'auto') {
    return (
      <span className="flex h-5 w-8 overflow-hidden rounded-[3px] border border-black/20 shrink-0">
        <span className="flex-1" style={{ background: '#e8dfc9' }} />
        <span className="flex-1" style={{ background: '#16181d' }} />
      </span>
    );
  }
  return <ThemeSwatch theme={theme} />;
}

function ThemeSelector() {
  const { theme, setTheme } = useTheme();
  const { t } = useI18n();
  const options: ThemeChoice[] = ['auto', ...CONCRETE_THEMES];
  return (
    <div className="flex items-center gap-3">
      <Label className="text-sm w-40">{t('settings.theme')}</Label>
      <Select value={theme} onValueChange={(v) => setTheme(v as ThemeChoice)}>
        <SelectTrigger className="h-9 w-64" title={t('settings.themeHint')}>
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {options.map((opt) => (
            <SelectItem key={opt} value={opt}>
              <span className="flex items-center gap-2.5">
                <ThemeOptionSwatch theme={opt} />
                {t(`theme.${opt}`)}
              </span>
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

function SectionHeader({ title, hint }: { title: string; hint?: string }) {
  return (
    <header className="mb-4">
      <h3 className="text-base font-semibold text-foreground">{title}</h3>
      {hint && <p className="text-xs text-muted-foreground mt-0.5">{hint}</p>}
    </header>
  );
}

// ProfileScopeNote flags that the panel's settings are saved per-profile, so
// the user knows which operating identity (F4BPO / TM2Q / …) they're editing.
function ProfileScopeNote({ profile }: { profile?: { name?: string; callsign?: string } }) {
  const { t } = useI18n();
  return (
    <div className="-mt-2 mb-4">
      <span className="inline-flex items-center gap-1.5 rounded-md bg-primary/10 text-primary text-xs px-2.5 py-1">
        <User className="size-3.5" />
        {t('profileScope.saved')} <strong className="font-semibold">{profile?.name || '—'}</strong>
        {profile?.callsign ? <span className="font-mono opacity-80">({profile.callsign})</span> : null}
        {' '}{t('profileScope.switch')}
      </span>
    </div>
  );
}

// AutostartPanelComponent manages the per-profile list of external programs to
// launch when OpsLog starts. It's a self-contained component (its own state) so
// it can use hooks — rendered via the `() => <AutostartPanelComponent/>` wrapper
// in PANELS. Changes persist immediately (config is local SQLite, cheap writes).
type AutostartProg = { id: string; name: string; path: string; args: string; enabled: boolean };

function AutostartPanelComponent() {
  const { t } = useI18n();
  const [progs, setProgs] = useState<AutostartProg[]>([]);
  const [loaded, setLoaded] = useState(false);
  const [err, setErr] = useState('');
  const [launchMsg, setLaunchMsg] = useState<Record<string, string>>({});

  async function load() {
    try { setProgs(((await GetAutostartPrograms()) ?? []) as any); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
    finally { setLoaded(true); }
  }
  useEffect(() => { load(); }, []);
  useEffect(() => {
    const off = EventsOn('profile:changed', () => load());
    return () => { if (typeof off === 'function') off(); };
  }, []);

  async function commit(next: AutostartProg[]) {
    setProgs(next);
    try { await SaveAutostartPrograms(next as any); setErr(''); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  const patch = (id: string, p: Partial<AutostartProg>) =>
    commit(progs.map((x) => (x.id === id ? { ...x, ...p } : x)));
  const remove = (id: string) => commit(progs.filter((x) => x.id !== id));

  async function addProgram() {
    try {
      const path = await BrowseExecutable();
      if (!path) return;
      const base = path.split(/[\\/]/).pop() || path;
      const name = base.replace(/\.(exe|bat|cmd)$/i, '');
      const id = (crypto as any)?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
      commit([...progs, { id, name, path, args: '', enabled: true }]);
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function rebrowse(id: string) {
    try { const path = await BrowseExecutable(); if (path) patch(id, { path }); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function launchNow(id: string) {
    try {
      const r: any = await LaunchAutostartProgram(id);
      const txt = r?.status === 'launched' ? '✓ launched'
        : r?.status === 'already_running' ? 'already running — not started again'
        : r?.status === 'missing' ? '✗ executable not found'
        : (r?.message || r?.status || 'done');
      setLaunchMsg((m) => ({ ...m, [id]: txt }));
    } catch (e: any) { setLaunchMsg((m) => ({ ...m, [id]: String(e?.message ?? e) })); }
  }

  return (
    <>
      <SectionHeader
        title={t('sec.autostart')}
        hint={t('autostart.hint')}
      />
      <div className="space-y-2 max-w-3xl">
        {loaded && progs.length === 0 && (
          <p className="text-sm text-muted-foreground italic">No programs yet — add one below.</p>
        )}
        {progs.map((p) => (
          <div key={p.id} className="rounded-lg border border-border bg-card p-3 space-y-2">
            <div className="flex items-center gap-2">
              <Checkbox checked={p.enabled} onCheckedChange={(c) => patch(p.id, { enabled: !!c })} title="Launch at startup" />
              <Input className="h-8 flex-1 font-medium" value={p.name} placeholder="Name"
                onChange={(e) => patch(p.id, { name: e.target.value })} />
              <Button size="sm" variant="outline" onClick={() => launchNow(p.id)} title="Launch now">
                <Play className="size-3.5" /> Launch
              </Button>
              <Button size="sm" variant="ghost" onClick={() => remove(p.id)} title="Remove">
                <Trash2 className="size-4 text-destructive" />
              </Button>
            </div>
            <div className="grid grid-cols-[78px_1fr] items-center gap-2">
              <Label className="text-xs text-muted-foreground">Program</Label>
              <div className="flex items-center gap-2">
                <Input className="h-8 flex-1 font-mono text-xs" value={p.path} readOnly title={p.path} />
                <Button size="sm" variant="outline" onClick={() => rebrowse(p.id)}>
                  <FolderOpen className="size-3.5" /> Browse
                </Button>
              </div>
              <Label className="text-xs text-muted-foreground">Arguments</Label>
              <Input className="h-8 font-mono text-xs" value={p.args} placeholder="optional command-line arguments"
                onChange={(e) => patch(p.id, { args: e.target.value })} />
            </div>
            {launchMsg[p.id] && <div className="text-xs text-muted-foreground pl-[86px]">{launchMsg[p.id]}</div>}
          </div>
        ))}
        <Button variant="outline" onClick={addProgram}>
          <Plus className="size-4" /> Add program…
        </Button>
        {err && <div className="text-xs text-destructive">{err}</div>}
      </div>
    </>
  );
}

// TelemetryToggle is a self-contained opt-out for the anonymous usage heartbeat
// (a random install ID + version + OS, sent once a day). Real component so it
// can own its state; embedded inside GeneralPanel.
function TelemetryToggle() {
  const { t } = useI18n();
  const [on, setOn] = useState(true);
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    GetTelemetryEnabled().then((v) => setOn(!!v)).catch(() => {}).finally(() => setLoaded(true));
  }, []);
  return (
    <label className="flex items-center gap-2 text-sm cursor-pointer">
      <Checkbox checked={on} disabled={!loaded}
        onCheckedChange={(c) => { const v = !!c; setOn(v); SetTelemetryEnabled(v).catch(() => {}); }} />
      {t('settings.telemetry')}
      <span className="text-xs text-muted-foreground">({t('settings.telemetryHint')})</span>
    </label>
  );
}

// ADIFMonitorPanel watches a list of external ADIF files (fldigi RTTY, N1MM,
// VarAC…) and auto-imports newly appended QSOs — deliberately option-free: a
// contact that arrives is imported and uploaded automatically like any log entry.
type ADIFWatchFileUI = { path: string; enabled: boolean; offset: number };
type ADIFMonitorCfgUI = { enabled: boolean; files: ADIFWatchFileUI[] };
function ADIFMonitorPanel() {
  const { t } = useI18n();
  const [cfg, setCfg] = useState<ADIFMonitorCfgUI>({ enabled: false, files: [] });
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    GetADIFMonitor()
      .then((c: any) => { if (c) setCfg({ enabled: !!c.enabled, files: (c.files ?? []) as ADIFWatchFileUI[] }); })
      .catch(() => {})
      .finally(() => setLoaded(true));
  }, []);
  // Offsets are managed backend-side; SaveADIFMonitor ignores the ones we send and
  // keeps each existing file's read position (a new file starts at end-of-file).
  const persist = (next: ADIFMonitorCfgUI) => { setCfg(next); SaveADIFMonitor(next as any).catch(() => {}); };
  const addFile = async () => {
    try {
      const p = await PickADIFMonitorFile();
      if (!p || cfg.files.some((f) => f.path === p)) return;
      persist({ ...cfg, files: [...cfg.files, { path: p, enabled: true, offset: -1 }] });
    } catch { /* dialog cancelled */ }
  };
  return (
    <div className="space-y-4 max-w-2xl">
      <p className="text-xs text-muted-foreground">{t('adifmon.hint')}</p>
      <label className="flex items-center gap-2 text-sm cursor-pointer">
        <Checkbox checked={cfg.enabled} disabled={!loaded} onCheckedChange={(c) => persist({ ...cfg, enabled: !!c })} />
        {t('adifmon.enable')}
      </label>
      <div className="space-y-1.5">
        {cfg.files.length === 0 && <p className="text-xs text-muted-foreground italic">{t('adifmon.empty')}</p>}
        {cfg.files.map((f, i) => (
          <div key={i} className="flex items-center gap-2 rounded-md border border-border bg-muted/20 px-2 py-1.5">
            <Checkbox checked={f.enabled}
              onCheckedChange={(c) => persist({ ...cfg, files: cfg.files.map((x, idx) => idx === i ? { ...x, enabled: !!c } : x) })} />
            <span className="flex-1 font-mono text-xs truncate" title={f.path}>{f.path}</span>
            <button type="button" title={t('adifmon.remove')}
              className="text-muted-foreground hover:text-destructive shrink-0"
              onClick={() => persist({ ...cfg, files: cfg.files.filter((_, idx) => idx !== i) })}>
              <Trash2 className="size-3.5" />
            </button>
          </div>
        ))}
      </div>
      <Button variant="outline" size="sm" onClick={addFile}>
        <FolderOpen className="size-3.5 mr-1" /> {t('adifmon.add')}
      </Button>
      <p className="text-[11px] text-muted-foreground">{t('adifmon.note')}</p>
    </div>
  );
}

// AmpUI mirrors the backend AmpConfig — one configured amplifier.
type AmpUI = { id: string; name: string; enabled: boolean; type: string; transport: string; host: string; port: number; com_port: string; baud: number;
  // Band-follow (ACOM): a SECOND serial port on which OpsLog answers the amp's
  // frequency polls, pretending to be a Kenwood-format transceiver.
  freq_out?: boolean; freq_com_port?: string; freq_baud?: number; freq_broadcast_ms?: number };

// RelayAutoPanel configures automatic control of the Station Control relay boards
// from the rig's frequency / band (PstRotator-style). Each relay carries one rule:
// off, a frequency window (kHz), or a set of bands.
type RelayRuleUI = { device_id: string; relay: number; mode: string; freq_lo_khz: number; freq_hi_khz: number; bands: string[] };
type StationDevUI = { id: string; type: string; name: string; labels: string[] };
const RELAY_BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '4m', '2m', '70cm'];
const relayCountUI = (type: string) => (type === 'kmtronic' || type === 'denkovi' ? 8 : 5);

// Live status + OPERATE/STANDBY toggle for ONE configured amplifier (by config
// id) — SPE / ACOM / PGXL alike. Module-scoped (not a nested component) so it
// isn't remounted on every parent render. Polls once a second while shown.
function AmpStatusCard({ id }: { id: string }) {
  const [amp, setAmp] = useState<any>(null);
  useEffect(() => {
    let alive = true;
    const tick = () => GetAmpStatuses().then((all: any[]) => {
      if (alive) setAmp((all ?? []).find((a) => a.id === id) ?? null);
    }).catch(() => {});
    tick();
    const t = window.setInterval(tick, 1000);
    return () => { alive = false; window.clearInterval(t); };
  }, [id]);
  const st: any = amp?.spe ?? amp?.acom ?? amp?.pgxl ?? { connected: false };
  const isSPE = !!amp?.spe;
  const isACOM = !!amp?.acom;
  const operate = !!st.operate;
  return (
    <div className="rounded-md border border-border p-3 space-y-2 text-xs max-w-xl">
      <div className="flex items-center gap-2">
        <span className={cn('size-2 rounded-full', st.connected ? 'bg-success animate-pulse' : 'bg-danger')} />
        <span className="font-semibold">{amp?.name || 'Amplifier'}{st.connected ? '' : ' — not connected'}</span>
        {!st.connected && st.last_error && <span className="text-danger truncate text-[10px]">{st.last_error}</span>}
        <span className="flex-1" />
        <Button size="sm" variant={operate ? 'default' : 'outline'} disabled={!st.connected}
          onClick={() => AmpOperate(id, !operate).catch(() => {})}
          title="Toggle OPERATE / STANDBY">
          {operate ? 'OPERATE' : 'STANDBY'}
        </Button>
      </div>
      {st.connected && isSPE && (
        <div className="grid grid-cols-4 gap-x-3 gap-y-1 font-mono text-[11px]">
          <div>{st.tx ? 'TX' : 'RX'}</div>
          <div>Band {st.band}</div>
          <div>Pwr {st.power_level}</div>
          <div>{st.output_w} W</div>
          <div>SWR {Number(st.swr_ant ?? 0).toFixed(2)}</div>
          <div>{st.temp_c}°C</div>
          <div>{st.volt_pa} V</div>
          <div>{st.curr_pa} A</div>
          {(st.warnings || st.alarms) && <div className="col-span-4 text-warning">⚠ {st.warnings} {st.alarms}</div>}
        </div>
      )}
      {st.connected && isACOM && (
        <div className="grid grid-cols-4 gap-x-3 gap-y-1 font-mono text-[11px]">
          <div>{st.state}</div>
          <div>Band {st.band || '—'}</div>
          <div>{st.fwd_w} W</div>
          <div>SWR {Number(st.swr ?? 0).toFixed(2)}</div>
          <div>Refl {st.refl_w} W</div>
          <div>Drive {st.drive_w} W</div>
          <div>{st.temp_c}°C</div>
          <div>Fan {st.fan}</div>
          {st.err_text && <div className="col-span-4 text-warning">⚠ {st.err_text} ({st.err_code})</div>}
        </div>
      )}
      {st.connected && !isSPE && !isACOM && (
        <div className="grid grid-cols-3 gap-x-3 gap-y-1 font-mono text-[11px]">
          <div>{st.state || ''}</div>
          <div>Fan {st.fan_mode || '—'}</div>
          <div>{st.temperature ? `${Math.round(st.temperature)}°C` : ''}</div>
        </div>
      )}
    </div>
  );
}
function RelayAutoPanel() {
  const { t } = useI18n();
  const [enabled, setEnabled] = useState(false);
  const [rules, setRules] = useState<RelayRuleUI[]>([]);
  const [devices, setDevices] = useState<StationDevUI[]>([]);
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    Promise.all([GetRelayAuto(), GetStationDevices()])
      .then(([cfg, devs]: any[]) => {
        setEnabled(!!cfg?.enabled);
        setRules((cfg?.rules ?? []) as RelayRuleUI[]);
        setDevices((devs ?? []) as StationDevUI[]);
      })
      .catch(() => {})
      .finally(() => setLoaded(true));
  }, []);
  const save = (en: boolean, rs: RelayRuleUI[]) => { SaveRelayAuto({ enabled: en, rules: rs } as any).catch(() => {}); };
  const ruleFor = (dev: string, relay: number): RelayRuleUI =>
    rules.find((r) => r.device_id === dev && r.relay === relay) ?? { device_id: dev, relay, mode: 'off', freq_lo_khz: 0, freq_hi_khz: 0, bands: [] };
  // Apply a patch and persist (commit=true) or keep local only (commit=false, for
  // freq inputs that persist on blur so we don't switch relays on every keystroke).
  const patchRule = (dev: string, relay: number, patch: Partial<RelayRuleUI>, commit = true) => {
    const next = { ...ruleFor(dev, relay), ...patch };
    const others = rules.filter((r) => !(r.device_id === dev && r.relay === relay));
    const all = [...others, next];
    setRules(all);
    if (commit) save(enabled, all);
  };
  const toggleBand = (dev: string, relay: number, band: string) => {
    const cur = ruleFor(dev, relay);
    const has = cur.bands.includes(band);
    patchRule(dev, relay, { bands: has ? cur.bands.filter((b) => b !== band) : [...cur.bands, band] });
  };

  return (
    <div className="space-y-4 max-w-3xl">
      <p className="text-xs text-muted-foreground">{t('relayauto.hint')}</p>
      <label className="flex items-center gap-2 text-sm cursor-pointer">
        <Checkbox checked={enabled} disabled={!loaded} onCheckedChange={(c) => { const v = !!c; setEnabled(v); save(v, rules); }} />
        {t('relayauto.enable')}
      </label>

      {devices.length === 0 && loaded && (
        <p className="text-xs text-muted-foreground italic">{t('relayauto.noDevices')}</p>
      )}

      {devices.map((dev) => (
        <div key={dev.id} className="rounded-md border border-border">
          <div className="px-3 py-1.5 border-b border-border bg-muted/40 text-xs font-semibold">{dev.name || dev.id}</div>
          <div className="divide-y divide-border/60">
            {Array.from({ length: (dev.labels?.length || relayCountUI(dev.type)) }, (_, i) => i + 1).map((relay) => {
              const r = ruleFor(dev.id, relay);
              const label = (dev.labels?.[relay - 1] || '').trim() || `${t('relayauto.relay')} ${relay}`;
              return (
                <div key={relay} className="flex items-start gap-3 px-3 py-2">
                  <span className="w-28 shrink-0 text-xs font-mono pt-1.5 truncate" title={label}>{label}</span>
                  <Select value={r.mode || 'off'} onValueChange={(v) => patchRule(dev.id, relay, { mode: v })}>
                    <SelectTrigger className="h-8 w-32 text-xs shrink-0"><SelectValue /></SelectTrigger>
                    <SelectContent>
                      <SelectItem value="off">{t('relayauto.modeOff')}</SelectItem>
                      <SelectItem value="freq">{t('relayauto.modeFreq')}</SelectItem>
                      <SelectItem value="band">{t('relayauto.modeBand')}</SelectItem>
                    </SelectContent>
                  </Select>
                  <div className="flex-1 min-w-0 pt-0.5">
                    {r.mode === 'freq' && (
                      <div className="flex items-center gap-1.5 text-xs">
                        <Input type="number" className="h-8 w-24 text-xs" placeholder={t('relayauto.from')}
                          defaultValue={r.freq_lo_khz || ''}
                          onChange={(e) => patchRule(dev.id, relay, { freq_lo_khz: parseFloat(e.target.value) || 0 }, false)}
                          onBlur={() => save(enabled, rules)} />
                        <span className="text-muted-foreground">–</span>
                        <Input type="number" className="h-8 w-24 text-xs" placeholder={t('relayauto.to')}
                          defaultValue={r.freq_hi_khz || ''}
                          onChange={(e) => patchRule(dev.id, relay, { freq_hi_khz: parseFloat(e.target.value) || 0 }, false)}
                          onBlur={() => save(enabled, rules)} />
                        <span className="text-muted-foreground">kHz</span>
                      </div>
                    )}
                    {r.mode === 'band' && (
                      <div className="flex flex-wrap gap-1">
                        {RELAY_BANDS.map((b) => {
                          const on = r.bands.includes(b);
                          return (
                            <button key={b} type="button" onClick={() => toggleBand(dev.id, relay, b)}
                              className={cn('px-1.5 py-0.5 rounded text-[11px] font-mono border transition-colors',
                                on ? 'bg-primary text-primary-foreground border-primary' : 'border-border text-muted-foreground hover:bg-muted')}>
                              {b}
                            </button>
                          );
                        })}
                      </div>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

// MainViewPanes lets the operator choose what the Main tab's left and right
// panes show, independently: the great-circle map, the locator street map, the
// cluster grid or the worked-before grid. Per-profile (stored via SetUIPref,
// which is profile-prefixed). Self-contained so it owns its async-loaded state.
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol'];
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
  const { t } = useI18n();
  const [left, setLeft] = useState('map1');
  const [right, setRight] = useState('map2');
  // Radio-control panes are only offered when that CAT backend is active. Sorted A→Z.
  const options = [
    ...MAIN_PANE_VALUES,
    ...(flexAvailable ? ['flex'] : []),
    ...(icomAvailable ? ['icom'] : []),
    ...(yaesuAvailable ? ['yaesu'] : []),
  ].map((value) => ({ value, label: t(`settings.pane.${value}`) }))
    .sort((a, b) => a.label.localeCompare(b.label));
  useEffect(() => {
    const valid = (v: string) => v === 'flex' || v === 'icom' || v === 'yaesu' || MAIN_PANE_VALUES.includes(v);
    Promise.all([GetUIPref('mainPaneLeft').catch(() => ''), GetUIPref('mainPaneRight').catch(() => '')])
      .then(([l, r]) => { if (valid(l)) setLeft(l); if (valid(r)) setRight(r); });
  }, []);
  const pick = (side: 'left' | 'right', v: string) => {
    if (side === 'left') setLeft(v); else setRight(v);
    // Persist (per-profile) AND tell the parent the new value directly, so the
    // Main view updates from the chosen value — never a stale DB re-read.
    SetUIPref(side === 'left' ? 'mainPaneLeft' : 'mainPaneRight', v).catch(() => {});
    onChanged?.(side, v);
  };
  return (
    <div className="border-t border-border/60 pt-4 space-y-2">
      <h4 className="text-sm font-semibold text-foreground">{t('settings.mainView')}</h4>
      <p className="text-xs text-muted-foreground">{t('settings.mainViewHint')}</p>
      <div className="grid grid-cols-2 gap-3 max-w-xl">
        <label className="flex flex-col gap-1 text-xs">
          <span className="text-muted-foreground">{t('settings.leftPane')}</span>
          <Select value={left} onValueChange={(v) => pick('left', v)}>
            <SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
            <SelectContent>
              {options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
            </SelectContent>
          </Select>
        </label>
        <label className="flex flex-col gap-1 text-xs">
          <span className="text-muted-foreground">{t('settings.rightPane')}</span>
          <Select value={right} onValueChange={(v) => pick('right', v)}>
            <SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
            <SelectContent>
              {options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
            </SelectContent>
          </Select>
        </label>
      </div>
    </div>
  );
}

// FlexDiscover scans the LAN for FlexRadio broadcasts and lets the user pick one
// (fills the IP/port). Self-contained so it can own its state (rendered inside
// the hook-less CATPanel).
function FlexDiscover({ onPick }: { onPick: (ip: string, port: number) => void }) {
  const [busy, setBusy] = useState(false);
  const [found, setFound] = useState<Array<{ ip: string; port: number; model?: string; nickname?: string }>>([]);
  const [msg, setMsg] = useState('');
  async function scan() {
    setBusy(true); setMsg('');
    try {
      const r = ((await DiscoverFlexRadios()) ?? []) as any[];
      setFound(r as any);
      if (r.length === 0) setMsg('No radio found — check it\'s on the same network, or enter the IP manually.');
    } catch (e: any) {
      setMsg(String(e?.message ?? e));
    } finally { setBusy(false); }
  }
  return (
    <div className="rounded-md border border-border bg-muted/20 p-2 space-y-1.5">
      <div className="flex items-center gap-2">
        <Button size="sm" variant="outline" onClick={scan} disabled={busy}>
          {busy ? <Loader2 className="size-3.5 animate-spin" /> : <Wifi className="size-3.5" />} Detect radios
        </Button>
        <span className="text-[11px] text-muted-foreground">listens for FlexRadio broadcast on the LAN</span>
      </div>
      {found.map((r) => (
        <button key={r.ip} type="button" onClick={() => onPick(r.ip, r.port || 4992)}
          className="w-full text-left text-xs rounded border border-border px-2 py-1 hover:bg-accent/50">
          <span className="font-mono font-semibold">{r.ip}</span>
          {r.model ? <span className="text-muted-foreground"> · {r.model}</span> : ''}
          {r.nickname ? <span className="text-muted-foreground"> ({r.nickname})</span> : ''}
        </button>
      ))}
      {msg && <div className="text-[11px] text-muted-foreground">{msg}</div>}
    </div>
  );
}

function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
  const label = SECTION_LABELS[id] ?? id;
  const IconCmp = Icon ?? Construction;
  return (
    <div className="flex flex-col items-center justify-center text-center text-muted-foreground gap-2 py-12">
      <IconCmp className="size-10 opacity-40" />
      <div className="text-base font-semibold text-foreground/70">{label}</div>
      <div className="text-sm">Module coming soon.</div>
    </div>
  );
}

// FlexBandAntennasPanel — pick the RX/TX antenna per band. Applied automatically
// when the band changes (frequency change / spot click). Antennas come live from
// the connected FlexRadio; bands come from Lists → Bands.
function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
  const [rxList, setRxList] = useState<string[]>([]);
  const [txList, setTxList] = useState<string[]>([]);
  const [map, setMap] = useState<Record<string, { rx: string; tx: string }>>({});
  const [msg, setMsg] = useState('');
  useEffect(() => {
    GetFlexState().then((s: any) => {
      setRxList((s?.ant_list ?? []) as string[]);
      setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
    }).catch(() => {});
    GetFlexBandAntennas().then((m: any) => setMap(m ?? {})).catch(() => {});
  }, []);
  const set = (band: string, side: 'rx' | 'tx', v: string) => {
    const key = band.toUpperCase();
    setMap((m) => {
      const cur = m[key] ?? { rx: '', tx: '' };
      const next = { ...m, [key]: { ...cur, [side]: v } };
      SaveFlexBandAntennas(next as any)
        .then(() => { setMsg('Saved'); window.setTimeout(() => setMsg(''), 1200); })
        .catch((e: any) => setMsg(String(e?.message ?? e)));
      return next;
    });
  };
  return (
    <div className="space-y-3">
      <div>
        <h3 className="text-base font-semibold text-foreground">FlexRadio — per-band antennas</h3>
        <p className="text-xs text-muted-foreground">
          Choose the RX and TX antenna for each band. They're applied automatically when the band
          changes (frequency change or clicking a spot).
        </p>
      </div>
      {rxList.length === 0 && (
        <div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-xl">
          No antennas reported yet — make sure the FlexRadio is connected (CAT interface), then reopen this panel.
        </div>
      )}
      {bands.length === 0 ? (
        <p className="text-xs text-muted-foreground">No bands configured — add them in Lists → Bands.</p>
      ) : (
        <div className="rounded-md border border-border overflow-hidden max-w-xl">
          <table className="w-full text-sm">
            <thead className="bg-muted/40 text-muted-foreground text-xs">
              <tr>
                <th className="text-left px-3 py-1.5 font-semibold">Band</th>
                <th className="text-left px-3 py-1.5 font-semibold">RX antenna</th>
                <th className="text-left px-3 py-1.5 font-semibold">TX antenna</th>
              </tr>
            </thead>
            <tbody>
              {bands.map((b) => {
                const e = map[b.toUpperCase()] ?? { rx: '', tx: '' };
                return (
                  <tr key={b} className="border-t border-border/50">
                    <td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
                    <td className="px-3 py-1.5">
                      <select value={e.rx ?? ''} onChange={(ev) => set(b, 'rx', ev.target.value)}
                        className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
                        <option value="">— none —</option>
                        {rxList.map((a) => <option key={a} value={a}>{a}</option>)}
                      </select>
                    </td>
                    <td className="px-3 py-1.5">
                      <select value={e.tx ?? ''} onChange={(ev) => set(b, 'tx', ev.target.value)}
                        className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
                        <option value="">— none —</option>
                        {txList.map((a) => <option key={a} value={a}>{a}</option>)}
                      </select>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
      {msg && <span className="text-[11px] text-muted-foreground">{msg}</span>}
    </div>
  );
}

// Known Icom models paired with their FACTORY default CI-V address. Picking a
// model sets icom_addr so the backend identifies it (civ.ModelName) and the UI
// adapts (e.g. the attenuator steps differ by model). Keep in lockstep with
// civ.ModelName in internal/cat/civ/civ.go. `addr` is decimal.
// The IC-7700 and IC-7800 were listed at 0x88 and 0x80 — which are the IC-7100's
// and the IC-7410's. Picking one of them set an address the rig never answers on,
// and the backend then named it as the other model.
const ICOM_MODELS: { name: string; addr: number }[] = [
  { name: 'IC-705', addr: 0xA4 },
  { name: 'IC-7100', addr: 0x88 },
  { name: 'IC-7300', addr: 0x94 },
  { name: 'IC-7300MKII', addr: 0xB6 },
  { name: 'IC-7410', addr: 0x80 },
  { name: 'IC-7600', addr: 0x7A },
  { name: 'IC-7610', addr: 0x98 },
  { name: 'IC-7700', addr: 0x74 },
  { name: 'IC-7800', addr: 0x6A },
  { name: 'IC-7851', addr: 0x8E },
  { name: 'IC-9100', addr: 0x7C },
  { name: 'IC-9700', addr: 0xA2 },
];

export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable }: Props) {
  const { t } = useI18n();
  const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [clearing, setClearing] = useState(false);
  const [msg, setMsg] = useState('');
  const [err, setErr] = useState('');

  const [lookup, setLookup] = useState<LookupSettings>({
    qrz_user: '', qrz_password: '',
    hamqth_user: '', hamqth_password: '',
    primary: '', failsafe: '',
    download_images: false,
    cache_ttl_days: 30,
  });
  // Per-provider Test state — keeps the success/error feedback adjacent
  // to the button. Cleared on the next test run for that provider.
  type TestResult = { ok: boolean; msg: string };
  const [lookupTest, setLookupTest] = useState<Record<string, TestResult | undefined>>({});
  const [lookupTesting, setLookupTesting] = useState<Record<string, boolean>>({});
  // The Station Information panel now edits the full active profile
  // (not a flat 6-field StationSettings). Profile selection happens in
  // the Profiles panel; any edit here saves back to whichever profile
  // is currently active.
  const [activeProfile, setActiveProfile] = useState<Profile | null>(null);
  const updateActive = (patch: Partial<Profile>) =>
    setActiveProfile((p) => (p ? { ...p, ...patch } : p));
  const [lists, setLists] = useState<ListsSettings>({ bands: [], modes: [], rst_phone: [], rst_cw: [], rst_digital: [] });
  // RST report lists edited as free text (one/space-separated values).
  const [rstText, setRstText] = useState({ phone: '', cw: '', digital: '' });
  // Custom band drafts (catalog covers ADIF spec but the user may have
  // exotic or experimental bands not listed).
  const [bandDraft, setBandDraft] = useState('');
  const [modeDraft, setModeDraft] = useState('');
  const [catCfg, setCatCfg] = useState<CATSettings>({
    enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120,
    yaesu_port: '', yaesu_baud: 38400, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70,
    icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
    tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
    digital_default: 'FT8', share_enabled: false, share_port: 4532,
  });
  const [rotator, setRotator] = useState<RotatorSettings>({
    enabled: false, type: 'pst', host: '127.0.0.1', port: 12000, has_elevation: false, rotator_num: 1,
  } as any);
  const [rotatorTesting, setRotatorTesting] = useState(false);
  const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);

  // Motorized antenna (Ultrabeam TCP or SteppIR TCP/serial) settings.
  const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com: string; baud: number; follow: boolean; step_khz: number; tx_inhibit: boolean; freq_min_mhz: number; freq_max_mhz: number }>({
    enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50, tx_inhibit: false, freq_min_mhz: 13, freq_max_mhz: 54,
  });
  const [ubTesting, setUbTesting] = useState(false);
  const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);

  // Antenna Genius (4O3A) switch settings — TCP port is fixed at 9007.
  const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });
  const [tunergenius, setTunergenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });

  // Amplifier list — operators can run SEVERAL amps (even two SPEs combined),
  // each with its own connection. Saved as a whole via SaveAmplifiers.
  const [amps, setAmps] = useState<AmpUI[]>([]);

  // WinKeyer CW keyer settings + macro editor.
  type WKMac = { label: string; text: string };
  type WKSettings = {
    enabled: boolean; engine: string; esc_clears_call: boolean; esm: boolean;
    port: string; baud: number; wpm: number; weight: number;
    lead_in_ms: number; tail_ms: number; ratio: number; farnsworth: number;
    sidetone_hz: number; mode: string; swap: boolean; autospace: boolean;
    use_ptt: boolean; serial_echo: boolean; cw_key_line: string; cw_invert: boolean; macros: WKMac[];
  };
  const [wk, setWk] = useState<WKSettings>({
    enabled: false, engine: 'winkeyer', esc_clears_call: true, esm: false,
    port: '', baud: 1200, wpm: 25, weight: 50, lead_in_ms: 10,
    tail_ms: 50, ratio: 50, farnsworth: 0, sidetone_hz: 600, mode: 'iambic_b',
    swap: false, autospace: true, use_ptt: false, serial_echo: true, cw_key_line: 'dtr', cw_invert: false, macros: [],
  });
  const [wkPorts, setWkPorts] = useState<string[]>([]);
  // Session-only: the byte trace is a diagnostic, never a saved preference.
  //
  // But it must still SHOW its real state. These start false on every mount, so
  // a trace already running came back unticked; the operator ticks the box to
  // turn it on, which turns it off, and the log they send has no trace in it.
  // Reported by a Xiegu user. Hydrated from the backend below.
  const [wkTrace, setWkTrace] = useState(false);
  const [civTrace, setCivTrace] = useState(false);
  useEffect(() => {
    CIVTraceEnabled().then((v) => setCivTrace(!!v)).catch(() => {});
    WinkeyerTraceEnabled().then((v) => setWkTrace(!!v)).catch(() => {});
  }, []);
  const setWkField = (patch: Partial<WKSettings>) => setWk((s) => ({ ...s, ...patch }));

  // ── Audio (DVK + QSO recorder) ──
  type AudioSettings = {
    from_radio: string; to_radio: string; recording_device: string; listening_device: string;
    qso_record: boolean; qso_dir: string; preroll_seconds: number;
    ptt_method: 'none' | 'cat' | 'rts' | 'dtr'; ptt_port: string; format: 'wav' | 'mp3';
    from_gain: number; mic_gain: number; tx_gain: number;
  };
  type AudioDev = { id: string; name: string; default: boolean };
  const [audioCfg, setAudioCfg] = useState<AudioSettings>({
    from_radio: '', to_radio: '', recording_device: '', listening_device: '',
    qso_record: false, qso_dir: '', preroll_seconds: 8, ptt_method: 'none', ptt_port: '', format: 'wav',
    from_gain: 100, mic_gain: 100, tx_gain: 100,
  });
  const [audioInputs, setAudioInputs] = useState<AudioDev[]>([]);
  const [audioOutputs, setAudioOutputs] = useState<AudioDev[]>([]);
  const setAudioField = (patch: Partial<AudioSettings>) => setAudioCfg((s) => ({ ...s, ...patch }));
  const reloadAudioDevices = () => {
    ListAudioInputDevices().then((d) => setAudioInputs((d ?? []) as AudioDev[])).catch(() => {});
    ListAudioOutputDevices().then((d) => setAudioOutputs((d ?? []) as AudioDev[])).catch(() => {});
  };
  // DVK voice-keyer messages (F1–F6).
  type DVKMsg = { slot: number; label: string; has_audio: boolean; duration_sec: number };
  type DVKStat = { recording: boolean; playing: boolean; rec_slot: number };
  const [dvkMsgs, setDvkMsgs] = useState<DVKMsg[]>([]);
  const [dvkStat, setDvkStat] = useState<DVKStat>({ recording: false, playing: false, rec_slot: 0 });
  const [dvkErr, setDvkErr] = useState('');
  const [monitorOn, setMonitorOn] = useState(false);
  const [txOn, setTxOn] = useState(false);
  useEffect(() => {
    AudioMonitorActive().then(setMonitorOn).catch(() => {});
    AudioTXActive().then(setTxOn).catch(() => {});
  }, []);
  const toggleMonitor = async () => {
    try {
      if (monitorOn) { await AudioStopMonitor(); setMonitorOn(false); }
      else { await AudioStartMonitor(); setMonitorOn(true); }
    } catch (err: any) { setDvkErr(String(err?.message ?? err)); }
  };
  const toggleTX = async () => {
    try {
      if (txOn) { await AudioStopTX(); setTxOn(false); }
      else { await AudioStartTX(); setTxOn(true); }
    } catch (err: any) { setDvkErr(String(err?.message ?? err)); }
  };

  // General behaviour prefs (mirrored to the DB so they travel with data/).
  const [autofocusWB, setAutofocusWB] = useState(() => localStorage.getItem('opslog.autofocusWB') !== '0');
  const [checkUpdates, setCheckUpdates] = useState(() => localStorage.getItem('opslog.checkUpdates') !== '0');
  const [showBeamMap, setShowBeamMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
  const [startEqEnd, setStartEqEnd] = useState(() => localStorage.getItem('opslog.startEqualsEnd') === '1');
  const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1');
  const [groupDigital, setGroupDigital] = useState(() => localStorage.getItem('opslog.groupDigitalSlots') === '1');
  const [showQsoRate, setShowQsoRate] = useState(() => localStorage.getItem('opslog.showQsoRate') === '1');
  const [catModeBeforeFreq, setCatModeBeforeFreq] = useState(() => localStorage.getItem('opslog.catModeBeforeFreq') === '1');
  // Password-encryption (secret vault) state.
  const [secret, setSecret] = useState<{ has_passphrase: boolean; unlocked: boolean }>({ has_passphrase: false, unlocked: false });
  const [ppNew, setPpNew] = useState('');
  const [ppConfirm, setPpConfirm] = useState('');
  const [ppErr, setPpErr] = useState('');
  const [ppBusy, setPpBusy] = useState(false);
  const refreshSecret = async () => { try { setSecret(await GetSecretStatus() as any); } catch {} };
  useEffect(() => { refreshSecret(); }, []);
  const applyPassphrase = async () => {
    if (ppNew !== ppConfirm) { setPpErr(t('gen.pwMismatch')); return; }
    setPpBusy(true); setPpErr('');
    try { await SetPassphrase(ppNew); setPpNew(''); setPpConfirm(''); await refreshSecret(); }
    catch (e: any) { setPpErr(String(e?.message ?? e)); }
    finally { setPpBusy(false); }
  };
  const removePassphrase = async () => {
    setPpBusy(true); setPpErr('');
    try { await RemovePassphrase(ppNew); setPpNew(''); setPpConfirm(''); await refreshSecret(); }
    catch (e: any) { setPpErr(String(e?.message ?? e)); }
    finally { setPpBusy(false); }
  };

  // E-mail / SMTP (send QSO recordings).
  type EmailCfg = {
    enabled: boolean; smtp_host: string; smtp_port: number; smtp_user: string; smtp_password: string;
    from: string; reply_to: string; encryption: 'ssl' | 'starttls' | 'none'; auth: boolean; auto_send: boolean; subject: string; body: string;
  };
  const [emailCfg, setEmailCfg] = useState<EmailCfg>({
    enabled: false, smtp_host: '', smtp_port: 587, smtp_user: '', smtp_password: '',
    from: '', reply_to: '', encryption: 'starttls', auth: true, auto_send: false, subject: '', body: '',
  });
  const [emailMsg, setEmailMsg] = useState('');
  const setEmailField = (patch: Partial<EmailCfg>) => setEmailCfg((s) => ({ ...s, ...patch }));
  // eQSL card e-mail (subject/body templates + auto-send on log).
  type EQSLCfg = { subject: string; body: string; auto_send: boolean };
  const [eqslCfg, setEqslCfg] = useState<EQSLCfg>({ subject: '', body: '', auto_send: false });
  const setEqslField = (patch: Partial<EQSLCfg>) => setEqslCfg((s) => ({ ...s, ...patch }));
  // ClubLog Country File (cty.xml) exception status.
  type ClubInfo = { enabled: boolean; loaded: boolean; date: string; count: number };
  const [clubInfo, setClubInfo] = useState<ClubInfo>({ enabled: false, loaded: false, date: '', count: 0 });
  const [clubBusy, setClubBusy] = useState(false);
  const [clubErr, setClubErr] = useState('');
  useEffect(() => { GetClublogCtyInfo().then((i) => setClubInfo(i as ClubInfo)).catch(() => {}); }, []);
  // ClubLog Most Wanted (rank shown in the entry matrix).
  type MWInfo = { enabled: boolean; loaded: boolean; callsign: string; date: string; count: number };
  const [mwInfo, setMwInfo] = useState<MWInfo>({ enabled: false, loaded: false, callsign: '', date: '', count: 0 });
  const [mwBusy, setMwBusy] = useState(false);
  const [mwErr, setMwErr] = useState('');
  useEffect(() => { GetClublogMostWantedInfo().then((i) => setMwInfo(i as MWInfo)).catch(() => {}); }, []);
  const reloadDvk = () => { GetDVKMessages().then((m) => setDvkMsgs((m ?? []) as DVKMsg[])).catch(() => {}); };
  useEffect(() => {
    GetDVKStatus().then((s) => setDvkStat(s as DVKStat)).catch(() => {});
    const off = EventsOn('audio:status', (s: any) => setDvkStat(s as DVKStat));
    return () => { off?.(); };
  }, []);

  type QSLDefaults = {
    qsl_sent: string; qsl_rcvd: string;
    lotw_sent: string; lotw_rcvd: string;
    eqsl_sent: string; eqsl_rcvd: string;
    clublog_status: string; hrdlog_status: string; qrzcom_status: string;
    qrzcom_confirmed: string;
  };
  const [qslDefaults, setQslDefaults] = useState<QSLDefaults>({
    qsl_sent: '', qsl_rcvd: '',
    lotw_sent: '', lotw_rcvd: '',
    eqsl_sent: '', eqsl_rcvd: '',
    clublog_status: '', hrdlog_status: '', qrzcom_status: '',
    qrzcom_confirmed: '',
  });

  // External services (logbook upload). One block per service; only QRZ is
  // wired today. upload_mode is 'immediate' | 'delayed' (per-service).
  type ExtServiceCfg = {
    api_key: string; email: string; username: string; password: string; callsign: string;
    code: string; qth_nickname: string;
    url: string; station_id: string; // Cloudlog/Wavelog: own instance + station profile
    force_station_callsign: string;
    tqsl_path: string; station_location: string; key_password: string;
    upload_flags: string[]; write_log: boolean;
    auto_upload: boolean; upload_mode: string;
  };
  type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg; hrdlog: ExtServiceCfg; eqsl: ExtServiceCfg; cloudlog: ExtServiceCfg; delete_remote?: boolean };
  const emptyExtCfg = (): ExtServiceCfg => ({
    api_key: '', url: '', station_id: '', email: '', username: '', password: '', callsign: '', code: '', qth_nickname: '',
    force_station_callsign: '', tqsl_path: '', station_location: '', key_password: '',
    upload_flags: ['N', 'R'], write_log: false,
    auto_upload: false, upload_mode: 'immediate',
  });
  const [extSvc, setExtSvc] = useState<ExternalServices>({
    qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(), hrdlog: emptyExtCfg(), eqsl: emptyExtCfg(), cloudlog: emptyExtCfg(), delete_remote: false,
  });
  const [qrzTest, setQrzTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [qrzTesting, setQrzTesting] = useState(false);
  const [clublogTest, setClublogTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [clublogTesting, setClublogTesting] = useState(false);
  const [lotwTest, setLotwTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [lotwTesting, setLotwTesting] = useState(false);
  // LoTW user-activity list (for the callsign badge): count + last refresh.
  const [lotwUsers, setLotwUsers] = useState<{ count: number; updated?: string }>({ count: 0 });
  const [lotwUsersBusy, setLotwUsersBusy] = useState(false);
  useEffect(() => { GetLoTWUsersStatus().then((s) => setLotwUsers(s as any)).catch(() => {}); }, []);
  const downloadLotwUsers = async () => {
    setLotwUsersBusy(true);
    try { await DownloadLoTWUsers(); const s = await GetLoTWUsersStatus(); setLotwUsers(s as any); }
    catch (e: any) { setLotwTest({ ok: false, msg: String(e?.message ?? e) }); }
    finally { setLotwUsersBusy(false); }
  };
  // Super Check Partial / N+1 callsign helper: enabled flag + master-list status.
  const [scp, setScp] = useState<{ enabled: boolean; count: number; updated?: string }>({ enabled: false, count: 0 });
  const [scpBusy, setScpBusy] = useState(false);
  useEffect(() => { GetScpStatus().then((s) => setScp(s as any)).catch(() => {}); }, []);
  const toggleScp = async (on: boolean) => {
    setScp((s) => ({ ...s, enabled: on }));
    setScpBusy(true);
    try { await SetScpEnabled(on); const s = await GetScpStatus(); setScp(s as any); } catch {}
    finally { setScpBusy(false); }
  };
  const downloadScp = async () => {
    setScpBusy(true);
    try { await DownloadScp(); const s = await GetScpStatus(); setScp(s as any); } catch {}
    finally { setScpBusy(false); }
  };
  // US Counties (offline FCC ULS) — download progress arrives via events.
  const [ulsStatus, setUlsStatus] = useState<{ count: number; updated_at?: string }>({ count: 0 });
  const [ulsBusy, setUlsBusy] = useState(false);
  const [ulsProgress, setUlsProgress] = useState<{ stage: string; pct: number } | null>(null);
  const [ulsMsg, setUlsMsg] = useState<{ ok: boolean; text: string } | null>(null);
  const [backfillBusy, setBackfillBusy] = useState(false);
  const [backfillMsg, setBackfillMsg] = useState<string | null>(null);
  useEffect(() => { ULSStatus().then((s) => setUlsStatus(s as any)).catch(() => {}); }, []);
  useEffect(() => {
    const off1 = EventsOn('uls:progress', (p: any) => setUlsProgress({ stage: p?.stage ?? '', pct: p?.pct ?? 0 }));
    const off2 = EventsOn('uls:done', async (r: any) => {
      setUlsBusy(false); setUlsProgress(null);
      setUlsMsg(r?.ok ? { ok: true, text: t('uscty.done', { n: r?.count ?? 0 }) } : { ok: false, text: r?.error || 'error' });
      try { const s = await ULSStatus(); setUlsStatus(s as any); } catch {}
    });
    return () => { off1(); off2(); };
  }, []);
  const downloadUls = async () => {
    setUlsBusy(true); setUlsMsg(null); setUlsProgress({ stage: '', pct: 0 });
    try { await DownloadULSCounties(); }
    catch (e: any) { setUlsBusy(false); setUlsProgress(null); setUlsMsg({ ok: false, text: String(e?.message ?? e) }); }
  };
  const runBackfill = async () => {
    setBackfillBusy(true); setBackfillMsg(null);
    try { const r: any = await BackfillUSCounties(); setBackfillMsg(t('uscty.backfillDone', { c: r?.county ?? 0, g: r?.grid ?? 0, s: r?.scanned ?? 0 })); }
    catch (e: any) { setBackfillMsg(String(e?.message ?? e)); }
    finally { setBackfillBusy(false); }
  };
  const [hrdlogTest, setHrdlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [hrdlogTesting, setHrdlogTesting] = useState(false);
  const [cloudlogTest, setCloudlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [cloudlogTesting, setCloudlogTesting] = useState(false);
  const [eqslTest, setEqslTest] = useState<{ ok: boolean; msg: string } | null>(null);
  const [eqslTesting, setEqslTesting] = useState(false);
  const [stationLocations, setStationLocations] = useState<string[]>([]);
  // Active tab in the External Services panel — lifted here because
  // PANELS[selected]() is called as a function, so panels can't hold hooks.
  const [extSvcTab, setExtSvcTab] = useState<'qrz' | 'clublog' | 'hrdlog' | 'eqsl' | 'lotw' | 'cloudlog' | 'pota'>('qrz');
  // POTA hunter-log sync (stamps pota_ref on local QSOs from your pota.app log).
  const [potaToken, setPotaToken] = useState('');
  const [potaBusy, setPotaBusy] = useState(false);
  const [potaResult, setPotaResult] = useState<{ ok: boolean; msg: string; unmatched?: any[] } | null>(null);
  useEffect(() => { GetPOTAToken().then((t) => setPotaToken(t || '')).catch(() => {}); }, []);

  const [backupCfg, setBackupCfg] = useState<mainModels.BackupSettings>({
    enabled: false, folder: '', rotation: 5, zip: false,
    last_backup_at: '', default_folder: '',
  } as any);
  const [backupRunning, setBackupRunning] = useState(false);
  const [backupResult, setBackupResult] = useState<{ ok: boolean; msg: string } | null>(null);

  const [dbSettings, setDbSettings] = useState<{ path: string; default_path: string; is_custom: boolean; logbook_default_path?: string }>({ path: '', default_path: '', is_custom: false });
  const [dbMsg, setDbMsg] = useState('');
  type MySQLCfg = { enabled: boolean; host: string; port: number; user: string; password: string; database: string; sqlite_path?: string };
  const [mysqlCfg, setMysqlCfg] = useState<MySQLCfg>({ enabled: false, host: '', port: 3306, user: '', password: '', database: '' });
  const setMysqlField = (patch: Partial<MySQLCfg>) => setMysqlCfg((s) => ({ ...s, ...patch }));
  const [mysqlMsg, setMysqlMsg] = useState('');
  const [restartMsg, setRestartMsg] = useState(''); // backend switch / save → "restart to apply"
  const [backendStatus, setBackendStatus] = useState<{ active: string; fallback: boolean; error: string } | null>(null);

  const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
  const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
  const [clusterStatuses, setClusterStatuses] = useState<ClusterServerStatus[]>([]);
  const [editingServer, setEditingServer] = useState<ClusterServer | null>(null);

  async function reloadClusterServers() {
    try {
      const [list, ac, st] = await Promise.all([
        ListClusterServers(),
        GetClusterAutoConnect(),
        GetClusterStatus(),
      ]);
      setClusterServers((list ?? []) as ClusterServer[]);
      setClusterAutoConnectState(ac);
      setClusterStatuses((st ?? []) as ClusterServerStatus[]);
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }

  // Live cluster status updates while Preferences is open — the user can
  // click Connect/Disconnect inside the modal and see the pills change
  // without saving + reopening.
  useEffect(() => {
    const unsub = EventsOn('cluster:state', async (st: any) => {
      setClusterStatuses((st ?? []) as ClusterServerStatus[]);
      try {
        const list = await ListClusterServers();
        setClusterServers((list ?? []) as ClusterServer[]);
      } catch {}
    });
    return () => { unsub?.(); };
  }, []);
  const [profiles, setProfiles] = useState<Profile[]>([]);
  // State for ProfilesPanel — lifted here because PANELS[selected]() calls
  // the panel as a plain function, not as a JSX element, so any useState
  // inside the panel function would violate the Rules of Hooks.
  const [profileSelectedId, setProfileSelectedId] = useState<number>(0);
  const [profileNameDraft, setProfileNameDraft] = useState<string>('');

  async function reloadProfiles() {
    try {
      const list = await ListProfiles();
      setProfiles(list);
      // Refresh the active-profile editor in case activation changed.
      const ap = await GetActiveProfile();
      setActiveProfile(ap as Profile);
    } catch (e: any) {
      setErr(String(e?.message ?? e));
    }
  }

  // Keep the ProfilesPanel selector in sync with the loaded list. If the
  // currently-selected profile is gone (post-delete) or none is selected
  // yet, default to the active one.
  useEffect(() => {
    if (!profiles.length) return;
    const stillThere = profiles.some((p) => (p.id as number) === profileSelectedId);
    if (!stillThere) {
      const next = profiles.find((p) => p.is_active) ?? profiles[0];
      setProfileSelectedId(next.id as number);
      setProfileNameDraft(next.name);
    }
  }, [profiles, profileSelectedId]);

  useEffect(() => {
    (async () => {
      try {
        const [l, ls, c, ap, r, b, qd, es] = await Promise.all([
          GetLookupSettings(), GetListsSettings(), GetCATSettings(), GetActiveProfile(),
          GetRotatorSettings(), GetBackupSettings(), GetQSLDefaults(), GetExternalServices(),
        ]);
        setLookup(l);
        setActiveProfile(ap as Profile);
        setLists(ls);
        setRstText({
          phone: ((ls as any).rst_phone ?? []).join(' '),
          cw: ((ls as any).rst_cw ?? []).join(' '),
          digital: ((ls as any).rst_digital ?? []).join(' '),
        });
        await reloadProfiles();
        await reloadClusterServers();
        setCatCfg(c);
        setRotator(r);
        try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
        try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
        try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
        try { setAmps(((await GetAmplifiers()) ?? []) as AmpUI[]); } catch {}
        setBackupCfg(b as any);
        setQslDefaults(qd as any);
        setExtSvc(es as any);
        try { setDbSettings(await GetDatabaseSettings() as any); } catch {}
        try { setMysqlCfg(await GetMySQLSettings() as any); } catch {}
        try { setBackendStatus(await GetDBBackendStatus() as any); } catch {}
        try {
          const locs: any = await ListTQSLStationLocations();
          setStationLocations((locs ?? []).map((l: any) => l.name).filter(Boolean));
        } catch { /* TQSL not installed — leave the dropdown empty */ }
        try { const s: any = await GetWinkeyerSettings(); if (Array.isArray(s.macros)) { while (s.macros.length < 9) s.macros.push({ label: '', text: '' }); } setWk(s); } catch {}
        try { setWkPorts((await ListSerialPorts() ?? []) as string[]); } catch {}
        try { setAudioCfg(await GetAudioSettings() as any); } catch {}
        try { setEmailCfg(await GetEmailSettings() as any); } catch {}
        try { setEqslCfg(await QSLGetEmailTemplates() as any); } catch {}
        reloadAudioDevices();
        reloadDvk();
      } catch (e: any) {
        setErr(String(e?.message ?? e));
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  // Every setting is per-profile, so when the active profile changes WHILE this
  // dialog is open, re-read the panels (MySQL connection, CAT, audio, accounts…)
  // — otherwise they keep showing the previous profile's values until reopen.
  useEffect(() => {
    const off = EventsOn('profile:changed', () => {
      (async () => {
        try { setMysqlCfg(await GetMySQLSettings() as any); } catch {}
        try { setBackendStatus(await GetDBBackendStatus() as any); } catch {}
        try { setActiveProfile(await GetActiveProfile() as Profile); } catch {}
        try { setLookup(await GetLookupSettings() as any); } catch {}
        try { setCatCfg(await GetCATSettings() as any); } catch {}
        try { setRotator(await GetRotatorSettings() as any); } catch {}
        try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
        try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
        try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
        try { setAmps(((await GetAmplifiers()) ?? []) as AmpUI[]); } catch {}
        try { setBackupCfg(await GetBackupSettings() as any); } catch {}
        try { setQslDefaults(await GetQSLDefaults() as any); } catch {}
        try { setExtSvc(await GetExternalServices() as any); } catch {}
        try { const s: any = await GetWinkeyerSettings(); if (Array.isArray(s.macros)) { while (s.macros.length < 9) s.macros.push({ label: '', text: '' }); } setWk(s); } catch {}
        try { setAudioCfg(await GetAudioSettings() as any); } catch {}
        try { setEmailCfg(await GetEmailSettings() as any); } catch {}
        try { setEqslCfg(await QSLGetEmailTemplates() as any); } catch {}
        try {
          const ls: any = await GetListsSettings();
          setLists(ls);
          setRstText({
            phone: (ls.rst_phone ?? []).join(' '),
            cw: (ls.rst_cw ?? []).join(' '),
            digital: (ls.rst_digital ?? []).join(' '),
          });
        } catch {}
      })();
    });
    return () => { off(); };
  }, []);

  // Which (profile, callsign, grid) the auto-fill below last derived from.
  // Without it, simply OPENING the settings counted as "the source changed" and
  // overwrote the operator's own values.
  const derivedFrom = useRef<string>('');

  // Auto-fill the active profile's MY_* DXCC metadata from the station
  // callsign (country, DXCC#, CQ/ITU zones) and the grid (lat/lon).
  //
  // These are derived values, so they recompute when the callsign or grid
  // changes. What they must NOT do is recompute on a plain load: cty.dat gives
  // the zones of the ENTITY, and a large country spans several — an operator in
  // CQ 14 / ITU 27 was handed 15 and 28 and corrected them by hand, and every
  // restart put the wrong pair back. cty.dat is a starting point, not an
  // authority, so on a load we only fill fields that are EMPTY; a value the
  // operator typed stays until the callsign or grid itself changes.
  // Debounced so we don't hammer cty.dat while typing.
  useEffect(() => {
    const call = (activeProfile?.callsign ?? '').trim();
    if (!call) return;
    const grid = (activeProfile?.my_grid ?? '').trim();
    const source = `${activeProfile?.id ?? 0}|${call.toUpperCase()}|${grid.toUpperCase()}`;
    // First sight of this profile — a load, not an edit.
    const isLoad = derivedFrom.current === '' || derivedFrom.current.split('|')[0] !== String(activeProfile?.id ?? 0);
    const t = window.setTimeout(async () => {
      try {
        const i: any = await ComputeStationInfo(call, grid);
        derivedFrom.current = source;
        setActiveProfile((p) => {
          if (!p) return p;
          const patch: any = {};
          // On a load, keep in patch only what the profile does not already have.
          const put = (k: string, v: any) => {
            if (!v) return;
            const cur = (p as any)[k];
            if (isLoad && cur !== undefined && cur !== null && cur !== '' && cur !== 0) return;
            patch[k] = v;
          };
          put('my_country', i.country);
          put('my_dxcc', i.dxcc);
          put('my_cqz', i.cqz);
          put('my_ituz', i.ituz);
          put('my_lat', i.lat);
          put('my_lon', i.lon);
          // Only re-render when a value actually changed (prevents loops).
          const changed = Object.keys(patch).some((k) => (p as any)[k] !== patch[k]);
          return changed ? { ...p, ...patch } : p;
        });
      } catch { /* offline / unknown prefix — leave fields as-is */ }
    }, 250);
    return () => window.clearTimeout(t);
  }, [activeProfile?.callsign, activeProfile?.my_grid]);

  // ── Band selection helpers (dual-list shuttle) ──────────────────────────
  function addBand(tag: string) {
    const b = tag.trim().toLowerCase();
    if (!b) return;
    setLists((l) => {
      if ((l.bands ?? []).includes(b)) return l;
      return { ...l, bands: [...(l.bands ?? []), b] };
    });
  }
  function removeBand(i: number) {
    setLists((l) => {
      const next = [...(l.bands ?? [])];
      next.splice(i, 1);
      return { ...l, bands: next };
    });
  }
  function moveBand(i: number, dir: -1 | 1) {
    setLists((l) => {
      const next = [...(l.bands ?? [])];
      const j = i + dir;
      if (j < 0 || j >= next.length) return l;
      [next[i], next[j]] = [next[j], next[i]];
      return { ...l, bands: next };
    });
  }

  // ── Mode helpers ────────────────────────────────────────────────────────
  function addMode() {
    setLists((l) => ({
      ...l,
      modes: [...(l.modes ?? []), { name: '', default_rst_sent: '59', default_rst_rcvd: '59' }],
    }));
  }
  function addModeFromCatalog(m: { name: string; sent: string; rcvd: string }) {
    setLists((l) => {
      if ((l.modes ?? []).some((x) => (x.name ?? '').toUpperCase() === m.name)) return l;
      return {
        ...l,
        modes: [...(l.modes ?? []), { name: m.name, default_rst_sent: m.sent, default_rst_rcvd: m.rcvd }],
      };
    });
  }
  function addCustomMode(name: string) {
    const n = name.trim().toUpperCase();
    if (!n) return;
    setLists((l) => {
      if ((l.modes ?? []).some((x) => (x.name ?? '').toUpperCase() === n)) return l;
      return {
        ...l,
        modes: [...(l.modes ?? []), { name: n, default_rst_sent: '59', default_rst_rcvd: '59' }],
      };
    });
  }
  function removeMode(i: number) {
    setLists((l) => {
      const next = [...(l.modes ?? [])];
      next.splice(i, 1);
      return { ...l, modes: next };
    });
  }
  function moveMode(i: number, dir: -1 | 1) {
    setLists((l) => {
      const next = [...(l.modes ?? [])];
      const j = i + dir;
      if (j < 0 || j >= next.length) return l;
      [next[i], next[j]] = [next[j], next[i]];
      return { ...l, modes: next };
    });
  }
  function updateMode(i: number, patch: Partial<ModePreset>) {
    setLists((l) => {
      const next = [...(l.modes ?? [])];
      next[i] = { ...next[i], ...patch } as ModePreset;
      return { ...l, modes: next };
    });
  }

  async function save(close = true) {
    setSaving(true); setErr(''); setMsg('');
    try {
      // Bands: dedup, lowercase, trim. Order = user's drag order.
      const seen = new Set<string>();
      const bands: string[] = [];
      for (const raw of lists.bands ?? []) {
        const b = (raw ?? '').trim().toLowerCase();
        if (b && !seen.has(b)) { seen.add(b); bands.push(b); }
      }
      const modes = (lists.modes ?? [])
        .map((m) => ({
          name: (m.name ?? '').trim().toUpperCase(),
          default_rst_sent: (m.default_rst_sent ?? '').trim(),
          default_rst_rcvd: (m.default_rst_rcvd ?? '').trim(),
        }))
        .filter((m) => m.name !== '');
      const splitList = (s: string) => s.split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
      await SaveListsSettings({
        bands, modes,
        rst_phone: splitList(rstText.phone),
        rst_cw: splitList(rstText.cw),
        rst_digital: splitList(rstText.digital),
      } as any);

      if (activeProfile) {
        await SaveProfile({
          ...activeProfile,
          callsign:     (activeProfile.callsign     ?? '').trim().toUpperCase(),
          operator:     (activeProfile.operator     ?? '').trim().toUpperCase(),
          my_grid:      (activeProfile.my_grid      ?? '').trim().toUpperCase(),
          my_sota_ref:  (activeProfile.my_sota_ref  ?? '').trim().toUpperCase(),
          my_pota_ref:  (activeProfile.my_pota_ref  ?? '').trim().toUpperCase(),
        } as any);
      }
      await SaveLookupSettings(lookup as any);
      await SaveCATSettings(catCfg as any);
      await SaveRotatorSettings(rotator as any);
      await SaveUltrabeamSettings(ultrabeam as any);
      await SaveAntGeniusSettings(antgenius as any);
      await SaveTunerGeniusSettings(tunergenius as any);
      await SaveAmplifiers(amps as any);
      await SaveWinkeyerSettings(wk as any);
      await SaveAudioSettings(audioCfg as any);
      await SaveEmailSettings(emailCfg as any);
      await QSLSaveEmailTemplates(eqslCfg as any);
      await SaveBackupSettings(backupCfg as any);
      await SaveQSLDefaults(qslDefaults as any);
      await SaveExternalServices(extSvc as any);
      await SetClusterAutoConnect(clusterAutoConnect);

      setMsg('Settings saved.');
      onSaved();
      if (close) setTimeout(onClose, 500);
      else setTimeout(() => setMsg(''), 2000);
    } catch (e: any) {
      setErr(String(e?.message ?? e));
    } finally {
      setSaving(false);
    }
  }

  async function clearCache() {
    setClearing(true); setErr(''); setMsg('');
    try {
      await ClearLookupCache();
      setMsg('Cache cleared.');
    } catch (e: any) {
      setErr(String(e?.message ?? e));
    } finally {
      setClearing(false);
    }
  }

  async function testProvider(provider: 'qrz' | 'hamqth') {
    setLookupTesting((s) => ({ ...s, [provider]: true }));
    setLookupTest((s) => ({ ...s, [provider]: undefined }));
    const user = provider === 'qrz' ? lookup.qrz_user : lookup.hamqth_user;
    const pwd  = provider === 'qrz' ? lookup.qrz_password : lookup.hamqth_password;
    try {
      const r = await TestLookupProvider(provider, '', user ?? '', pwd ?? '');
      setLookupTest((s) => ({ ...s, [provider]: { ok: true, msg: `OK — ${r.callsign} (${r.name || r.country || 'no name'})` } }));
    } catch (e: any) {
      setLookupTest((s) => ({ ...s, [provider]: { ok: false, msg: String(e?.message ?? e) } }));
    } finally {
      setLookupTesting((s) => ({ ...s, [provider]: false }));
    }
  }

  const breadcrumb = useMemo(() => { const k = SECTION_KEY[selected]; return k ? t(k) : (SECTION_LABELS[selected] ?? selected); }, [selected, t]);

  // === Section content renderers ===

  function StationPanel() {
    if (!activeProfile) {
      return <div className="text-muted-foreground text-sm">{t('station.loading')}</div>;
    }
    const p = activeProfile;
    return (
      <>
        <SectionHeader
          title={t('sec.station')}
          hint={t('station.hint', { name: p.name })}
        />
        <div className="grid grid-cols-2 gap-3 max-w-2xl">
          <div className="space-y-1">
            <Label>{t('station.stationCall')}</Label>
            <Input className="font-mono uppercase" value={p.callsign ?? ''} onChange={(e) => updateActive({ callsign: e.target.value })} placeholder="F4XYZ" />
            <div className="text-[10px] text-muted-foreground">{t('station.stationCallHint')}</div>
          </div>
          <div className="space-y-1">
            <Label>{t('station.opCall')}</Label>
            <Input className="font-mono uppercase" value={p.operator ?? ''} onChange={(e) => updateActive({ operator: e.target.value })} placeholder="F4XYZ" />
            <div className="text-[10px] text-muted-foreground">{t('station.opCallHint')}</div>
          </div>
          <div className="space-y-1 col-span-2">
            <Label>{t('station.ownerCall')}</Label>
            <Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder={t('station.ownerBlank')} />
            <div className="text-[10px] text-muted-foreground">{t('station.ownerHint')}</div>
          </div>
          <div className="space-y-1 col-span-2">
            <Label>{t('station.opName')}</Label>
            <Input className="max-w-xs" value={p.op_name ?? ''} onChange={(e) => updateActive({ op_name: e.target.value })} placeholder="e.g. Greg" />
            <div className="text-[10px] text-muted-foreground">{t('station.opNameHint')}</div>
          </div>
          <div className="col-span-2 text-[10px] text-muted-foreground uppercase tracking-wider mt-1">
            {t('station.autofill')}
          </div>
          <div className="space-y-1">
            <Label>{t('station.myGrid')}</Label>
            <Input className="font-mono uppercase" value={p.my_grid ?? ''} onChange={(e) => updateActive({ my_grid: e.target.value })} placeholder="JN18BU" />
          </div>
          <div className="space-y-1">
            <Label>{t('station.myCountry')}</Label>
            <Input value={p.my_country ?? ''} onChange={(e) => updateActive({ my_country: e.target.value })} placeholder="France" />
          </div>
          <div className="grid grid-cols-3 gap-2 col-span-2">
            <div className="space-y-1">
              <Label>{t('station.dxcc')}</Label>
              <Input type="number" className="font-mono" value={(p as any).my_dxcc ?? ''}
                onChange={(e) => updateActive({ my_dxcc: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
            </div>
            <div className="space-y-1">
              <Label>{t('station.cqz')}</Label>
              <Input type="number" className="font-mono" value={(p as any).my_cqz ?? ''}
                onChange={(e) => updateActive({ my_cqz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
            </div>
            <div className="space-y-1">
              <Label>{t('station.ituz')}</Label>
              <Input type="number" className="font-mono" value={(p as any).my_ituz ?? ''}
                onChange={(e) => updateActive({ my_ituz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
            </div>
            <div className="space-y-1">
              <Label>{t('station.lat')}</Label>
              <Input type="number" step="0.001" className="font-mono" value={(p as any).my_lat ?? ''}
                onChange={(e) => updateActive({ my_lat: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
            </div>
            <div className="space-y-1">
              <Label>{t('station.lon')}</Label>
              <Input type="number" step="0.001" className="font-mono" value={(p as any).my_lon ?? ''}
                onChange={(e) => updateActive({ my_lon: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
            </div>
          </div>
          <div className="space-y-1">
            <Label>{t('station.state')}</Label>
            <Input value={p.my_state ?? ''} onChange={(e) => updateActive({ my_state: e.target.value })} />
          </div>
          <div className="space-y-1">
            <Label>{t('station.county')}</Label>
            <Input value={p.my_cnty ?? ''} onChange={(e) => updateActive({ my_cnty: e.target.value })} />
          </div>
          <div className="space-y-1 col-span-2">
            <Label>{t('station.street')}</Label>
            <Input value={p.my_street ?? ''} onChange={(e) => updateActive({ my_street: e.target.value })} />
          </div>
          <div className="space-y-1">
            <Label>{t('station.postal')}</Label>
            <Input value={p.my_postal_code ?? ''} onChange={(e) => updateActive({ my_postal_code: e.target.value })} />
          </div>
          <div className="space-y-1">
            <Label>{t('station.city')}</Label>
            <Input value={p.my_city ?? ''} onChange={(e) => updateActive({ my_city: e.target.value })} />
          </div>
          <div className="space-y-1">
            <Label>{t('station.sota')}</Label>
            <Input className="font-mono uppercase" value={p.my_sota_ref ?? ''} onChange={(e) => updateActive({ my_sota_ref: e.target.value })} placeholder="F/AB-001" />
          </div>
          <div className="space-y-1">
            <Label>{t('station.pota')}</Label>
            <Input className="font-mono uppercase" value={p.my_pota_ref ?? ''} onChange={(e) => updateActive({ my_pota_ref: e.target.value })} placeholder="FF-1234" />
          </div>
        </div>
      </>
    );
  }

  // Profile actions — kept at the SettingsModal level so the ProfilesPanel
  // renderer can stay hooks-free (the PANELS map calls it as a plain
  // function, not as a JSX component).
  const activeProfileObj = profiles.find((p) => p.is_active) ?? profiles[0];
  const currentProfile = profiles.find((p) => (p.id as number) === profileSelectedId);

  async function profileActivate() {
    if (!currentProfile) return;
    try {
      await ActivateProfile(currentProfile.id as number);
      await reloadProfiles();
      // Per-profile settings follow the active identity — reload the panels
      // that are now scoped to the newly-active profile.
      const [ap, qd, es] = await Promise.all([GetActiveProfile(), GetQSLDefaults(), GetExternalServices()]);
      setActiveProfile(ap as Profile);
      setQslDefaults(qd as any);
      setExtSvc(es as any);
      onSaved();
    }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function profileRemove() {
    if (!currentProfile) return;
    if (!confirm(t('prof.deleteConfirm', { name: currentProfile.name }))) return;
    try { await DeleteProfile(currentProfile.id as number); await reloadProfiles(); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function profileDuplicate() {
    if (!currentProfile) return;
    const name = prompt(t('prof.dupPrompt', { name: currentProfile.name }), t('prof.dupSuffix', { name: currentProfile.name }));
    if (!name?.trim()) return;
    try {
      const dup = await DuplicateProfile(currentProfile.id as number, name.trim());
      await reloadProfiles();
      setProfileSelectedId(dup.id as number);
      setProfileNameDraft(dup.name);
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function profileCreateBlank() {
    const name = prompt(t('prof.newPrompt'), t('prof.newDefault'));
    if (!name?.trim()) return;
    try {
      const blank = emptyProfile();
      blank.name = name.trim();
      const saved = await SaveProfile(blank as any);
      await reloadProfiles();
      setProfileSelectedId(saved.id as number);
      setProfileNameDraft(saved.name);
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function profileRenameCurrent() {
    if (!currentProfile || profileNameDraft.trim() === currentProfile.name) return;
    try {
      await SaveProfile({ ...currentProfile, name: profileNameDraft.trim() } as any);
      await reloadProfiles();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }

  function ProfilesPanel() {
    const current = currentProfile;
    const active = activeProfileObj;
    return (
      <>
        <SectionHeader
          title={t('sec.profiles')}
          hint={t('prof.hint')}
        />
        <div className="space-y-4">
          <div className="grid grid-cols-[140px_1fr] items-center gap-x-3 gap-y-3">
            <Label>{t('prof.configId')}</Label>
            <Select
              value={String(profileSelectedId)}
              onValueChange={(v) => {
                const id = parseInt(v, 10);
                setProfileSelectedId(id);
                setProfileNameDraft(profiles.find((p) => (p.id as number) === id)?.name ?? '');
              }}
            >
              <SelectTrigger><SelectValue /></SelectTrigger>
              <SelectContent>
                {profiles.map((p) => (
                  <SelectItem key={p.id as number} value={String(p.id)}>
                    {p.name}{p.callsign ? ` — ${p.callsign}` : ''}{p.is_active ? t('prof.activeSuffix') : ''}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>

            <Label>{t('prof.description')}</Label>
            <div className="flex items-center gap-2">
              <Input
                value={profileNameDraft}
                onChange={(e) => setProfileNameDraft(e.target.value)}
                onBlur={profileRenameCurrent}
                placeholder={t('prof.profileName')}
                disabled={!current}
              />
              {current?.is_active && (
                <span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold tracking-wider bg-success-muted text-success-muted-foreground border border-success-border">
                  {t('prof.active')}
                </span>
              )}
            </div>
          </div>

          <div className="flex items-center gap-2 pt-2 border-t border-border">
            <Button variant="outline" size="sm" onClick={profileCreateBlank} title={t('prof.newTitle')}>
              <Plus className="size-3.5 mr-1" /> {t('prof.new')}
            </Button>
            <Button variant="outline" size="sm" onClick={profileDuplicate} disabled={!current} title={t('prof.dupTitle')}>
              <Copy className="size-3.5 mr-1" /> {t('prof.duplicate')}
            </Button>
            <Button
              variant="outline" size="sm"
              onClick={profileActivate}
              disabled={!current || current.is_active}
              title={t('prof.setActiveTitle')}
            >
              <Star className="size-3.5 mr-1" /> {t('prof.setActive')}
            </Button>
            <Button
              variant="outline" size="sm"
              onClick={profileRemove}
              disabled={!current || profiles.length <= 1}
              className="text-destructive hover:text-destructive ml-auto"
              title={profiles.length <= 1 ? t('prof.cantDeleteLast') : t('prof.deleteTitle')}
            >
              <Trash2 className="size-3.5 mr-1" /> {t('prof.delete')}
            </Button>
          </div>

          {current && !current.is_active && (
            <div className="text-xs text-muted-foreground bg-muted/30 border border-border rounded-md p-2.5">
              {t('prof.viewingNote', { name: current.name, active: active?.name ?? '' })}
            </div>
          )}
        </div>
      </>
    );
  }

  function LookupPanel() {
    // Per-row provider editor — kept inline because it's only used twice
    // and needs closure access to the parent state.
    const row = (
      key: 'qrz' | 'hamqth', label: string, userField: 'qrz_user' | 'hamqth_user',
      pwdField: 'qrz_password' | 'hamqth_password',
    ) => {
      const test = lookupTest[key];
      const testing = lookupTesting[key];
      const hasCreds = !!(lookup[userField] && lookup[pwdField]);
      return (
        <tr className="border-t border-border align-middle">
          <td className="px-3 py-2 font-semibold whitespace-nowrap">{label}</td>
          <td className="px-2 py-2 text-center">
            <input
              type="radio"
              name="lookup-primary"
              checked={lookup.primary === key}
              onChange={() => setLookup((s) => ({ ...s, primary: key, failsafe: s.failsafe === key ? '' : s.failsafe }))}
              disabled={!hasCreds}
            />
          </td>
          <td className="px-2 py-2 text-center">
            <input
              type="radio"
              name="lookup-failsafe"
              checked={lookup.failsafe === key}
              onChange={() => setLookup((s) => ({ ...s, failsafe: key, primary: s.primary === key ? '' : s.primary }))}
              disabled={!hasCreds || lookup.primary === key}
            />
          </td>
          <td className="px-2 py-2">
            <Input
              className="h-8"
              value={lookup[userField] ?? ''}
              onChange={(e) => setLookup((s) => ({ ...s, [userField]: e.target.value }))}
              placeholder={t('lk.user')}
              autoComplete="off"
            />
          </td>
          <td className="px-2 py-2">
            <Input
              className="h-8"
              type="password"
              value={lookup[pwdField] ?? ''}
              onChange={(e) => setLookup((s) => ({ ...s, [pwdField]: e.target.value }))}
              placeholder={t('lk.password')}
              autoComplete="off"
            />
          </td>
          <td className="px-2 py-2 whitespace-nowrap">
            <Button
              variant="outline" size="sm"
              onClick={() => testProvider(key)}
              disabled={!hasCreds || testing}
              title={t('lk.testTitle')}
            >
              {testing ? t('lk.testing') : t('lk.test')}
            </Button>
          </td>
          <td className={cn('px-2 py-2 text-xs', test?.ok ? 'text-success' : 'text-destructive')}>
            {test?.msg}
          </td>
        </tr>
      );
    };
    return (
      <>
        <SectionHeader
          title={t('sec.lookup')}
          hint={t('lk.hint')}
        />
        <div className="rounded-md border border-border overflow-hidden">
          <table className="w-full text-sm">
            <thead className="bg-muted/40 text-[10px] uppercase tracking-wider text-muted-foreground">
              <tr>
                <th className="text-left px-3 py-2">{t('lk.provider')}</th>
                <th className="px-2 py-2 w-20">{t('lk.primary')}</th>
                <th className="px-2 py-2 w-20">{t('lk.failsafe')}</th>
                <th className="text-left px-2 py-2">{t('lk.user')}</th>
                <th className="text-left px-2 py-2">{t('lk.password')}</th>
                <th className="px-2 py-2 w-20"></th>
                <th className="text-left px-2 py-2">{t('lk.result')}</th>
              </tr>
            </thead>
            <tbody>
              {row('qrz',    'QRZ.com', 'qrz_user',    'qrz_password')}
              {row('hamqth', 'HamQTH',  'hamqth_user', 'hamqth_password')}
            </tbody>
          </table>
        </div>
        <p className="text-xs text-muted-foreground mt-2">
          {t('lk.failsafeNote')}
        </p>

        <div className="mt-6 pt-4 border-t border-border">
          <h3 className="text-sm font-semibold mb-2">{t('lk.display')}</h3>
          <label className="flex items-start gap-2 text-sm cursor-pointer">
            <Checkbox
              checked={lookup.download_images}
              onCheckedChange={(c) => setLookup((s) => ({ ...s, download_images: !!c }))}
              className="mt-0.5"
            />
            <span>
              {t('lk.showPics')}
              <span className="block text-xs text-muted-foreground mt-0.5">
                {t('lk.showPicsHint')}
              </span>
            </span>
          </label>
        </div>

        <div className="mt-6 pt-4 border-t border-border">
          <h3 className="text-sm font-semibold mb-2">{t('lk.cache')}</h3>
          <p className="text-xs text-muted-foreground mb-3">
            {t('lk.cacheHint')}
          </p>
          <div className="flex gap-3 items-end">
            <div className="space-y-1 w-40">
              <Label>{t('lk.ttl')}</Label>
              <Input
                type="number" min={1} max={3650}
                value={lookup.cache_ttl_days}
                onChange={(e) => setLookup((s) => ({ ...s, cache_ttl_days: parseInt(e.target.value) || 30 }))}
              />
            </div>
            <Button variant="outline" onClick={clearCache} disabled={clearing}>
              {clearing ? t('lk.clearing') : t('lk.clearCache')}
            </Button>
          </div>
        </div>
      </>
    );
  }

  function BandsPanel() {
    const selected = lists.bands ?? [];
    const selectedSet = new Set(selected.map((b) => (b ?? '').toLowerCase()));
    const available = BAND_CATALOG.filter((b) => !selectedSet.has(b.toLowerCase()));
    return (
      <>
        <SectionHeader
          title={t('sec.bands')}
          hint={t('bnd.hint')}
        />
        <div className="grid grid-cols-[1fr_auto_1fr] gap-3 max-w-3xl">
          {/* Left: available catalog */}
          <div className="rounded-md border border-border overflow-hidden">
            <div className="px-3 py-1.5 bg-muted text-[10px] uppercase tracking-wider font-semibold text-muted-foreground">
              {t('bnd.available')}
            </div>
            <div className="max-h-[320px] overflow-y-auto divide-y divide-border">
              {available.length === 0 ? (
                <div className="px-3 py-2 text-xs text-muted-foreground italic">{t('bnd.allSelected')}</div>
              ) : (
                available.map((b) => (
                  <button
                    key={b}
                    type="button"
                    onDoubleClick={() => addBand(b)}
                    onClick={() => addBand(b)}
                    className="w-full text-left px-3 py-1.5 text-sm font-mono hover:bg-accent/40 transition-colors"
                  >
                    {b}
                  </button>
                ))
              )}
            </div>
            <div className="flex gap-1 px-2 py-1.5 border-t border-border bg-muted/40">
              <Input
                value={bandDraft}
                onChange={(e) => setBandDraft(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') {
                    addBand(bandDraft);
                    setBandDraft('');
                  }
                }}
                placeholder={t('bnd.customPh')}
                className="font-mono h-7 text-xs"
              />
              <Button
                size="sm"
                variant="outline"
                className="h-7"
                onClick={() => { addBand(bandDraft); setBandDraft(''); }}
                disabled={!bandDraft.trim()}
              >
                <Plus className="size-3" />
              </Button>
            </div>
          </div>

          {/* Center: shuttle hint */}
          <div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
            <ArrowRight className="size-4" />
            <ArrowLeft className="size-4" />
          </div>

          {/* Right: selected */}
          <div className="rounded-md border border-border overflow-hidden">
            <div className="px-3 py-1.5 bg-muted text-[10px] uppercase tracking-wider font-semibold text-muted-foreground flex items-center justify-between">
              <span>{t('bnd.selected', { n: selected.length })}</span>
            </div>
            <div className="max-h-[360px] overflow-y-auto divide-y divide-border">
              {selected.length === 0 ? (
                <div className="px-3 py-2 text-xs text-muted-foreground italic">
                  {t('bnd.none')}
                </div>
              ) : (
                selected.map((b, i) => (
                  <div key={`${b}-${i}`} className="grid grid-cols-[auto_1fr_auto] items-center gap-1 px-2 py-1">
                    <div className="flex gap-0.5">
                      <Button size="icon" variant="ghost" className="size-6" onClick={() => moveBand(i, -1)} disabled={i === 0}>
                        <ArrowUp className="size-3" />
                      </Button>
                      <Button size="icon" variant="ghost" className="size-6" onClick={() => moveBand(i, 1)} disabled={i === selected.length - 1}>
                        <ArrowDown className="size-3" />
                      </Button>
                    </div>
                    <span className="font-mono text-sm">{b}</span>
                    <Button size="icon" variant="ghost" className="size-6 text-destructive hover:bg-destructive/10" onClick={() => removeBand(i)}>
                      <Trash2 className="size-3" />
                    </Button>
                  </div>
                ))
              )}
            </div>
          </div>
        </div>
      </>
    );
  }

  function ModesPanel() {
    const selected = lists.modes ?? [];
    const selectedSet = new Set(selected.map((m) => (m.name ?? '').toUpperCase()));
    const available = MODE_CATALOG.filter((m) => !selectedSet.has(m.name));
    return (
      <>
        <SectionHeader
          title={t('sec.modes')}
          hint={t('mds.hint')}
        />
        <div className="grid grid-cols-[1fr_auto_1.5fr] gap-3 max-w-4xl">
          {/* Left: available catalog */}
          <div className="rounded-md border border-border overflow-hidden">
            <div className="px-3 py-1.5 bg-muted text-[10px] uppercase tracking-wider font-semibold text-muted-foreground">
              {t('bnd.available')}
            </div>
            <div className="max-h-[360px] overflow-y-auto divide-y divide-border">
              {available.length === 0 ? (
                <div className="px-3 py-2 text-xs text-muted-foreground italic">{t('mds.allSelected')}</div>
              ) : (
                available.map((m) => (
                  <button
                    key={m.name}
                    type="button"
                    onDoubleClick={() => addModeFromCatalog(m)}
                    onClick={() => addModeFromCatalog(m)}
                    className="w-full text-left px-3 py-1.5 text-sm font-mono hover:bg-accent/40 transition-colors flex items-center justify-between gap-2"
                    title={t('mds.defaultRstTitle', { sent: m.sent, rcvd: m.rcvd })}
                  >
                    <span>{m.name}</span>
                    <span className="text-[10px] text-muted-foreground">{m.sent}/{m.rcvd}</span>
                  </button>
                ))
              )}
            </div>
            <div className="flex gap-1 px-2 py-1.5 border-t border-border bg-muted/40">
              <Input
                value={modeDraft}
                onChange={(e) => setModeDraft(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') {
                    addCustomMode(modeDraft);
                    setModeDraft('');
                  }
                }}
                placeholder={t('mds.customPh')}
                className="font-mono uppercase h-7 text-xs"
              />
              <Button
                size="sm"
                variant="outline"
                className="h-7"
                onClick={() => { addCustomMode(modeDraft); setModeDraft(''); }}
                disabled={!modeDraft.trim()}
              >
                <Plus className="size-3" />
              </Button>
            </div>
          </div>

          {/* Center: shuttle hint */}
          <div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
            <ArrowRight className="size-4" />
            <ArrowLeft className="size-4" />
          </div>

          {/* Right: selected with editable RST */}
          <div className="rounded-md border border-border overflow-hidden">
            <div className="grid grid-cols-[auto_1fr_72px_72px_auto] gap-2 px-3 py-1.5 bg-muted text-[10px] uppercase tracking-wider font-semibold text-muted-foreground">
              <span className="w-12">{t('mds.order')}</span>
              <span>{t('mds.mode')}</span>
              <span>{t('mds.rstSnt')}</span>
              <span>{t('mds.rstRcv')}</span>
              <span className="w-6"></span>
            </div>
            <div className="max-h-[360px] overflow-y-auto divide-y divide-border">
              {selected.length === 0 ? (
                <div className="px-3 py-2 text-xs text-muted-foreground italic">
                  {t('mds.none')}
                </div>
              ) : (
                selected.map((m, i) => (
                  <div key={i} className="grid grid-cols-[auto_1fr_72px_72px_auto] gap-2 px-3 py-1 items-center">
                    <div className="flex gap-0.5 w-12">
                      <Button size="icon" variant="ghost" className="size-6" onClick={() => moveMode(i, -1)} disabled={i === 0}>
                        <ArrowUp className="size-3" />
                      </Button>
                      <Button size="icon" variant="ghost" className="size-6" onClick={() => moveMode(i, 1)} disabled={i === selected.length - 1}>
                        <ArrowDown className="size-3" />
                      </Button>
                    </div>
                    <Input className="font-mono uppercase h-7" value={m.name} onChange={(e) => updateMode(i, { name: e.target.value })} placeholder="SSB" />
                    <Input className="font-mono h-7" value={m.default_rst_sent ?? ''} onChange={(e) => updateMode(i, { default_rst_sent: e.target.value })} placeholder="59" />
                    <Input className="font-mono h-7" value={m.default_rst_rcvd ?? ''} onChange={(e) => updateMode(i, { default_rst_rcvd: e.target.value })} placeholder="59" />
                    <Button size="icon" variant="ghost" className="size-6 text-destructive hover:bg-destructive/10" onClick={() => removeMode(i)}>
                      <Trash2 className="size-3" />
                    </Button>
                  </div>
                ))
              )}
            </div>
            <div className="px-3 py-1.5 border-t border-border bg-muted/40">
              <Button variant="ghost" size="sm" onClick={addMode} className="h-6 text-xs">
                <Plus className="size-3" /> {t('mds.addBlank')}
              </Button>
            </div>
          </div>
        </div>

        {/* RST report lists — the dropdown choices in the entry form. */}
        <div className="mt-6 max-w-4xl">
          <div className="text-sm font-semibold mb-1">{t('mds.rstLists')}</div>
          <div className="text-[11px] text-muted-foreground mb-2">
            {t('mds.rstListsHint')}
          </div>
          <div className="grid grid-cols-3 gap-3">
            <div className="space-y-1">
              <Label className="text-xs">{t('mds.phone')}</Label>
              <Textarea rows={8} className="font-mono text-xs" value={rstText.phone} onChange={(e) => setRstText((s) => ({ ...s, phone: e.target.value }))} />
            </div>
            <div className="space-y-1">
              <Label className="text-xs">{t('mds.cw')}</Label>
              <Textarea rows={8} className="font-mono text-xs" value={rstText.cw} onChange={(e) => setRstText((s) => ({ ...s, cw: e.target.value }))} />
            </div>
            <div className="space-y-1">
              <Label className="text-xs">{t('mds.digital')}</Label>
              <Textarea rows={8} className="font-mono text-xs" value={rstText.digital} onChange={(e) => setRstText((s) => ({ ...s, digital: e.target.value }))} />
            </div>
          </div>
        </div>
      </>
    );
  }

  function CATPanel() {
    return (
      <>
        <SectionHeader
          title={t('sec.cat')}
          hint={t('cat.hint')}
        />
        <div className="space-y-4 max-w-3xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={catCfg.enabled} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, enabled: !!c }))} />
            {t('cat.enable')}
          </label>

          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-1">
              <Label>{t('cat.backend')}</Label>
              <Select value={catCfg.backend} onValueChange={(v) => setCatCfg((s) => ({ ...s, backend: v }))}>
                <SelectTrigger><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="omnirig">{t('cat.optOmnirig')}</SelectItem>
                  <SelectItem value="flex">{t('cat.optFlex')}</SelectItem>
                  <SelectItem value="yaesu">{t('cat.optYaesu')}</SelectItem>
                  <SelectItem value="kenwood">{t('cat.optKenwood')}</SelectItem>
                  <SelectItem value="xiegu">{t('cat.optXiegu')}</SelectItem>
                  <SelectItem value="icom">{t('cat.optIcom')}</SelectItem>
                  <SelectItem value="icom-net">{t('cat.optIcomNet')}</SelectItem>
                  <SelectItem value="tci">{t('cat.optTci')}</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {catCfg.backend === 'omnirig' && (
            <div className="space-y-1">
              <Label>{t('cat.omnirigRig')}</Label>
              <Select value={String(catCfg.omnirig_rig)} onValueChange={(v) => setCatCfg((s) => ({ ...s, omnirig_rig: parseInt(v) as 1 | 2 }))}>
                <SelectTrigger><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="1">Rig 1</SelectItem>
                  <SelectItem value="2">Rig 2</SelectItem>
                </SelectContent>
              </Select>
            </div>
            )}
            {catCfg.backend === 'omnirig' && (
            <div className="space-y-1">
              <Label>{t('cat.omnirigVfo')}</Label>
              <Select value={catCfg.omnirig_vfo || 'auto'}
                onValueChange={(v) => setCatCfg((s) => ({ ...s, omnirig_vfo: v === 'auto' ? '' : v }))}>
                <SelectTrigger><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="auto">{t('cat.omnirigVfoAuto')}</SelectItem>
                  <SelectItem value="A">{t('cat.omnirigVfoA')}</SelectItem>
                  <SelectItem value="B">{t('cat.omnirigVfoB')}</SelectItem>
                </SelectContent>
              </Select>
              <p className="text-[10px] text-muted-foreground">{t('cat.omnirigVfoHint')}</p>
            </div>
            )}
            {catCfg.backend === 'flex' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.flexIp')}</Label>
                <Input placeholder="192.168.1.50" value={catCfg.flex_host ?? ''}
                  onChange={(e) => setCatCfg((s) => ({ ...s, flex_host: e.target.value }))} />
              </div>
              <div className="space-y-1">
                <Label>{t('cat.port')}</Label>
                <PortInput value={catCfg.flex_port || 4992}
                  onChange={(n) => setCatCfg((s) => ({ ...s, flex_port: n }))} fallback={4992} />
              </div>
              <div className="col-span-2">
                <FlexDiscover onPick={(ip, port) => setCatCfg((s) => ({ ...s, flex_host: ip, flex_port: port }))} />
              </div>
              <label className="col-span-2 flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox checked={!!catCfg.flex_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_spots: !!c }))} />
                {t('cat.flexSpots')} <span className="text-xs text-muted-foreground">{t('cat.flexSpotsHint')}</span>
              </label>
              <label className="col-span-2 flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox checked={!!catCfg.flex_decode_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, flex_decode_spots: !!c }))} />
                {t('cat.flexDecodeSpots')} <span className="text-xs text-muted-foreground">{t('cat.flexDecodeSpotsHint')}</span>
              </label>
              {catCfg.flex_decode_spots && (
                <div className="col-span-2 flex items-center gap-2 pl-6">
                  <Label className="text-sm">{t('cat.flexDecodeSecs')}</Label>
                  <Input type="number" className="w-24" min={10} max={3600}
                    value={catCfg.flex_decode_secs || 120}
                    onChange={(e) => setCatCfg((s) => ({ ...s, flex_decode_secs: parseInt(e.target.value) || 120 }))} />
                  <span className="text-xs text-muted-foreground">{t('cat.flexDecodeSecsHint')}</span>
                </div>
              )}
            </>
            )}
            {catCfg.backend === 'xiegu' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.xieguPort')}</Label>
                <div className="flex gap-2">
                  <Select value={catCfg.xiegu_port || ''} onValueChange={(v) => setCatCfg((s) => ({ ...s, xiegu_port: v }))}>
                    <SelectTrigger><SelectValue placeholder={t('cat.selectCom')} /></SelectTrigger>
                    <SelectContent>
                      {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('cat.noPorts')}</SelectItem>}
                      {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <Button type="button" variant="outline" size="sm"
                    onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>↻</Button>
                </div>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.baud')}</Label>
                <Select value={String(catCfg.xiegu_baud || 19200)} onValueChange={(v) => setCatCfg((s) => ({ ...s, xiegu_baud: parseInt(v) || 19200 }))}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[4800, 9600, 19200, 38400, 57600, 115200].map((r) => <SelectItem key={r} value={String(r)}>{r}</SelectItem>)}
                  </SelectContent>
                </Select>
                <span className="text-xs text-muted-foreground">{t('cat.xieguBaudHint')}</span>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.civAddr')}</Label>
                <Input value={'0x' + (catCfg.xiegu_addr || 0x70).toString(16).toUpperCase().padStart(2, '0')}
                  onChange={(e) => {
                    const n = parseInt(e.target.value.replace(/^0x/i, ''), 16);
                    if (!isNaN(n) && n > 0 && n <= 0xFF) setCatCfg((s) => ({ ...s, xiegu_addr: n }));
                  }} />
                <span className="text-xs text-muted-foreground">{t('cat.xieguAddrHint')}</span>
              </div>
            </>
            )}
            {catCfg.backend === 'yaesu' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.yaesuPort')}</Label>
                <div className="flex gap-2">
                  <Select value={catCfg.yaesu_port || ''} onValueChange={(v) => setCatCfg((s) => ({ ...s, yaesu_port: v }))}>
                    <SelectTrigger><SelectValue placeholder={t('cat.selectCom')} /></SelectTrigger>
                    <SelectContent>
                      {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('cat.noPorts')}</SelectItem>}
                      {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <Button type="button" variant="outline" size="sm"
                    onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>↻</Button>
                </div>
                <span className="text-xs text-muted-foreground">{t('cat.yaesuPortHint')}</span>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.baud')}</Label>
                <Select value={String(catCfg.yaesu_baud || 38400)} onValueChange={(v) => setCatCfg((s) => ({ ...s, yaesu_baud: parseInt(v) || 38400 }))}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[4800, 9600, 19200, 38400, 57600, 115200].map((r) => <SelectItem key={r} value={String(r)}>{r}</SelectItem>)}
                  </SelectContent>
                </Select>
                <span className="text-xs text-muted-foreground">{t('cat.yaesuBaudHint')}</span>
              </div>
            </>
            )}
            {['icom', 'xiegu', 'kenwood'].includes(catCfg.backend) && (
              <div className="border-t border-border/60 pt-3">
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={civTrace} onCheckedChange={(c) => { setCivTrace(!!c); SetCIVTrace(!!c); }} />
                  {t('cat.civTrace')}
                </label>
                <p className="text-[10px] text-muted-foreground mt-1">{t('cat.civTraceHint')}</p>
              </div>
            )}
            {catCfg.backend === 'kenwood' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.kenwoodPort')}</Label>
                <div className="flex gap-2">
                  <Select value={catCfg.kenwood_port || ''} onValueChange={(v) => setCatCfg((s) => ({ ...s, kenwood_port: v }))}>
                    <SelectTrigger><SelectValue placeholder={t('cat.selectCom')} /></SelectTrigger>
                    <SelectContent>
                      {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('cat.noPorts')}</SelectItem>}
                      {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <Button type="button" variant="outline" size="sm"
                    onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>↻</Button>
                </div>
                <span className="text-xs text-muted-foreground">{t('cat.kenwoodPortHint')}</span>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.baud')}</Label>
                <Select value={String(catCfg.kenwood_baud || 9600)} onValueChange={(v) => setCatCfg((s) => ({ ...s, kenwood_baud: parseInt(v) || 9600 }))}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[4800, 9600, 19200, 38400, 57600, 115200].map((r) => <SelectItem key={r} value={String(r)}>{r}</SelectItem>)}
                  </SelectContent>
                </Select>
                <span className="text-xs text-muted-foreground">{t('cat.kenwoodBaudHint')}</span>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.kenwoodHost')}</Label>
                <Input
                  value={catCfg.kenwood_host || ''}
                  placeholder="192.168.1.50:4532"
                  onChange={(e) => setCatCfg((s) => ({ ...s, kenwood_host: e.target.value }))}
                />
                <span className="text-xs text-muted-foreground">{t('cat.kenwoodHostHint')}</span>
              </div>
            </>
            )}
            {catCfg.backend === 'icom' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.icomPort')}</Label>
                <div className="flex gap-2">
                  <Select value={catCfg.icom_port || ''} onValueChange={(v) => setCatCfg((s) => ({ ...s, icom_port: v }))}>
                    <SelectTrigger><SelectValue placeholder={t('cat.selectCom')} /></SelectTrigger>
                    <SelectContent>
                      {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('cat.noPorts')}</SelectItem>}
                      {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <Button type="button" variant="outline" size="sm"
                    onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>↻</Button>
                </div>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.baud')}</Label>
                <Select value={String(catCfg.icom_baud || 115200)} onValueChange={(v) => setCatCfg((s) => ({ ...s, icom_baud: parseInt(v) || 115200 }))}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[4800, 9600, 19200, 38400, 57600, 115200].map((r) => <SelectItem key={r} value={String(r)}>{r}</SelectItem>)}
                  </SelectContent>
                </Select>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.icomModel')}</Label>
                <Select
                  value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
                  onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
                    <SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.civAddr')}</Label>
                <Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
                  onChange={(e) => { const n = parseInt(e.target.value.replace(/[^0-9a-fA-F]/g, ''), 16); setCatCfg((s) => ({ ...s, icom_addr: (n >= 0 && n <= 0xFF) ? n : s.icom_addr })); }} />
              </div>
              <p className="col-span-2 text-xs text-muted-foreground">{t('cat.civHint')}</p>
            </>
            )}
            {catCfg.backend === 'icom-net' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.icomNetHost')}</Label>
                <Input placeholder="192.168.1.60" value={catCfg.icom_net_host ?? ''}
                  onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_host: e.target.value }))} />
              </div>
              <div className="space-y-1">
                <Label>{t('cat.icomModel')}</Label>
                <Select
                  value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
                  onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
                  <SelectTrigger><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
                    <SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>
              <div className="space-y-1">
                <Label>{t('cat.civAddr')}</Label>
                <Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
                  onChange={(e) => { const n = parseInt(e.target.value.replace(/[^0-9a-fA-F]/g, ''), 16); setCatCfg((s) => ({ ...s, icom_addr: (n >= 0 && n <= 0xFF) ? n : s.icom_addr })); }} />
              </div>
              <div className="space-y-1">
                <Label>{t('cat.icomNetUser')}</Label>
                <Input value={catCfg.icom_net_user ?? ''}
                  onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_user: e.target.value }))} />
              </div>
              <div className="space-y-1">
                <Label>{t('cat.icomNetPass')}</Label>
                <Input type="password" value={catCfg.icom_net_pass ?? ''}
                  onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_pass: e.target.value }))} />
              </div>
              <p className="col-span-2 text-xs text-muted-foreground">{t('cat.icomNetHint')}</p>
              <label className="col-span-2 flex items-start gap-2 text-sm cursor-pointer">
                <Checkbox checked={!!(catCfg as any).icom_net_audio}
                  onCheckedChange={(c) => setCatCfg((s) => ({ ...s, icom_net_audio: !!c } as any))} />
                <span>
                  {t('cat.icomNetAudio')}
                  <span className="block text-[11px] text-muted-foreground">{t('cat.icomNetAudioHint')}</span>
                </span>
              </label>
            </>
            )}
            {catCfg.backend === 'tci' && (
            <>
              <div className="space-y-1">
                <Label>{t('cat.tciHost')}</Label>
                <Input placeholder="127.0.0.1" value={catCfg.tci_host ?? ''}
                  onChange={(e) => setCatCfg((s) => ({ ...s, tci_host: e.target.value }))} />
              </div>
              <div className="space-y-1">
                <Label>{t('cat.port')}</Label>
                <PortInput value={catCfg.tci_port || 40001}
                  onChange={(n) => setCatCfg((s) => ({ ...s, tci_port: n }))} fallback={40001} />
              </div>
              <p className="col-span-2 text-xs text-muted-foreground">
                {t('cat.tciHint')}
              </p>
              <label className="col-span-2 flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox checked={!!catCfg.tci_spots} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, tci_spots: !!c }))} />
                {t('cat.tciSpots')} <span className="text-xs text-muted-foreground">{t('cat.tciSpotsHint')}</span>
              </label>
            </>
            )}
            {(catCfg.backend === 'omnirig' || catCfg.backend === 'icom' || catCfg.backend === 'icom-net') && (
            <>
            <div className="space-y-1">
              <Label>{t('cat.pollMs')}</Label>
              <Input
                type="number" min={50} max={2000} step={50}
                value={catCfg.poll_ms}
                onChange={(e) => setCatCfg((s) => ({ ...s, poll_ms: parseInt(e.target.value) || 250 }))}
              />
            </div>
            <div className="space-y-1">
              <Label>{t('cat.delayMs')}</Label>
              <Input
                type="number" min={0} max={500} step={10}
                value={catCfg.delay_ms}
                onChange={(e) => setCatCfg((s) => ({ ...s, delay_ms: parseInt(e.target.value) || 0 }))}
              />
            </div>
            </>
            )}
            <div className="space-y-1 col-span-2">
              <Label>{t('cat.digitalDefault')}</Label>
              <Select
                value={catCfg.digital_default || 'FT8'}
                onValueChange={(v) => setCatCfg((s) => ({ ...s, digital_default: v }))}
              >
                <SelectTrigger><SelectValue /></SelectTrigger>
                <SelectContent>
                  {['FT8','FT4','RTTY','PSK31','MFSK','JS8','JT65','JT9','OLIVIA','DIGITALVOICE','DATA'].map(m => (
                    <SelectItem key={m} value={m}>{m}</SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
          </div>
          {/* CAT sharing. A native backend owns the rig's serial port, so without
              this WSJT-X and friends are locked out of the radio entirely. */}
          <div className="border-t border-border/60 pt-3 space-y-2">
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox
                checked={!!catCfg.share_enabled}
                onCheckedChange={(c) => setCatCfg((s) => ({ ...s, share_enabled: !!c }))}
              />
              {t('cat.share')}
            </label>
            <p className="text-[11px] text-muted-foreground">{t('cat.shareHint')}</p>
            {catCfg.share_enabled && (
              <div className="space-y-1 max-w-[200px]">
                <Label>{t('cat.sharePort')}</Label>
                <PortInput
                  value={catCfg.share_port || 4532}
                  fallback={4532}
                  onChange={(n) => setCatCfg((s) => ({ ...s, share_port: n }))}
                />
              </div>
            )}
          </div>
          {catCfg.backend === 'omnirig' && (
          <>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox
              checked={catModeBeforeFreq}
              onCheckedChange={(c) => { const v = !!c; setCatModeBeforeFreq(v); writeUiPref('opslog.catModeBeforeFreq', v ? '1' : '0'); }}
            />
            {t('cat.modeBeforeFreq')} <span className="text-xs text-muted-foreground">{t('cat.modeBeforeFreqHint')}</span>
          </label>
          <p className="text-xs text-muted-foreground">
            {t('cat.omnirigHint')}
          </p>
          </>
          )}
        </div>
      </>
    );
  }

  async function testRotator() {
    setRotatorTesting(true);
    setRotatorTest(null);
    try {
      await TestRotator(rotator as any);
      setRotatorTest({ ok: true, msg: (rotator as any).type === 'rotgenius' ? t('rot.testOkRG') : t('cat.rotatorOk') });
    } catch (e: any) {
      setRotatorTest({ ok: false, msg: String(e?.message ?? e) });
    } finally {
      setRotatorTesting(false);
    }
  }

  async function testUltrabeam() {
    setUbTesting(true);
    setUbTest(null);
    try {
      await TestUltrabeam(ultrabeam as any);
      setUbTest({ ok: true, msg: t('cat.ubOk') });
    } catch (e: any) {
      setUbTest({ ok: false, msg: String(e?.message ?? e) });
    } finally {
      setUbTesting(false);
    }
  }

  function UltrabeamPanel() {
    const isSteppir = ultrabeam.type === 'steppir';
    const isSerial = isSteppir && ultrabeam.transport === 'serial';
    return (
      <>
        <SectionHeader
          title={t('hw.motorAntenna')}
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={ultrabeam.enabled} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, enabled: !!c }))} />
            {t('hw.motorEnable')}
          </label>
          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-1">
              <Label>{t('hw.motorType')}</Label>
              {/* Ultrabeam is TCP only; picking it forces the transport back to TCP
                  so the serial fields never apply to it. */}
              <Select value={ultrabeam.type ?? 'ultrabeam'}
                onValueChange={(v) => setUltrabeam((s) => ({ ...s, type: v, transport: v === 'ultrabeam' ? 'tcp' : s.transport }))}>
                <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="ultrabeam">Ultrabeam</SelectItem>
                  <SelectItem value="steppir">SteppIR</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {isSteppir && (
              <div className="space-y-1">
                <Label>{t('hw.motorTransport')}</Label>
                <Select value={ultrabeam.transport ?? 'tcp'}
                  onValueChange={(v) => setUltrabeam((s) => ({ ...s, transport: v }))}>
                  <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="tcp">{t('hw.motorTcp')}</SelectItem>
                    <SelectItem value="serial">{t('hw.motorSerial')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>
            )}
          </div>
          {isSerial ? (
            <div className="grid grid-cols-3 gap-3">
              <div className="space-y-1 col-span-2">
                <Label>{t('hw.motorCom')}</Label>
                <Input
                  value={ultrabeam.com ?? ''}
                  onChange={(e) => setUltrabeam((s) => ({ ...s, com: e.target.value }))}
                  placeholder="COM3"
                  className="font-mono"
                />
              </div>
              <div className="space-y-1">
                <Label>{t('hw.motorBaud')}</Label>
                <Select value={String(ultrabeam.baud || 9600)} onValueChange={(v) => setUltrabeam((s) => ({ ...s, baud: parseInt(v, 10) || 9600 }))}>
                  <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[1200, 4800, 9600, 19200].map((b) => <SelectItem key={b} value={String(b)}>{b}</SelectItem>)}
                  </SelectContent>
                </Select>
              </div>
            </div>
          ) : (
            <div className="grid grid-cols-3 gap-3">
              <div className="space-y-1 col-span-2">
                <Label>Host / IP</Label>
                <Input
                  value={ultrabeam.host ?? ''}
                  onChange={(e) => setUltrabeam((s) => ({ ...s, host: e.target.value }))}
                  placeholder="192.168.1.50"
                  className="font-mono"
                />
              </div>
              <div className="space-y-1">
                <Label>TCP port</Label>
                <PortInput
                  value={ultrabeam.port}
                  onChange={(n) => setUltrabeam((s) => ({ ...s, port: n }))} fallback={23}
                  className="font-mono"
                />
              </div>
            </div>
          )}
          {isSteppir && (
            <p className="text-xs text-muted-foreground">{t('hw.steppirHint')}</p>
          )}
          <div className="border-t border-border/60 pt-3 space-y-2">
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={ultrabeam.follow} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, follow: !!c }))} />
              Follow rig frequency (auto-tune the antenna)
            </label>
            {ultrabeam.follow && (
              <div className="flex items-center gap-3 pl-6">
                <Label className="text-sm">Re-tune step</Label>
                <Select value={String(ultrabeam.step_khz)} onValueChange={(v) => setUltrabeam((s) => ({ ...s, step_khz: parseInt(v, 10) || 50 }))}>
                  <SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="25">25 kHz</SelectItem>
                    <SelectItem value="50">50 kHz</SelectItem>
                    <SelectItem value="100">100 kHz</SelectItem>
                  </SelectContent>
                </Select>
                <span className="text-xs text-muted-foreground">re-tune only when the frequency moves this far</span>
              </div>
            )}
          </div>
          {isSteppir && (
            <div className="border-t border-border/60 pt-3 space-y-1">
              <div className="flex items-center gap-2 flex-wrap">
                <Label className="text-sm">{t('hw.steppirRange')}</Label>
                <input type="number" min={1} max={60} value={ultrabeam.freq_min_mhz}
                  onChange={(e) => setUltrabeam((s) => ({ ...s, freq_min_mhz: parseInt(e.target.value, 10) || 0 }))}
                  className="h-8 w-16 rounded-md border border-input bg-background px-2 text-sm" />
                <span className="text-xs text-muted-foreground">–</span>
                <input type="number" min={1} max={60} value={ultrabeam.freq_max_mhz}
                  onChange={(e) => setUltrabeam((s) => ({ ...s, freq_max_mhz: parseInt(e.target.value, 10) || 0 }))}
                  className="h-8 w-16 rounded-md border border-input bg-background px-2 text-sm" />
                <span className="text-xs text-muted-foreground">MHz</span>
              </div>
              <p className="text-xs text-muted-foreground">{t('hw.steppirRangeHint')}</p>
            </div>
          )}
          <div className="border-t border-border/60 pt-3 space-y-1">
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={ultrabeam.tx_inhibit} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, tx_inhibit: !!c }))} />
              {t('hw.motorTxInhibit')}
            </label>
            <p className="text-xs text-muted-foreground pl-6">{t('hw.motorTxInhibitHint')}</p>
          </div>
          <div className="flex items-center gap-2 pt-2">
            <Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || (isSerial ? !ultrabeam.com.trim() : !ultrabeam.host.trim())}>
              {ubTesting ? t('hw.connecting') : t('hw.testConn')}
            </Button>
          </div>
          {ubTest && (
            <div className={cn(
              'text-xs rounded-md p-2.5 border',
              ubTest.ok
                ? 'bg-success-muted text-success-muted-foreground border-success-border'
                : 'bg-destructive/10 text-destructive border-destructive/30',
            )}>
              {ubTest.msg}
            </div>
          )}
        </div>
      </>
    );
  }

  function AntGeniusPanelSettings() {
    return (
      <>
        <SectionHeader
          title="Antenna Genius (4O3A)"
          hint={t('ag2.hint')}
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={antgenius.enabled} onCheckedChange={(c) => setAntgenius((s) => ({ ...s, enabled: !!c }))} />
            Enable Antenna Genius control
          </label>
          <div className="space-y-1">
            <Label>Host / IP</Label>
            <Input
              value={antgenius.host ?? ''}
              onChange={(e) => setAntgenius((s) => ({ ...s, host: e.target.value }))}
              placeholder="192.168.1.60"
              className="font-mono"
            />
          </div>
          <div className="space-y-1">
            <Label>{t('ag2.password')}</Label>
            <Input
              type="password"
              value={antgenius.password ?? ''}
              onChange={(e) => setAntgenius((s) => ({ ...s, password: e.target.value }))}
              placeholder={t('ag2.passwordPh')}
              className="font-mono"
            />
            <p className="text-xs text-muted-foreground">{t('ag2.passwordHint')}</p>
          </div>
        </div>
      </>
    );
  }

  function TunerGeniusPanelSettings() {
    return (
      <>
        <SectionHeader
          title="Tuner Genius XL (4O3A)"
          hint={t('tg2.hint')}
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={tunergenius.enabled} onCheckedChange={(c) => setTunergenius((s) => ({ ...s, enabled: !!c }))} />
            {t('tg2.enable')}
          </label>
          <div className="space-y-1">
            <Label>Host / IP</Label>
            <Input
              value={tunergenius.host ?? ''}
              onChange={(e) => setTunergenius((s) => ({ ...s, host: e.target.value }))}
              placeholder="192.168.1.61"
              className="font-mono"
            />
            <p className="text-xs text-muted-foreground">{t('tg2.portHint')}</p>
          </div>
          <div className="space-y-1">
            <Label>{t('tg2.password')}</Label>
            <Input
              type="password"
              value={tunergenius.password ?? ''}
              onChange={(e) => setTunergenius((s) => ({ ...s, password: e.target.value }))}
              placeholder={t('tg2.passwordPh')}
              className="font-mono"
            />
            <p className="text-xs text-muted-foreground">{t('tg2.passwordHint')}</p>
          </div>
        </div>
      </>
    );
  }

  function PGXLPanelSettings() {
    // The stored `type` stays a flat value ("spe13", "acom700", "pgxl"); the UI
    // presents it as brand + model.
    const brandModels: Record<string, { value: string; label: string }[]> = {
      spe: [
        { value: 'spe13', label: 'Expert 1.3K-FA' },
        { value: 'spe15', label: 'Expert 1.5K-FA' },
        { value: 'spe2k', label: 'Expert 2K-FA' },
      ],
      acom: [
        { value: 'acom500', label: '500S' },
        { value: 'acom600', label: '600S' },
        { value: 'acom700', label: '700S' },
        { value: 'acom1200', label: '1200S' },
        { value: 'acom2020', label: '2020S' },
      ],
    };
    const brandOf = (ty: string) => (!ty || ty === 'pgxl') ? 'pgxl' : ty.startsWith('acom') ? 'acom' : 'spe';
    const patchAmp = (i: number, patch: Partial<AmpUI>) => setAmps((l) => l.map((a, j) => (j === i ? { ...a, ...patch } : a)));
    // Each family has a fixed serial speed: SPE talks 115200, the ACOM S-series is
    // 9600 8N1 — preset it so switching brand just works. PGXL is TCP-only.
    const applyType = (i: number, v: string) => patchAmp(i, {
      type: v,
      transport: v === 'pgxl' ? 'tcp' : amps[i].transport,
      baud: v.startsWith('acom') ? 9600 : v.startsWith('spe') ? 115200 : amps[i].baud,
    });
    const addAmp = () => setAmps((l) => [...l, {
      id: '', name: '', enabled: true, type: 'spe13', transport: 'tcp', host: '', port: 9008, com_port: '', baud: 115200,
      freq_out: false, freq_com_port: '', freq_baud: 9600, freq_broadcast_ms: 0,
    }]);
    return (
      <>
        <SectionHeader title="Amplifier" hint={t('amp.hint')} />
        <div className="space-y-4 max-w-xl">
          {amps.length === 0 && (
            <p className="text-sm text-muted-foreground">{t('amp.none')}</p>
          )}
          {amps.map((amp, i) => {
            const brand = brandOf(amp.type);
            const isPGXL = brand === 'pgxl';
            const isACOM = brand === 'acom';
            const isSerial = !isPGXL && amp.transport === 'serial';
            return (
              <div key={amp.id || `new-${i}`} className="rounded-lg border border-border p-3 space-y-3">
                <div className="flex items-center gap-2">
                  <Checkbox checked={amp.enabled} onCheckedChange={(c) => patchAmp(i, { enabled: !!c })} />
                  <Input className="h-8 flex-1" value={amp.name}
                    placeholder={t('amp.namePh')}
                    onChange={(e) => patchAmp(i, { name: e.target.value })} />
                  <Button variant="ghost" size="sm" className="h-8 text-danger" title={t('amp.remove')}
                    onClick={() => setAmps((l) => l.filter((_, j) => j !== i))}>
                    <Trash2 className="size-3.5" />
                  </Button>
                </div>

                {/* Connection gets the widest column — "Network (RS232-to-Ethernet)"
                    was truncated with 3 equal columns. */}
                <div className="grid grid-cols-[1fr_1fr_1.7fr] gap-3">
                  <div className="space-y-1">
                    <Label>Brand</Label>
                    <Select value={brand} onValueChange={(b) => {
                      if (b === brand) return;
                      applyType(i, b === 'pgxl' ? 'pgxl' : brandModels[b][0].value);
                    }}>
                      <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                      <SelectContent>
                        <SelectItem value="pgxl">4O3A</SelectItem>
                        <SelectItem value="spe">SPE</SelectItem>
                        <SelectItem value="acom">ACOM</SelectItem>
                      </SelectContent>
                    </Select>
                  </div>
                  <div className="space-y-1">
                    <Label>Model</Label>
                    {isPGXL ? (
                      // The PowerGenius is the brand's single model — fixed, no choice.
                      <Select value="pgxl" disabled>
                        <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          <SelectItem value="pgxl">PowerGenius XL</SelectItem>
                        </SelectContent>
                      </Select>
                    ) : (
                      <Select value={amp.type} onValueChange={(v) => applyType(i, v)}>
                        <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          {brandModels[brand].map((m) => <SelectItem key={m.value} value={m.value}>{m.label}</SelectItem>)}
                        </SelectContent>
                      </Select>
                    )}
                  </div>
                  {/* PowerGenius is TCP-only; SPE and ACOM amps connect over USB serial
                      or an RS232-to-Ethernet bridge, so they offer both. */}
                  {!isPGXL && (
                    <div className="space-y-1">
                      <Label>Connection</Label>
                      <Select value={amp.transport} onValueChange={(v) => patchAmp(i, { transport: v })}>
                        <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          <SelectItem value="serial">USB (serial COM)</SelectItem>
                          <SelectItem value="tcp">Network (RS232-to-Ethernet)</SelectItem>
                        </SelectContent>
                      </Select>
                    </div>
                  )}
                </div>

                {isSerial ? (
                  <div className="grid grid-cols-3 gap-3">
                    <div className="space-y-1 col-span-2">
                      <Label>COM port</Label>
                      <div className="flex items-center gap-2">
                        <Select value={amp.com_port || '_'} onValueChange={(v) => patchAmp(i, { com_port: v === '_' ? '' : v })}>
                          <SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
                          <SelectContent>
                            {wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
                            {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                          </SelectContent>
                        </Select>
                        <Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                          <ArrowDown className="size-3.5 rotate-90" />
                        </Button>
                      </div>
                    </div>
                    <div className="space-y-1">
                      <Label>Baud</Label>
                      <Input type="number" min={1200} value={amp.baud}
                        onChange={(e) => patchAmp(i, { baud: parseInt(e.target.value) || 115200 })} className="font-mono" />
                    </div>
                  </div>
                ) : (
                  <div className="grid grid-cols-3 gap-3">
                    <div className="space-y-1 col-span-2">
                      <Label>Host / IP</Label>
                      <Input value={amp.host ?? ''} onChange={(e) => patchAmp(i, { host: e.target.value })}
                        placeholder="192.168.1.70" className="font-mono" />
                    </div>
                    <div className="space-y-1">
                      <Label>TCP port</Label>
                      <PortInput value={amp.port}
                        onChange={(n) => patchAmp(i, { port: n })} fallback={9008} className="font-mono" />
                    </div>
                  </div>
                )}

                {/* Band-follow, for any amp that takes its band from a transceiver
                    CAT link (ACOM and SPE both do) — never PowerGenius, which is
                    driven over its network protocol. A SECOND serial port,
                    separate from the metering one above. */}
                {!isPGXL && (
                  <div className="border-t border-border/60 pt-3 space-y-3">
                    <label className="flex items-center gap-2 text-sm cursor-pointer">
                      <Checkbox
                        checked={!!amp.freq_out}
                        onCheckedChange={(c) => patchAmp(i, { freq_out: !!c })}
                      />
                      {t('amp.freqOut')}
                    </label>
                    {amp.freq_out && (
                      <>
                        <div className="grid grid-cols-3 gap-3">
                          <div className="space-y-1 col-span-2">
                            <Label>{t('amp.freqPort')}</Label>
                            <div className="flex items-center gap-2">
                              <Select value={amp.freq_com_port || '_'} onValueChange={(v) => patchAmp(i, { freq_com_port: v === '_' ? '' : v })}>
                                <SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
                                <SelectContent>
                                  {wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
                                  {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                                </SelectContent>
                              </Select>
                              <Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                                <ArrowDown className="size-3.5 rotate-90" />
                              </Button>
                            </div>
                          </div>
                          <div className="space-y-1">
                            <Label>Baud</Label>
                            <Input type="number" min={1200} value={amp.freq_baud ?? 9600}
                              onChange={(e) => patchAmp(i, { freq_baud: parseInt(e.target.value) || 9600 })} className="font-mono" />
                          </div>
                        </div>
                        <div className="grid grid-cols-3 gap-3">
                          <div className="space-y-1 col-span-2">
                            <Label>{t('amp.freqBroadcast')}</Label>
                            <Select value={String(amp.freq_broadcast_ms ?? 0)} onValueChange={(v) => patchAmp(i, { freq_broadcast_ms: parseInt(v) })}>
                              <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                              <SelectContent>
                                <SelectItem value="0">{t('amp.freqPollOnly')}</SelectItem>
                                <SelectItem value="500">{t('amp.freqEvery', { ms: 500 })}</SelectItem>
                                <SelectItem value="1000">{t('amp.freqEvery', { ms: 1000 })}</SelectItem>
                              </SelectContent>
                            </Select>
                          </div>
                        </div>
                        <p className="text-[10px] text-muted-foreground">{t('amp.freqHint')}</p>
                      </>
                    )}
                  </div>
                )}

                {amp.enabled && amp.id && <AmpStatusCard id={amp.id} />}
                {!isPGXL && !isACOM && (
                  <p className="text-[10px] text-muted-foreground">
                    SPE control uses the amplifier's proprietary serial protocol (OPERATE toggle + live status). Save to (re)connect. Band / power-level / antenna are still managed on the amp from the transceiver CAT.
                  </p>
                )}
                {isACOM && (
                  <p className="text-[10px] text-muted-foreground">
                    ACOM control uses the amplifier's serial protocol (9600 8N1): OPERATE / STANDBY / OFF + live telemetry. Save to (re)connect. Power-ON needs the serial DTR/RTS pins wired in the cable (not available over a network bridge). Band tracking stays on the amp itself.
                  </p>
                )}
              </div>
            );
          })}
          <Button variant="outline" size="sm" onClick={addAmp}>
            <Plus className="size-3.5 mr-1" /> {t('amp.add')}
          </Button>
        </div>
      </>
    );
  }

  function RotatorPanel() {
    const isRG = (rotator as any).type === 'rotgenius';
    const isARCO = (rotator as any).type === 'arco';
    return (
      <>
        <SectionHeader
          title="Rotator"
          hint={isRG || isARCO ? undefined : t('rot.hint')}
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={rotator.enabled} onCheckedChange={(c) => setRotator((s) => ({ ...s, enabled: !!c }))} />
            {t('rot.enable')}
          </label>
          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-1">
              <Label>{t('rot.type')}</Label>
              {/* Each backend gets its default port: Rotator Genius 9006, ARCO 4001
                  (placeholder — must match the ARCO's LAN menu), PstRotator 12000. */}
              <Select value={(rotator as any).type ?? 'pst'}
                onValueChange={(v) => setRotator((s) => ({ ...s, type: v, port: v === 'rotgenius' ? 9006 : v === 'arco' ? 4001 : 12000 } as any))}>
                <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="pst">PstRotator (UDP)</SelectItem>
                  <SelectItem value="rotgenius">Rotator Genius (4O3A, native)</SelectItem>
                  <SelectItem value="arco">GS-232A controller (microHAM ARCO, ERC…)</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {isRG && (
              <div className="space-y-1">
                <Label>{t('rot.rotatorNum')}</Label>
                <Select value={String((rotator as any).rotator_num ?? 1)}
                  onValueChange={(v) => setRotator((s) => ({ ...s, rotator_num: parseInt(v, 10) || 1 } as any))}>
                  <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="1">1</SelectItem>
                    <SelectItem value="2">2</SelectItem>
                  </SelectContent>
                </Select>
              </div>
            )}
            {/* The ARCO is reachable over the LAN (TCP) or its USB virtual COM. */}
            {isARCO && (
              <div className="space-y-1">
                <Label>Connection</Label>
                <Select value={(rotator as any).transport ?? 'tcp'}
                  onValueChange={(v) => setRotator((s) => ({ ...s, transport: v } as any))}>
                  <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="tcp">Network (LAN)</SelectItem>
                    <SelectItem value="serial">USB (serial COM)</SelectItem>
                  </SelectContent>
                </Select>
              </div>
            )}
          </div>
          {isARCO && (rotator as any).transport === 'serial' ? (
            <div className="space-y-1 max-w-xs">
              <Label>COM port</Label>
              <div className="flex items-center gap-2">
                <Select value={(rotator as any).com_port || '_'}
                  onValueChange={(v) => setRotator((s) => ({ ...s, com_port: v === '_' ? '' : v } as any))}>
                  <SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
                  <SelectContent>
                    {wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
                    {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                  <ArrowDown className="size-3.5 rotate-90" />
                </Button>
                <Select value={String((rotator as any).baud || 9600)}
                  onValueChange={(v) => setRotator((s) => ({ ...s, baud: Number(v) } as any))}>
                  <SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {[4800, 9600, 19200, 38400, 57600].map((b) => (
                      <SelectItem key={b} value={String(b)}>{b} baud</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            </div>
          ) : (
            <div className="grid grid-cols-3 gap-3">
              <div className="space-y-1 col-span-2">
                <Label>Host / IP</Label>
                <Input
                  value={rotator.host ?? ''}
                  onChange={(e) => setRotator((s) => ({ ...s, host: e.target.value }))}
                  placeholder={isRG || isARCO ? '192.168.1.60' : '127.0.0.1'}
                  className="font-mono"
                />
              </div>
              <div className="space-y-1">
                <Label>{isRG || isARCO ? 'TCP port' : 'UDP port'}</Label>
                <PortInput
                  value={rotator.port}
                  onChange={(n) => setRotator((s) => ({ ...s, port: n }))} fallback={isRG ? 9006 : isARCO ? 4001 : 12000}
                  className="font-mono"
                />
              </div>
            </div>
          )}
          {!isRG && !isARCO && (
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={rotator.has_elevation} onCheckedChange={(c) => setRotator((s) => ({ ...s, has_elevation: !!c }))} />
              This rotator supports elevation (VHF / satellite)
            </label>
          )}
          {isRG && <p className="text-xs text-muted-foreground">{t('rot.rgHint')}</p>}
          {isARCO && <p className="text-xs text-muted-foreground">{t('rot.arcoHint')}</p>}
          <div className="flex items-center gap-2 pt-2">
            <Button variant="outline" size="sm" onClick={testRotator} disabled={rotatorTesting}>
              {rotatorTesting ? t('hw.sending') : t('hw.testRotator')}
            </Button>
            <Button variant="outline" size="sm" onClick={() => RotatorStop().catch((e) => setErr(String(e?.message ?? e)))} disabled={!rotator.enabled}>
              Stop
            </Button>
            {!isRG && (
              <Button variant="outline" size="sm" onClick={() => RotatorPark().catch((e) => setErr(String(e?.message ?? e)))} disabled={!rotator.enabled}>
                Park
              </Button>
            )}
          </div>
          {rotatorTest && (
            <div className={cn(
              'text-xs rounded-md p-2.5 border',
              rotatorTest.ok
                ? 'bg-success-muted text-success-muted-foreground border-success-border'
                : 'bg-destructive/10 text-destructive border-destructive/30',
            )}>
              {rotatorTest.msg}
            </div>
          )}
          <p className="text-xs text-muted-foreground">
            From the main entry strip, click the bearing pill to rotate to the short-path azimuth.
            Shift+click for long-path, Ctrl+click to stop.
          </p>
        </div>
      </>
    );
  }

  function WinkeyerPanel() {
    const setMacro = (i: number, patch: Partial<WKMac>) => setWk((s) => {
      const macros = [...s.macros];
      while (macros.length <= i) macros.push({ label: '', text: '' });
      macros[i] = { ...macros[i], ...patch };
      return { ...s, macros };
    });
    const num = (v: string, d: number) => { const n = parseInt(v, 10); return isNaN(n) ? d : n; };
    return (
      <>
        <SectionHeader
          title={t('sec.winkeyer')}
        />
        <div className="space-y-4 max-w-2xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={wk.enabled} onCheckedChange={(c) => setWkField({ enabled: !!c })} />
            {t('wk.enable')}
          </label>

          <div className="grid grid-cols-4 gap-3 items-end">
            <div className="space-y-1">
              <Label>{t('wk.engine')}</Label>
              {/* Picking the serial engine defaults PTT-keying ON: without RTS held,
                  the rig drops to RX between words (semi-break-in). */}
              <Select value={wk.engine} onValueChange={(v) => setWkField(v === 'serial' ? { engine: v, use_ptt: true } : { engine: v })}>
                <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="winkeyer">{t('wk.engWinkeyer')}</SelectItem>
                  <SelectItem value="serial">{t('wk.engSerial')}</SelectItem>
                  <SelectItem value="icom">{t('wk.engIcom')}</SelectItem>
                  <SelectItem value="yaesu">{t('wk.engYaesu')}</SelectItem>
                  <SelectItem value="flex">{t('wk.engFlex')}</SelectItem>
                  <SelectItem value="tci" disabled>{t('wk.engTci')}</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <label className="flex items-center gap-2 text-sm cursor-pointer col-span-3 pb-1.5">
              <Checkbox checked={wk.esc_clears_call} onCheckedChange={(c) => setWkField({ esc_clears_call: !!c })} />
              {t('wk.escClears')}
            </label>
            <label className="flex items-center gap-2 text-sm cursor-pointer col-span-3 pb-1.5">
              <Checkbox checked={wk.esm} onCheckedChange={(c) => setWkField({ esm: !!c })} />
              {t('wk.esm')}
            </label>
          </div>

          {wk.engine === 'icom' ? (
            <>
              {(!catCfg.enabled || catCfg.backend !== 'icom') && (
                <p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
                  <span aria-hidden>⚠</span>
                  <span>{t('wk.catWarnIcom', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
                </p>
              )}
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>{t('wk.speed')}</Label>
                  <Input type="number" min={6} max={48} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
                </div>
              </div>
            </>
          ) : wk.engine === 'yaesu' ? (
            <>
              {(!catCfg.enabled || catCfg.backend !== 'yaesu') && (
                <p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
                  <span aria-hidden>⚠</span>
                  <span>{t('wk.catWarnYaesu', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
                </p>
              )}
              <p className="text-xs text-muted-foreground -mt-1">{t('wk.yaesuHint')}</p>
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>{t('wk.speed')}</Label>
                  <Input type="number" min={4} max={60} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
                </div>
              </div>
            </>
          ) : wk.engine === 'flex' ? (
            <>
              {(!catCfg.enabled || catCfg.backend !== 'flex') && (
                <p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
                  <span aria-hidden>⚠</span>
                  <span>{t('wk.catWarnFlex', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
                </p>
              )}
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>{t('wk.speed')}</Label>
                  <Input type="number" min={6} max={48} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
                </div>
              </div>
            </>
          ) : wk.engine === 'serial' ? (
            <>
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1 col-span-2">
                  <Label>{t('wk.keyingPort')}</Label>
                  <div className="flex items-center gap-2">
                    <Select value={wk.port || '_'} onValueChange={(v) => setWkField({ port: v === '_' ? '' : v })}>
                      <SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('wk.comPort')} /></SelectTrigger>
                      <SelectContent>
                        {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('wk.noPorts')}</SelectItem>}
                        {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                      </SelectContent>
                    </Select>
                    <Button variant="ghost" size="sm" className="h-8 px-2" title={t('wk.reloadPorts')}
                      onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                      <ArrowDown className="size-3.5 rotate-90" />
                    </Button>
                  </div>
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.cwKeyLine')}</Label>
                  <Select value={wk.cw_key_line === 'rts' ? 'rts' : 'dtr'} onValueChange={(v) => setWkField({ cw_key_line: v })}>
                    <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
                    <SelectContent>
                      <SelectItem value="dtr">{t('wk.lineDtr')}</SelectItem>
                      <SelectItem value="rts">{t('wk.lineRts')}</SelectItem>
                    </SelectContent>
                  </Select>
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.speed')}</Label>
                  <Input type="number" min={5} max={99} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
                </div>
              </div>
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>{t('wk.farnsworth')}</Label>
                  <Input type="number" min={0} max={99} value={wk.farnsworth} onChange={(e) => setWkField({ farnsworth: num(e.target.value, 0) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.leadIn')}</Label>
                  <Input type="number" min={0} value={wk.lead_in_ms} onChange={(e) => setWkField({ lead_in_ms: num(e.target.value, 10) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.tail')}</Label>
                  <Input type="number" min={0} value={wk.tail_ms} onChange={(e) => setWkField({ tail_ms: num(e.target.value, 50) })} className="font-mono" />
                </div>
                <label className="flex items-center gap-2 text-sm cursor-pointer pb-1.5 self-end">
                  <Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> {t('wk.keyPtt')}
                </label>
              </div>
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox checked={wk.cw_invert} onCheckedChange={(c) => setWkField({ cw_invert: !!c })} />
                {t('wk.invert')}
              </label>
            </>
          ) : (
            <>
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1 col-span-2">
                  <Label>{t('wk.serialPort')}</Label>
                  <div className="flex items-center gap-2">
                    <Select value={wk.port || '_'} onValueChange={(v) => setWkField({ port: v === '_' ? '' : v })}>
                      <SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('wk.comPort')} /></SelectTrigger>
                      <SelectContent>
                        {wkPorts.length === 0 && <SelectItem value="_" disabled>{t('wk.noPorts')}</SelectItem>}
                        {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                      </SelectContent>
                    </Select>
                    <Button variant="ghost" size="sm" className="h-8 px-2" title={t('wk.reloadPorts')}
                      onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                      <ArrowDown className="size-3.5 rotate-90" />
                    </Button>
                  </div>
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.baud')}</Label>
                  <Select value={String(wk.baud)} onValueChange={(v) => setWkField({ baud: num(v, 1200) })}>
                    <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
                    <SelectContent>
                      {[1200, 9600].map((b) => <SelectItem key={b} value={String(b)}>{b}</SelectItem>)}
                    </SelectContent>
                  </Select>
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.speed')}</Label>
                  <Input type="number" min={5} max={99} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
                </div>
              </div>

              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>{t('wk.weight')}</Label>
                  <Input type="number" min={10} max={90} value={wk.weight} onChange={(e) => setWkField({ weight: num(e.target.value, 50) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.leadIn')}</Label>
                  <Input type="number" min={0} value={wk.lead_in_ms} onChange={(e) => setWkField({ lead_in_ms: num(e.target.value, 10) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.tail')}</Label>
                  <Input type="number" min={0} value={wk.tail_ms} onChange={(e) => setWkField({ tail_ms: num(e.target.value, 50) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.ratio')}</Label>
                  <Input type="number" min={33} max={66} value={wk.ratio} onChange={(e) => setWkField({ ratio: num(e.target.value, 50) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.farnsworth')}</Label>
                  <Input type="number" min={0} max={99} value={wk.farnsworth} onChange={(e) => setWkField({ farnsworth: num(e.target.value, 0) })} className="font-mono" />
                </div>
                <div className="space-y-1">
                  <Label>{t('wk.sidetone')}</Label>
                  <Input type="number" min={0} value={wk.sidetone_hz} onChange={(e) => setWkField({ sidetone_hz: num(e.target.value, 600) })} className="font-mono" />
                </div>
                <div className="space-y-1 col-span-2">
                  <Label>{t('wk.paddleMode')}</Label>
                  <Select value={wk.mode} onValueChange={(v) => setWkField({ mode: v })}>
                    <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
                    <SelectContent>
                      <SelectItem value="iambic_b">Iambic B</SelectItem>
                      <SelectItem value="iambic_a">Iambic A</SelectItem>
                      <SelectItem value="ultimatic">Ultimatic</SelectItem>
                      <SelectItem value="bug">Bug</SelectItem>
                    </SelectContent>
                  </Select>
                </div>
              </div>

              <div className="flex flex-wrap gap-x-5 gap-y-2">
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.swap} onCheckedChange={(c) => setWkField({ swap: !!c })} /> {t('wk.swapPaddles')}
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.autospace} onCheckedChange={(c) => setWkField({ autospace: !!c })} /> {t('wk.autospace')}
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> {t('wk.keyPttShort')}
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.serial_echo} onCheckedChange={(c) => setWkField({ serial_echo: !!c })} /> {t('wk.serialEcho')}
                </label>
              </div>

              {/* Protocol trace — for reporting a keyer that misbehaves. Session
                  only, deliberately not persisted: it is a diagnostic, and a log
                  full of hex bytes helps nobody who forgot it was on. */}
              <div className="border-t border-border/60 pt-3">
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wkTrace} onCheckedChange={(c) => { setWkTrace(!!c); SetWinkeyerTrace(!!c); }} />
                  {t('wk.trace')}
                </label>
                <p className="text-[10px] text-muted-foreground mt-1">{t('wk.traceHint')}</p>
              </div>
            </>
          )}

          {/* Macro editor */}
          <div className="border-t border-border/60 pt-3">
            <Label className="text-sm font-medium">{t('wk.macroTitle')}</Label>
            <p className="text-[11px] text-muted-foreground mb-2">
              {t('wk.macroVars')} <span className="font-mono">&lt;MY_CALL&gt; &lt;CALL&gt; &lt;STX&gt; &lt;STRX&gt; &lt;MY_NAME&gt; &lt;HIS_NAME&gt; &lt;MY_QTH&gt; &lt;GRID&gt; &lt;CONT_TX&gt; &lt;n&gt;</span> {t('wk.macroCut')} <span className="font-mono">*</span>={t('wk.macroMyCall')}, <span className="font-mono">!</span>={t('wk.macroHisCall')}. <span className="font-mono">&lt;LOGQSO&gt;</span> = {t('wk.macroLog')} (<span className="font-mono">73 TU &lt;LOGQSO&gt;</span>).
            </p>
            <div className="space-y-1.5 max-h-[34vh] overflow-y-auto pr-1">
              {Array.from({ length: 12 }).map((_, i) => {
                const m = wk.macros[i] ?? { label: '', text: '' };
                return (
                  <div key={i} className="flex items-center gap-2">
                    <span className="font-mono text-[10px] text-primary font-semibold w-6 shrink-0">F{i + 1}</span>
                    <Input className="h-8 w-28 shrink-0 text-xs" placeholder={t('wk.label')} value={m.label} onChange={(e) => setMacro(i, { label: e.target.value })} />
                    <Input className="h-8 flex-1 font-mono text-xs" placeholder="CQ CQ DE <MY_CALL> K" value={m.text} onChange={(e) => setMacro(i, { text: e.target.value })} />
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      </>
    );
  }

  function statusForServer(id: number): ClusterServerStatus | undefined {
    return clusterStatuses.find((s) => (s.server_id as number) === id);
  }

  async function clusterToggleEnabled(srv: ClusterServer, on: boolean) {
    try {
      await SaveClusterServer({ ...srv, enabled: on } as any);
      await reloadClusterServers();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function clusterDeleteServer(srv: ClusterServer) {
    if (!confirm(`Delete cluster "${srv.name}"? Active session will be closed.`)) return;
    try { await DeleteClusterServer(srv.id as number); await reloadClusterServers(); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function clusterMove(srv: ClusterServer, dir: -1 | 1) {
    const sorted = [...clusterServers].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
    const idx = sorted.findIndex((s) => s.id === srv.id);
    const j = idx + dir;
    if (idx < 0 || j < 0 || j >= sorted.length) return;
    const a = sorted[idx], b = sorted[j];
    try {
      await SaveClusterServer({ ...a, sort_order: b.sort_order } as any);
      await SaveClusterServer({ ...b, sort_order: a.sort_order } as any);
      await reloadClusterServers();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  function clusterAddNew() {
    const next: ClusterServer = {
      id: 0, name: '', host: '', port: 7300,
      login_override: '', password: '', init_commands: '',
      enabled: true, sort_order: clusterServers.length,
    };
    setEditingServer(next);
  }

  function ClusterPanel() {
    const sorted = [...clusterServers].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
    return (
      <>
        <SectionHeader
          title={t('sec.cluster')}
          hint={t('clu.hint')}
        />
        <div className="space-y-4">
          <div className="rounded-md border border-border overflow-hidden">
            <table className="w-full text-sm">
              <thead className="bg-muted/40 text-[10px] uppercase tracking-wider text-muted-foreground">
                <tr>
                  <th className="px-3 py-2 w-10"></th>
                  <th className="text-left px-3 py-2">{t('clu.name')}</th>
                  <th className="text-left px-3 py-2">{t('clu.hostPort')}</th>
                  <th className="text-left px-3 py-2 w-28">{t('clu.status')}</th>
                  <th className="px-3 py-2 w-40">{t('clu.actions')}</th>
                </tr>
              </thead>
              <tbody>
                {sorted.map((s, i) => {
                  const st = statusForServer(s.id as number);
                  const state = (st?.state ?? 'disconnected') as string;
                  const isMaster = i === sorted.findIndex((x) => x.enabled);
                  return (
                    <tr key={s.id as number} className="border-t border-border align-middle">
                      <td className="px-2 py-2 text-center">
                        <Checkbox
                          checked={s.enabled}
                          onCheckedChange={(c) => clusterToggleEnabled(s, !!c)}
                        />
                      </td>
                      <td className="px-3 py-2 font-medium">
                        {s.name}
                        {isMaster && s.enabled && (
                          <span className="ml-2 inline-flex items-center px-1.5 py-0 rounded text-[9px] font-bold tracking-wider bg-warning-muted text-warning-muted-foreground border border-warning-border">MASTER</span>
                        )}
                      </td>
                      <td className="px-3 py-2 font-mono text-xs">{s.host}:{s.port}</td>
                      <td className="px-3 py-2">
                        <span className={cn(
                          'inline-flex items-center px-1.5 py-0 rounded text-[9px] font-bold tracking-wider border',
                          state === 'connected' ? 'bg-success-muted text-success-muted-foreground border-success-border' :
                          state === 'connecting' || state === 'reconnecting' ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' :
                          state === 'error' ? 'bg-danger-muted text-danger-muted-foreground border-danger-border' :
                          'bg-muted text-muted-foreground border-border',
                        )}>
                          {state.toUpperCase()}
                          {st?.retries ? ` #${st.retries}` : ''}
                        </span>
                      </td>
                      <td className="px-2 py-2">
                        <div className="flex items-center gap-0.5 justify-end">
                          {state === 'connected' || state === 'connecting' || state === 'reconnecting' ? (
                            <Button variant="ghost" size="icon" className="size-6 text-success hover:text-success"
                              onClick={async () => { await DisconnectClusterServer(s.id as number).catch(() => {}); await reloadClusterServers(); }}
                              title={t('clu.disconnect')}><Power className="size-3.5" /></Button>
                          ) : (
                            <Button variant="ghost" size="icon" className="size-6"
                              onClick={async () => { await ConnectClusterServer(s.id as number).catch(() => {}); await reloadClusterServers(); }}
                              title={t('clu.connect')}><Power className="size-3.5" /></Button>
                          )}
                          <Button variant="ghost" size="icon" className="size-6" disabled={i === 0} onClick={() => clusterMove(s, -1)} title={t('clu.moveUp')}><ArrowUp className="size-3" /></Button>
                          <Button variant="ghost" size="icon" className="size-6" disabled={i === sorted.length - 1} onClick={() => clusterMove(s, 1)} title={t('clu.moveDown')}><ArrowDown className="size-3" /></Button>
                          <Button variant="ghost" size="icon" className="size-6" onClick={() => setEditingServer(s)} title={t('clu.edit')}><Cog className="size-3.5" /></Button>
                          <Button variant="ghost" size="icon" className="size-6 text-destructive hover:text-destructive" onClick={() => clusterDeleteServer(s)} title={t('clu.delete')}><Trash2 className="size-3.5" /></Button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
                {sorted.length === 0 && (
                  <tr><td colSpan={5} className="px-3 py-4 text-center text-muted-foreground text-xs">{t('clu.none')}</td></tr>
                )}
              </tbody>
            </table>
          </div>

          <div className="flex items-center gap-3 pt-2 border-t border-border">
            <Button variant="outline" size="sm" onClick={clusterAddNew}>
              <Plus className="size-3.5 mr-1" /> {t('clu.add')}
            </Button>
            <Button variant="outline" size="sm" onClick={async () => { await ConnectAllClusters().catch(() => {}); await reloadClusterServers(); }}>
              {t('clu.connectAll')}
            </Button>
            <Button variant="outline" size="sm" onClick={async () => { await DisconnectAllClusters().catch(() => {}); await reloadClusterServers(); }}>
              {t('clu.disconnectAll')}
            </Button>
            <label className="flex items-center gap-2 text-sm cursor-pointer ml-auto">
              <Checkbox checked={clusterAutoConnect} onCheckedChange={(c) => setClusterAutoConnectState(!!c)} />
              {t('clu.autoConnect')}
            </label>
          </div>
          <p className="text-xs text-muted-foreground">
            {t('clu.freeNodes')} <span className="font-mono">dxc.k0xm.net:7300</span>,{' '}
            <span className="font-mono">dx.maritimecontestclub.net:7300</span>,{' '}
            <span className="font-mono">w8avi.net:7300</span>.
          </p>
        </div>

        {editingServer && (
          <ClusterServerEditor
            value={editingServer}
            onCancel={() => setEditingServer(null)}
            onSave={async (srv) => {
              try {
                await SaveClusterServer(srv as any);
                await reloadClusterServers();
                setEditingServer(null);
              } catch (e: any) { setErr(String(e?.message ?? e)); }
            }}
          />
        )}
      </>
    );
  }

  function ConfirmationsPanel() {
    // ADIF status codes. FULL set for paper QSL/eQSL/Clublog/HRDLog,
    // SIMPLE Y/N set for LoTW (the only values the LoTW protocol returns).
    const FULL_OPTIONS = [
      { value: '_', label: t('conf.blank') },
      { value: 'Y', label: t('conf.y') },
      { value: 'N', label: t('conf.n') },
      { value: 'R', label: t('conf.r') },
      { value: 'Q', label: t('conf.q') },
      { value: 'I', label: t('conf.i') },
    ];
    // LoTW / Clublog / HRDLog also use ADIF-style status codes — keep
    // R (requested) available so users can mark "queued for upload"
    // and filter on it later.

    // Renderer inlined as a constant — declaring this as a function
    // INSIDE ConfirmationsPanel would re-instantiate the component on
    // every render, which unmounts and re-mounts the Radix Select
    // (closing it the moment you click the trigger).
    const renderSelect = (
      key: keyof QSLDefaults,
      options: { value: string; label: string }[],
    ) => (
      <Select
        value={qslDefaults[key] || '_'}
        onValueChange={(v) => setQslDefaults((d) => ({ ...d, [key]: v === '_' ? '' : v }))}
      >
        <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
        <SelectContent>
          {options.map((o) => (
            <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
          ))}
        </SelectContent>
      </Select>
    );

    return (
      <>
        <SectionHeader
          title={t('sec.confirmations')}
          hint={t('conf.hint')}
        />
        <ProfileScopeNote profile={activeProfileObj} />
        <div className="space-y-3 max-w-2xl">
          {/* Paper QSL */}
          <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
            <Label className="text-sm font-medium pb-1.5">{t('conf.paperQsl')}</Label>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
              {renderSelect('qsl_sent', FULL_OPTIONS)}
            </div>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.rcvd')}</Label>
              {renderSelect('qsl_rcvd', FULL_OPTIONS)}
            </div>
          </div>
          {/* eQSL */}
          <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
            <Label className="text-sm font-medium pb-1.5">eQSL</Label>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
              {renderSelect('eqsl_sent', FULL_OPTIONS)}
            </div>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.rcvd')}</Label>
              {renderSelect('eqsl_rcvd', FULL_OPTIONS)}
            </div>
          </div>
          {/* LoTW */}
          <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
            <Label className="text-sm font-medium pb-1.5">LoTW</Label>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
              {renderSelect('lotw_sent', FULL_OPTIONS)}
            </div>
            <div>
              <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.rcvd')}</Label>
              {renderSelect('lotw_rcvd', FULL_OPTIONS)}
            </div>
          </div>

          <div className="border-t border-border/60 pt-3 space-y-3">
            <div className="text-[11px] text-muted-foreground">
              {t('conf.note')}
            </div>
            {/* Clublog */}
            <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
              <Label className="text-sm font-medium pb-1.5">Clublog</Label>
              <div>
                <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
                {renderSelect('clublog_status', FULL_OPTIONS)}
              </div>
              <div />
            </div>
            {/* HRDLog */}
            <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
              <Label className="text-sm font-medium pb-1.5">HRDLog</Label>
              <div>
                <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
                {renderSelect('hrdlog_status', FULL_OPTIONS)}
              </div>
              <div />
            </div>
            {/* QRZ.com */}
            <div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
              <Label className="text-sm font-medium pb-1.5">QRZ.com</Label>
              <div>
                <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.sent')}</Label>
                {renderSelect('qrzcom_status', FULL_OPTIONS)}
              </div>
              <div>
                <Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">{t('conf.rcvd')}</Label>
                {renderSelect('qrzcom_confirmed', FULL_OPTIONS)}
              </div>
            </div>
          </div>
        </div>
      </>
    );
  }

  function UDPIntegrationsPanelWrapper() {
    return (
      <>
        <SectionHeader
          title={t('sec.udp')}
          hint={t('udp.hint')}
        />
        <UDPIntegrationsPanel onError={(m) => setErr(m)} />
      </>
    );
  }

  function OperatingPanelWrapper() {
    return (
      <>
        <SectionHeader
          title={t('sec.operating')}
          hint={t('operating.hint')}
        />
        <OperatingPanel
          bands={lists.bands ?? []}
          onError={(m) => setErr(m)}
        />
      </>
    );
  }

  function BackupPanel() {
    const fmtLast = (iso: string) => {
      if (!iso) return t('bk.never');
      const d = new Date(iso);
      if (isNaN(d.getTime())) return iso;
      const p = (n: number) => String(n).padStart(2, '0');
      return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())} UTC`;
    };
    const effectiveFolder = (backupCfg.folder || backupCfg.default_folder || '').replace(/\\/g, '/');
    async function backupNow() {
      setBackupRunning(true); setBackupResult(null);
      try {
        // Save current draft first so the backup runs with the values
        // the user just typed (folder, rotation, zip) — otherwise the
        // backend would use stale persisted config.
        await SaveBackupSettings(backupCfg as any);
        const path = await RunBackupNow();
        setBackupResult({ ok: true, msg: t('bk.writtenTo') + ' ' + path });
        const refreshed = await GetBackupSettings();
        setBackupCfg(refreshed as any);
      } catch (e: any) {
        setBackupResult({ ok: false, msg: String(e?.message ?? e) });
      } finally { setBackupRunning(false); }
    }
    return (
      <>
        <SectionHeader
          title={t('sec.backup')}
          hint={mysqlCfg.enabled ? t('bk.hintMysql') : t('bk.hint')}
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox
              checked={!!backupCfg.enabled}
              onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, enabled: !!c }))}
            />
            <span>{t('bk.auto')}</span>
          </label>

          <div className="space-y-1.5">
            <Label className="text-xs text-muted-foreground">{t('bk.folder')}</Label>
            <div className="flex gap-2">
              <Input
                className="font-mono text-xs flex-1"
                placeholder={backupCfg.default_folder || t('bk.folderPh')}
                value={backupCfg.folder ?? ''}
                onChange={(e) => setBackupCfg((b) => ({ ...b, folder: e.target.value }))}
              />
              <Button
                variant="outline"
                size="sm"
                onClick={async () => {
                  try {
                    const p = await PickBackupFolder();
                    if (p) setBackupCfg((b) => ({ ...b, folder: p }));
                  } catch (e: any) {
                    setBackupResult({ ok: false, msg: String(e?.message ?? e) });
                  }
                }}
              >
                {t('bk.browse')}
              </Button>
            </div>
            <div className="text-[10px] text-muted-foreground">
              {backupCfg.folder
                ? <>{t('bk.effective')} <span className="font-mono">{effectiveFolder}</span></>
                : <>{t('bk.defaultUse')} <span className="font-mono">{(backupCfg.default_folder ?? '').replace(/\\/g, '/')}</span></>
              }
            </div>
          </div>

          <div className="flex items-end gap-3">
            <div className="space-y-1.5">
              <Label className="text-xs text-muted-foreground">{t('bk.rotation')}</Label>
              <Input
                type="number"
                min={1}
                max={365}
                className="w-24 font-mono text-xs"
                value={backupCfg.rotation || 5}
                onChange={(e) => {
                  const n = Number(e.target.value);
                  if (Number.isFinite(n) && n > 0) setBackupCfg((b) => ({ ...b, rotation: Math.floor(n) }));
                }}
              />
            </div>
            <label className="flex items-center gap-2 text-sm cursor-pointer pb-2">
              <Checkbox
                checked={!!backupCfg.zip}
                onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, zip: !!c }))}
              />
              <span>{t('bk.zip')}</span>
            </label>
          </div>

          <div className="border-t border-border/60 pt-3 flex items-center gap-3">
            <Button size="sm" onClick={backupNow} disabled={backupRunning}>
              {backupRunning ? t('bk.backingUp') : t('bk.backupNow')}
            </Button>
            <span className="text-xs text-muted-foreground">
              {t('bk.lastRun')} <strong className="text-foreground">{fmtLast(backupCfg.last_backup_at)}</strong>
            </span>
          </div>

          {backupResult && (
            <div className={cn(
              'text-xs px-3 py-2 rounded-md border',
              backupResult.ok
                ? 'bg-success-muted border-success-border text-success-muted-foreground'
                : 'bg-danger-muted border-danger-border text-danger-muted-foreground',
            )}>
              {backupResult.msg}
            </div>
          )}
        </div>
      </>
    );
  }

  function ExternalServicesPanel() {
    const TABS: { k: typeof extSvcTab; label: string; ready?: boolean }[] = [
      { k: 'qrz',     label: 'QRZ.COM', ready: true },
      { k: 'clublog', label: 'CLUBLOG', ready: true },
      { k: 'hrdlog',  label: 'HRDLOG.NET', ready: true },
      { k: 'eqsl',    label: 'EQSL', ready: true },
      { k: 'lotw',    label: 'LOTW', ready: true },
      { k: 'cloudlog', label: 'CLOUDLOG', ready: true },
      { k: 'pota',    label: 'POTA', ready: true },
    ];

    async function savePotaToken() {
      setPotaBusy(true);
      setPotaResult(null);
      try {
        await SavePOTAToken(potaToken);
        setPotaResult({ ok: true, msg: 'Token saved. Run the sync from the QSL Manager → POTA hunter log.' });
      } catch (e: any) {
        setPotaResult({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setPotaBusy(false);
      }
    }
    const qrz = extSvc.qrz;
    const setQrz = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, qrz: { ...s.qrz, ...patch } }));
    const clublog = extSvc.clublog;
    const setClublog = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, clublog: { ...s.clublog, ...patch } }));

    async function testQrz() {
      setQrzTesting(true);
      setQrzTest(null);
      try {
        // Persist first so the backend test reads the key just typed.
        await SaveExternalServices(extSvc as any);
        const msg = await TestQRZUpload();
        setQrzTest({ ok: true, msg });
      } catch (e: any) {
        setQrzTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setQrzTesting(false);
      }
    }

    async function testClublog() {
      setClublogTesting(true);
      setClublogTest(null);
      try {
        await SaveExternalServices(extSvc as any);
        const msg = await TestClublogUpload();
        setClublogTest({ ok: true, msg });
      } catch (e: any) {
        setClublogTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setClublogTesting(false);
      }
    }

    const hrdlog = extSvc.hrdlog;
    const setHrdlog = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, hrdlog: { ...s.hrdlog, ...patch } }));

    async function testHrdlog() {
      setHrdlogTesting(true);
      setHrdlogTest(null);
      try {
        await SaveExternalServices(extSvc as any);
        const msg = await TestHRDLogUpload();
        setHrdlogTest({ ok: true, msg });
      } catch (e: any) {
        setHrdlogTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setHrdlogTesting(false);
      }
    }

    const cloudlog = extSvc.cloudlog;
    const setCloudlog = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, cloudlog: { ...s.cloudlog, ...patch } }));

    async function testCloudlog() {
      setCloudlogTesting(true);
      setCloudlogTest(null);
      try {
        // Save first: the backend test reads the stored config, so it must see
        // what was just typed (same as the other services' Test buttons).
        await SaveExternalServices(extSvc as any);
        const msg = await TestCloudlogUpload();
        setCloudlogTest({ ok: true, msg });
      } catch (e: any) {
        setCloudlogTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setCloudlogTesting(false);
      }
    }

    const eqsl = extSvc.eqsl;
    const setEqsl = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, eqsl: { ...s.eqsl, ...patch } }));

    async function testEqsl() {
      setEqslTesting(true);
      setEqslTest(null);
      try {
        await SaveExternalServices(extSvc as any);
        const msg = await TestEQSLUpload();
        setEqslTest({ ok: true, msg });
      } catch (e: any) {
        setEqslTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setEqslTesting(false);
      }
    }

    const lotw = extSvc.lotw;
    const setLotw = (patch: Partial<ExtServiceCfg>) =>
      setExtSvc((s) => ({ ...s, lotw: { ...s.lotw, ...patch } }));

    async function refreshLocations() {
      try {
        const locs: any = await ListTQSLStationLocations();
        setStationLocations((locs ?? []).map((l: any) => l.name).filter(Boolean));
      } catch (e: any) {
        setLotwTest({ ok: false, msg: String(e?.message ?? e) });
      }
    }

    async function testLotw() {
      setLotwTesting(true);
      setLotwTest(null);
      try {
        await SaveExternalServices(extSvc as any);
        const msg = await TestLoTWUpload();
        setLotwTest({ ok: true, msg });
      } catch (e: any) {
        setLotwTest({ ok: false, msg: String(e?.message ?? e) });
      } finally {
        setLotwTesting(false);
      }
    }

    return (
      <>
        <SectionHeader
          title={t('sec.external')}
          hint={t('extsvc.hint')}
        />

        <ProfileScopeNote profile={activeProfileObj} />

        {/* Applies to BOTH services, so it sits above the tab strip rather than
            inside one tab where it would look like a QRZ-only setting (and be
            invisible to anyone who only opens the Club Log tab). */}
        <label className="flex items-start gap-2 text-sm cursor-pointer mb-4 max-w-2xl">
          <Checkbox
            checked={!!extSvc.delete_remote}
            onCheckedChange={(c) => setExtSvc((v) => ({ ...v, delete_remote: !!c }))}
            className="mt-0.5"
          />
          <span>
            {t('es.deleteRemote')}
            <span className="block text-xs text-muted-foreground mt-0.5">{t('es.deleteRemoteHint')}</span>
          </span>
        </label>

        {/* Tab strip */}
        <div className="flex flex-wrap gap-1 border-b border-border mb-4">
          {TABS.map((tab) => (
            <button
              key={tab.k}
              type="button"
              onClick={() => setExtSvcTab(tab.k)}
              className={cn(
                'px-3 py-1.5 text-xs font-semibold rounded-t-md border-x border-t -mb-px transition-colors',
                extSvcTab === tab.k
                  ? 'bg-card border-border text-foreground'
                  : 'bg-muted/40 border-transparent text-muted-foreground hover:text-foreground',
              )}
            >
              {tab.label}
              {!tab.ready && <span className="ml-1 text-[9px] opacity-60">{t('es.soon')}</span>}
            </button>
          ))}
        </div>

        {extSvcTab === 'qrz' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.apiKey')}</Label>
              <Input
                value={qrz.api_key}
                onChange={(e) => setQrz({ api_key: e.target.value })}
                placeholder={t('es.qrzApiPh')}
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.forceCall')}</Label>
              <Input
                value={qrz.force_station_callsign}
                onChange={(e) => setQrz({ force_station_callsign: e.target.value.toUpperCase() })}
                placeholder={t('es.forceCallPh')}
                className="font-mono text-xs"
              />
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={qrz.auto_upload}
                  onCheckedChange={(c) => setQrz({ auto_upload: !!c })}
                />
                {t('es.autoUpload')}
              </label>

              <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
                <Label className="text-sm">{t('es.uploadTiming')}</Label>
                <Select
                  value={qrz.upload_mode || 'immediate'}
                  onValueChange={(v) => setQrz({ upload_mode: v })}
                >
                  <SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="immediate">{t('es.immediate')}</SelectItem>
                    <SelectItem value="delayed">{t('es.delayed')}</SelectItem>
                    <SelectItem value="on_close">{t('es.onClose')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testQrz} disabled={qrzTesting || !qrz.api_key}>
                  <UploadCloud className="size-3.5" /> {qrzTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {qrzTest && (
                  <span className={cn('text-xs', qrzTest.ok ? 'text-success' : 'text-danger')}>
                    {qrzTest.msg}
                  </span>
                )}
              </div>
            </div>
          </div>
        ) : extSvcTab === 'clublog' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.accountEmail')}</Label>
              <Input
                type="email"
                value={clublog.email}
                onChange={(e) => setClublog({ email: e.target.value })}
                placeholder={t('es.clubEmailPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.password')}</Label>
              <Input
                type="password"
                value={clublog.password}
                onChange={(e) => setClublog({ password: e.target.value })}
                placeholder={t('es.clubPwPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.logbookCall')}</Label>
              <Input
                value={clublog.callsign}
                onChange={(e) => setClublog({ callsign: e.target.value.toUpperCase() })}
                placeholder={t('es.callDefaultPh')}
                className="font-mono text-xs"
              />
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={clublog.auto_upload}
                  onCheckedChange={(c) => setClublog({ auto_upload: !!c })}
                />
                {t('es.autoUpload')}
              </label>

              <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
                <Label className="text-sm">{t('es.uploadTiming')}</Label>
                <Select
                  value={clublog.upload_mode || 'immediate'}
                  onValueChange={(v) => setClublog({ upload_mode: v })}
                >
                  <SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="immediate">{t('es.immediate')}</SelectItem>
                    <SelectItem value="delayed">{t('es.delayed')}</SelectItem>
                    <SelectItem value="on_close">{t('es.onClose')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testClublog} disabled={clublogTesting}>
                  <UploadCloud className="size-3.5" /> {clublogTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {clublogTest && (
                  <span className={cn('text-xs', clublogTest.ok ? 'text-success' : 'text-danger')}>
                    {clublogTest.msg}
                  </span>
                )}
              </div>
            </div>
          </div>
        ) : extSvcTab === 'hrdlog' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.stationCall')}</Label>
              <Input
                value={hrdlog.callsign}
                onChange={(e) => setHrdlog({ callsign: e.target.value.toUpperCase() })}
                placeholder={t('es.callDefaultPh')}
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.uploadCode')}</Label>
              <Input
                type="password"
                value={hrdlog.code}
                onChange={(e) => setHrdlog({ code: e.target.value })}
                placeholder={t('es.hrdCodePh')}
                className="text-xs"
              />
            </div>
            <div className="text-[10px] text-muted-foreground -mt-1">
              {t('es.hrdHint')}
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={hrdlog.auto_upload}
                  onCheckedChange={(c) => setHrdlog({ auto_upload: !!c })}
                />
                {t('es.autoUpload')}
              </label>

              <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
                <Label className="text-sm">{t('es.uploadTiming')}</Label>
                <Select
                  value={hrdlog.upload_mode || 'immediate'}
                  onValueChange={(v) => setHrdlog({ upload_mode: v })}
                >
                  <SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="immediate">{t('es.immediate')}</SelectItem>
                    <SelectItem value="delayed">{t('es.delayed')}</SelectItem>
                    <SelectItem value="on_close">{t('es.onClose')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testHrdlog} disabled={hrdlogTesting}>
                  <UploadCloud className="size-3.5" /> {hrdlogTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {hrdlogTest && (
                  <span className={cn('text-xs', hrdlogTest.ok ? 'text-success' : 'text-danger')}>
                    {hrdlogTest.msg}
                  </span>
                )}
              </div>
            </div>
          </div>
        ) : extSvcTab === 'cloudlog' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.cloudlogUrl')}</Label>
              <Input
                value={cloudlog.url}
                onChange={(e) => setCloudlog({ url: e.target.value })}
                placeholder={t('es.cloudlogUrlPh')}
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.apiKey')}</Label>
              <Input
                type="password"
                value={cloudlog.api_key}
                onChange={(e) => setCloudlog({ api_key: e.target.value })}
                placeholder={t('es.cloudlogKeyPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.cloudlogStationId')}</Label>
              <Input
                value={cloudlog.station_id}
                onChange={(e) => setCloudlog({ station_id: e.target.value })}
                placeholder="1"
                className="font-mono text-xs w-24"
              />
            </div>
            <div className="text-[10px] text-muted-foreground -mt-1">
              {t('es.cloudlogHint')}
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={cloudlog.auto_upload}
                  onCheckedChange={(c) => setCloudlog({ auto_upload: !!c })}
                />
                {t('es.autoUpload')}
              </label>

              <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
                <Label className="text-sm">{t('es.uploadTiming')}</Label>
                <Select
                  value={cloudlog.upload_mode === 'delayed' ? 'delayed' : 'immediate'}
                  onValueChange={(v) => setCloudlog({ upload_mode: v })}
                >
                  <SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="immediate">{t('es.immediate')}</SelectItem>
                    <SelectItem value="delayed">{t('es.delayed')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>
              {/* No "on close" option: unlike the other services, Cloudlog has no
                  per-QSO status column in the logbook, so there is nothing to
                  select a backlog from at shutdown. */}

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testCloudlog} disabled={cloudlogTesting}>
                  <UploadCloud className="size-3.5" /> {cloudlogTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {cloudlogTest && (
                  <span className={cn('text-xs', cloudlogTest.ok ? 'text-success' : 'text-danger')}>
                    {cloudlogTest.msg}
                  </span>
                )}
              </div>
            </div>
          </div>
        ) : extSvcTab === 'eqsl' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.usernameCall')}</Label>
              <Input
                value={eqsl.username}
                onChange={(e) => setEqsl({ username: e.target.value.toUpperCase() })}
                placeholder={t('es.eqslUserPh')}
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.password')}</Label>
              <Input
                type="password"
                value={eqsl.password}
                onChange={(e) => setEqsl({ password: e.target.value })}
                placeholder={t('es.eqslPwPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.qthNick')}</Label>
              <Input
                value={eqsl.qth_nickname}
                onChange={(e) => setEqsl({ qth_nickname: e.target.value })}
                placeholder={t('es.qthNickPh')}
                className="text-xs"
              />
            </div>
            <div className="text-[10px] text-muted-foreground -mt-1">
              {t('es.eqslHint')}
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={eqsl.auto_upload}
                  onCheckedChange={(c) => setEqsl({ auto_upload: !!c })}
                />
                {t('es.autoUpload')}
              </label>

              <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
                <Label className="text-sm">{t('es.uploadTiming')}</Label>
                <Select
                  value={eqsl.upload_mode || 'immediate'}
                  onValueChange={(v) => setEqsl({ upload_mode: v })}
                >
                  <SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="immediate">{t('es.immediate')}</SelectItem>
                    <SelectItem value="delayed">{t('es.delayed')}</SelectItem>
                    <SelectItem value="on_close">{t('es.onClose')}</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testEqsl} disabled={eqslTesting}>
                  <UploadCloud className="size-3.5" /> {eqslTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {eqslTest && (
                  <span className={cn('text-xs', eqslTest.ok ? 'text-success' : 'text-danger')}>
                    {eqslTest.msg}
                  </span>
                )}
              </div>
            </div>
          </div>
        ) : extSvcTab === 'lotw' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">{t('es.lotwUser')}</Label>
              <Input
                value={lotw.username}
                onChange={(e) => setLotw({ username: e.target.value })}
                placeholder={t('es.lotwUserPh')}
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.lotwPw')}</Label>
              <Input
                type="password"
                value={lotw.password}
                onChange={(e) => setLotw({ password: e.target.value })}
                placeholder={t('es.lotwPwPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.tqslPath')}</Label>
              <Input
                value={lotw.tqsl_path}
                onChange={(e) => setLotw({ tqsl_path: e.target.value })}
                placeholder="C:\Program Files (x86)\TrustedQSL\tqsl.exe"
                className="font-mono text-xs"
              />
              <Label className="text-sm">{t('es.stationLoc')}</Label>
              <div className="flex items-center gap-2">
                <Select value={lotw.station_location || '_'} onValueChange={(v) => setLotw({ station_location: v === '_' ? '' : v })}>
                  <SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('es.pickLoc')} /></SelectTrigger>
                  <SelectContent>
                    {stationLocations.length === 0 && <SelectItem value="_" disabled>{t('es.noLoc')}</SelectItem>}
                    {stationLocations.map((n) => <SelectItem key={n} value={n}>{n}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Button variant="ghost" size="sm" className="h-8 px-2" onClick={refreshLocations} title={t('es.reloadLoc')}>
                  <ArrowDown className="size-3.5 rotate-90" />
                </Button>
              </div>
              <Label className="text-sm">{t('es.forceCall')}</Label>
              <div>
                <Input
                  value={lotw.force_station_callsign}
                  onChange={(e) => setLotw({ force_station_callsign: e.target.value.toUpperCase() })}
                  placeholder={t('es.lotwForcePh')}
                  className="font-mono uppercase w-64"
                />
                <div className="text-[10px] text-muted-foreground mt-1">
                  {t('es.lotwForceHint')}
                </div>
              </div>
              <Label className="text-sm">{t('es.keyPw')}</Label>
              <Input
                type="password"
                value={lotw.key_password}
                onChange={(e) => setLotw({ key_password: e.target.value })}
                placeholder={t('es.keyPwPh')}
                className="text-xs"
              />
              <Label className="text-sm">{t('es.considerUnsent')}</Label>
              <div>
                <div className="flex items-center gap-4">
                  {(['N', 'R'] as const).map((f) => {
                    const flags = lotw.upload_flags ?? [];
                    const checked = flags.includes(f);
                    return (
                      <label key={f} className="flex items-center gap-2 text-sm cursor-pointer">
                        <Checkbox
                          checked={checked}
                          onCheckedChange={(c) => {
                            const next = c
                              ? Array.from(new Set([...flags, f]))
                              : flags.filter((x) => x !== f);
                            setLotw({ upload_flags: next });
                          }}
                        />
                        {f === 'N' ? t('es.flagN') : t('es.flagR')}
                      </label>
                    );
                  })}
                </div>
                <div className="text-[10px] text-muted-foreground mt-1">
                  {t('es.lotwUnsentHint')}
                </div>
              </div>
            </div>

            <div className="border-t border-border/60 pt-3 space-y-3">
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={lotw.auto_upload}
                  onCheckedChange={(c) => setLotw({ auto_upload: !!c, upload_mode: 'on_close' })}
                />
                {t('es.lotwAutoClose')}
              </label>
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={lotw.write_log}
                  onCheckedChange={(c) => setLotw({ write_log: !!c })}
                />
                {t('es.lotwWriteLog')}
              </label>

              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={testLotw} disabled={lotwTesting}>
                  <UploadCloud className="size-3.5" /> {lotwTesting ? t('es.testing') : t('es.testConn')}
                </Button>
                {lotwTest && (
                  <span className={cn('text-xs', lotwTest.ok ? 'text-success' : 'text-danger')}>
                    {lotwTest.msg}
                  </span>
                )}
              </div>
            </div>

            {/* LoTW user list — powers the colour-coded "LoTW" badge next to the
                entered callsign (green < 1 week · amber 1–4 weeks · red > 30 days). */}
            <div className="border-t border-border/60 pt-3 space-y-2">
              <Label className="text-sm font-medium">{t('lotw.usersTitle')}</Label>
              <p className="text-[11px] text-muted-foreground">{t('lotw.usersHint')}</p>
              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={downloadLotwUsers} disabled={lotwUsersBusy}>
                  <ArrowDown className="size-3.5" /> {lotwUsersBusy ? t('lotw.usersDownloading') : t('lotw.usersDownload')}
                </Button>
                <span className="text-xs text-muted-foreground">
                  {lotwUsers.count > 0
                    ? t('lotw.usersLoaded', { n: lotwUsers.count.toLocaleString(), date: lotwUsers.updated ? new Date(lotwUsers.updated).toLocaleDateString() : '?' })
                    : t('lotw.usersNone')}
                </span>
              </div>
            </div>
          </div>
        ) : extSvcTab === 'pota' ? (
          <div className="space-y-4 max-w-2xl">
            <p className="text-xs text-muted-foreground leading-relaxed">
              {t('es.potaHint')}
            </p>
            <div className="grid grid-cols-[170px_1fr] gap-3 items-start">
              <Label className="text-sm pt-2">{t('es.sessionToken')}</Label>
              <Textarea
                value={potaToken}
                onChange={(e) => setPotaToken(e.target.value)}
                placeholder="eyJ… (Authorization header from pota.app)"
                className="font-mono text-[11px] h-20"
              />
            </div>
            <div className="flex items-center gap-3">
              <Button onClick={savePotaToken} disabled={potaBusy}>
                {potaBusy ? <><Loader2 className="size-4 mr-1.5 animate-spin" /> {t('es.saving')}</> : t('es.saveToken')}
              </Button>
              <span className="text-[11px] text-muted-foreground">
                {t('es.potaThen')}
              </span>
            </div>
            {potaResult && (
              <div className={cn('text-xs rounded-md px-3 py-2 border',
                potaResult.ok ? 'border-success-border bg-success-muted text-success-muted-foreground' : 'border-destructive/30 bg-destructive/10 text-destructive')}>
                {potaResult.msg}
              </div>
            )}
          </div>
        ) : (
          <div className="flex flex-col items-center justify-center text-muted-foreground gap-2 py-16">
            <Construction className="size-10 opacity-30" />
            <div className="text-sm font-semibold text-foreground/70">
              {t('es.comingSoon', { name: TABS.find((tab) => tab.k === extSvcTab)?.label ?? '' })}
            </div>
            <div className="text-xs">{t('es.notWired')}</div>
          </div>
        )}
      </>
    );
  }

  function DatabasePanel() {
    async function refreshDb() { try { setDbSettings(await GetDatabaseSettings() as any); } catch {} }
    async function refreshBackend() { try { setBackendStatus(await GetDBBackendStatus() as any); } catch {} }
    // The chosen file is persisted (config.json) the moment it's picked; dbMsg
    // holds the pending path so the banner can offer a one-click restart to load
    // it (the DB pointer is only read at startup).
    async function openExisting() {
      try {
        const p = await PickOpenDatabase();
        if (!p) return;
        await OpenDatabase(p);
        await refreshDb();
        setDbMsg(p);
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    async function saveCopy() {
      try {
        const p = await PickSaveDatabase();
        if (!p) return;
        await MoveDatabase(p);
        await refreshDb();
        setDbMsg(p);
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    async function createNew() {
      try {
        const p = await PickSaveDatabase();
        if (!p) return;
        await CreateDatabase(p);
        await refreshDb();
        setDbMsg(p);
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    // Rename the CURRENT database (keeps all config), unlike New database which
    // starts empty. The old file is removed on the next launch.
    async function renameDb() {
      try {
        const p = await PickSaveDatabase();
        if (!p) return;
        await RenameDatabase(p);
        await refreshDb();
        setDbMsg(p);
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    async function resetDefault() {
      try {
        await ResetDatabaseToDefault();
        await refreshDb();
        setDbMsg(dbSettings.default_path || '');
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    function revealFolder() { RevealDataFolder().catch((e: any) => setErr(String(e?.message ?? e))); }
    // Rename/relocate THIS profile's logbook, carrying the QSOs across.
    async function renameLogbook() {
      try {
        const p = await PickSaveDatabase();
        if (!p) return;
        await RenameLogbook(p);
        setMysqlField({ enabled: false, sqlite_path: p });
        setRestartMsg(t('db.logbookRenamed'));
        await refreshBackend();
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    // Switching the logbook backend applies immediately (no restart): the local
    // SQLite file always stays the config store; only the QSO logbook moves.
    function useLocalLogbook() {
      SaveMySQLSettings({ ...mysqlCfg, enabled: false, sqlite_path: '' } as any)
        .then(async () => { setMysqlField({ enabled: false, sqlite_path: '' }); setRestartMsg(t('db.switchedSqlite')); await refreshBackend(); })
        .catch((e: any) => setErr(String(e?.message ?? e)));
    }
    // Point THIS profile's logbook at a SQLite file (settings stay in the
    // settings db). A NEW name is created + migrated empty; an existing file is
    // opened with its QSOs.
    async function newLogbook() {
      try {
        const p = await PickSaveDatabase();
        if (!p) return;
        await SaveMySQLSettings({ ...mysqlCfg, enabled: false, sqlite_path: p } as any);
        setMysqlField({ enabled: false, sqlite_path: p });
        setRestartMsg(t('db.switchedSqliteFile'));
        await refreshBackend();
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    async function openLogbook() {
      try {
        const p = await PickOpenDatabase();
        if (!p) return;
        await SaveMySQLSettings({ ...mysqlCfg, enabled: false, sqlite_path: p } as any);
        setMysqlField({ enabled: false, sqlite_path: p });
        setRestartMsg(t('db.switchedSqliteFile'));
        await refreshBackend();
      } catch (e: any) { setErr(String(e?.message ?? e)); }
    }
    function connectMysql() {
      SaveMySQLSettings(mysqlCfg as any)
        .then(async () => { setRestartMsg(t('db.switchedMysql')); await refreshBackend(); })
        .catch((e: any) => setErr(String(e?.message ?? e)));
    }
    return (
      <>
        <SectionHeader title={t('sec.database')} />

        {/* Settings / application database (settings + profiles) — always shown,
            distinct from the QSO logbook so the two are never confused. */}
        <div className="space-y-2 max-w-2xl mb-5 border border-border/60 rounded-md p-3">
          <Label>{t('db.appDb')}</Label>
          <div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
            {dbSettings.path || '—'}
            {dbSettings.is_custom
              ? <span className="ml-2 text-[10px] text-success">{t('db.customLoc')}</span>
              : <span className="ml-2 text-[10px] text-muted-foreground">{t('db.default')}</span>}
          </div>
          <p className="text-[11px] text-muted-foreground">{t('db.appDbHint')}</p>
          <div className="flex flex-wrap gap-2">
            <Button variant="outline" size="sm" onClick={revealFolder}><FolderOpen className="size-3.5" /> {t('db.openFolder')}</Button>
          </div>
        </div>

        {/* Logbook (QSOs) — this profile: default SQLite file, a dedicated file, or MySQL. */}
        <div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-1">
          <Label className="text-sm">{t('db.logbookLabel')}</Label>
          <Select
            value={mysqlCfg.enabled ? 'mysql' : 'sqlite'}
            onValueChange={(v) => {
              setRestartMsg('');
              if (v === 'mysql') { setMysqlField({ enabled: true }); return; }
              useLocalLogbook(); // SQLite → default logbook file (clears any per-profile path)
            }}
          >
            <SelectTrigger className="h-8 w-72"><SelectValue /></SelectTrigger>
            <SelectContent>
              <SelectItem value="sqlite">{t('db.optSqlite')}</SelectItem>
              <SelectItem value="mysql">{t('db.optMysql')}</SelectItem>
            </SelectContent>
          </Select>
        </div>
        <p className="text-[11px] text-muted-foreground max-w-2xl mb-3">{t('db.profileHint')}</p>

        {/* Compact active-backend confirmation / MySQL-fallback warning. */}
        {backendStatus && (
          backendStatus.fallback ? (
            <div className="max-w-2xl mb-4 text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
              {t('db.fallback')}
              <div className="font-mono text-[10px] mt-1 break-all">{backendStatus.error}</div>
            </div>
          ) : (
            <div className="max-w-2xl mb-4 text-[11px] text-muted-foreground">
              {t('db.activeBackend')} <strong className="uppercase text-foreground">{backendStatus.active}</strong>
            </div>
          )
        )}

        {/* SQLite logbook file: default logbook.db, or a dedicated file for this profile. */}
        {!mysqlCfg.enabled && (
        <div className="space-y-3 max-w-2xl">
          <div className="space-y-1">
            <Label>{t('db.logbookFile')}</Label>
            <div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
              {mysqlCfg.sqlite_path || dbSettings.logbook_default_path || '—'}
              {mysqlCfg.sqlite_path
                ? <span className="ml-2 text-[10px] text-success">{t('db.dedicatedFile')}</span>
                : <span className="ml-2 text-[10px] text-muted-foreground">{t('db.default')}</span>}
            </div>
            <p className="text-[11px] text-muted-foreground">{t('db.logbookFileHint')}</p>
          </div>
          <div className="flex flex-wrap gap-2">
            <Button variant="outline" size="sm" onClick={newLogbook}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
            <Button variant="outline" size="sm" onClick={openLogbook}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
            <Button variant="outline" size="sm" onClick={renameLogbook} title={t('db.renameLogbookTip')}><Pencil className="size-3.5" /> {t('db.renameLogbook')}</Button>
            {mysqlCfg.sqlite_path && <Button variant="ghost" size="sm" onClick={useLocalLogbook}>{t('db.useDefaultLogbook')}</Button>}
          </div>
        </div>
        )}

        {/* MySQL: shared logbook connection (multi-operator) */}
        {mysqlCfg.enabled && (
        <div className="space-y-3 max-w-2xl">
          <div className="text-[11px] text-muted-foreground leading-relaxed">{t('db.mysqlHint')}</div>
          <div className="grid grid-cols-[130px_1fr] gap-2 items-center">
            <Label className="text-sm">{t('db.host')}</Label>
            <Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
            <Label className="text-sm">{t('db.port')}</Label>
            <PortInput className="h-8 w-28 font-mono" value={mysqlCfg.port} fallback={3306} onChange={(n) => setMysqlField({ port: n })} />
            <Label className="text-sm">{t('db.database')}</Label>
            <Input className="h-8" placeholder="opslog" value={mysqlCfg.database} onChange={(e) => setMysqlField({ database: e.target.value })} />
            <Label className="text-sm">{t('db.user')}</Label>
            <Input className="h-8" value={mysqlCfg.user} onChange={(e) => setMysqlField({ user: e.target.value })} />
            <Label className="text-sm">{t('es.password')}</Label>
            <Input type="password" className="h-8" value={mysqlCfg.password} onChange={(e) => setMysqlField({ password: e.target.value })} />
          </div>
          <div className="flex items-center gap-3 flex-wrap">
            <Button variant="outline" size="sm" className="h-8"
              onClick={() => { setMysqlMsg(t('db.testing')); TestMySQLConnection(mysqlCfg as any).then(() => setMysqlMsg(t('db.connectedReady'))).catch((e: any) => setMysqlMsg(t('db.failed') + String(e?.message ?? e))); }}>
              {t('db.testCreate')}
            </Button>
            <Button size="sm" className="h-8" onClick={connectMysql}>{t('db.connectUse')}</Button>
            <span className="text-[11px] text-muted-foreground">{mysqlMsg}</span>
          </div>
        </div>
        )}

        {restartMsg && <div className="max-w-2xl mt-3 text-[11px] text-success">{restartMsg}</div>}

        {/* Backup settings, merged into this Database section. */}
        <div className="border-t border-border/60 mt-6 pt-5">
          {BackupPanel()}
        </div>
      </>
    );
  }

  function AudioPanel() {
    // Names the CAT backend the operator has configured, so the PTT dropdown can
    // say "CAT — Icom CI-V (USB)" rather than the flatly wrong "CAT (OmniRig)".
    const catBackendLabel = ({
      omnirig: t('cat.optOmnirig'),
      flex: t('cat.optFlex'),
      icom: t('cat.optIcom'),
      'icom-net': t('cat.optIcomNet'),
      tci: t('cat.optTci'),
      yaesu: t('cat.optYaesu'),
      xiegu: t('cat.optXiegu'),
    } as Record<string, string>)[catCfg.backend] ?? '';

    const deviceSelect = (
      field: keyof AudioSettings,
      devices: AudioDev[],
      placeholder: string,
    ) => (
      <Select
        value={(audioCfg[field] as string) || '_'}
        onValueChange={(v) => setAudioField({ [field]: v === '_' ? '' : v } as any)}
      >
        <SelectTrigger className="h-8"><SelectValue placeholder={placeholder} /></SelectTrigger>
        <SelectContent>
          <SelectItem value="_">{t('aud.noneDefault')}</SelectItem>
          {devices.map((d) => (
            <SelectItem key={d.id} value={d.id}>{d.name}{d.default ? '  ' + t('aud.defaultTag') : ''}</SelectItem>
          ))}
        </SelectContent>
      </Select>
    );
    return (
      <>
        <div className="flex items-start justify-between">
          <SectionHeader
            title={t('hw.audioVoice')}/>
          <Button variant="outline" size="sm" className="h-7 text-[11px] shrink-0" onClick={reloadAudioDevices}>
            {t('aud.refreshDevices')}
          </Button>
        </div>

        <div className="space-y-3 max-w-2xl">
          <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
            <Label className="text-sm">{t('aud.fromRadio')}</Label>
            {deviceSelect('from_radio', audioInputs, t('aud.phFromRadio'))}
            <Label className="text-sm">{t('aud.toRadio')}</Label>
            {deviceSelect('to_radio', audioOutputs, t('aud.phToRadio'))}
            <Label className="text-sm">{t('aud.recMic')}</Label>
            {deviceSelect('recording_device', audioInputs, t('aud.phRecMic'))}
            <Label className="text-sm">{t('aud.listening')}</Label>
            {deviceSelect('listening_device', audioOutputs, t('aud.phListening'))}
          </div>
          <p className="text-[11px] text-muted-foreground">
            <strong>{t('aud.fromRadioShort')}</strong> {t('aud.explainFrom')}{' '}
            <strong>{t('aud.toRadioShort')}</strong> {t('aud.explainTo')}
          </p>
          <div className="flex items-center gap-3">
            <Button
              variant={monitorOn ? 'default' : 'outline'}
              size="sm"
              className="h-8"
              onClick={toggleMonitor}
              disabled={!monitorOn && !audioCfg.from_radio}
              title={t('aud.monitorTitle')}
            >
              {monitorOn ? t('aud.stopListening') : t('aud.listenRadio')}
            </Button>
            <span className="text-[11px] text-muted-foreground">
              {monitorOn ? t('aud.monitorOn') : t('aud.monitorHint')}
            </span>
          </div>
          <div className="flex items-center gap-3">
            <Button
              variant={txOn ? 'destructive' : 'outline'}
              size="sm"
              className="h-8"
              onClick={toggleTX}
              disabled={!txOn && !audioCfg.to_radio}
              title={t('aud.txTitle')}
            >
              {txOn ? t('aud.stopTalk') : t('aud.talkRadio')}
            </Button>
            <span className="text-[11px] text-muted-foreground">
              {txOn ? t('aud.txOn') : t('aud.txHint')}
            </span>
          </div>
        </div>

        <div className="border-t border-border/60 pt-3 space-y-3 max-w-2xl">
          <h4 className="text-sm font-semibold text-foreground">{t('aud.recorder')}</h4>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={audioCfg.qso_record} onCheckedChange={(c) => setAudioField({ qso_record: !!c })} />
            {t('aud.recordEvery')}
          </label>
          <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
            <Label className="text-sm">{t('aud.recFolder')}</Label>
            <div className="flex gap-2">
              <Input value={audioCfg.qso_dir} onChange={(e) => setAudioField({ qso_dir: e.target.value })}
                placeholder="C:\…\OpsLog\Recordings" className="h-8 font-mono text-xs" />
              <Button variant="outline" size="sm" className="h-8 shrink-0"
                onClick={() => PickAudioFolder().then((d) => { if (d) setAudioField({ qso_dir: d }); }).catch(() => {})}>
                {t('aud.browse')}
              </Button>
            </div>
            <Label className="text-sm">{t('aud.preroll')}</Label>
            <Input type="number" min={0} max={60} value={audioCfg.preroll_seconds}
              onChange={(e) => setAudioField({ preroll_seconds: Math.max(0, Math.min(60, parseInt(e.target.value, 10) || 0)) })}
              className="h-8 w-24 font-mono" />
            <Label className="text-sm">{t('aud.format')}</Label>
            <Select value={audioCfg.format} onValueChange={(v) => setAudioField({ format: v as any })}>
              <SelectTrigger className="h-8 w-40"><SelectValue /></SelectTrigger>
              <SelectContent>
                <SelectItem value="wav">{t('aud.wav')}</SelectItem>
                <SelectItem value="mp3">{t('aud.mp3')}</SelectItem>
              </SelectContent>
            </Select>
            <Label className="text-sm">{t('aud.fromLevel')}</Label>
            <div className="flex items-center gap-2">
              <input type="range" min={10} max={300} step={5} value={audioCfg.from_gain}
                onChange={(e) => setAudioField({ from_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" />
              <span className="font-mono text-xs w-12 text-right">{audioCfg.from_gain}%</span>
            </div>
            <Label className="text-sm">{t('aud.micLevel')}</Label>
            <div className="flex items-center gap-2">
              <input type="range" min={10} max={300} step={5} value={audioCfg.mic_gain}
                onChange={(e) => setAudioField({ mic_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" />
              <span className="font-mono text-xs w-12 text-right">{audioCfg.mic_gain}%</span>
            </div>
          </div>
          <p className="text-xs text-muted-foreground">{t('aud.levelHint')}</p>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={emailCfg.auto_send} onCheckedChange={(c) => setEmailField({ auto_send: !!c })} />
            {t('aud.autoSend')}
          </label>
        </div>

        <div className="border-t border-border/60 pt-3 space-y-2 max-w-2xl">
          <h4 className="text-sm font-semibold text-foreground">{t('aud.dvkTitle')}</h4>
          <div className="rounded-md border border-border/60 p-2.5 space-y-2">
            <div className="grid grid-cols-[120px_1fr] gap-2 items-center">
              <Label className="text-sm">{t('aud.pttMethod')}</Label>
              <div className="flex gap-2 items-center">
                <Select value={audioCfg.ptt_method} onValueChange={(v) => setAudioField({ ptt_method: v as any })}>
                  <SelectTrigger className="h-8 w-44"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="none">{t('aud.pttNone')}</SelectItem>
                    {/* This has ALWAYS driven whichever CAT backend is active —
                        it calls the generic manager, and every backend (OmniRig,
                        FlexRadio, Icom CI-V over USB and over the network, TCI)
                        implements SetPTT. The label said "CAT (OmniRig)", so
                        operators on a CI-V rig concluded there was no CAT PTT for
                        them and reached for RTS/DTR or VOX instead. Name the
                        backend actually configured. */}
                    <SelectItem value="cat">{t('aud.pttCat')}{catBackendLabel ? ` — ${catBackendLabel}` : ''}</SelectItem>
                    <SelectItem value="rts">{t('aud.pttRts')}</SelectItem>
                    <SelectItem value="dtr">{t('aud.pttDtr')}</SelectItem>
                  </SelectContent>
                </Select>
                {audioCfg.ptt_method !== 'none' && (
                  <Button variant="outline" size="sm" className="h-8" onClick={() => { setDvkErr(''); TestPTT(audioCfg as any).catch((e: any) => setDvkErr(t('aud.errPttTest') + String(e?.message ?? e))); }}>
                    {t('aud.testPtt')}
                  </Button>
                )}
              </div>
              {(audioCfg.ptt_method === 'rts' || audioCfg.ptt_method === 'dtr') && (
                <>
                  <Label className="text-sm">{t('aud.pttPort')}</Label>
                  <div className="flex gap-2 items-center">
                    <Select value={audioCfg.ptt_port || '_'} onValueChange={(v) => setAudioField({ ptt_port: v === '_' ? '' : v })}>
                      <SelectTrigger className="h-8 w-44"><SelectValue placeholder={t('aud.pickPort')} /></SelectTrigger>
                      <SelectContent>
                        <SelectItem value="_">{t('aud.selectPort')}</SelectItem>
                        {wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
                      </SelectContent>
                    </Select>
                    <Button variant="ghost" size="sm" className="h-8 text-[11px]"
                      onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
                      {t('aud.refresh')}
                    </Button>
                  </div>
                </>
              )}
            </div>
          </div>
          {/* Playback level. Nothing else could raise it: the message went to the
              rig exactly as recorded, so a quietly recorded mic drove the radio
              quietly and the only remedies were the rig's own USB input menu or
              the Windows mixer. */}
          <div className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2">
            <Label className="text-sm">{t('aud.txLevel')}</Label>
            <div className="flex items-center gap-2">
              <input type="range" min={10} max={400} step={5} value={audioCfg.tx_gain}
                onChange={(e) => setAudioField({ tx_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" />
              <span className="font-mono text-xs w-12 text-right">{audioCfg.tx_gain}%</span>
            </div>
          </div>
          <p className="text-xs text-muted-foreground">{t('aud.txLevelHint')}</p>
          {dvkErr && <p className="text-[11px] text-destructive">{dvkErr}</p>}
          <div className="space-y-1.5">
            {dvkMsgs.map((m) => {
              const recHere = dvkStat.recording && dvkStat.rec_slot === m.slot;
              const recBusy = dvkStat.recording && !recHere;
              return (
                <div key={m.slot} className="flex items-center gap-2">
                  <span className="w-7 font-mono text-xs font-bold text-muted-foreground">F{m.slot}</span>
                  <Input
                    className="h-8 flex-1"
                    placeholder={t('aud.msgPlaceholder', { n: m.slot })}
                    value={m.label}
                    onChange={(e) => setDvkMsgs((ms) => ms.map((x) => x.slot === m.slot ? { ...x, label: e.target.value } : x))}
                    onBlur={(e) => SetDVKLabel(m.slot, e.target.value).catch(() => {})}
                  />
                  <span className="w-16 text-[11px] text-muted-foreground text-right">
                    {m.has_audio ? `✓ ${m.duration_sec.toFixed(1)}s` : '—'}
                  </span>
                  <Button
                    type="button"
                    variant={recHere ? 'destructive' : 'outline'} size="sm" className="h-8 w-28 shrink-0 select-none touch-none"
                    disabled={recBusy}
                    onPointerDown={(e) => {
                      e.preventDefault();
                      (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
                      setDvkErr('');
                      DVKStartRecord(m.slot).catch((err) => setDvkErr(t('aud.errRecord') + String(err?.message ?? err)));
                    }}
                    onPointerUp={() => {
                      DVKStopRecord().then(reloadDvk).catch((err) => setDvkErr(t('aud.errSave') + String(err?.message ?? err)));
                    }}
                  >
                    {recHere ? t('aud.recordingNow') : t('aud.holdRec')}
                  </Button>
                  <Button
                    type="button"
                    variant="outline" size="sm" className="h-8 w-20 shrink-0"
                    disabled={!m.has_audio || dvkStat.recording}
                    onClick={() => (dvkStat.playing ? DVKStop() : DVKPreview(m.slot).catch((err) => setDvkErr(t('aud.errPlay') + String(err?.message ?? err))))}
                  >
                    {dvkStat.playing ? t('aud.stop') : t('aud.play')}
                  </Button>
                </div>
              );
            })}
          </div>
        </div>
      </>
    );
  }

  function GeneralPanel() {
    const { lang, setLang, t } = useI18n();
    return (
      <>
        <SectionHeader title={t('sec.general')} hint={t('gen.hint')} />
        <div className="space-y-3 max-w-3xl">
          <div className="flex items-center gap-3">
            <Label className="text-sm w-40">{t('settings.language')}</Label>
            <div className="inline-flex rounded-md border border-border overflow-hidden">
              {([['en', FlagGB, 'English'], ['fr', FlagFR, 'Français']] as [Lang, typeof FlagGB, string][]).map(([code, Flag, label]) => (
                <button key={code} type="button" onClick={() => setLang(code)}
                  className={cn('flex items-center gap-2 px-3 py-1.5 text-sm font-medium border-l border-border first:border-l-0',
                    lang === code ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
                  <Flag className="w-5 rounded-[2px] border border-border/30" />
                  {label}
                </button>
              ))}
            </div>
          </div>

          <ThemeSelector />

          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={autofocusWB} onCheckedChange={(c) => { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }} />
            {t('gen.autofocusWB')}
          </label>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={showBeamMap} onCheckedChange={(c) => { const v = !!c; setShowBeamMap(v); writeUiPref('opslog.showBeamOnMap', v ? '1' : '0'); }} />
            {t('gen.showBeam')}
          </label>

          {/* Super Check Partial / N+1 — downloads the community MASTER.SCP list and
              shows a two-column callsign helper (partial matches + one-edit calls). */}
          <div className="border-t border-border/60 pt-3 space-y-2">
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={scp.enabled} disabled={scpBusy} onCheckedChange={(c) => toggleScp(!!c)} />
              {t('scp.enable')}
            </label>
            <p className="text-[11px] text-muted-foreground">{t('scp.settingsHint')}</p>
            {scp.enabled && (
              <div className="flex items-center gap-3">
                <Button variant="outline" size="sm" onClick={downloadScp} disabled={scpBusy}>
                  <ArrowDown className="size-3.5" /> {scpBusy ? t('scp.downloading') : t('scp.download')}
                </Button>
                <span className="text-xs text-muted-foreground">
                  {scp.count > 0
                    ? t('scp.loaded', { n: scp.count.toLocaleString(), date: scp.updated ? new Date(scp.updated).toLocaleDateString() : '?' })
                    : t('scp.notLoaded')}
                </span>
              </div>
            )}
          </div>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={startEqEnd} onCheckedChange={(c) => { const v = !!c; setStartEqEnd(v); writeUiPref('opslog.startEqualsEnd', v ? '1' : '0'); }} />
            {t('gen.startEqEnd')} <span className="text-xs text-muted-foreground">{t('gen.startEqEndHint')}</span>
          </label>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={showQsoRate} onCheckedChange={(c) => { const v = !!c; setShowQsoRate(v); writeUiPref('opslog.showQsoRate', v ? '1' : '0'); }} />
            {t('gen.showQsoRate')} <span className="text-xs text-muted-foreground">{t('gen.showQsoRateHint')}</span>
          </label>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={lookupOnBlur} onCheckedChange={(c) => { const v = !!c; setLookupOnBlur(v); writeUiPref('opslog.lookupOnBlur', v ? '1' : '0'); }} />
            {t('gen.lookupOnBlur')} <span className="text-xs text-muted-foreground">{t('gen.lookupOnBlurHint')}</span>
          </label>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={groupDigital} onCheckedChange={(c) => { const v = !!c; setGroupDigital(v); writeUiPref('opslog.groupDigitalSlots', v ? '1' : '0'); }} />
            {t('gen.groupDigital')} <span className="text-xs text-muted-foreground">{t('gen.groupDigitalHint')}</span>
          </label>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={checkUpdates} onCheckedChange={(c) => { const v = !!c; setCheckUpdates(v); writeUiPref('opslog.checkUpdates', v ? '1' : '0'); }} />
            {t('gen.checkUpdates')} <span className="text-xs text-muted-foreground">{t('gen.checkUpdatesHint')}</span>
          </label>
          <TelemetryToggle />

          <MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} yaesuAvailable={yaesuAvailable} />

          <div className="border-t border-border/60 pt-4 space-y-2">
            <h4 className="text-sm font-semibold text-foreground">{t('gen.pwEnc')}</h4>
            {secret.has_passphrase ? (
              <>
                <p className="text-xs text-muted-foreground">
                  {t('gen.pwEncrypted')}{' '}
                  {secret.unlocked
                    ? <span className="text-success font-medium">{t('gen.pwUnlocked')}</span>
                    : <span className="text-warning font-medium">{t('gen.pwLocked')}</span>}.
                </p>
                {secret.unlocked && (
                  <>
                    <div className="flex items-center gap-2 max-w-md">
                      <Input type="password" placeholder={t('gen.pwNewPh')} value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
                      <Input type="password" placeholder={t('gen.pwConfirm')} value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
                      <Button size="sm" disabled={!ppNew || ppBusy} onClick={applyPassphrase}>{t('gen.pwChange')}</Button>
                    </div>
                    <Button variant="outline" size="sm" disabled={ppBusy} onClick={removePassphrase}>{t('gen.pwRemove')}</Button>
                  </>
                )}
              </>
            ) : (
              <>
                <p className="text-xs text-muted-foreground">
                  {t('gen.pwHint')}
                </p>
                <div className="flex items-center gap-2 max-w-md">
                  <Input type="password" placeholder={t('gen.pwPassphrase')} value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
                  <Input type="password" placeholder={t('gen.pwConfirm')} value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
                  <Button size="sm" disabled={!ppNew || ppNew !== ppConfirm || ppBusy} onClick={applyPassphrase}>{t('gen.pwEncrypt')}</Button>
                </div>
              </>
            )}
            {ppErr && <div className="text-xs text-destructive">{ppErr}</div>}
          </div>

          <div className="border-t border-border/60 pt-4 space-y-2">
            <h4 className="text-sm font-semibold text-foreground">{t('gen.clubTitle')}</h4>
            <label className="flex items-start gap-2 text-sm cursor-pointer">
              <Checkbox
                checked={clubInfo.enabled}
                onCheckedChange={async (c) => {
                  const v = !!c; setClubInfo((s) => ({ ...s, enabled: v })); setClubErr('');
                  try {
                    await SetClublogCtyEnabled(v);
                    let info = (await GetClublogCtyInfo()) as ClubInfo;
                    // First enable with no cached data → download it now.
                    if (v && !info.loaded) {
                      setClubBusy(true);
                      try { info = (await DownloadClublogCty()) as ClubInfo; }
                      finally { setClubBusy(false); }
                    }
                    setClubInfo(info);
                  } catch (e: any) { setClubErr(String(e?.message ?? e)); }
                }}
                className="mt-0.5"
              />
              <span>
                {t('gen.clubUse')}
                <span className="block text-xs text-muted-foreground mt-0.5">
                  {t('gen.clubDesc')}
                </span>
              </span>
            </label>
            <div className="flex items-center gap-3 pl-6">
              <Button variant="outline" size="sm" className="h-8" disabled={clubBusy}
                onClick={() => { setClubBusy(true); setClubErr(''); DownloadClublogCty().then((i) => setClubInfo(i as ClubInfo)).catch((e: any) => setClubErr(String(e?.message ?? e))).finally(() => setClubBusy(false)); }}>
                {clubBusy ? t('gen.downloading') : (clubInfo.loaded ? t('gen.clubUpdate') : t('gen.clubDownload'))}
              </Button>
              <span className="text-[11px] text-muted-foreground">
                {clubInfo.loaded
                  ? t('gen.clubCount', { n: clubInfo.count.toLocaleString() }) + (clubInfo.date ? ' · ' + clubInfo.date.slice(0, 10) : '')
                  : t('gen.notDownloaded')}
              </span>
            </div>
            {clubErr && <p className="text-[11px] text-destructive pl-6">{clubErr}</p>}
          </div>

          <div className="border-t border-border/60 pt-4 space-y-2">
            <h4 className="text-sm font-semibold text-foreground">{t('gen.mwTitle')}</h4>
            <label className="flex items-start gap-2 text-sm cursor-pointer">
              <Checkbox
                checked={mwInfo.enabled}
                onCheckedChange={async (c) => {
                  const v = !!c; setMwInfo((s) => ({ ...s, enabled: v })); setMwErr('');
                  try {
                    await SetClublogMostWantedEnabled(v);
                    let info = (await GetClublogMostWantedInfo()) as MWInfo;
                    // First enable with no cached list → fetch it now (for the active call).
                    if (v && !info.loaded) {
                      setMwBusy(true);
                      try { info = (await DownloadClublogMostWanted()) as MWInfo; }
                      finally { setMwBusy(false); }
                    }
                    setMwInfo(info);
                  } catch (e: any) { setMwErr(String(e?.message ?? e)); }
                }}
                className="mt-0.5"
              />
              <span>
                {t('gen.mwShow')}
                <span className="block text-xs text-muted-foreground mt-0.5">
                  {t('gen.mwDesc')}
                </span>
              </span>
            </label>
            <div className="flex items-center gap-3 pl-6">
              <Button variant="outline" size="sm" className="h-8" disabled={mwBusy}
                onClick={() => { setMwBusy(true); setMwErr(''); DownloadClublogMostWanted().then((i) => setMwInfo(i as MWInfo)).catch((e: any) => setMwErr(String(e?.message ?? e))).finally(() => setMwBusy(false)); }}>
                {mwBusy ? t('gen.downloading') : (mwInfo.loaded ? t('gen.mwUpdate') : t('gen.mwDownload'))}
              </Button>
              <span className="text-[11px] text-muted-foreground">
                {mwInfo.loaded
                  ? t('gen.mwCount', { n: mwInfo.count.toLocaleString() }) + (mwInfo.callsign ? ' · ' + mwInfo.callsign : '') + (mwInfo.date ? ' · ' + mwInfo.date : '')
                  : t('gen.notDownloaded')}
              </span>
            </div>
            {mwErr && <p className="text-[11px] text-destructive pl-6">{mwErr}</p>}
          </div>
        </div>
      </>
    );
  }

  function EmailPanel() {
    return (
      <>
        <SectionHeader title={t('email.title')}/>
        <div className="space-y-3 max-w-2xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={emailCfg.enabled} onCheckedChange={(c) => setEmailField({ enabled: !!c })} />
            Enable e-mail sending
          </label>
          <div className="grid grid-cols-[130px_1fr] gap-2 items-center">
            <Label className="text-sm">SMTP server</Label>
            <Input className="h-8" placeholder="ex5.mail.ovh.net" value={emailCfg.smtp_host} onChange={(e) => setEmailField({ smtp_host: e.target.value })} />
            <Label className="text-sm">Port / encryption</Label>
            <div className="flex gap-2 items-center">
              <PortInput className="h-8 w-24 font-mono" value={emailCfg.smtp_port} fallback={587} onChange={(n) => setEmailField({ smtp_port: n })} />
              <Select value={emailCfg.encryption} onValueChange={(v) => setEmailField({ encryption: v as any })}>
                <SelectTrigger className="h-8 w-44"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="starttls">STARTTLS (587)</SelectItem>
                  <SelectItem value="ssl">SSL/TLS (465)</SelectItem>
                  <SelectItem value="none">{t('em.none')}</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div />
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={emailCfg.auth} onCheckedChange={(c) => setEmailField({ auth: !!c })} />
              {t('em.smtpAuth')}
            </label>
            <Label className="text-sm">{t('em.username')}</Label>
            <Input className="h-8" disabled={!emailCfg.auth} value={emailCfg.smtp_user} onChange={(e) => setEmailField({ smtp_user: e.target.value })} />
            <Label className="text-sm">{t('es.password')}</Label>
            {/* No reveal button: the settings window is often open while the
                screen is shared or shown to someone in the shack. */}
            <Input type="password" className="h-8" disabled={!emailCfg.auth} value={emailCfg.smtp_password} onChange={(e) => setEmailField({ smtp_password: e.target.value })} />
            <Label className="text-sm">{t('em.fromAddr')}</Label>
            <Input className="h-8" placeholder="you@example.com" value={emailCfg.from} onChange={(e) => setEmailField({ from: e.target.value })} />
            <Label className="text-sm">{t('em.replyTo')}</Label>
            <div>
              <Input className="h-8" placeholder={t('em.replyToPh')} value={emailCfg.reply_to} onChange={(e) => setEmailField({ reply_to: e.target.value })} />
              <div className="text-[10px] text-muted-foreground mt-1">{t('em.replyToHint')}</div>
            </div>
          </div>
          <div className="flex items-center gap-3">
            <Button variant="outline" size="sm" className="h-8"
              onClick={() => { setEmailMsg(t('em.sendingTest')); SaveEmailSettings(emailCfg as any).then(() => TestEmail('')).then(() => setEmailMsg(t('em.testSent'))).catch((e: any) => setEmailMsg(t('em.testFailed') + String(e?.message ?? e))); }}>
              {t('em.sendTest')}
            </Button>
            <span className="text-[11px] text-muted-foreground">{emailMsg}</span>
          </div>

          <div className="pt-2 mt-2 border-t border-border space-y-2">
            <Label className="text-sm font-semibold">{t('em.qslCardEmail')}</Label>
            <div className="text-[11px] text-muted-foreground">
              {t('em.qslVarsHint')} {'{CALL}'} {'{DATE}'} {'{BAND}'} {'{MODE}'} {'{MYCALL}'}.
            </div>
            <Input className="h-8" placeholder={t('em.subject')} value={eqslCfg.subject}
              onChange={(e) => setEqslField({ subject: e.target.value })} />
            <Textarea rows={3} className="text-sm" placeholder={t('em.body')} value={eqslCfg.body}
              onChange={(e) => setEqslField({ body: e.target.value })} />
            <label className="flex items-center gap-2 text-sm cursor-pointer">
              <Checkbox checked={eqslCfg.auto_send} onCheckedChange={(c) => setEqslField({ auto_send: !!c })} />
              {t('em.autoSend')}
            </label>
            <div className="text-[11px] text-muted-foreground">
              {t('em.autoSendHint')}
            </div>
          </div>
        </div>
      </>
    );
  }

  function USCountiesPanel() {
    const loaded = ulsStatus.count > 0;
    return (
      <div className="max-w-2xl space-y-5">
        <div>
          <h3 className="text-sm font-semibold mb-1">{t('uscty.title')}</h3>
          <p className="text-xs text-muted-foreground leading-relaxed">{t('uscty.intro')}</p>
        </div>

        {/* Required-download notice */}
        <div className="rounded-md border border-warning/40 bg-warning/10 p-3 text-xs text-foreground/90 leading-relaxed">
          {t('uscty.needDownload')}
        </div>

        {/* Status + download */}
        <div className="rounded-md border border-border p-3 space-y-3">
          <div className="flex items-center justify-between gap-3">
            <div className="text-xs">
              <div className="font-medium">{t('uscty.dbStatus')}</div>
              <div className="text-muted-foreground">
                {loaded
                  ? t('uscty.loaded', { n: ulsStatus.count.toLocaleString(), date: ulsStatus.updated_at ? new Date(ulsStatus.updated_at).toLocaleDateString() : '—' })
                  : t('uscty.notLoaded')}
              </div>
            </div>
            <Button size="sm" onClick={downloadUls} disabled={ulsBusy}>
              {ulsBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : <ArrowDown className="size-3.5 mr-1.5" />}
              {loaded ? t('uscty.update') : t('uscty.download')}
            </Button>
          </div>

          {ulsProgress && (
            <div className="space-y-1">
              <div className="text-[11px] text-muted-foreground flex justify-between">
                <span>{ulsProgress.stage}</span><span>{ulsProgress.pct}%</span>
              </div>
              <div className="h-1.5 w-full rounded bg-muted overflow-hidden">
                <div className="h-full bg-primary transition-all" style={{ width: `${ulsProgress.pct}%` }} />
              </div>
            </div>
          )}
          {ulsMsg && (
            <div className={cn('text-xs', ulsMsg.ok ? 'text-success' : 'text-destructive')}>{ulsMsg.text}</div>
          )}
        </div>

        {/* Backfill existing QSOs */}
        <div className="rounded-md border border-border p-3 space-y-2">
          <div className="text-xs font-medium">{t('uscty.backfillTitle')}</div>
          <p className="text-[11px] text-muted-foreground leading-relaxed">{t('uscty.backfillIntro')}</p>
          <div className="flex items-center gap-3">
            <Button size="sm" variant="secondary" onClick={runBackfill} disabled={backfillBusy || !loaded}>
              {backfillBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
              {t('uscty.backfillRun')}
            </Button>
            {backfillMsg && <span className="text-xs text-muted-foreground">{backfillMsg}</span>}
          </div>
        </div>
      </div>
    );
  }

  // Map sections to their content + icon (for placeholder).
  const PANELS: Record<SectionId, () => JSX.Element> = {
    general:        GeneralPanel,
    email:          EmailPanel,
    station:        StationPanel,
    profiles:       ProfilesPanel,
    operating:      OperatingPanelWrapper,
    confirmations:  ConfirmationsPanel,
    'external-services': ExternalServicesPanel,
    lookup:         LookupPanel,
    'lists-bands':  BandsPanel,
    'lists-modes':  ModesPanel,
    cluster:        ClusterPanel,
    udp:            UDPIntegrationsPanelWrapper,
    // Rendered as a real element (not called as a bare function) so its own hooks
    // — useState/useEffect/useI18n — get a proper component context; PANELS[x]()
    // is a plain call and hook-holding panels must go through JSX like this.
    adifmon:        () => <ADIFMonitorPanel />,
    relayauto:      () => <RelayAutoPanel />,
    backup:         BackupPanel,
    database:       DatabasePanel,
    uscounties:     USCountiesPanel,
    autostart:      () => <AutostartPanelComponent />,
    awards:         () => <ComingSoon id="awards" icon={Award} />,
    cat:            CATPanel,
    rotator:        RotatorPanel,
    winkeyer:       WinkeyerPanel,
    antenna:        UltrabeamPanel,
    antgenius:      AntGeniusPanelSettings,
    tunergenius:    TunerGeniusPanelSettings,
    pgxl:           PGXLPanelSettings,
    flex:           () => <FlexBandAntennasPanel bands={lists.bands ?? []} />,
    audio:          AudioPanel,
  };

  return (
    <Dialog open onOpenChange={(o) => { if (!o) onClose(); }}>
      <DialogContent className="max-w-[1180px] w-full max-h-[90vh] grid grid-rows-[auto_1fr_auto] gap-0 p-0">
        <DialogHeader>
          <DialogTitle>{t('settings.title')}</DialogTitle>
          <DialogDescription className="sr-only">Configure OpsLog modules — station, lookup, hardware…</DialogDescription>
        </DialogHeader>

        {loading ? (
          <div className="p-6 text-muted-foreground">Loading…</div>
        ) : (
          <div className="grid grid-cols-[320px_1fr] min-h-0 overflow-hidden">
            {/* Left sidebar tree */}
            <aside className="border-r border-border bg-muted/30 overflow-y-auto p-2">
              <Tree selected={selected} onSelect={setSelected} flexAvailable={flexAvailable} />
            </aside>

            {/* Right content pane */}
            <div className="overflow-y-auto p-6">
              <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-3 font-semibold">
                {breadcrumb}
              </div>
              {PANELS[selected]?.()}

              {err && (
                <div className="mt-6 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded-md px-3 py-2 max-w-2xl">
                  {err}
                </div>
              )}
              {msg && (
                <div className="mt-6 text-xs rounded-md px-3 py-2 max-w-2xl text-[color:var(--color-ok)] bg-[color:var(--color-ok)]/10 border border-[color:var(--color-ok)]/30">
                  {msg}
                </div>
              )}
            </div>
          </div>
        )}

        <DialogFooter>
          <Button variant="outline" onClick={onClose} disabled={saving}>{t('btn.cancel')}</Button>
          <Button variant="outline" onClick={() => save(false)} disabled={saving || loading}>{saving ? t('btn.savingLong') : t('btn.save')}</Button>
          <Button onClick={() => save(true)} disabled={saving || loading}>{saving ? t('btn.savingLong') : t('btn.saveClose')}</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

// PortInput — a TCP port field you can actually clear.
//
// Every port box was written as `parseInt(e.target.value) || <default>`. Delete
// the contents and parseInt gives NaN, so the default is written straight back:
// the digits reappear under the cursor and the only way to enter a different
// port is to overwrite character by character. Reported on the cluster editor;
// the same line existed in eight other places.
//
// The raw TEXT is the state here, and the parent is only told about a value that
// is actually a port. An empty box stays empty while you type; on blur it falls
// back to the last good value, so nothing is ever saved as 0 or NaN.
function PortInput({ value, onChange, fallback, className, min = 1, max = 65535 }: {
  value: number; onChange: (n: number) => void; fallback: number; className?: string; min?: number; max?: number;
}) {
  const [text, setText] = useState(value ? String(value) : '');
  const typed = useRef(false);
  // Only adopt the incoming value while the operator has NOT started typing —
  // settings arrive from the backend after mount, and re-syncing mid-edit is
  // precisely the behaviour being fixed.
  useEffect(() => {
    if (!typed.current) setText(value ? String(value) : '');
  }, [value]);
  return (
    <Input
      type="text"
      inputMode="numeric"
      className={className}
      value={text}
      onChange={(e) => {
        typed.current = true;
        const digits = e.target.value.replace(/[^0-9]/g, '').slice(0, 5);
        setText(digits);
        const n = parseInt(digits, 10);
        if (n >= min && n <= max) onChange(n);
      }}
      onBlur={() => {
        typed.current = false;
        const n = parseInt(text, 10);
        if (!(n >= min && n <= max)) {
          setText(String(fallback));
          onChange(fallback);
        }
      }}
    />
  );
}

// ClusterServerEditor edits one row of cluster_servers. Init commands are
// free-form (one per line); the backend strips blanks and "//" comments.
interface ClusterEditorProps {
  value: Omit<clusterModels.ServerConfig, 'convertValues'>;
  onCancel: () => void;
  onSave: (s: Omit<clusterModels.ServerConfig, 'convertValues'>) => void | Promise<void>;
}

function ClusterServerEditor({ value, onCancel, onSave }: ClusterEditorProps) {
  const [s, setS] = useState(value);
  const update = (patch: Partial<typeof s>) => setS((cur) => ({ ...cur, ...patch }));
  return (
    <Dialog open onOpenChange={(o) => { if (!o) onCancel(); }}>
      <DialogContent className="max-w-[640px] px-6">
        <DialogHeader className="px-2">
          <DialogTitle>{s.id ? `Edit cluster · ${s.name || 'unnamed'}` : 'New cluster'}</DialogTitle>
          <DialogDescription className="text-xs">
            Telnet endpoint + optional login override and init commands. Init commands are sent one per line, 0.5s apart, right after login.
          </DialogDescription>
        </DialogHeader>
        <div className="grid grid-cols-2 gap-3 py-2 px-2">
          <div className="space-y-1 col-span-2">
            <Label>Display name</Label>
            <Input autoFocus value={s.name} onChange={(e) => update({ name: e.target.value })} placeholder="VE7CC, F4BPO home…" />
          </div>
          <div className="space-y-1">
            <Label>Host</Label>
            <Input className="font-mono" value={s.host} onChange={(e) => update({ host: e.target.value })} placeholder="dxc.k0xm.net" />
          </div>
          <div className="space-y-1">
            <Label>Port</Label>
            <PortInput className="font-mono" value={s.port} fallback={7300} onChange={(n) => update({ port: n })} />
          </div>
          <div className="space-y-1">
            <Label>Login callsign (optional)</Label>
            <Input className="font-mono uppercase" value={s.login_override} onChange={(e) => update({ login_override: e.target.value })} placeholder="Active profile if empty" />
          </div>
          <div className="space-y-1">
            <Label>Password (optional)</Label>
            <Input type="password" value={s.password} onChange={(e) => update({ password: e.target.value })} autoComplete="off" />
          </div>
          <div className="space-y-1 col-span-2">
            <Label>Init commands (one per line, // for comments)</Label>
            <Textarea
              className="font-mono text-xs min-h-[120px]"
              value={s.init_commands}
              onChange={(e) => update({ init_commands: e.target.value })}
              placeholder={`// turn on DXCC info\nset/needsdxcc\nsh/dx 30`}
            />
          </div>
          <label className="flex items-center gap-2 text-sm cursor-pointer col-span-2">
            <Checkbox checked={s.enabled} onCheckedChange={(c) => update({ enabled: !!c })} />
            Enabled (will be connected at startup if Auto-connect is on)
          </label>
        </div>
        <DialogFooter className="px-2">
          <Button variant="outline" onClick={onCancel}>Cancel</Button>
          <Button
            onClick={() => onSave({ ...s, name: s.name.trim(), host: s.host.trim(), login_override: s.login_override.trim().toUpperCase() })}
            disabled={!s.name.trim() || !s.host.trim()}
          >
            {s.id ? 'Save changes' : 'Create cluster'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
