
    /* ── what the machine says ───────────────────────────────────────── */
    /* A tab's own name: the section, and when the dot is up, what the
       number counts. "You, 3 transfers" — not a stray 3 read out before
       the word it belongs to. */
    function tabCount(id) {
        if (id === "you") return activeTransfers().length;
        if (id === "community") return unreadTotal();
        return 0;
    }
    function tabName(id, label) {
        const n = tabCount(id);
        if (!n) return label;
        return label + ", " + n + (id === "you" ? " transfer" + (n === 1 ? "" : "s") : " unread");
    }

    /* The one live region. It is mounted for the life of the app on
       purpose: a region that appears with its words already inside it is
       announced reliably by neither TalkBack nor VoiceOver, so an
       announcement here is a TEXT CHANGE in a node that was always there.
       The desktop does the same (FollowToast's always-mounted .layer,
       PreviewOverlay's .sr-announce). Never drawn — shell.css clips it to
       a pixel. */
    const announce = $derived([player.error || lib.error, notice.text, notice.action?.label].filter(Boolean).join(". "));
