// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

"use strict";

type NoticeKind = "info" | "error" | "success" | "warning";
interface NoticeOptions {
  kind?: NoticeKind;
  duration?: number;
  action?: { label?: string; run: () => void } | null;
}
interface ConfirmOptions {
  title?: string;
  message?: string;
  confirmLabel?: string;
  danger?: boolean;
}
interface ButtonStateOptions {
  active?: boolean;
  activeLabel?: string;
  inactiveLabel?: string;
  busy?: boolean;
  disabled?: boolean;
}
type TabKind = "primary" | "secondary";
type LayoutCapability = "broadcast" | "digital";
type LayoutName = "compact" | "broadcast" | "digital" | "full";
interface OperatorLayout {
  label: string;
  unavailable?: string;
  advanced: boolean;
  audio: boolean;
  scheduler: boolean;
  preferredTab: string;
  capability?: LayoutCapability;
}
interface LayoutOptions { persist?: boolean; navigate?: boolean }
interface TrxUi {
  notify(message: string, options?: NoticeOptions): HTMLDivElement;
  confirm(options?: ConfirmOptions): Promise<boolean>;
  setButtonState(button: HTMLButtonElement | null, options?: ButtonStateOptions): void;
  prepareTabList(bar: HTMLElement | null, kind?: TabKind): void;
  syncSelectedTab(bar: HTMLElement | null, selected: Element): void;
  setLayoutCapabilities(capabilities?: Partial<Record<LayoutCapability, boolean>>): void;
  setActiveRig(rigId: string | null): void;
  applyLayout(name: string, options?: LayoutOptions): void;
  closeMobileOverlays?: (restoreFocus?: boolean) => void;
  init(): void;
}

const browserWindow = window as typeof window & {
  trxUi?: TrxUi;
  navigateToTab?: (tab: string) => void;
};
const preparedTabLists = new WeakSet<HTMLElement>();

function elementById<T extends HTMLElement>(id: string): T {
  const element = document.getElementById(id);
  if (!element) throw new Error(`Missing required UI element #${id}`);
  return element as T;
}

