import { useEffect, useMemo, 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, Eye, EyeOff,
} 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,
  GetPGXLSettings, SavePGXLSettings,
  GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
  GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
  GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
  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,
  GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
  GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
  GetTelemetryEnabled, SetTelemetryEnabled,
  GetLiveStatusEnabled, SetLiveStatusEnabled,
  GetQSLDefaults, SaveQSLDefaults,
  GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
  GetPOTAToken, SavePOTAToken,
  TestLoTWUpload, ListTQSLStationLocations,
  DownloadLoTWUsers, GetLoTWUsersStatus,
  DownloadULSCounties, ULSStatus, BackfillUSCounties,
  ComputeStationInfo,
  GetUIPref, SetUIPref,
  GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
} 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
}

// 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'
  | 'lookup'
  | 'lists-bands'
  | 'lists-modes'
  | 'cluster'
  | 'backup'
  | 'database'
  | 'autostart'
  | 'uscounties'
  | 'awards'
  | 'cat'
  | 'rotator'
  | 'winkeyer'
  | 'antenna'
  | 'antgenius'
  | 'pgxl'
  | 'flex'
  | 'audio';

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

// 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' },
    { kind: 'item', label: t('sec.pgxl'),         id: 'pgxl' },
    ...(flexAvailable ? [{ kind: 'item', label: t('sec.flex'), id: 'flex' } as TreeNode] : []),
    { 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.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',
  uscounties: 'sec.uscounties',
  awards: 'sec.awards', cat: 'sec.cat', rotator: 'sec.rotator', winkeyer: 'sec.winkeyer', antenna: 'sec.antenna',
  antgenius: 'sec.antgenius', pgxl: 'sec.pgxl', flex: 'sec.flex', audio: 'sec.audio', general: 'sec.general', email: 'sec.email',
};

// 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',
  awards: 'Awards',
  cat: 'CAT interface',
  rotator: 'Rotator',
  winkeyer: 'CW Keyer',
  antenna: 'Ultrabeam / Steppir',
  antgenius: 'Antenna Genius',
  pgxl: 'Power Genius',
  flex: 'FlexRadio',
  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.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>
  );
}

// LiveStatusToggle publishes this operator's current activity (call + band +
// freq + mode) to the shared MySQL `live_status` table every ~15s, for multi-op
// events — a small web script on your server renders it for the QRZ page. Only
// useful on a MySQL logbook. Self-contained component (owns its async state).
function LiveStatusToggle() {
  const { t } = useI18n();
  const [on, setOn] = useState(false);
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    GetLiveStatusEnabled().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); SetLiveStatusEnabled(v).catch(() => {}); }} />
      {t('settings.liveStatus')} <span className="text-xs text-muted-foreground">({t('settings.liveStatusHint')})</span>
    </label>
  );
}

// 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 }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: 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'] : []),
  ].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' || 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.
const ICOM_MODELS: { name: string; addr: number }[] = [
  { name: 'IC-705', addr: 0xA4 },
  { name: 'IC-7300', addr: 0x94 },
  { name: 'IC-7610', addr: 0x98 },
  { name: 'IC-7700', addr: 0x88 },
  { name: 'IC-7800', addr: 0x80 },
  { name: 'IC-9100', addr: 0x7C },
  { name: 'IC-9700', addr: 0xA2 },
];