// Shared UI primitives. Keeping these outside app.js prevents navigation,
// feedback, dialogs, and layout preferences from growing separate state models.
(function initUiCore() {
  const api = (browserWindow.trxUi ?? {}) as TrxUi;
  browserWindow.trxUi = api;

  function ensureLiveRegions() {
    if (!document.getElementById("toast-region")) {
      const region = document.createElement("div");
      region.id = "toast-region";
      region.className = "toast-region";
      region.setAttribute("aria-live", "polite");
      region.setAttribute("aria-atomic", "false");
      document.body.appendChild(region);
    }
    if (!document.getElementById("ui-confirm-dialog")) {
      const dialog = document.createElement("dialog");
      dialog.id = "ui-confirm-dialog";
      dialog.className = "ui-dialog";
      dialog.innerHTML = `
        <form method="dialog" class="ui-dialog-card">
          <h2 id="ui-confirm-title">Confirm action</h2>
          <p id="ui-confirm-message"></p>
          <div class="ui-dialog-actions">
            <button value="cancel" type="submit">Cancel</button>
            <button value="confirm" type="submit" class="danger">Confirm</button>
          </div>
        </form>`;
      document.body.appendChild(dialog);
    }
  }

  api.notify = function notify(message: string, options: NoticeOptions = {}) {
    ensureLiveRegions();
    const { kind = "info", duration = kind === "error" ? 7000 : 3200, action = null } = options;
    const toast = document.createElement("div");
    toast.className = `toast toast-${kind}`;
    toast.setAttribute("role", kind === "error" ? "alert" : "status");
    const text = document.createElement("span");
    text.textContent = message;
    toast.appendChild(text);
    if (action && typeof action.run === "function") {
      const button = document.createElement("button");
      button.type = "button";
      button.textContent = action.label || "Retry";
      button.addEventListener("click", () => { action.run(); toast.remove(); });
      toast.appendChild(button);
    }
    elementById("toast-region").appendChild(toast);
    requestAnimationFrame(() => { toast.classList.add("toast-visible"); });
    if (duration > 0) setTimeout(() => { toast.remove(); }, duration);
    return toast;
  };

  api.confirm = function confirmAction(options: ConfirmOptions = {}) {
    ensureLiveRegions();
    const dialog = elementById<HTMLDialogElement>("ui-confirm-dialog");
    elementById("ui-confirm-title").textContent = options.title || "Confirm action";
    elementById("ui-confirm-message").textContent = options.message || "Continue?";
    const confirmButton = dialog.querySelector<HTMLButtonElement>('[value="confirm"]');
    if (!confirmButton) throw new Error("Confirmation dialog has no confirm button");
    confirmButton.textContent = options.confirmLabel || "Confirm";
    confirmButton.classList.toggle("danger", options.danger !== false);
    return new Promise((resolve) => {
      const finish = () => { resolve(dialog.returnValue === "confirm"); };
      dialog.addEventListener("close", finish, { once: true });
      dialog.showModal();
    });
  };

  api.setButtonState = function setButtonState(button: HTMLButtonElement | null, options: ButtonStateOptions = {}) {
    if (!button) return;
    const { active = false, activeLabel, inactiveLabel, busy = false, disabled = false } = options;
    button.classList.toggle("is-active", active);
    button.classList.toggle("is-busy", busy);
    button.setAttribute("aria-pressed", String(active));
    button.setAttribute("aria-busy", String(busy));
    button.disabled = disabled || busy;
    const label = active ? activeLabel : inactiveLabel;
    if (label) button.textContent = label;
  };

  api.prepareTabList = function prepareTabList(bar: HTMLElement | null, kind: TabKind = "primary") {
    if (!bar) return;
    if (preparedTabLists.has(bar)) return;
    preparedTabLists.add(bar);
    const selector = kind === "primary" ? ".tab[data-tab]" : ".sub-tab[data-subtab]";
    const buttons = Array.from(bar.querySelectorAll<HTMLElement>(selector));
    bar.setAttribute("role", "tablist");
    buttons.forEach((button, index) => {
      button.setAttribute("role", "tab");
      button.setAttribute("aria-selected", String(button.classList.contains("active")));
      button.tabIndex = button.classList.contains("active") || (!buttons.some(b => b.classList.contains("active")) && index === 0) ? 0 : -1;
      const key = button.dataset.tab || button.dataset.subtab;
      if (!key) return;
      button.setAttribute("aria-controls", `${kind === "primary" ? "tab-" : "subtab-"}${key}`);
      const panel = document.getElementById(`${kind === "primary" ? "tab-" : "subtab-"}${key}`);
      if (panel) {
        if (!button.id) button.id = `${kind}-tab-${key}`;
        panel.setAttribute("role", "tabpanel");
        panel.setAttribute("aria-labelledby", button.id);
      }
    });
    bar.addEventListener("keydown", (event) => {
      if (!(event.target instanceof HTMLElement) || !buttons.includes(event.target)) return;
      const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1
        : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 0;
      if (!direction) return;
      event.preventDefault();
      const next = buttons[(buttons.indexOf(event.target) + direction + buttons.length) % buttons.length];
      if (!next) return;
      next.focus();
      next.click();
    });
  };

  api.syncSelectedTab = function syncSelectedTab(bar: HTMLElement | null, selected: Element) {
    if (!bar) return;
    bar.querySelectorAll<HTMLElement>('[role="tab"]').forEach((tab) => {
      const active = tab === selected;
      tab.setAttribute("aria-selected", String(active));
      tab.tabIndex = active ? 0 : -1;
    });
  };

  const layouts: Record<LayoutName, OperatorLayout> = {
    compact: { label: "Compact", advanced: false, audio: false, scheduler: false, preferredTab: "main" },
    broadcast: { label: "Broadcast", unavailable: "Broadcast requires an enumerated WFM-capable receiver", advanced: false, audio: true, scheduler: false, preferredTab: "main", capability: "broadcast" },
    digital: { label: "Digital", unavailable: "Digital requires a compatible rig mode and an available decoder", advanced: false, audio: false, scheduler: false, preferredTab: "digital-modes", capability: "digital" },
    full: { label: "Full controls", advanced: true, audio: true, scheduler: true, preferredTab: "main" },
  };
  const layoutCapabilities: Record<LayoutCapability, boolean> = { broadcast: false, digital: false };
  let activeRigId: string | null = null;

  // A layout seeds the collapsible sections; it does not hold them there.
  // applyLayout runs on every rig-state refresh, so re-applying the disclosure
  // state unconditionally would reopen or close sections under the operator
  // once a second — the reason a section could not be expanded while a layout
  // that collapses it was selected.  Sections are therefore written only when
  // the layout actually changes, or the first time each one appears in the DOM
  // (the advanced controls are built after the first applyLayout call).
  const layoutSections: { id: string; key: "advanced" | "audio" | "scheduler" }[] = [
    { id: "advanced-radio-controls", key: "advanced" },
    { id: "audio-controls", key: "audio" },
    { id: "scheduler-controls", key: "scheduler" },
  ];
  const seededSections = new Set<string>();
  let appliedLayoutName: LayoutName | null = null;

  function seedLayoutSections(layout: OperatorLayout, layoutChanged: boolean) {
    layoutSections.forEach(({ id, key }) => {
      const section = document.getElementById(id) as HTMLDetailsElement | null;
      if (!section) return;
      if (!layoutChanged && seededSections.has(id)) return;
      seededSections.add(id);
      section.open = layout[key];
    });
  }

  function layoutStorageKey() {
    return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout";
  }

  function savedLayoutName() {
    return localStorage.getItem(layoutStorageKey()) || localStorage.getItem("trxOperatorLayout") || "compact";
  }

  function layoutAvailable(layout: OperatorLayout): boolean {
    return !layout.capability || layoutCapabilities[layout.capability];
  }

  function unavailableLayoutMessage() {
    const unavailable = Object.values(layouts).filter(layout => !layoutAvailable(layout) && layout.unavailable);
    return unavailable.length ? `Unavailable: ${unavailable.map(layout => layout.unavailable).join("; ")}.` : "";
  }

  function refreshLayoutOptions() {
    const select = document.getElementById("operator-layout-select") as HTMLSelectElement | null;
    if (!select) return;
    const previous = select.value || document.body.dataset.operatorLayout || "compact";
    select.replaceChildren();
    Object.entries(layouts).forEach(([value, layout]) => {
      if (!layoutAvailable(layout)) return;
      select.add(new Option(layout.label, value));
    });
    const available = Array.from(select.options).some(option => option.value === previous);
    select.value = available ? previous : "compact";
    if (!available && previous !== "compact") api.applyLayout("compact", { persist: false });
    select.title = unavailableLayoutMessage();
  }

  api.setLayoutCapabilities = function setLayoutCapabilities(capabilities: Partial<Record<LayoutCapability, boolean>> = {}) {
    (Object.keys(layoutCapabilities) as LayoutCapability[]).forEach((name) => {
      if (name in capabilities) layoutCapabilities[name] = Boolean(capabilities[name]);
    });
    refreshLayoutOptions();
    const select = document.getElementById("operator-layout-select") as HTMLSelectElement | null;
    const saved = savedLayoutName();
    if (select && Array.from(select.options).some(option => option.value === saved)) {
      select.value = saved;
      api.applyLayout(saved, { persist: false });
    }
  };

  api.setActiveRig = function setActiveRig(rigId: string | null) {
    activeRigId = typeof rigId === "string" && rigId ? rigId : null;
    const saved = savedLayoutName();
    const select = document.getElementById("operator-layout-select") as HTMLSelectElement | null;
    if (select) select.value = Array.from(select.options).some(option => option.value === saved) ? saved : "compact";
    api.applyLayout(select?.value || saved, { persist: false });
  };

  api.applyLayout = function applyLayout(name: string, options: LayoutOptions = {}) {
    const requestedName = name in layouts ? name as LayoutName : "compact";
    const requestedLayout = layouts[requestedName];
    const permittedName: LayoutName = layoutAvailable(requestedLayout) ? requestedName : "compact";
    const layout = layouts[permittedName];
    document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact";
    if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName);
    const layoutChanged = appliedLayoutName !== permittedName;
    appliedLayoutName = permittedName;
    seedLayoutSections(layout, layoutChanged);
    if (options.navigate && typeof browserWindow.navigateToTab === "function") {
      browserWindow.navigateToTab(layout.preferredTab);
    }
  };

  function installLayoutControls() {
    const actions = document.querySelector(".top-bar-actions");
    if (actions && !document.getElementById("operator-layout-select")) {
      const label = document.createElement("label");
      label.className = "operator-layout-picker";
      label.innerHTML = '<span class="visually-hidden">Operator layout</span><select id="operator-layout-select" aria-label="Operator layout"></select>';
      const select = label.querySelector<HTMLSelectElement>("select");
      if (!select) throw new Error("Operator layout picker has no select element");
      const savedLayout = savedLayoutName();
      actions.insertBefore(label, actions.firstChild);
      select.value = savedLayout;
      refreshLayoutOptions();
      if (savedLayout !== "broadcast" && savedLayout in layouts) select.value = savedLayout;
      select.addEventListener("change", () => { api.applyLayout(select.value, { navigate: true }); });
      api.applyLayout(select.value);
    }

    const tray = document.querySelector(".controls-tray");
    if (tray && !document.getElementById("advanced-radio-controls")) {
      const details = document.createElement("details");
      details.id = "advanced-radio-controls";
      details.className = "advanced-radio-controls";
      details.innerHTML = '<summary>Advanced radio controls</summary><div class="advanced-radio-body"></div>';
      const body = details.querySelector<HTMLElement>(".advanced-radio-body");
      if (!body) throw new Error("Advanced controls have no body");
      ["sdr-settings-row", "vchan-row", "tx-limit-row"].forEach((id) => {
        const element = document.getElementById(id);
        if (element) body.appendChild(element);
      });
      // Ahead of the collapsibles the markup ships: the radio's own settings
      // come before audio and the scheduler, and appending would put the
      // section built here last whatever the markup says.
      tray.insertBefore(details, document.getElementById("audio-controls"));
      api.applyLayout(savedLayoutName(), { persist: false });
    }
  }

  // Secondary controls, in the order they leave the bar when it gets tight.
  // Audio, record and the rig picker are the operating controls and stay.
  const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];

  // Anchored at paint time in fixed coordinates.  An absolutely positioned
  // dropdown is clipped by any scrolling ancestor and trapped inside the
  // stacking context its ancestors create, so it can end up squashed inside
  // the bar or painted underneath page content.  Fixed positioning answers to
  // the viewport instead, and the menu flips above its button near the bottom
  // edge.
  function anchorMenu(button: HTMLElement, menu: HTMLElement) {
    const rect = button.getBoundingClientRect();
    // Reparent to the body.  Fixed positioning escapes clipping but not
    // stacking: the header sets `z-index: 2`, which makes it a stacking
    // context, so any z-index the menu carries is confined below every panel
    // that paints above level 2 — the spectrum overlays reach 9600.  Only
    // leaving that subtree lets the menu sit above the page.
    if (menu.parentElement !== document.body) document.body.appendChild(menu);
    menu.style.position = "fixed";
    menu.style.zIndex = "9700";
    menu.style.top = "0px";
    menu.style.bottom = "auto";
    // The stylesheet pins these menus to the right of their anchor; leaving
    // that in place while setting a left would stretch them across the bar.
    menu.style.right = "auto";
    const width = menu.offsetWidth || 200;
    const height = menu.offsetHeight || 0;
    menu.style.left = `${String(Math.round(Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8)))}px`;
    const below = rect.bottom + 8;
    if (below + height <= window.innerHeight - 8) {
      menu.style.top = `${String(Math.round(below))}px`;
    } else {
      menu.style.top = "auto";
      menu.style.bottom = `${String(Math.round(window.innerHeight - rect.top + 8))}px`;
    }
  }

  function installTopBarOverflow() {
    const actions = document.querySelector<HTMLElement>(".top-bar-actions");
    if (!actions || document.getElementById("top-bar-more")) return;
    const wrap = document.createElement("div");
    wrap.id = "top-bar-more";
    wrap.className = "top-bar-more";
    const button = document.createElement("button");
    button.type = "button";
    button.id = "top-bar-more-btn";
    button.className = "header-bar-btn top-bar-more-btn";
    button.textContent = "⋯";
    button.setAttribute("aria-haspopup", "menu");
    button.setAttribute("aria-expanded", "false");
    button.setAttribute("aria-label", "More controls");
    button.title = "More controls";
    const menu = document.createElement("div");
    menu.id = "top-bar-more-menu";
    menu.className = "top-bar-more-menu";
    menu.setAttribute("role", "menu");
    button.setAttribute("aria-controls", menu.id);
    wrap.append(button, menu);
    actions.appendChild(wrap);

    const closeMenu = () => {
      menu.classList.remove("is-open");
      button.setAttribute("aria-expanded", "false");
    };
    button.addEventListener("click", () => {
      const open = menu.classList.toggle("is-open");
      button.setAttribute("aria-expanded", String(open));
      if (open) anchorMenu(button, menu);
    });
    document.addEventListener("click", (event) => {
      if (!(event.target instanceof Node)) { closeMenu(); return; }
      if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu();
    });
    document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });

    // Measured against the bar, not the actions container: the actions are
    // sized by their content, so their own scrollWidth never exceeds their
    // clientWidth.  A viewport-width threshold is not enough either — how much
    // fits depends on the rig name and the translated labels, so a bar that is
    // wide enough on one rig clips a control on another.
    // `bar.scrollWidth > bar.clientWidth` is true even when nothing is clipped,
    // so it cannot be the test.  What actually matters is that the controls stay
    // inside the bar and the page tabs are not squeezed into a scroller: seeing
    // every tab beats keeping the style picker inline.
    // Compare natural widths against the space available.  Rendered widths
    // cannot answer this: the nav has min-width 0 and scrolls, so it always
    // shrinks to the leftover space and always reports "scrolling", while the
    // bar reports overflow even when nothing is clipped.  scrollWidth on a
    // scroll container is its unconstrained content width, which is what a fit
    // test needs.
    const barFits = () => {
      const bar = actions.closest<HTMLElement>(".tab-bar");
      if (!bar) return true;
      const identity = bar.querySelector<HTMLElement>(".header-main");
      const nav = bar.querySelector<HTMLElement>(".tab-bar-nav");
      const gutters = 48;
      const needed = (identity?.offsetWidth ?? 0) + (nav?.scrollWidth ?? 0) + actions.scrollWidth + gutters;
      return needed <= bar.clientWidth;
    };
    const reflowOverflow = () => {
      const nav = document.querySelector<HTMLElement>(".tab-bar-nav");
      const bar = actions.closest<HTMLElement>(".tab-bar");
      // Measure from the roomiest state every time, so the decision is a
      // function of the current widths alone and cannot ratchet.
      nav?.classList.remove("nav-icons-only");
      bar?.classList.remove("bar-tight");
      overflowOrder.forEach((selector) => {
        const element = menu.querySelector<HTMLElement>(selector);
        if (element) actions.insertBefore(element, wrap);
      });
      wrap.hidden = true;
      for (const selector of overflowOrder) {
        if (barFits()) break;
        const element = actions.querySelector<HTMLElement>(selector);
        if (!element) continue;
        wrap.hidden = false;
        menu.appendChild(element);
      }
      // Last resort, once every movable control is already in the menu: drop
      // the tabs to their icons.  Without it the nav — which may shrink below
      // its content — keeps its tabs at full width and runs them under the
      // controls, so the destinations nearest the controls become unclickable.
      // Icon widths are fixed, so this always buys back the labels' width.
      if (nav && !barFits()) nav.classList.add("nav-icons-only");
      // Still short with the tabs down to icons: hand the squeeze to the
      // identity block, which can ellipsise, rather than to the strip, which
      // can only clip destinations out of reach.
      if (bar && !barFits()) bar.classList.add("bar-tight");
      wrap.hidden = menu.children.length === 0;
      if (wrap.hidden) closeMenu();
    };
    reflowOverflow();
    window.addEventListener("resize", reflowOverflow);
  }

  function installMobileMore() {
    const nav = document.querySelector(".tab-bar-nav");
    if (!nav || document.getElementById("mobile-more-btn")) return;
    const more = document.createElement("button");
    more.id = "mobile-more-btn";
    more.className = "tab mobile-more-btn";
    more.type = "button";
    more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">Tools</span>';
    more.setAttribute("aria-label", "Tools and settings");
    more.setAttribute("aria-haspopup", "menu");
    more.setAttribute("aria-expanded", "false");
    const menu = document.createElement("div");
    menu.id = "mobile-more-menu";
    menu.className = "mobile-more-menu";
    menu.setAttribute("role", "menu");
    more.setAttribute("aria-controls", menu.id);
    const closeMore = (restoreFocus = false) => {
      if (!menu.classList.contains("is-open")) return;
      menu.classList.remove("is-open");
      more.setAttribute("aria-expanded", "false");
      if (restoreFocus) more.focus();
    };
    api.closeMobileOverlays = closeMore;
    ["statistics", "recorder", "settings", "about"].forEach((tabName) => {
      const source = nav.querySelector(`[data-tab="${tabName}"]`);
      if (!source) return;
      const item = document.createElement("button");
      item.type = "button";
      item.setAttribute("role", "menuitem");
      item.dataset.navigateTab = tabName;
      item.textContent = source.textContent.trim();
      item.addEventListener("click", () => {
        if (typeof browserWindow.navigateToTab === "function") browserWindow.navigateToTab(tabName);
        closeMore();
      });
      menu.appendChild(item);
    });
    more.addEventListener("click", () => {
      const open = menu.classList.toggle("is-open");
      more.setAttribute("aria-expanded", String(open));
      if (open) {
        anchorMenu(more, menu);
        menu.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
      }
    });
    document.addEventListener("click", (event) => {
      if (!(event.target instanceof Node) || (!menu.contains(event.target) && !more.contains(event.target))) closeMore();
    });
    document.addEventListener("keydown", (event) => {
      if (event.key === "Escape") closeMore(true);
    });
    window.addEventListener("resize", () => { closeMore(); });
    window.addEventListener("popstate", () => { closeMore(); });
    nav.append(more, menu);
  }

  function installDecoderPicker() {
    const bar = document.querySelector("#tab-digital-modes > .sub-tab-bar");
    if (!bar || document.getElementById("decoder-tab-select")) return;
    const select = document.createElement("select");
    select.id = "decoder-tab-select";
    select.className = "decoder-tab-select";
    select.setAttribute("aria-label", "Decoder view");
    const groups: Array<[string, string[]]> = [
      ["Overview", ["overview"]], ["Marine & packet", ["ais", "vdes", "aprs", "hf-aprs"]],
      ["Weak signal", ["cw", "ft8", "ft4", "ft2", "wspr"]], ["Broadcast & images", ["rds", "sat", "wefax"]],
    ];
    groups.forEach(([label, ids]) => {
      const group = document.createElement("optgroup");
      group.label = label;
      ids.forEach((id) => {
        const button = bar.querySelector<HTMLButtonElement>(`[data-subtab="${id}"]`);
        if (button) group.appendChild(new Option(button.textContent.trim(), id));
      });
      select.appendChild(group);
    });
    select.addEventListener("change", () => bar.querySelector<HTMLElement>(`[data-subtab="${select.value}"]`)?.click());
    bar.insertAdjacentElement("afterend", select);
  }

  function installDecoderBadges() {
    const bar = document.querySelector("#tab-digital-modes > .sub-tab-bar");
    if (!bar) return;
    bar.querySelectorAll<HTMLElement>(".sub-tab[data-subtab]").forEach((button) => {
      const id = button.dataset.subtab;
      if (!id) return;
      if (id === "overview" || button.querySelector(".decoder-state-dot")) return;
      const dot = document.createElement("span");
      dot.className = "decoder-state-dot";
      dot.setAttribute("aria-hidden", "true");
      button.appendChild(dot);
      const status = document.getElementById(`${id}-status`);
      if (!status) return;
      const sync = () => {
        const value = status.textContent.toLowerCase();
        const state = /receiv|decod|connected|listening/.test(value) ? "active"
          : /error|fail|disconnected/.test(value) ? "error" : "idle";
        dot.dataset.state = state;
        button.title = `${button.childNodes[0]?.textContent?.trim() || id}: ${status.textContent.trim()}`;
      };
      new MutationObserver(sync).observe(status, { childList: true, characterData: true, subtree: true });
      sync();
    });
  }

  api.init = function init() {
    ensureLiveRegions();
    installLayoutControls();
    installTopBarOverflow();
    installMobileMore();
    installDecoderPicker();
    installDecoderBadges();
    api.prepareTabList(document.querySelector(".tab-bar-nav"), "primary");
    document.querySelectorAll<HTMLElement>(".sub-tab-bar").forEach(bar => { api.prepareTabList(bar, "secondary"); });
    window.addEventListener("unhandledrejection", (event) => {
      const message = event.reason instanceof Error ? event.reason.message : "An operation failed unexpectedly";
      api.notify(message, { kind: "error" });
    });
  };

  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => { api.init(); }, { once: true });
  else api.init();
})();