export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable }: 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, flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120,
    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',
  });
  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 }>({
    enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50, tx_inhibit: false,
  });
  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: '' });

  // PowerGenius XL (4O3A) amp fan-control settings.
  const [pgxl, setPgxl] = useState<{ enabled: boolean; host: string; port: number }>({ enabled: false, host: '', port: 9008 });

  // WinKeyer CW keyer settings + macro editor.
  type WKMac = { label: string; text: string };
  type WKSettings = {
    enabled: boolean; engine: string; esc_clears_call: 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; macros: WKMac[];
  };
  const [wk, setWk] = useState<WKSettings>({
    enabled: false, engine: 'winkeyer', esc_clears_call: true,
    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, macros: [],
  });
  const [wkPorts, setWkPorts] = useState<string[]>([]);
  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;
  };
  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,
  });
  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 [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('Passphrases do not match'); 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 [showSmtpPass, setShowSmtpPass] = useState(false);
  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(() => {}); }, []);
  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;
    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 };
  const emptyExtCfg = (): ExtServiceCfg => ({
    api_key: '', 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(),
  });
  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); }
  };
  // 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 [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' | '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 }>({ path: '', default_path: '', is_custom: false });
  const [dbMsg, setDbMsg] = useState('');
  type MySQLCfg = { enabled: boolean; host: string; port: number; user: string; password: string; database: 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 { setPgxl(await GetPGXLSettings() as any); } 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 { setWk(await GetWinkeyerSettings() as any); } 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 { setPgxl(await GetPGXLSettings() as any); } catch {}
        try { setBackupCfg(await GetBackupSettings() as any); } catch {}
        try { setQslDefaults(await GetQSLDefaults() as any); } catch {}
        try { setExtSvc(await GetExternalServices() as any); } catch {}
        try { setWk(await GetWinkeyerSettings() as any); } 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(); };
  }, []);

  // 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 always recompute when the callsign or grid
  // changes — the user can still edit a field, it just re-populates when the
  // source 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 t = window.setTimeout(async () => {
      try {
        const i: any = await ComputeStationInfo(call, grid);
        setActiveProfile((p) => {
          if (!p) return p;
          const patch: any = {};
          if (i.country) patch.my_country = i.country;
          if (i.dxcc) patch.my_dxcc = i.dxcc;
          if (i.cqz) patch.my_cqz = i.cqz;
          if (i.ituz) patch.my_ituz = i.ituz;
          if (i.lat) patch.my_lat = i.lat;
          if (i.lon) patch.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 SavePGXLSettings(pgxl 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="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 === '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>
                <Input type="number" value={catCfg.flex_port || 4992}
                  onChange={(e) => setCatCfg((s) => ({ ...s, flex_port: parseInt(e.target.value) || 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 === '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>
                <Input type="number" value={catCfg.tci_port || 40001}
                  onChange={(e) => setCatCfg((s) => ({ ...s, tci_port: parseInt(e.target.value) || 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>
          {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>
          </>
          )}
          {catCfg.backend === 'flex' && (
            <p className="text-xs text-muted-foreground">
              {t('cat.flexHint')}
            </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>
                <Input
                  type="number" min={1} max={65535}
                  value={ultrabeam.port}
                  onChange={(e) => setUltrabeam((s) => ({ ...s, port: parseInt(e.target.value) || 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>
          <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 PGXLPanelSettings() {
    return (
      <>
        <SectionHeader
          title="Power Genius XL"
        />
        <div className="space-y-4 max-w-xl">
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={pgxl.enabled} onCheckedChange={(c) => setPgxl((s) => ({ ...s, enabled: !!c }))} />
            Enable PowerGenius fan control
          </label>
          <div className="grid grid-cols-3 gap-3">
            <div className="space-y-1 col-span-2">
              <Label>Host / IP</Label>
              <Input
                value={pgxl.host ?? ''}
                onChange={(e) => setPgxl((s) => ({ ...s, host: e.target.value }))}
                placeholder="192.168.1.70"
                className="font-mono"
              />
            </div>
            <div className="space-y-1">
              <Label>TCP port</Label>
              <Input
                type="number" min={1} max={65535}
                value={pgxl.port}
                onChange={(e) => setPgxl((s) => ({ ...s, port: parseInt(e.target.value) || 9008 }))}
                className="font-mono"
              />
            </div>
          </div>
        </div>
      </>
    );
  }

  function RotatorPanel() {
    const isRG = (rotator as any).type === 'rotgenius';
    return (
      <>
        <SectionHeader
          title="Rotator"
          hint={isRG ? 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>
              {/* Switching to Rotator Genius moves the default port to its native
                  9006; back to PstRotator restores 12000. */}
              <Select value={(rotator as any).type ?? 'pst'}
                onValueChange={(v) => setRotator((s) => ({ ...s, type: v, port: v === 'rotgenius' ? 9006 : 12000 } as any))}>
                <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="pst">PstRotator (UDP)</SelectItem>
                  <SelectItem value="rotgenius">Rotator Genius (4O3A, native)</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>
            )}
          </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 ? '192.168.1.60' : '127.0.0.1'}
                className="font-mono"
              />
            </div>
            <div className="space-y-1">
              <Label>{isRG ? 'TCP port' : 'UDP port'}</Label>
              <Input
                type="number" min={1} max={65535}
                value={rotator.port}
                onChange={(e) => setRotator((s) => ({ ...s, port: parseInt(e.target.value) || (isRG ? 9006 : 12000) }))}
                className="font-mono"
              />
            </div>
          </div>
          {!isRG && (
            <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>}
          <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 })} />
            Enable CW keyer (shows the keyer panel)
          </label>

          <div className="grid grid-cols-4 gap-3 items-end">
            <div className="space-y-1">
              <Label>Keyer engine</Label>
              <Select value={wk.engine} onValueChange={(v) => setWkField({ engine: v })}>
                <SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="winkeyer">WinKeyer (serial)</SelectItem>
                  <SelectItem value="icom">Icom CI-V (rig keyer)</SelectItem>
                  <SelectItem value="tci" disabled>TCI (coming soon)</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 })} />
              ESC clears the callsign too (otherwise ESC only stops transmission)
            </label>
          </div>

          {wk.engine === 'icom' ? (
            <>
              <p className="text-xs text-muted-foreground -mt-2">
                Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).
              </p>
              {(!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>
                    Your CAT backend is set to <strong>{catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}</strong>. Icom CI-V CW needs the CAT backend set to <strong>Icom</strong> and connected — change it under Settings → CAT interface, otherwise sending CW will fail.
                  </span>
                </p>
              )}
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1">
                  <Label>Speed (WPM)</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>
            </>
          ) : (
            <>
              <div className="grid grid-cols-4 gap-3">
                <div className="space-y-1 col-span-2">
                  <Label>Serial port</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="— COM port —" /></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 variant="ghost" size="sm" className="h-8 px-2" title="Reload ports"
                      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>
                  <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>Speed (WPM)</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>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>Lead-in (ms)</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>Tail (ms)</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>Ratio (33-66)</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>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>Sidetone (Hz)</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>Paddle mode</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 })} /> Swap paddles
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.autospace} onCheckedChange={(c) => setWkField({ autospace: !!c })} /> Auto-space
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT
                </label>
                <label className="flex items-center gap-2 text-sm cursor-pointer">
                  <Checkbox checked={wk.serial_echo} onCheckedChange={(c) => setWkField({ serial_echo: !!c })} /> Serial echo
                </label>
              </div>
            </>
          )}

          {/* Macro editor */}
          <div className="border-t border-border/60 pt-3">
            <Label className="text-sm font-medium">CW message macros (F1…)</Label>
            <p className="text-[11px] text-muted-foreground mb-2">
              Use variables: <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> (cut numbers: 9→N, 0→T). <span className="font-mono">*</span>=my call, <span className="font-mono">!</span>=his call. <span className="font-mono">&lt;LOGQSO&gt;</span> = log the QSO when the macro is sent (e.g. <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="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: '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 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} />

        {/* Tab strip */}
        <div className="flex flex-wrap gap-1 border-b border-border mb-4">
          {TABS.map((t) => (
            <button
              key={t.k}
              type="button"
              onClick={() => setExtSvcTab(t.k)}
              className={cn(
                'px-3 py-1.5 text-xs font-semibold rounded-t-md border-x border-t -mb-px transition-colors',
                extSvcTab === t.k
                  ? 'bg-card border-border text-foreground'
                  : 'bg-muted/40 border-transparent text-muted-foreground hover:text-foreground',
              )}
            >
              {t.label}
              {!t.ready && <span className="ml-1 text-[9px] opacity-60">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="QRZ.com logbook API key (XXXX-XXXX-XXXX-XXXX)"
                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="e.g. F4BPO — optional"
                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="your Club Log account email"
                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="Club Log account password"
                className="text-xs"
              />
              <Label className="text-sm">{t('es.logbookCall')}</Label>
              <Input
                value={clublog.callsign}
                onChange={(e) => setClublog({ callsign: e.target.value.toUpperCase() })}
                placeholder="defaults to the active profile's callsign"
                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">Station callsign</Label>
              <Input
                value={hrdlog.callsign}
                onChange={(e) => setHrdlog({ callsign: e.target.value.toUpperCase() })}
                placeholder="defaults to the active profile's callsign"
                className="font-mono text-xs"
              />
              <Label className="text-sm">Upload code</Label>
              <Input
                type="password"
                value={hrdlog.code}
                onChange={(e) => setHrdlog({ code: e.target.value })}
                placeholder="HRDLog account → My Account → Upload code"
                className="text-xs"
              />
            </div>
            <div className="text-[10px] text-muted-foreground -mt-1">
              Find the upload code on HRDLog.net under <em>My Account</em>. It authorises uploads for this callsign.
            </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 === 'eqsl' ? (
          <div className="space-y-4 max-w-2xl">
            <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
              <Label className="text-sm">Username (callsign)</Label>
              <Input
                value={eqsl.username}
                onChange={(e) => setEqsl({ username: e.target.value.toUpperCase() })}
                placeholder="your eQSL.cc login (callsign)"
                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="eQSL.cc account password"
                className="text-xs"
              />
              <Label className="text-sm">QTH nickname</Label>
              <Input
                value={eqsl.qth_nickname}
                onChange={(e) => setEqsl({ qth_nickname: e.target.value })}
                placeholder="optional — required only if your eQSL account has several QTHs"
                className="text-xs"
              />
            </div>
            <div className="text-[10px] text-muted-foreground -mt-1">
              The QTH nickname tells eQSL which location profile to file the QSO under. Leave blank if you have only one.
            </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">LoTW user</Label>
              <Input
                value={lotw.username}
                onChange={(e) => setLotw({ username: e.target.value })}
                placeholder="LoTW website login (for downloading confirmations)"
                className="font-mono text-xs"
              />
              <Label className="text-sm">LoTW password</Label>
              <Input
                type="password"
                value={lotw.password}
                onChange={(e) => setLotw({ password: e.target.value })}
                placeholder="LoTW website password"
                className="text-xs"
              />
              <Label className="text-sm">TQSL path</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">Station location</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="— pick a TQSL location —" /></SelectTrigger>
                  <SelectContent>
                    {stationLocations.length === 0 && <SelectItem value="_" disabled>No TQSL locations found</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="Reload locations from TQSL">
                  <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="e.g. F4BPO/P — leave blank to use the QSO's own call"
                  className="font-mono uppercase w-64"
                />
                <div className="text-[10px] text-muted-foreground mt-1">
                  Overrides STATION_CALLSIGN at sign time so one certificate can sign several calls
                  (F4BPO, F4BPO/P, TM2Q). Pick the matching Station Location above.
                </div>
              </div>
              <Label className="text-sm">Key password</Label>
              <Input
                type="password"
                value={lotw.key_password}
                onChange={(e) => setLotw({ key_password: e.target.value })}
                placeholder="only if your certificate key has a password"
                className="text-xs"
              />
              <Label className="text-sm">Consider as unsent</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' ? 'No (N)' : 'Requested (R)'}
                      </label>
                    );
                  })}
                </div>
                <div className="text-[10px] text-muted-foreground mt-1">
                  At app close, every QSO whose LoTW <em>sent</em> status is one of these is signed and
                  uploaded in one TQSL batch — including QSOs imported from an ADIF. Uploaded QSOs become
                  <em> Y</em> and won't be re-sent. Must include your default <em>sent</em> status from Confirmations.
                </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' })}
                />
                Automatic upload on application close
              </label>
              <label className="flex items-center gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={lotw.write_log}
                  onCheckedChange={(c) => setLotw({ write_log: !!c })}
                />
                Write TQSL diagnostic log (-t)
              </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">
              Update your QSOs with the park reference from your <strong>pota.app hunter log</strong>.
              Paste your session token: log in at <span className="font-mono">pota.app</span>, open the browser
              DevTools → <strong>Network</strong> tab, click any <span className="font-mono">api.pota.app</span> request,
              and copy the full <strong>Authorization</strong> header value. The token expires after a while — re-copy it if the sync fails.
            </p>
            <div className="grid grid-cols-[170px_1fr] gap-3 items-start">
              <Label className="text-sm pt-2">Session token</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" /> Saving…</> : 'Save token'}
              </Button>
              <span className="text-[11px] text-muted-foreground">
                Then run the sync from the <strong>QSL Manager</strong> tab → service <strong>POTA hunter log</strong> (you can see and fix unmatched QSOs there).
              </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">
              {TABS.find((t) => t.k === extSvcTab)?.label} — coming soon
            </div>
            <div className="text-xs">This external service isn't wired up yet.</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)); }
    }
    async function resetDefault() {
      try {
        await ResetDatabaseToDefault();
        await refreshDb();
        setDbMsg(dbSettings.default_path || '');
      } 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 } as any)
        .then(async () => { setMysqlField({ enabled: false }); setRestartMsg(t('db.switchedSqlite')); 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')} />

        {/* Logbook backend: local SQLite file (solo) or shared MySQL (multi-op).
            Switching is instant — no restart. Only changing the local SQLite
            FILE needs a restart (it also stores this operator's settings). */}
        <div className="grid grid-cols-[130px_1fr] gap-2 items-center max-w-2xl mb-1">
          <Label className="text-sm">{t('db.backend')}</Label>
          <Select
            value={mysqlCfg.enabled ? 'mysql' : 'sqlite'}
            onValueChange={(v) => {
              const enabled = v === 'mysql';
              setMysqlField({ enabled });
              setRestartMsg('');
              if (!enabled) useLocalLogbook();   // switching to local applies at once
            }}
          >
            <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>
              {backendStatus.active === 'mysql' && <span> · {t('db.configLocal')}</span>}
            </div>
          )
        )}

        {/* SQLite: local logbook file management */}
        {!mysqlCfg.enabled && (
        <div className="space-y-4 max-w-2xl">
          <div className="space-y-1">
            <Label>{t('db.current')}</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>
            <div className="text-[10px] text-muted-foreground">{t('db.defaultLabel')} <span className="font-mono">{dbSettings.default_path}</span></div>
          </div>

          <div className="flex flex-wrap gap-2">
            <Button size="sm" onClick={createNew}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
            <Button variant="outline" size="sm" onClick={openExisting}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
            <Button variant="outline" size="sm" onClick={saveCopy}><Copy className="size-3.5" /> {t('db.saveCopy')}</Button>
            {dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>{t('db.resetDefault')}</Button>}
          </div>

          {dbMsg && (
            <div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-3 space-y-2">
              <div className="flex items-start gap-2">
                <Check className="size-4 mt-0.5 shrink-0" />
                <div>
                  <div className="font-medium">{t('db.savedRestart')}</div>
                  <div className="font-mono text-[10px] mt-1 break-all opacity-90">{dbMsg}</div>
                </div>
              </div>
              <div className="flex items-center gap-2 pl-6">
                <Button size="sm" onClick={() => { RestartApp().catch((e: any) => setErr(String(e?.message ?? e))); }}>
                  <Power className="size-3.5" /> {t('db.restartNow')}
                </Button>
                <span className="text-[10px] opacity-80">{t('db.restartHint')}</span>
              </div>
            </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>
            <Input type="number" className="h-8 w-28 font-mono" value={mysqlCfg.port} onChange={(e) => setMysqlField({ port: parseInt(e.target.value, 10) || 0 })} />
            <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() {
    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="_">— none / system default —</SelectItem>
          {devices.map((d) => (
            <SelectItem key={d.id} value={d.id}>{d.name}{d.default ? '  (default)' : ''}</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}>
            Refresh devices
          </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">From Radio (RX in)</Label>
            {deviceSelect('from_radio', audioInputs, 'Rig audio output → soundcard input')}
            <Label className="text-sm">To Radio (TX out)</Label>
            {deviceSelect('to_radio', audioOutputs, 'Soundcard output → rig mic/data in')}
            <Label className="text-sm">Recording mic</Label>
            {deviceSelect('recording_device', audioInputs, 'Your microphone (record DVK messages)')}
            <Label className="text-sm">Listening (preview)</Label>
            {deviceSelect('listening_device', audioOutputs, 'Local speakers for preview')}
          </div>
          <p className="text-[11px] text-muted-foreground">
            <strong>From Radio</strong> = what you receive (used by the QSO recorder).{' '}
            <strong>To Radio</strong> = where voice-keyer messages are transmitted.
          </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="Hear the rig's RX audio (From Radio) through your Listening device"
            >
              {monitorOn ? '■ Stop listening' : '▶ Listen to radio'}
            </Button>
            <span className="text-[11px] text-muted-foreground">
              {monitorOn
                ? 'RX monitor running — From Radio → Listening device.'
                : 'Live-monitor the rig here (USB codec now; network audio later).'}
            </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="Key PTT and pipe your live mic into the rig (To Radio device)"
            >
              {txOn ? '■ Stop talking (TX)' : '🎙 Talk to radio (TX)'}
            </Button>
            <span className="text-[11px] text-muted-foreground">
              {txOn
                ? 'TRANSMITTING — mic → To Radio, PTT keyed. Click to stop.'
                : 'Live mic → rig with PTT (USB now; network TX later).'}
            </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">QSO recorder</h4>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={audioCfg.qso_record} onCheckedChange={(c) => setAudioField({ qso_record: !!c })} />
            Record every QSO to an audio file (From Radio + your mic)
          </label>
          <div className="grid grid-cols-[170px_1fr] gap-3 items-center">
            <Label className="text-sm">Recordings folder</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(() => {})}>
                Browse…
              </Button>
            </div>
            <Label className="text-sm">Pre-roll (seconds)</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">File 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">WAV (lossless, larger)</SelectItem>
                <SelectItem value="mp3">MP3 (compressed, small)</SelectItem>
              </SelectContent>
            </Select>
            <Label className="text-sm">From Radio level</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">Mic level</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">If your voice is louder than the station, lower Mic level.</p>
          <label className="flex items-center gap-2 text-sm cursor-pointer">
            <Checkbox checked={emailCfg.auto_send} onCheckedChange={(c) => setEmailField({ auto_send: !!c })} />
            Auto-send the recording to the station by e-mail when I log a QSO
          </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">Voice keyer messages (F1–F6)</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">PTT method</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">None (VOX)</SelectItem>
                    <SelectItem value="cat">CAT (OmniRig)</SelectItem>
                    <SelectItem value="rts">Serial RTS</SelectItem>
                    <SelectItem value="dtr">Serial DTR</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('PTT test: ' + String(e?.message ?? e))); }}>
                    Test PTT
                  </Button>
                )}
              </div>
              {(audioCfg.ptt_method === 'rts' || audioCfg.ptt_method === 'dtr') && (
                <>
                  <Label className="text-sm">PTT COM port</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="Pick a COM port" /></SelectTrigger>
                      <SelectContent>
                        <SelectItem value="_">— select —</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(() => {})}>
                      Refresh
                    </Button>
                  </div>
                </>
              )}
            </div>
          </div>
          {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={`Message ${m.slot} label (CQ, report, 73…)`}
                    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('Record: ' + String(err?.message ?? err)));
                    }}
                    onPointerUp={() => {
                      DVKStopRecord().then(reloadDvk).catch((err) => setDvkErr('Save: ' + String(err?.message ?? err)));
                    }}
                  >
                    {recHere ? '● Recording…' : '● Hold to rec'}
                  </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('Play: ' + String(err?.message ?? err))))}
                  >
                    {dvkStat.playing ? '■ Stop' : '▶ 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>
          <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={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 />
          <LiveStatusToggle />

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

          <div className="border-t border-border/60 pt-4 space-y-2">
            <h4 className="text-sm font-semibold text-foreground">Password encryption</h4>
            {secret.has_passphrase ? (
              <>
                <p className="text-xs text-muted-foreground">
                  Passwords encrypted{' '}
                  {secret.unlocked
                    ? <span className="text-success font-medium">— unlocked</span>
                    : <span className="text-warning font-medium">— locked (unlock at launch)</span>}.
                </p>
                {secret.unlocked && (
                  <>
                    <div className="flex items-center gap-2 max-w-md">
                      <Input type="password" placeholder="New passphrase (to change)" value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
                      <Input type="password" placeholder="Confirm" value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
                      <Button size="sm" disabled={!ppNew || ppBusy} onClick={applyPassphrase}>Change</Button>
                    </div>
                    <Button variant="outline" size="sm" disabled={ppBusy} onClick={removePassphrase}>Remove encryption (store passwords in clear)</Button>
                  </>
                )}
              </>
            ) : (
              <>
                <p className="text-xs text-muted-foreground">
                  Encrypt saved passwords with a passphrase (asked at launch). Stays portable — re-enter it on another PC.
                </p>
                <div className="flex items-center gap-2 max-w-md">
                  <Input type="password" placeholder="Passphrase" value={ppNew} onChange={(e) => { setPpNew(e.target.value); setPpErr(''); }} className="h-8" />
                  <Input type="password" placeholder="Confirm" value={ppConfirm} onChange={(e) => { setPpConfirm(e.target.value); setPpErr(''); }} className="h-8" />
                  <Button size="sm" disabled={!ppNew || ppNew !== ppConfirm || ppBusy} onClick={applyPassphrase}>Encrypt</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">ClubLog exceptions (DXpedition overrides)</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>
                Use ClubLog Country File for callsign resolution
                <span className="block text-xs text-muted-foreground mt-0.5">
                  Applies ClubLog's date-ranged full-callsign <strong>exceptions</strong> that cty.dat lacks — e.g. VK2/SP9FIH
                  resolves to Lord Howe Island (not Australia) for the DXpedition dates. Used on entry, import, UDP, and the
                  right-click <em>Update from ClubLog</em>.
                </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 ? 'Downloading…' : (clubInfo.loaded ? 'Update ClubLog data' : 'Download ClubLog data')}
              </Button>
              <span className="text-[11px] text-muted-foreground">
                {clubInfo.loaded
                  ? `${clubInfo.count.toLocaleString()} exceptions${clubInfo.date ? ' · ' + clubInfo.date.slice(0, 10) : ''}`
                  : 'not downloaded'}
              </span>
            </div>
            {clubErr && <p className="text-[11px] text-destructive pl-6">{clubErr}</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">
              <Input type="number" className="h-8 w-24 font-mono" value={emailCfg.smtp_port} onChange={(e) => setEmailField({ smtp_port: parseInt(e.target.value, 10) || 0 })} />
              <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>
            <div className="relative">
              <Input type={showSmtpPass ? 'text' : 'password'} className="h-8 pr-9" disabled={!emailCfg.auth} value={emailCfg.smtp_password} onChange={(e) => setEmailField({ smtp_password: e.target.value })} />
              <button type="button" tabIndex={-1} onClick={() => setShowSmtpPass((v) => !v)}
                title={showSmtpPass ? t('es.hidePass') : t('es.showPass')}
                className="absolute inset-y-0 right-0 flex items-center px-2.5 text-muted-foreground hover:text-foreground disabled:opacity-40"
                disabled={!emailCfg.auth}>
                {showSmtpPass ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
              </button>
            </div>
            <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,
    backup:         BackupPanel,
    database:       DatabasePanel,
    uscounties:     USCountiesPanel,
    autostart:      () => <AutostartPanelComponent />,
    awards:         () => <ComingSoon id="awards" icon={Award} />,
    cat:            CATPanel,
    rotator:        RotatorPanel,
    winkeyer:       WinkeyerPanel,
    antenna:        UltrabeamPanel,
    antgenius:      AntGeniusPanelSettings,
    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>
  );
}

// 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>
            <Input type="number" min={1} max={65535} className="font-mono" value={s.port} onChange={(e) => update({ port: parseInt(e.target.value) || 7300 })} />
          </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>
  );
}
