{"version":3,"sources":["../src/detect/i18n.ts","../src/detect/banner.ts"],"names":["EN_STRINGS","IOS_GRANT_WORDING_CANONICAL","DE_STRINGS","PACKS","primarySubtag","lang","navigatorLanguage","deepMerge","base","override","out","key","o","resolveStrings","options","explicit","selected","SETUP_STEPS","STATE_STEP_INDICES","DEFAULT_ONBOARDING_URL","SETUP_URL","READY_SHOWN_KEY","READY_EVENT","BEACIO_EVENTS","EXTENSION_READY_EVENT","RECHECK_ATTEMPTS","RECHECK_INTERVAL_MS","resolveOnboardingUrl","buildOnboardingUrl","url","opts","parsed","idSegment","APP_STORE_URL","redirectToOnboarding","apiKey","operatorName","saveReturnContext","returnUrl","getReturnContext","esc","s","d","fill","template","deviceName","safeLogoUrl","raw","u","showBottomSheet","dismissDays","state","onboardingUrl","t","buttonText","sheetState","title","body","setupUrl","accent","logoUrl","themed","privacyBody","stepsHtml","i","ctaHtml","pageUrl","openInSafariUrl","buildOpenInSafariUrl","roundTrip","returnHtml","overlay","persistent","pollTimer","detached","detachLifecycle","onReady","onExtensionReady","onVisibility","clearIfActive","isExtensionActive","startBoundedRecheck","attempts","tick","dismissShort","dismiss","e","showBarBanner","position","style","text","el","posStyle","customStyle","k","v","hasReadyBeenShown","markReadyShown","showReadyToast","showInstallBanner","isDismissed","barCanTellTheStory","removeInstallBanner"],"mappings":"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","file":"chunk-ACKA3CAM.mjs","sourcesContent":["/**\n * @beacio/detect#i18n — SB-SDK-07\n *\n * The shared localized-string seam for the two user-facing surfaces in\n * @beacio/detect: the install banner (banner.ts) and the branded error\n * presenter (error-presenter.ts). The install banner is the SINGLE end-user\n * onboarding screen that replaces S&B's Bluefy alert, and it was hardcoded\n * English — only `text`/`buttonText` were overridable. S&B is HQ'd in Bayreuth\n * and its German users would see English at the make-or-break moment. This\n * module centralises every visible token into a typed string pack, ships a\n * built-in German (`de`) pack alongside the English (`en`) default, and exposes\n * one PURE selector with a documented policy.\n *\n * Selection policy (resolveStrings):\n *   1. an explicit BCP-47 `lang` ALWAYS wins (prefix-matched: 'de', 'de-DE',\n *      'de-AT' all select the German pack);\n *   2. else the runtime's `navigator.language` is prefix-matched the same way\n *      (so a German-locale iPhone gets German with zero config);\n *   3. else English.\n * A caller-supplied partial `strings` object then deep-merges OVER the selected\n * pack, so an operator can override one field (e.g. just `buttonText`) without\n * restating the whole pack, in any language.\n *\n * Design constraints (mirroring banner.ts / error-presenter.ts):\n *  - The per-code packs are typed against the ONE `BeacioErrorCode` union in\n *    `../error-taxonomy` (a TYPE-only import — erased at build time). The ONE\n *    runtime import from that leaf is `IOS_GRANT_WORDING_CANONICAL` (R-52),\n *    already bundled into detect via `error-presenter.ts` — zero new weight.\n *    A code added to the taxonomy\n *    fails THIS file's compile until both packs cover it; that is stronger than\n *    the hand-maintained test Record it replaced, and it fails in src.\n *  - All copy uses neutral install-path framing only — no \"App Store approved /\n *    cleared / reviewed\" language (feedback_no_app_store_status_claims).\n *  - `{operator}` is the ONLY interpolation token; banner.ts substitutes the\n *    resolved operator name into it (see fill()).\n *\n * SDK has zero external consumers, so adding the `lang`/`strings` seam is a free,\n * non-breaking change (project_sdk_no_consumers).\n */\n\n// The stable code contract, from the single taxonomy. Type-only, so nothing is\n// emitted; re-exported because `@beacio/core/detect` publishes it (index.ts).\nimport { IOS_GRANT_WORDING_CANONICAL, type BeacioErrorCode } from '../error-taxonomy';\n\nexport type { BeacioErrorCode };\n\n/** Funnel-state lead copy: a title + body shown at the top of the bottom sheet. */\nexport interface StateCopy {\n  title: string;\n  body: string;\n}\n\n/** A single setup step: the imperative label the user taps + its one-line \"why\". */\nexport interface SetupStepCopy {\n  label: string;\n  why: string;\n}\n\n/** A branded error card's headline + body. */\nexport interface ErrorCopy {\n  title: string;\n  body: string;\n}\n\n/** The error-presenter half of the pack — shared by presentError. */\nexport interface ErrorStrings {\n  /** Dismiss button label on the error card. */\n  dismiss: string;\n  /** Retry affordance label (retriable errors only). */\n  retry: string;\n  /** Per-code branded headline. EVERY BeacioErrorCode is present (parity-guarded). */\n  titles: Record<BeacioErrorCode, string>;\n  /** Per-code branded body. EVERY BeacioErrorCode is present (parity-guarded). */\n  messages: Record<BeacioErrorCode, string>;\n  /** Fallback copy for an unrecognised error (bare string / unknown DOMException). */\n  generic: ErrorCopy;\n}\n\n/**\n * The complete visible-string surface of @beacio/detect. The built-in `en`/`de`\n * packs both define EXACTLY these keys (i18n.test.ts pins the parity), so a new\n * English-only string cannot silently bypass localization.\n */\nexport interface LocaleStrings {\n  /** Primary CTA label on the not-installed sheet + the lightweight bar. */\n  buttonText: string;\n  /** Sheet soft-dismiss (\"Not now\") label — short suppression (SB-PRD-08). */\n  dismiss: string;\n  /**\n   * SB-PRD-08: the EXPLICIT \"Don't show again\" opt-out label — the long-suppression\n   * control, distinct from the soft \"Not now\" {@link dismiss} above.\n   */\n  dontShowAgain: string;\n  /** Per-funnel-state lead copy (the 'active' state renders the toast instead). */\n  states: {\n    'not-installed': StateCopy;\n    'installed-inactive': StateCopy;\n    denied: StateCopy;\n    /**\n     * SB-SDK-17: Private Browsing dead end. iOS Safari disables web extensions in\n     * Private Browsing (no per-extension opt-in), so beacio is inert and the app\n     * may already be installed — the recovery is to reopen the page in a normal\n     * tab, NOT to install anything. A distinct hint (no install CTA, no steps).\n     */\n    'private-browsing': StateCopy;\n    /**\n     * GH #448: the Home Screen web-app dead end. A page opened from a Home Screen\n     * icon added with \"Open as Web App\" ON runs standalone, and Safari extensions\n     * NEVER load there — so beacio is inert even when the app IS installed (the\n     * standalone UA even drops the `Safari/` token, so the page looks like an\n     * unsupported browser). The recovery is Safari itself, NOT another install, so\n     * this is a distinct hint: no install CTA, no steps, and the\n     * {@link openInSafari} escape hatch below instead.\n     */\n    'home-screen-web-app': StateCopy;\n  };\n  /** The ordered first-run step list (install → … → return). */\n  steps: SetupStepCopy[];\n  /** Visible \"Return to {operator}\" CTA label. */\n  returnCta: string;\n  /** Sub-line explaining the link was also copied to the clipboard. */\n  clipboardHint: string;\n  /** GH #448: label of the `x-safari-` CTA on the Home Screen web-app sheet. */\n  openInSafari: string;\n  /**\n   * GH #448: lead-in for the copy/paste fallback line, rendered immediately before\n   * the page address. The `x-safari-` scheme can silently no-op inside a web app,\n   * so the address itself is always VISIBLE and the sheet never dead-ends.\n   */\n  openInSafariFallback: string;\n  /** \"Reload page to re-check\" control label. */\n  reload: string;\n  /** \"How does setup work?\" <details> summary. */\n  howSummary: string;\n  /** \"How does setup work?\" <details> body (ends with the linked guide phrase). */\n  howBody: string;\n  /** Linked phrase inside howBody that points at the setup guide. */\n  howLink: string;\n  /** \"Privacy: No data collected\" <details> summary. */\n  privacySummary: string;\n  /** \"Privacy: No data collected\" <details> body. */\n  privacyBody: string;\n  /** \"Still stuck? Open the setup guide\" affordance. */\n  stillStuck: string;\n  /** Lightweight bar banner heading (\"Enable Bluetooth\"). */\n  barTitle: string;\n  /** Lightweight bar banner body text. */\n  barText: string;\n  /** Once-only success toast text (\"beacio is ready — tap Connect …\"). */\n  readyToast: string;\n  /** Error-presenter strings (shared with presentError). */\n  error: ErrorStrings;\n}\n\n/**\n * English (default) pack — the byte-identical source of today's rendered copy.\n * `{operator}` is substituted by banner.ts with the resolved operator name.\n */\nexport const EN_STRINGS: LocaleStrings = {\n  buttonText: 'Start Setup',\n  dismiss: 'Not now',\n  dontShowAgain: \"Don't show again\",\n  states: {\n    'not-installed': {\n      title: 'Set Up Bluetooth in Safari',\n      body: 'Follow the steps below to enable Bluetooth and return to {operator}.',\n    },\n    'installed-inactive': {\n      title: 'Enable beacio in Safari',\n      body: 'beacio is installed but the Safari extension is off. Open Settings → Apps → Safari → Extensions → beacio and turn on Allow Extension, then return here.',\n    },\n    denied: {\n      title: 'Allow beacio on this site',\n      body: `beacio is enabled but not yet allowed here. Tap the aA button in the address bar → Manage Extensions → beacio → ${IOS_GRANT_WORDING_CANONICAL}, then reload this page.`,\n    },\n    'private-browsing': {\n      title: 'Private Browsing blocks extensions',\n      body: 'Private Browsing disables Safari extensions, so beacio cannot run here — even if it is installed. Open this page in a normal tab to connect your device.',\n    },\n    'home-screen-web-app': {\n      title: 'Open this page in Safari',\n      body: 'This page is running as a Home Screen web app, where Safari extensions are unavailable — so beacio cannot run here even if it is installed. Open it in Safari, or re-add it to your Home Screen with “Open as Web App” turned off.',\n    },\n  },\n  steps: [\n    { label: 'Install beacio', why: 'A free one-time companion app from the App Store.' },\n    { label: 'Open the app once', why: 'This registers the Safari extension with iOS.' },\n    {\n      label: 'Enable in Safari Settings',\n      why: 'Settings → Apps → Safari → Extensions → beacio → turn on Allow Extension.',\n    },\n    {\n      label: 'Allow website access',\n      why: `On the site, tap the aA button in the address bar → Manage Extensions → beacio → ${IOS_GRANT_WORDING_CANONICAL}.`,\n    },\n    {\n      label: 'Allow Bluetooth on first scan',\n      why: 'The first time you connect, Safari will ask to allow this site — tap Allow.',\n    },\n    { label: 'Return and reload', why: 'Come back to this page, reload, and tap Connect.' },\n  ],\n  returnCta: 'Return to {operator}',\n  clipboardHint: 'Link also copied — paste it into Safari if this button does not reopen {operator}.',\n  openInSafari: 'Open in Safari',\n  openInSafariFallback: 'If nothing happens, copy this address into Safari:',\n  reload: 'Reload page to re-check',\n  howSummary: 'How does setup work?',\n  howBody: `beacio uses a one-time iPhone app to enable the Safari extension. After enabling it and allowing access on this site (aA button → Manage Extensions → ${IOS_GRANT_WORDING_CANONICAL}), Bluetooth works in Safari.`,\n  howLink: 'See the full setup guide',\n  privacySummary: 'Privacy: No data collected',\n  privacyBody: 'beacio processes all Bluetooth data locally on your device. No browsing data, device data, or personal information is ever collected or transmitted.',\n  stillStuck: 'Still stuck? Open the setup guide',\n  barTitle: 'Enable Bluetooth',\n  barText: 'Install Beacio, open the app, enable the Safari extension, then return here.',\n  readyToast: 'beacio is ready — tap Connect to pair your device with {operator}.',\n  error: {\n    dismiss: 'Dismiss',\n    retry: 'Try again',\n    titles: {\n      INVALID_PARAMETER: 'Something went wrong',\n      BLUETOOTH_UNAVAILABLE: 'Bluetooth is unavailable',\n      EXTENSION_NOT_INSTALLED: 'Finish Bluetooth setup',\n      EXTENSION_NOT_ENABLED: 'Allow beacio on this site',\n      PERMISSION_DENIED: 'Allow Bluetooth to continue',\n      DEVICE_NOT_FOUND: 'No device found',\n      DEVICE_DISCONNECTED: 'Device disconnected',\n      CONNECTION_TIMEOUT: 'Connection timed out',\n      SERVICE_NOT_FOUND: 'Device feature unavailable',\n      CHARACTERISTIC_NOT_FOUND: 'Device feature unavailable',\n      CHARACTERISTIC_NOT_READABLE: 'Cannot read from device',\n      CHARACTERISTIC_NOT_WRITABLE: 'Cannot send to device',\n      CHARACTERISTIC_NOT_NOTIFIABLE: 'Live updates unavailable',\n      GATT_OPERATION_FAILED: 'Connection interrupted',\n      SCAN_ALREADY_IN_PROGRESS: 'Already searching',\n      CONNECTION_LIMIT_REACHED: 'Too many devices connected',\n      USER_CANCELLED: 'Connection cancelled',\n      TIMEOUT: 'Operation timed out',\n      WRITE_INCOMPLETE: 'Send incomplete',\n    },\n    messages: {\n      INVALID_PARAMETER: 'The request could not be completed. Please reload the page and try again.',\n      BLUETOOTH_UNAVAILABLE: 'Turn Bluetooth on, then try again.',\n      EXTENSION_NOT_INSTALLED: 'Bluetooth is not enabled for this site yet. Finish setup, then try connecting again.',\n      EXTENSION_NOT_ENABLED: `beacio is not allowed on this site yet. Tap aA in the address bar → Manage Extensions → beacio → ${IOS_GRANT_WORDING_CANONICAL}, then reload.`,\n      PERMISSION_DENIED: 'Bluetooth access was not granted. Tap Connect yourself (Bluetooth needs a tap), then allow access when asked.',\n      DEVICE_NOT_FOUND: 'No matching device was found. Switch your device on, keep it close, then try again.',\n      DEVICE_DISCONNECTED: 'The connection to your device was lost. Reconnect to continue.',\n      CONNECTION_TIMEOUT: 'Your device did not respond in time. Keep it close and powered on, then try again.',\n      SERVICE_NOT_FOUND: 'A required feature was not found on this device. Check that it is the right device and try again.',\n      CHARACTERISTIC_NOT_FOUND: 'A required feature was not found on this device. Check that it is the right device and try again.',\n      CHARACTERISTIC_NOT_READABLE: 'This value cannot be read from your device. No action is needed for this control.',\n      CHARACTERISTIC_NOT_WRITABLE: 'This value cannot be sent to your device. No action is needed for this control.',\n      CHARACTERISTIC_NOT_NOTIFIABLE: 'This value does not support live updates on your device.',\n      GATT_OPERATION_FAILED: 'Something interrupted the connection. Switch your device off and on, then try again.',\n      SCAN_ALREADY_IN_PROGRESS: 'A device search is already running. Wait a moment, then try again.',\n      CONNECTION_LIMIT_REACHED: 'Disconnect another device before connecting a new one.',\n      USER_CANCELLED: 'No device was selected. Tap Connect to try again whenever you are ready.',\n      TIMEOUT: 'That took too long. Check your device is close and powered on, then try again.',\n      WRITE_INCOMPLETE: 'Only part of the data reached your device. Try again to resend it.',\n    },\n    generic: {\n      title: 'Something went wrong',\n      body: 'Something interrupted the connection. Please try again.',\n    },\n  },\n};\n\n/**\n * German (`de`) pack. Mirrors EN_STRINGS key-for-key (i18n.test.ts pins the\n * parity, so this pack can never fall behind a new English string). Native,\n * neutral install-path German; iOS-26 Settings paths use the localized Settings\n * labels (Apps → Safari → Erweiterungen) a German iPhone actually shows. The\n * stylized brand word \"beacio\" stays lowercase mid-sentence, matching the\n * English copy and the app's lowercase display name.\n */\nexport const DE_STRINGS: LocaleStrings = {\n  buttonText: 'Einrichtung starten',\n  dismiss: 'Jetzt nicht',\n  dontShowAgain: 'Nicht mehr anzeigen',\n  states: {\n    'not-installed': {\n      title: 'Bluetooth in Safari einrichten',\n      body: 'Folge den Schritten unten, um Bluetooth zu aktivieren und zu {operator} zurückzukehren.',\n    },\n    'installed-inactive': {\n      title: 'beacio in Safari aktivieren',\n      body: 'beacio ist installiert, aber die Safari-Erweiterung ist deaktiviert. Öffne Einstellungen → Apps → Safari → Erweiterungen → beacio und aktiviere „Erweiterung erlauben“, kehre dann hierher zurück.',\n    },\n    denied: {\n      title: 'beacio für diese Seite erlauben',\n      body: 'beacio ist aktiviert, aber für diese Seite noch nicht erlaubt. Tippe auf die Schaltfläche „aA“ in der Adressleiste → Erweiterungen verwalten → beacio → „Auf allen Websites erlauben“ und lade diese Seite dann neu.',\n    },\n    'private-browsing': {\n      title: 'Privates Surfen blockiert Erweiterungen',\n      body: 'Im privaten Surfmodus sind Safari-Erweiterungen deaktiviert, daher kann beacio hier nicht laufen — auch wenn es installiert ist. Öffne diese Seite in einem normalen Tab, um dein Gerät zu verbinden.',\n    },\n    'home-screen-web-app': {\n      title: 'Diese Seite in Safari öffnen',\n      body: 'Diese Seite läuft als Home-Bildschirm-Web-App, in der Safari-Erweiterungen nicht verfügbar sind — beacio kann hier also nicht laufen, auch wenn es installiert ist. Öffne sie in Safari oder füge sie mit deaktiviertem „Als Web-App öffnen“ erneut zum Home-Bildschirm hinzu.',\n    },\n  },\n  steps: [\n    { label: 'beacio installieren', why: 'Eine kostenlose, einmalige Begleit-App aus dem App Store.' },\n    { label: 'App einmal öffnen', why: 'Damit wird die Safari-Erweiterung bei iOS registriert.' },\n    {\n      label: 'In den Safari-Einstellungen aktivieren',\n      why: 'Einstellungen → Apps → Safari → Erweiterungen → beacio → „Erweiterung erlauben“ aktivieren.',\n    },\n    {\n      label: 'Website-Zugriff erlauben',\n      why: 'Tippe auf der Seite auf die Schaltfläche „aA“ in der Adressleiste → Erweiterungen verwalten → beacio → „Auf allen Websites erlauben“.',\n    },\n    {\n      label: 'Bluetooth beim ersten Scan erlauben',\n      why: 'Beim ersten Verbinden fragt Safari, ob diese Seite zugreifen darf — tippe auf „Erlauben“.',\n    },\n    { label: 'Zurückkehren und neu laden', why: 'Komm zu dieser Seite zurück, lade sie neu und tippe auf „Verbinden“.' },\n  ],\n  returnCta: 'Zurück zu {operator}',\n  clipboardHint: 'Link wurde außerdem kopiert — füge ihn in Safari ein, falls diese Schaltfläche {operator} nicht erneut öffnet.',\n  openInSafari: 'In Safari öffnen',\n  openInSafariFallback: 'Falls nichts passiert, kopiere diese Adresse in Safari:',\n  reload: 'Seite neu laden und erneut prüfen',\n  howSummary: 'Wie funktioniert die Einrichtung?',\n  howBody: 'beacio nutzt eine einmalige iPhone-App, um die Safari-Erweiterung zu aktivieren. Sobald sie aktiviert und der Zugriff auf dieser Seite erlaubt ist (Schaltfläche „aA“ → Erweiterungen verwalten → „Auf allen Websites erlauben“), funktioniert Bluetooth in Safari.',\n  howLink: 'Zur vollständigen Einrichtungsanleitung',\n  privacySummary: 'Datenschutz: Keine Datenerfassung',\n  privacyBody: 'beacio verarbeitet alle Bluetooth-Daten lokal auf deinem Gerät. Es werden niemals Browserdaten, Gerätedaten oder persönliche Informationen erfasst oder übertragen.',\n  stillStuck: 'Kommst du nicht weiter? Einrichtungsanleitung öffnen',\n  barTitle: 'Bluetooth aktivieren',\n  barText: 'Installiere beacio, öffne die App, aktiviere die Safari-Erweiterung und kehre dann hierher zurück.',\n  readyToast: 'beacio ist bereit — tippe auf „Verbinden“, um dein Gerät mit {operator} zu koppeln.',\n  error: {\n    dismiss: 'Schließen',\n    retry: 'Erneut versuchen',\n    titles: {\n      INVALID_PARAMETER: 'Etwas ist schiefgelaufen',\n      BLUETOOTH_UNAVAILABLE: 'Bluetooth ist nicht verfügbar',\n      EXTENSION_NOT_INSTALLED: 'Bluetooth-Einrichtung abschließen',\n      EXTENSION_NOT_ENABLED: 'beacio für diese Seite erlauben',\n      PERMISSION_DENIED: 'Bluetooth erlauben, um fortzufahren',\n      DEVICE_NOT_FOUND: 'Kein Gerät gefunden',\n      DEVICE_DISCONNECTED: 'Gerät getrennt',\n      CONNECTION_TIMEOUT: 'Zeitüberschreitung der Verbindung',\n      SERVICE_NOT_FOUND: 'Gerätefunktion nicht verfügbar',\n      CHARACTERISTIC_NOT_FOUND: 'Gerätefunktion nicht verfügbar',\n      CHARACTERISTIC_NOT_READABLE: 'Lesen vom Gerät nicht möglich',\n      CHARACTERISTIC_NOT_WRITABLE: 'Senden an das Gerät nicht möglich',\n      CHARACTERISTIC_NOT_NOTIFIABLE: 'Live-Aktualisierungen nicht verfügbar',\n      GATT_OPERATION_FAILED: 'Verbindung unterbrochen',\n      SCAN_ALREADY_IN_PROGRESS: 'Suche läuft bereits',\n      CONNECTION_LIMIT_REACHED: 'Zu viele Geräte verbunden',\n      USER_CANCELLED: 'Verbindung abgebrochen',\n      TIMEOUT: 'Zeitüberschreitung des Vorgangs',\n      WRITE_INCOMPLETE: 'Senden unvollständig',\n    },\n    messages: {\n      INVALID_PARAMETER: 'Die Anfrage konnte nicht abgeschlossen werden. Lade die Seite neu und versuche es erneut.',\n      BLUETOOTH_UNAVAILABLE: 'Schalte Bluetooth ein und versuche es erneut.',\n      EXTENSION_NOT_INSTALLED: 'Bluetooth ist für diese Seite noch nicht aktiviert. Schließe die Einrichtung ab und versuche dann erneut, dich zu verbinden.',\n      EXTENSION_NOT_ENABLED: 'beacio ist für diese Seite noch nicht erlaubt. Tippe auf „aA“ in der Adressleiste → Erweiterungen verwalten → beacio → „Auf allen Websites erlauben“ und lade die Seite neu.',\n      PERMISSION_DENIED: 'Der Bluetooth-Zugriff wurde nicht gewährt. Tippe selbst auf „Verbinden“ (Bluetooth erfordert eine Berührung) und erlaube den Zugriff, wenn du gefragt wirst.',\n      DEVICE_NOT_FOUND: 'Es wurde kein passendes Gerät gefunden. Schalte dein Gerät ein, halte es in der Nähe und versuche es erneut.',\n      DEVICE_DISCONNECTED: 'Die Verbindung zu deinem Gerät wurde unterbrochen. Verbinde dich erneut, um fortzufahren.',\n      CONNECTION_TIMEOUT: 'Dein Gerät hat nicht rechtzeitig geantwortet. Halte es in der Nähe und eingeschaltet und versuche es erneut.',\n      SERVICE_NOT_FOUND: 'Eine erforderliche Funktion wurde auf diesem Gerät nicht gefunden. Prüfe, ob es das richtige Gerät ist, und versuche es erneut.',\n      CHARACTERISTIC_NOT_FOUND: 'Eine erforderliche Funktion wurde auf diesem Gerät nicht gefunden. Prüfe, ob es das richtige Gerät ist, und versuche es erneut.',\n      CHARACTERISTIC_NOT_READABLE: 'Dieser Wert kann nicht von deinem Gerät gelesen werden. Für dieses Element ist keine Aktion erforderlich.',\n      CHARACTERISTIC_NOT_WRITABLE: 'Dieser Wert kann nicht an dein Gerät gesendet werden. Für dieses Element ist keine Aktion erforderlich.',\n      CHARACTERISTIC_NOT_NOTIFIABLE: 'Dieser Wert unterstützt auf deinem Gerät keine Live-Aktualisierungen.',\n      GATT_OPERATION_FAILED: 'Etwas hat die Verbindung unterbrochen. Schalte dein Gerät aus und wieder ein und versuche es erneut.',\n      SCAN_ALREADY_IN_PROGRESS: 'Es läuft bereits eine Gerätesuche. Warte einen Moment und versuche es erneut.',\n      CONNECTION_LIMIT_REACHED: 'Trenne ein anderes Gerät, bevor du ein neues verbindest.',\n      USER_CANCELLED: 'Es wurde kein Gerät ausgewählt. Tippe auf „Verbinden“, um es erneut zu versuchen, wann immer du bereit bist.',\n      TIMEOUT: 'Das hat zu lange gedauert. Prüfe, ob dein Gerät in der Nähe und eingeschaltet ist, und versuche es erneut.',\n      WRITE_INCOMPLETE: 'Nur ein Teil der Daten hat dein Gerät erreicht. Versuche es erneut, um sie noch einmal zu senden.',\n    },\n    generic: {\n      title: 'Etwas ist schiefgelaufen',\n      body: 'Etwas hat die Verbindung unterbrochen. Bitte versuche es erneut.',\n    },\n  },\n};\n\n/** The built-in packs, keyed by their primary BCP-47 language subtag. */\nconst PACKS: Record<string, LocaleStrings> = {\n  en: EN_STRINGS,\n  de: DE_STRINGS,\n};\n\n/**\n * The primary (language) subtag of a BCP-47 tag, lower-cased: 'de-DE' → 'de',\n * 'EN' → 'en'. Returns '' for an empty/garbage value so the caller falls back.\n */\nfunction primarySubtag(lang: string | undefined): string {\n  if (!lang || typeof lang !== 'string') return '';\n  return lang.split('-', 1)[0]!.trim().toLowerCase();\n}\n\n/** The runtime UI language, or undefined when navigator is unavailable (SSR). */\nfunction navigatorLanguage(): string | undefined {\n  if (typeof navigator === 'undefined') return undefined;\n  return navigator.language;\n}\n\n/**\n * Deep-merge a caller-supplied partial pack OVER a base pack, producing a new\n * pack (the bases are never mutated). Only plain objects are recursed; arrays\n * and scalars are replaced wholesale by the override when present. This is the\n * narrow merge the seam needs (no array element-wise merge), kept dependency-free.\n */\nfunction deepMerge<T>(base: T, override: DeepPartial<T> | undefined): T {\n  if (override === undefined || override === null) return base;\n  if (Array.isArray(base) || typeof base !== 'object' || base === null) {\n    // Scalar / array leaf: an override of the right kind replaces it.\n    return override as T;\n  }\n  const out: { [key: string]: string | number | boolean | object | null } = { ...(base as { [key: string]: string | number | boolean | object | null }) };\n  for (const key of Object.keys(override as { [key: string]: string | number | boolean | object | null })) {\n    const o = (override as { [key: string]: string | number | boolean | object | null })[key];\n    if (o === undefined) continue;\n    out[key] = deepMerge((base as { [key: string]: string | number | boolean | object | null })[key], o as never);\n  }\n  return out as T;\n}\n\n/** A recursively-optional view of a type, for partial `strings` overrides. */\nexport type DeepPartial<T> = T extends (infer U)[]\n  ? U[]\n  : T extends object\n    ? { [K in keyof T]?: DeepPartial<T[K]> }\n    : T;\n\n/** Options shared by the banner + error presenter for selecting localized copy. */\nexport interface ResolveStringsOptions {\n  /** Explicit BCP-47 language tag. Always wins when its primary subtag is known. */\n  lang?: string;\n  /** Partial overrides deep-merged over the selected pack (any field, any depth). */\n  strings?: DeepPartial<LocaleStrings>;\n}\n\n/**\n * PURE locale selector implementing the SB-SDK-07 policy:\n *   explicit `lang` (prefix-matched) > navigator.language (prefix-matched) > English,\n * then a partial `strings` override deep-merged over the selected pack.\n *\n * Pure + side-effect-free: it reads navigator.language only when no explicit\n * `lang` is given, and never mutates the built-in packs. An unknown subtag falls\n * through to English (never throws). Returns a fresh object when an override is\n * supplied, else the shared pack reference (so identity checks against EN/DE_STRINGS\n * hold for the no-override path the tests assert).\n */\nexport function resolveStrings(options: ResolveStringsOptions = {}): LocaleStrings {\n  const explicit = primarySubtag(options.lang);\n  const selected =\n    (explicit && PACKS[explicit]) ||\n    (PACKS[primarySubtag(navigatorLanguage())]) ||\n    EN_STRINGS;\n  return options.strings ? deepMerge(selected, options.strings) : selected;\n}\n","/**\n * Install prompt UI for Beacio\n *\n * Two modes:\n * 1. Bottom sheet (default) — iOS-native feel, shown on requestDevice() trigger\n * 2. Banner — lightweight top/bottom bar for passive prompting\n *\n * Features:\n * - Clipboard context saving for return-to-web-app flow\n * - 14-day dismissal frequency capping\n * - Configurable install/onboarding redirect\n * - Dark mode support via prefers-color-scheme\n */\n\n// The canonical /setup URL is the single source of truth in core (urls.ts),\n// shared by this install banner and the react-sdk InstallationWizard. detect now\n// lives INSIDE @beacio/core, so this is a plain intra-package import — no more\n// optional-peer inline-pin/`typeof import(...)` dance.\nimport { SETUP_URL } from '../urls';\n\n// SB-NAT-01: the canonical event-name map — the sheet's live teardown listens on\n// BOTH the package-lifecycle READY ('beacio:ready') and the extension's in-page\n// handshake EXTENSION_READY ('beacio:extension:ready'), sourced from the single\n// source of truth so neither literal can drift (events.test.ts is the control).\nimport { BEACIO_EVENTS } from '../events';\n\n// SB-SDK-07: the shared localized-string seam. Every visible token routes\n// through a resolved LocaleStrings pack (explicit `lang` > navigator.language >\n// English), so a German S&B user sees German at the make-or-break moment. The\n// built-in `en` pack reproduces today's copy byte-for-byte, so a caller that\n// passes no lang/strings is unchanged.\nimport { type DeepPartial, EN_STRINGS, type LocaleStrings, resolveStrings } from './i18n';\n\n// SB-SDK-12: the install-state marker read, the return-link/clipboard context\n// helpers, and the dismissal frequency-cap now live in the single shared\n// install-state module (also re-exported from the package root as the headless,\n// zero-DOM onboarding API for vanilla-JS partners). The banner consumes them\n// here with NO behavior change — same localStorage keys, same return-link form —\n// so its private call sites below are unchanged.\nimport {\n  APP_STORE_URL,\n  buildOpenInSafariUrl,\n  dismiss,\n  dismissShort,\n  getReturnContext,\n  isDismissed,\n  isExtensionActive,\n  saveReturnContext,\n} from './install-state';\n\n/**\n * Where the user is in the irreducibly-manual iOS-26 setup funnel, so the sheet\n * can render the SPECIFIC remaining step instead of restarting the whole flow:\n *  - 'not-installed'      → app not installed; full install→enable→grant walkthrough\n *  - 'installed-inactive' → installed but the Safari extension toggle is off\n *  - 'denied'             → enabled but per-origin access not granted on THIS site\n *  - 'private-browsing'   → Private Browsing disables extensions; reopen in a normal tab\n *  - 'home-screen-web-app' → running standalone from the Home Screen, where Safari\n *                            extensions never load; open the page in Safari\n *  - 'active'             → ready; render the once-only success toast\n * Mirrors ExtensionInstallState ('active' | 'installed-inactive' | 'not-installed')\n * plus the in-page refinements only the page flow can distinguish: the per-site\n * 'denied' grant, the SB-SDK-17 'private-browsing' dead end and the GH #448\n * Home Screen web-app dead end.\n */\nexport type BannerState =\n  | 'not-installed'\n  | 'installed-inactive'\n  | 'denied'\n  | 'private-browsing'\n  | 'home-screen-web-app'\n  | 'active';\n\nexport interface BannerOptions {\n  /** 'sheet' (default) for iOS bottom sheet, 'banner' for lightweight bar. The 'home-screen-web-app' state always renders the sheet (the bar has no state-specific copy and would show the install CTA). */\n  mode?: 'sheet' | 'banner';\n  position?: 'top' | 'bottom';\n  style?: Record<string, string>;\n  /** Preferred install or onboarding URL to open when the user taps the CTA */\n  startOnboardingUrl?: string;\n  /** Legacy install destination option; still supported for compatibility */\n  appStoreUrl?: string;\n  /** Operator/app name shown in the prompt (e.g. \"FitTracker\") */\n  operatorName?: string;\n  /** API key for campaign tracking */\n  apiKey?: string;\n  /**\n   * Days to suppress the PASSIVE on-load banner after the EXPLICIT \"Don't show\n   * again\" opt-out (default: 14). SB-PRD-08: the soft \"Not now\"/backdrop tap uses\n   * a separate, short (1-day) window and is NOT governed by this option, so one\n   * reflexive dismiss no longer silences guidance for a fortnight.\n   */\n  dismissDays?: number;\n  /**\n   * SB-PRD-08 (AC3): ignore the active dismissal cooldown and render anyway. The\n   * passive on-load banner leaves this false so a dismissed user is not nagged;\n   * a USER-INITIATED recovery gesture (e.g. tapping Connect, or a \"Set up\n   * Bluetooth\"/\"Can't connect?\" affordance) passes `forceShow: true` to re-open\n   * the activation flow without the integrator having to clear localStorage.\n   */\n  forceShow?: boolean;\n  /**\n   * Funnel position. Lets initBeacio render state-specific guidance (and, on\n   * 'active', the once-only \"ready\" toast) without restarting setup. Defaults to\n   * 'not-installed' for the legacy \"show the full walkthrough\" call site.\n   */\n  state?: BannerState;\n  /**\n   * Setup destination shown behind the \"still stuck?\" affordance and the\n   * \"How does setup work?\" disclosure. Defaults to the canonical /setup page;\n   * an operator (e.g. Storz & Bickel) can point it at their own branded help.\n   */\n  setupUrl?: string;\n  /**\n   * SB-SDK-07: BCP-47 UI language (e.g. 'de'). When set, its primary subtag\n   * selects the built-in pack; when omitted, the language is derived from\n   * navigator.language, else English. Always wins over navigator.language.\n   */\n  lang?: string;\n  /**\n   * SB-SDK-07: partial copy overrides deep-merged over the selected language\n   * pack — override one field (e.g. `buttonText`) without restating the rest.\n   */\n  strings?: DeepPartial<LocaleStrings>;\n  /**\n   * SB-SDK-11 (tier-2 co-brand): partner accent colour applied to the sheet/bar\n   * chrome (icon tile, step bullets, primary CTA, disclosure links). Routed\n   * through a `--bc-accent` CSS variable so every accent rule switches to\n   * var(--bc-accent); when omitted the variable defaults to the beacio Apple-blue\n   * (#007aff) and the prompt renders exactly as before. Any CSS colour token.\n   */\n  accentColor?: string;\n  /**\n   * SB-SDK-11: partner logo, restricted to a URL (no raw SVG markup) so it can\n   * never inject script. Validated with `new URL()` against the page origin and\n   * accepted ONLY when the resolved protocol is http(s); a `javascript:`/`data:`/\n   * `ftp:` value is dropped and the default beacio chrome icon is kept. Rendered\n   * as an <img> in place of the inline beacio <svg>.\n   */\n  brandLogoUrl?: string;\n  /**\n   * SB-SDK-11: the specific device being connected (e.g. \"VOLCANO HYBRID\"). When\n   * set it is interpolated into the `{device}` token of any copy that carries it,\n   * so a co-brand sheet can read \"Connect your VOLCANO HYBRID in Safari\".\n   */\n  deviceName?: string;\n  /**\n   * SB-SDK-11: a one-shot override for the sheet's lead body copy. Wins over the\n   * resolved language pack's state body (HTML-escaped via esc(), like all copy).\n   * For finer-grained per-field overrides use the SB-SDK-07 `strings` seam.\n   */\n  body?: string;\n  /**\n   * SB-SDK-11: override for the privacy reassurance body (the medical-market\n   * trust line). HTML-escaped. Defaults to the resolved pack's privacyBody.\n   */\n  privacyBody?: string;\n}\n\n// AIDEV-NOTE: SB-PRD-03 — the single canonical onboarding copy block. The\n// irreducibly-manual iOS-26 enable + per-origin grant is the weakest funnel point\n// (memory project_ios26_safari_extension_settings_readonly), and the decisive\n// GESTURE — tap aA in the address bar → Manage Extensions → Allow Every Website —\n// previously lived ONLY in troubleshooting.md, surfaced nowhere the user is stuck.\n// This is the verbatim source of truth reused by the bottom sheet AND mirrored by\n// the react-sdk InstallationWizard so web and react paths never drift. iOS-26\n// accurate; uses neutral install-path framing only (feedback_no_app_store_status_claims).\nexport interface SetupStep {\n  /** Imperative step label the user taps. */\n  label: string;\n  /** One-line \"why this is required\", shown under the label. */\n  why: string;\n}\n\n/**\n * The real sequence a first-run owner actually taps on a physical iPhone, each\n * grant with its own \"why\" so no system prompt is a surprise. Ordering and count\n * are the contract: install → open app → enable extension → allow website access\n * (the aA gesture) → allow Bluetooth on first scan → return.\n *\n * SB-SDK-07: this is the ENGLISH step list, now sourced from EN_STRINGS.steps so\n * the exported constant (mirrored by the react-sdk InstallationWizard) and the\n * localized pack never drift. Localized rendering reads the resolved pack's\n * steps; the per-state filtering below is by INDEX into this canonical order, so\n * it is language-independent (German labels do not match the old English regex).\n */\nexport const SETUP_STEPS: readonly SetupStep[] = EN_STRINGS.steps;\n\n/**\n * Per-state index sets into the canonical SETUP_STEPS order. installed-inactive →\n * enable toggle + Bluetooth prompt + return (indices 2,4,5); denied → the aA\n * website-access gesture + Bluetooth prompt + return (indices 3,4,5). Keyed by\n * index (not label text) so localization cannot change which steps show.\n *\n * SB-SDK-17: 'private-browsing' shows NO steps — the only action is \"open this\n * page in a normal tab\" (no Settings walkthrough), so the sheet renders just the\n * distinct hint with an empty step strip. GH #448: 'home-screen-web-app' is the\n * same shape — the only action is \"open this page in Safari\", and no amount of\n * Settings walkthrough makes an extension load inside a web app.\n */\nconst STATE_STEP_INDICES: Record<Exclude<BannerState, 'active' | 'not-installed'>, readonly number[]> = {\n  'installed-inactive': [2, 4, 5],\n  denied: [3, 4, 5],\n  'private-browsing': [],\n  'home-screen-web-app': [],\n};\n\n// AIDEV-NOTE: Canonical zero-config onboarding default — the guided /setup page (install →\n// enable Safari extension → return), not a bare App Store search. Lets an agent wire the banner\n// with no URL config and still send users to a flow that actually completes setup. Imported from\n// @beacio/core (SETUP_URL) so the banner and the react-sdk InstallationWizard share one constant.\nconst DEFAULT_ONBOARDING_URL = SETUP_URL;\n// SB-PRD-03 AC5: once-only success toast. Set on the first load that observes the\n// 'active' transition so returning users get the fast path (no banner, no toast).\nconst READY_SHOWN_KEY = 'beacio_ready_shown';\n\n// SB-SDK-03 AC4: the install sheet is a LIVE affordance. The irreducibly-manual\n// iOS-26 enable step (project_ios26_safari_extension_settings_readonly) happens\n// OUTSIDE the page in Settings, so a visible sheet must re-check on foreground\n// return + on the extension's READY signal and remove itself the moment the\n// extension goes active — with no manual reload. These mirror the canonical\n// names: the active DOM marker the content script sets (detect.ts hasActiveMarker)\n// and BEACIO_EVENTS.READY dispatched by initBeacio (index.ts:114).\nconst READY_EVENT = BEACIO_EVENTS.READY;\n// SB-NAT-01 regression fix (2026-07-21): READY alone is DEAF to same-load late\n// activation. 'beacio:ready' is dispatched ONLY by initBeacio's active path — a\n// path that, by construction, never ran in the page-load that rendered this\n// non-active sheet — and visibilitychange never fires while Safari stays\n// foregrounded. On a cold-appex first load the injected polyfill announces\n// itself AFTER detect's 2s window via its OWN in-page handshake event\n// (BEACIO_EVENTS.EXTENSION_READY, the same seam observeInstallState awaits), so\n// the sheet must ALSO listen there or it sits over the operator's app forever\n// and swallows the Connect tap (hardware-observed on the S&B fork).\nconst EXTENSION_READY_EVENT = BEACIO_EVENTS.EXTENSION_READY;\n// Bounded foreground re-check poll, modeled on website-src/scripts/setup-verify.js\n// (PING_ATTEMPTS/PING_INTERVAL_MS): a capped number of attempts so we never leave\n// a live timer running for battery safety if activation never happens.\nconst RECHECK_ATTEMPTS = 5;\nconst RECHECK_INTERVAL_MS = 300;\n\n// SB-SDK-12: isExtensionActiveNow / isDismissed / dismiss / saveReturnContext\n// / getReturnContext were promoted into the shared install-state module (imported\n// above) and re-exported from the package root as the headless API. They keep the\n// same localStorage keys and return-link form, so the call sites below are\n// unchanged. `isExtensionActiveNow` is now `isExtensionActive` from that module.\n\nfunction resolveOnboardingUrl(options: Pick<BannerOptions, 'startOnboardingUrl' | 'appStoreUrl'>): string {\n  return options.startOnboardingUrl ?? options.appStoreUrl ?? DEFAULT_ONBOARDING_URL;\n}\n\n/**\n * SB-PRD-07 AC4: PURE URL builder for the onboarding redirect — the durable seam\n * the click handlers delegate to (jsdom 26 makes window.location.href set/readback\n * un-observable, so the branded-continuity contract is pinned here, not on a\n * navigation). Two mutually-exclusive shapes by destination:\n *  - App Store (apps.apple.com): campaign ct/mt ONLY, exactly as before — the deep\n *    link must NOT carry the operator identity / return origin. SB-SDK-12 AC4: the\n *    path is normalized to the id form (apps.apple.com/app/id<digits>) using the\n *    canonical APP_STORE_URL, so a caller-supplied NAME slug (/app/<slug>/id…) — the\n *    form that 404s / misleads if Apple's slug differs from \"beacio\" — is stripped.\n *  - canonical onboarding (beacio.com/setup, or an operator's own help URL): thread\n *    operatorName + the originating return URL as query params so the guided /setup\n *    page renders \"Return to <operator>\" instead of generic copy. Existing params\n *    on the target are preserved; we never overwrite an operatorName/return already\n *    present on the URL.\n */\nexport function buildOnboardingUrl(\n  url: string,\n  opts: { apiKey?: string; operatorName?: string; returnUrl?: string } = {}\n): string {\n  const base = typeof window !== 'undefined' ? window.location.href : undefined;\n  const parsed = new URL(url, base);\n  const isAppStore = parsed.hostname === 'apps.apple.com';\n\n  if (isAppStore) {\n    // SB-SDK-12 AC4: collapse the path to the id form. Keep the app id segment\n    // (idNNNN) if the caller supplied one; otherwise fall back to the canonical\n    // APP_STORE_URL path. Either way no NAME slug survives onto the deep link.\n    const idSegment = parsed.pathname.match(/id\\d+/)?.[0];\n    parsed.pathname = idSegment ? `/app/${idSegment}` : new URL(APP_STORE_URL).pathname;\n    if (opts.apiKey && !parsed.searchParams.has('ct')) {\n      parsed.searchParams.set('ct', opts.apiKey);\n      parsed.searchParams.set('mt', '8');\n    }\n    return parsed.toString();\n  }\n\n  // Non-App-Store onboarding target → carry the branded continuity through.\n  if (opts.operatorName && !parsed.searchParams.has('operatorName')) {\n    parsed.searchParams.set('operatorName', opts.operatorName);\n  }\n  if (opts.returnUrl && !parsed.searchParams.has('return')) {\n    parsed.searchParams.set('return', opts.returnUrl);\n  }\n  return parsed.toString();\n}\n\nfunction redirectToOnboarding(url: string, apiKey?: string, operatorName?: string): void {\n  saveReturnContext();\n\n  // SB-PRD-07 AC4: thread the operator identity + the originating page through the\n  // redirect so /setup (and /setup-verify) can render the branded return CTA.\n  const returnUrl = getReturnContext().url;\n  window.location.href = buildOnboardingUrl(url, { apiKey, operatorName, returnUrl });\n}\n\nfunction esc(s: string): string {\n  const d = document.createElement('div');\n  d.textContent = s;\n  return d.innerHTML;\n}\n\n// SB-SDK-07: substitute the resolved operator name into a localized template's\n// `{operator}` token. All operator-bearing strings (the not-installed body, the\n// return CTA, the clipboard hint, the ready toast) carry `{operator}` in BOTH\n// language packs, so this is the single, language-independent interpolation.\n// SB-SDK-11: also substitutes the optional `{device}` token (the connected\n// device's display name) where co-brand copy names it; an empty deviceName\n// leaves a bare token-free string.\nfunction fill(template: string, operatorName: string, deviceName = ''): string {\n  return template.replace(/\\{operator\\}/g, operatorName).replace(/\\{device\\}/g, deviceName);\n}\n\n/**\n * SB-SDK-11 AC2 injection guard: a partner logo is accepted ONLY as a URL that\n * resolves (against the page origin) to an http(s) resource. A `javascript:`,\n * `data:`, `ftp:` (or any non-http) value — the XSS vectors — is rejected and the\n * caller falls back to the default beacio chrome icon. Relative URLs (e.g. a\n * site-hosted `logo-sb.svg`) resolve to the page's http(s) origin and are allowed.\n * Returns the resolved absolute URL string when safe, else null. Never throws.\n */\nfunction safeLogoUrl(raw: string | undefined): string | null {\n  if (!raw || typeof raw !== 'string') return null;\n  const base = typeof window !== 'undefined' ? window.location.href : 'https://beacio.com';\n  try {\n    const u = new URL(raw, base);\n    return u.protocol === 'http:' || u.protocol === 'https:' ? u.href : null;\n  } catch {\n    return null;\n  }\n}\n\n// ─── Bottom Sheet ──────────────────────────────────────────────────────────\n\nfunction showBottomSheet(options: BannerOptions): HTMLElement {\n  const {\n    operatorName = document.title || window.location.hostname,\n    apiKey,\n    dismissDays = 14,\n    state = 'not-installed',\n  } = options;\n  const onboardingUrl = resolveOnboardingUrl(options);\n  // SB-SDK-07: resolve the localized pack ONCE (explicit lang > navigator.language\n  // > English), then deep-merge any partial `strings` override.\n  const t = resolveStrings({ lang: options.lang, strings: options.strings });\n  const buttonText = t.buttonText;\n  // 'active' never reaches here (showInstallBanner routes it to the toast); the\n  // remaining states each carry their own lead copy + only-the-relevant steps.\n  const sheetState = (state === 'active' ? 'not-installed' : state) as Exclude<BannerState, 'active'>;\n  const { title } = t.states[sheetState];\n  // SB-SDK-11: a top-level `body` override wins over the resolved pack body so a\n  // co-brand caller can pass device-specific lead copy; else use the pack's state\n  // body with {operator}/{device} interpolated.\n  const body = options.body ?? t.states[sheetState].body;\n  const setupUrl = options.setupUrl ?? onboardingUrl;\n\n  // SB-SDK-11 (tier-2 co-brand): resolve the optional theme. The accent is routed\n  // through a `--bc-accent` CSS variable defaulting to the beacio Apple-blue, so\n  // every accent rule below reads var(--bc-accent) and an unthemed sheet is\n  // byte-identical to before. A partner logo is URL-validated (no raw SVG) and,\n  // when safe, swaps the inline beacio chrome <svg> for an <img>. `themed` is true\n  // for any co-brand signal and gates the always-on trust surfaces (the VISIBLE\n  // privacy line + the \"not affiliated with the device maker\" microcopy).\n  const accent = options.accentColor ?? '#007aff';\n  const logoUrl = safeLogoUrl(options.brandLogoUrl);\n  const deviceName = options.deviceName ?? '';\n  const themed = Boolean(options.accentColor || logoUrl || options.deviceName);\n  const privacyBody = options.privacyBody ?? t.privacyBody;\n\n  // installed-inactive → just the enable toggle; denied → just the aA gesture;\n  // not-installed → the full first-run walkthrough (AC6: show the SPECIFIC step,\n  // do not restart). The Bluetooth-prompt + return steps stay so the user is\n  // never surprised by the first-scan permission and always has a next action.\n  // SB-SDK-07: filter by INDEX into the canonical order (language-independent),\n  // and render the RESOLVED pack's step copy (German labels/why when lang='de').\n  const steps =\n    sheetState === 'not-installed'\n      ? t.steps\n      : STATE_STEP_INDICES[sheetState].map((i) => t.steps[i]);\n\n  const stepsHtml = steps\n    .map(\n      (s) =>\n        `<li class=\"bc-step\"><span class=\"bc-step-l\">${esc(s.label)}</span><span class=\"bc-step-w\">${esc(s.why)}</span></li>`\n    )\n    .join('');\n\n  // The PRIMARY CTA is per-state, as an exhaustive switch (a future BannerState\n  // cannot silently inherit the install button): 'not-installed' opens the install\n  // flow; GH #448's Home Screen web app gets the x-safari- escape hatch INSTEAD of\n  // an install CTA — the app may well be installed already, and re-installing it\n  // is exactly the loop this state exists to break. The remaining states carry\n  // their next action in the step strip above, so they add no button here.\n  const ctaHtml = ((): string => {\n    switch (sheetState) {\n      case 'not-installed':\n        return `<button class=\"bc-btn\" id=\"bc-install\">${esc(buttonText)}</button>`;\n      case 'home-screen-web-app': {\n        // The address is ALWAYS visible, not just linked: x-safari- can silently\n        // no-op inside a web app, and re-typing the URL in Safari is the recovery\n        // that always works. buildOpenInSafariUrl returns the '' sentinel for a\n        // non-http(s) page (file:/blob:/about: — unreachable from initBeacio, which\n        // only routes here inside a real, always-https web app, but reachable via\n        // the PUBLIC showInstallBanner({ state: 'home-screen-web-app' }) API). On\n        // that sentinel the anchor is OMITTED rather than emitted with href=\"\": an\n        // empty href is a SAME-DOCUMENT link, so the sheet's only primary action\n        // would silently RELOAD the page — exactly the loop this state exists to\n        // break. The visible address is then the entire recovery.\n        //\n        // That address is a single unbreakable token under the default\n        // `overflow-wrap: normal`, and #bc-s declares only overflow-y (so overflow-x\n        // computes to auto): the `#bc-open-safari-fallback` rule in the <style> block\n        // below therefore carries overflow-wrap/word-break, or a realistic partner URL\n        // scrolls the sheet sideways off a narrow viewport and the recovery is\n        // unreadable. That rule ships WITHOUT an explanatory CSS comment on purpose —\n        // comments inside this template literal are shipped bytes (TypeScript comments\n        // like this one are not), and the browser-auto gzip tripwire has ~31 B of\n        // headroom left. See GH #458.\n        const pageUrl = window.location.href;\n        const openInSafariUrl = buildOpenInSafariUrl(pageUrl);\n        const openInSafariCta =\n          openInSafariUrl === ''\n            ? ''\n            : `<a class=\"bc-btn\" id=\"bc-open-safari\" href=\"${esc(openInSafariUrl)}\">${esc(t.openInSafari)}</a>`;\n        return (\n          openInSafariCta +\n          `<p class=\"bc-cb\" id=\"bc-open-safari-fallback\">${esc(t.openInSafariFallback)} ${esc(pageUrl)}</p>`\n        );\n      }\n      case 'installed-inactive':\n      case 'denied':\n      case 'private-browsing':\n        return '';\n    }\n  })();\n\n  // GH #448: the /setup ROUND-TRIP affordances — the #bc-return button, the\n  // \"link also copied\" hint, and the clipboard write behind them — belong to every\n  // state whose recovery comes BACK to this page (leave for Settings, return here).\n  // 'home-screen-web-app' is the one state whose recovery LEAVES: its sheet prints\n  // the page address and asks the user to copy it into Safari. Rendering the footer\n  // there would (1) hand the user a SECOND, different address to paste (the return\n  // link is link.beacio.com/return?url=…, not the page), (2) let saveReturnContext()\n  // silently clobber the clipboard with that other URL, and (3) — because\n  // getReturnContext() falls back to the current href before anything is saved —\n  // present a full-width primary-looking button that merely RELOADS the broken web\n  // app, i.e. the loop this state exists to break. An exhaustive switch, not a\n  // `!==` test, so a future BannerState has to decide which kind of recovery it is.\n  const roundTrip = ((): boolean => {\n    switch (sheetState) {\n      case 'home-screen-web-app':\n        return false;\n      case 'not-installed':\n      case 'installed-inactive':\n      case 'denied':\n      case 'private-browsing':\n        return true;\n    }\n  })();\n\n  const returnHtml = roundTrip\n    ? `<a class=\"bc-ret\" id=\"bc-return\" href=\"${esc(getReturnContext().returnLink)}\">${esc(\n        fill(t.returnCta, operatorName)\n      )}</a>` + `<p class=\"bc-cb\">${esc(fill(t.clipboardHint, operatorName))}</p>`\n    : '';\n\n  const overlay = document.createElement('div');\n  overlay.id = 'beacio-banner';\n  overlay.dataset.beacioState = sheetState;\n  overlay.innerHTML = `\n<style>\n/* SB-SDK-11: the partner accent is exposed as a single CSS custom property on the\n   sheet root; every accent rule below reads var(--bc-accent). When unthemed the\n   value defaults to the beacio Apple-blue, so the rendered sheet is unchanged. */\n#bc-s{--bc-accent:${esc(accent)}}\n#beacio-overlay{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:flex-end;\n  justify-content:center;background:rgba(0,0,0,.4);font-family:-apple-system,BlinkMacSystemFont,\n  'SF Pro Text',system-ui,sans-serif;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);\n  animation:bc-fi .25s ease-out}\n@keyframes bc-fi{from{opacity:0}to{opacity:1}}\n@keyframes bc-su{from{transform:translateY(100%)}to{transform:translateY(0)}}\n#bc-s{background:#fff;border-radius:16px 16px 0 0;padding:12px 20px 28px;max-width:420px;\n  width:100%;animation:bc-su .3s ease-out;max-height:90vh;overflow-y:auto;\n  -webkit-overflow-scrolling:touch}\n#bc-s *{box-sizing:border-box;margin:0;padding:0}\n.bc-h{width:36px;height:5px;border-radius:3px;background:#d1d1d6;margin:0 auto 10px}\n.bc-hdr{display:flex;align-items:center;gap:10px;margin-bottom:8px}\n.bc-ic{width:36px;height:36px;border-radius:9px;background:var(--bc-accent);display:flex;\n  align-items:center;justify-content:center;flex-shrink:0;overflow:hidden}\n.bc-ic svg{width:20px;height:20px;fill:#fff}\n.bc-ic img{width:100%;height:100%;object-fit:contain}\n.bc-tt{font-size:16px;font-weight:600;color:#000}\n.bc-bd{font-size:13px;line-height:1.35;color:#8e8e93;margin-bottom:12px}\n.bc-steps{list-style:none;margin:0 0 14px;padding:0;counter-reset:bc-step}\n.bc-step{position:relative;padding:0 0 8px 28px;font-size:13px;line-height:1.35}\n.bc-step::before{counter-increment:bc-step;content:counter(bc-step);position:absolute;left:0;top:0;\n  width:18px;height:18px;border-radius:50%;background:var(--bc-accent);color:#fff;font-size:11px;\n  font-weight:600;display:flex;align-items:center;justify-content:center}\n.bc-step-l{display:block;font-weight:600;color:#1c1c1e}\n.bc-step-w{display:block;color:#8e8e93;margin-top:1px;font-size:12px}\n.bc-btn{display:block;width:100%;padding:12px;background:var(--bc-accent);color:#fff;border:none;\n  border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;text-align:center;\n  text-decoration:none;-webkit-tap-highlight-color:transparent}\n.bc-btn:active{opacity:.85}\n.bc-ret{display:block;width:100%;padding:12px;margin-top:8px;background:#34c759;color:#fff;border:none;\n  border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;text-align:center;\n  text-decoration:none;-webkit-tap-highlight-color:transparent}\n.bc-ret:active{opacity:.85}\n.bc-cb{font-size:11px;color:#8e8e93;text-align:center;margin-top:6px}\n#bc-open-safari-fallback{overflow-wrap:anywhere;word-break:break-all;text-align:left}\n/* SB-SDK-11 AC3: VISIBLE trust surfaces (not the collapsed <details>) — the\n   medical-market \"No data collected\" reassurance + the no-affiliation microcopy. */\n.bc-privacy{font-size:12px;color:#8e8e93;line-height:1.4;margin-top:12px}\n.bc-noaff{font-size:11px;color:#8e8e93;line-height:1.3;margin-top:6px;text-align:center}\n.bc-det{margin-top:10px}\n.bc-det summary{font-size:13px;color:var(--bc-accent);cursor:pointer;list-style:none;padding:2px 0}\n.bc-det summary::before{content:'\\\\25B8  '}\n.bc-det[open] summary::before{content:'\\\\25BE  '}\n.bc-det p{font-size:12px;color:#8e8e93;line-height:1.4;padding:6px 0 2px}\n.bc-det a{color:var(--bc-accent)}\n.bc-stuck{display:block;font-size:12px;color:var(--bc-accent);text-align:center;margin-top:10px;\n  text-decoration:none}\n.bc-reload{display:block;width:100%;padding:11px;margin-top:8px;background:none;\n  border:1px solid var(--bc-accent);border-radius:12px;font-size:15px;font-weight:600;color:var(--bc-accent);\n  cursor:pointer;text-align:center;-webkit-tap-highlight-color:transparent}\n.bc-reload:active{opacity:.7}\n.bc-dis{display:block;width:100%;padding:8px;background:none;border:none;font-size:14px;\n  color:#8e8e93;cursor:pointer;text-align:center;margin-top:4px;\n  -webkit-tap-highlight-color:transparent}\n/* SB-PRD-08: the explicit long opt-out (#bc-dont-show) is visually quieter than the\n   soft dismiss (#bc-dismiss) above it — smaller, less padding — so the soft dismiss\n   stays the default gesture and the long opt-out is a deliberate secondary choice.\n   NB keep this comment free of literal UI copy: the <style> block is part of the\n   banner innerHTML, so any English token here would leak into the localized DOM\n   (i18n.test.ts no-English-leak guard). */\n.bc-dont{font-size:12px;padding:4px 12px;margin-top:0}\n@media(prefers-color-scheme:dark){\n  #bc-s{background:#1c1c1e}\n  .bc-tt,.bc-step-l{color:#fff}\n  .bc-bd,.bc-step-w,.bc-cb,.bc-det p,.bc-privacy,.bc-noaff{color:#98989f}\n  .bc-dis{color:#98989f}\n  .bc-reload{color:#0a84ff;border-color:#0a84ff}\n  .bc-h{background:#48484a}\n}\n</style>\n<div id=\"beacio-overlay\">\n<div id=\"bc-s\" role=\"dialog\" aria-label=\"${esc(title)}\">\n  <div class=\"bc-h\"></div>\n  <div class=\"bc-hdr\">\n    <div class=\"bc-ic\">${\n      logoUrl\n        ? `<img src=\"${esc(logoUrl)}\" alt=\"\" aria-hidden=\"true\">`\n        : '<svg viewBox=\"0 0 24 24\"><path d=\"M12 2L7 7l5 5-5 5 5 5V2zm0 6.83L10.83 7 12 5.83v2.34zm0 8.34L10.83 17 12 15.83v1.34zM17 7l-5 5 5 5-2.12 2.12L12 17l-2.88 2.12L7 17l5-5-5-5 2.12-2.12L12 7l2.88-2.12L17 7z\"/></svg>'\n    }</div>\n    <div class=\"bc-tt\">${esc(title)}</div>\n  </div>\n  <div class=\"bc-bd\">${esc(fill(body, operatorName, deviceName))}</div>\n  <ol class=\"bc-steps\">${stepsHtml}</ol>\n  ${ctaHtml}\n  ${returnHtml}\n  <button class=\"bc-reload\" id=\"bc-reload\">${esc(t.reload)}</button>\n  <details class=\"bc-det\"><summary>${esc(t.howSummary)}</summary><p>${esc(t.howBody)} <a href=\"${esc(setupUrl)}\" target=\"_blank\" rel=\"noopener\">${esc(t.howLink)}</a>.</p></details>\n  <details class=\"bc-det\"><summary>${esc(t.privacySummary)}</summary><p>${esc(privacyBody)}</p></details>\n  ${\n    themed\n      ? // SB-SDK-11 AC3: a VISIBLE privacy reassurance (the medical-market trust\n        // angle) — distinct from the collapsed <details> above — plus a\n        // no-affiliation microcopy line. Device-agnostic, neutral install-path\n        // framing only (no App-Store-approved/cleared/audited claims).\n        `<p class=\"bc-privacy\" id=\"bc-privacy\">${esc(t.privacySummary)} — ${esc(privacyBody)}</p>` +\n        `<p class=\"bc-noaff\" id=\"bc-noaff\">beacio is an independent Safari extension and is not affiliated with the device maker.</p>`\n      : ''\n  }\n  <a class=\"bc-stuck\" id=\"bc-stuck\" href=\"${esc(setupUrl)}\" target=\"_blank\" rel=\"noopener\">${esc(t.stillStuck)}</a>\n  <button class=\"bc-dis\" id=\"bc-dismiss\">${esc(t.dismiss)}</button>\n  <button class=\"bc-dis bc-dont\" id=\"bc-dont-show\">${esc(t.dontShowAgain)}</button>\n</div>\n</div>`;\n\n  // SB-PRD-03 AC4: persist the originating page so the return link survives the\n  // round trip, and (best-effort) copy it — but the link above is the primary,\n  // VISIBLE affordance and the copy is announced in the .bc-cb line, not silent.\n  // GH #448: gated on the SAME `roundTrip` discriminant that renders that line, so\n  // the clipboard is only ever written when the sheet has told the user it was — and\n  // never on the Home Screen sheet, whose own copy/paste instruction names a\n  // different address.\n  if (roundTrip) saveReturnContext();\n\n  // SB-SDK-03 AC4/AC5: wire the LIVE lifecycle synchronously (not deferred to\n  // rAF) so a foreground return / READY signal that lands in the same tick — e.g.\n  // the content script setting the active marker the instant the sheet renders —\n  // still tears the sheet down. Each listener and the bounded poll are unwound by\n  // detachLifecycle() so nothing keeps firing after the sheet is gone (battery).\n  //\n  // SB-PRD-08 AC3 (2026-07-21 device evidence): a `forceShow: true` sheet is a\n  // USER-INITIATED recovery gesture (\"Can't connect?\" / the E2E selector-liveness\n  // control) — the user explicitly asked for the guidance even though the markers\n  // may already read 'active', so the automatic self-clearing lifecycle is\n  // SKIPPED for it (on hardware it removed the requested sheet in the same tick,\n  // rendering the affordance blank). The explicit dismiss controls and the\n  // reload re-check remain the ways a forced sheet leaves the page.\n  const persistent = options.forceShow === true;\n  let pollTimer: ReturnType<typeof setTimeout> | null = null;\n  let detached = false;\n\n  function detachLifecycle(): void {\n    detached = true;\n    if (pollTimer !== null) {\n      clearTimeout(pollTimer);\n      pollTimer = null;\n    }\n    window.removeEventListener(READY_EVENT, onReady);\n    window.removeEventListener(EXTENSION_READY_EVENT, onExtensionReady);\n    document.removeEventListener('visibilitychange', onVisibility);\n  }\n\n  // AC5: the extension went active — drop the sheet with no manual reload. The\n  // once-only \"you can scan now\" toast is the caller's job (initBeacio re-derives\n  // 'active' on the next load); here we just clear the stale guidance.\n  function clearIfActive(): boolean {\n    if (!isExtensionActive()) return false;\n    detachLifecycle();\n    overlay.remove();\n    return true;\n  }\n\n  function onReady(): void {\n    clearIfActive();\n  }\n\n  // SB-NAT-01: the injected polyfill just announced it is live in THIS page-load\n  // (cold-appex late activation — the scenario READY structurally cannot cover).\n  // The active marker normally lands in the same tick as the event; when the\n  // ordering races, the bounded re-check absorbs it (battery-safe, capped).\n  function onExtensionReady(): void {\n    if (clearIfActive()) return;\n    startBoundedRecheck();\n  }\n\n  // AC4: bounded ping-style re-check on foreground return, modeled on\n  // setup-verify.js startPingLoop — capped attempts, fully torn down when it\n  // gives up so no timer survives if activation never happens.\n  function startBoundedRecheck(): void {\n    if (detached || pollTimer !== null) return;\n    let attempts = 0;\n    const tick = (): void => {\n      pollTimer = null;\n      if (detached) return;\n      if (clearIfActive()) return;\n      attempts += 1;\n      if (attempts >= RECHECK_ATTEMPTS) return; // give up; leave no live timer\n      pollTimer = setTimeout(tick, RECHECK_INTERVAL_MS);\n    };\n    tick();\n  }\n\n  function onVisibility(): void {\n    if (document.visibilityState !== 'visible') return;\n    // Immediate check (the marker may already be set), then a short bounded poll\n    // to absorb the content-script/injected.js activation race on return.\n    if (clearIfActive()) return;\n    startBoundedRecheck();\n  }\n\n  if (!persistent) {\n    window.addEventListener(READY_EVENT, onReady);\n    window.addEventListener(EXTENSION_READY_EVENT, onExtensionReady);\n    document.addEventListener('visibilitychange', onVisibility);\n  }\n\n  requestAnimationFrame(() => {\n    overlay.querySelector('#bc-install')?.addEventListener('click', () => {\n      redirectToOnboarding(onboardingUrl, apiKey, operatorName);\n    });\n    overlay.querySelector('#bc-reload')?.addEventListener('click', () => {\n      // AC3: re-check after the user changed Settings, without manual navigation.\n      // If the extension is already active, drop the sheet in place; otherwise\n      // reload so getExtensionInstallState() re-runs against fresh markers.\n      if (clearIfActive()) return;\n      window.location.reload();\n    });\n    // SB-PRD-08 (AC1): \"Not now\" is a SOFT dismissal — suppress the passive banner\n    // for one day only, not the full dismissDays window, so an interested user is\n    // not silenced for a fortnight by one reflexive tap.\n    overlay.querySelector('#bc-dismiss')?.addEventListener('click', () => {\n      detachLifecycle();\n      overlay.remove();\n      dismissShort();\n    });\n    // SB-PRD-08 (AC1): the EXPLICIT \"Don't show again\" opt-out — the LONG\n    // (dismissDays, default 14) suppression. Only this deliberate control earns\n    // the long silence.\n    overlay.querySelector('#bc-dont-show')?.addEventListener('click', () => {\n      detachLifecycle();\n      overlay.remove();\n      dismiss(dismissDays);\n    });\n    // Backdrop tap is incidental, like \"Not now\" → soft dismissal.\n    overlay.querySelector('#beacio-overlay')?.addEventListener('click', (e) => {\n      if ((e.target as HTMLElement).id === 'beacio-overlay') {\n        detachLifecycle();\n        overlay.remove();\n        dismissShort();\n      }\n    });\n  });\n\n  document.body.appendChild(overlay);\n  // The sheet may render into an already-active page (marker set before init ran);\n  // clear immediately so we never show stale guidance over a working extension.\n  // A forced (user-initiated) sheet is exempt — see the `persistent` note above.\n  if (!persistent) clearIfActive();\n  return overlay;\n}\n\n// ─── Lightweight Banner ────────────────────────────────────────────────────\n\nfunction showBarBanner(options: BannerOptions): HTMLElement {\n  const {\n    position = 'bottom',\n    style = {},\n    apiKey,\n    operatorName,\n  } = options;\n  // SB-SDK-07: resolve the localized pack for the bar copy.\n  const t = resolveStrings({ lang: options.lang, strings: options.strings });\n  const text = t.barText;\n  const buttonText = t.buttonText;\n  const onboardingUrl = resolveOnboardingUrl(options);\n  // SB-SDK-11: the same co-brand theme as the sheet — accent (default beacio\n  // Apple-blue) + URL-validated partner logo (else the default beacio glyph).\n  const accent = options.accentColor ?? '#007AFF';\n  const logoUrl = safeLogoUrl(options.brandLogoUrl);\n\n  const el = document.createElement('div');\n  el.id = 'beacio-banner';\n\n  const posStyle =\n    position === 'top'\n      ? 'top:0;border-bottom:1px solid #e5e7eb;'\n      : 'bottom:0;border-top:1px solid #e5e7eb;';\n\n  const customStyle = Object.entries(style)\n    .map(([k, v]) => `${k}:${v}`)\n    .join(';');\n\n  el.innerHTML = `\n    <div style=\"position:fixed;${posStyle}left:0;right:0;z-index:2147483646;\n      background:#fff;padding:16px;\n      display:flex;align-items:center;gap:12px;font-family:system-ui,-apple-system,sans-serif;\n      box-shadow:0 ${position === 'top' ? '2px' : '-2px'} 10px rgba(0,0,0,0.1);${customStyle}\">\n      ${\n        logoUrl\n          ? `<img src=\"${esc(logoUrl)}\" alt=\"\" aria-hidden=\"true\" width=\"24\" height=\"24\" style=\"object-fit:contain;flex-shrink:0\">`\n          : `<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n        <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"${esc(accent)}\"/>\n        <path d=\"M12 7a1 1 0 0 1 1 1v4a1 1 0 0 1-2 0V8a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0 2 1 1 0 0 1 0-2z\" fill=\"white\"/>\n      </svg>`\n      }\n      <div style=\"flex:1\">\n        <div style=\"font-size:14px;font-weight:600;color:#1f2937\">${esc(t.barTitle)}</div>\n        <div style=\"font-size:12px;color:#6b7280;margin-top:2px\">${esc(text)}</div>\n      </div>\n      <button id=\"beacio-banner-install\"\n         style=\"background:${esc(accent)};color:white;padding:8px 16px;border-radius:8px;\n         border:none;font-size:14px;font-weight:500;white-space:nowrap;cursor:pointer\">\n        ${esc(buttonText)}</button>\n      <button id=\"beacio-banner-close\"\n              style=\"background:none;border:none;color:#9ca3af;font-size:20px;\n              cursor:pointer;padding:4px;line-height:1\"\n              aria-label=\"Close\">&times;</button>\n    </div>`;\n\n  el.querySelector('#beacio-banner-install')?.addEventListener('click', () => {\n    redirectToOnboarding(onboardingUrl, apiKey, operatorName);\n  });\n  // SB-PRD-08 (AC1): the bar's × is an incidental close → SOFT (1-day) dismissal,\n  // matching the sheet's \"Not now\"; the lightweight bar has no explicit\n  // \"Don't show again\" opt-out, so it never triggers the long window.\n  el.querySelector('#beacio-banner-close')?.addEventListener('click', () => {\n    el.remove();\n    dismissShort();\n  });\n\n  document.body.appendChild(el);\n  return el;\n}\n\n// ─── Success Toast ─────────────────────────────────────────────────────────\n\nfunction hasReadyBeenShown(): boolean {\n  try {\n    return localStorage.getItem(READY_SHOWN_KEY) === '1';\n  } catch {\n    return false;\n  }\n}\n\nfunction markReadyShown(): void {\n  try {\n    localStorage.setItem(READY_SHOWN_KEY, '1');\n  } catch {\n    /* noop */\n  }\n}\n\n/**\n * SB-PRD-03 AC5: the FIRST page load that observes the 'active' transition gets a\n * dismissible \"beacio is ready — tap Connect\" toast, exactly once. Returning users\n * (key already set) get the fast path: no toast, no banner. Returns null on the\n * second+ call so callers can treat it like the suppressed banner.\n */\nfunction showReadyToast(options: BannerOptions): HTMLElement | null {\n  if (hasReadyBeenShown()) return null;\n  markReadyShown();\n\n  const operatorName = options.operatorName || document.title || window.location.hostname;\n  // SB-SDK-07: localized success-toast copy (explicit lang > navigator.language >\n  // English), with `{operator}` filled by the resolved operator name.\n  const t = resolveStrings({ lang: options.lang, strings: options.strings });\n  const el = document.createElement('div');\n  el.id = 'beacio-banner';\n  el.dataset.beacioState = 'active';\n  el.innerHTML = `\n<style>\n#bc-toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:2147483647;\n  max-width:420px;width:calc(100% - 32px);background:#34c759;color:#fff;border-radius:14px;\n  padding:14px 16px;display:flex;align-items:center;gap:12px;\n  font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif;\n  box-shadow:0 6px 20px rgba(0,0,0,.2);animation:bc-tu .3s ease-out}\n@keyframes bc-tu{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%,0)}}\n#bc-toast svg{width:22px;height:22px;flex-shrink:0;fill:#fff}\n.bc-toast-tx{flex:1;font-size:15px;font-weight:600;line-height:1.3}\n#bc-toast-x{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0 4px;\n  line-height:1;-webkit-tap-highlight-color:transparent}\n</style>\n<div id=\"bc-toast\" role=\"status\">\n  <svg viewBox=\"0 0 24 24\"><path d=\"M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\"/></svg>\n  <span class=\"bc-toast-tx\">${esc(fill(t.readyToast, operatorName))}</span>\n  <button id=\"bc-toast-x\" aria-label=\"Dismiss\">&times;</button>\n</div>`;\n\n  requestAnimationFrame(() => {\n    el.querySelector('#bc-toast-x')?.addEventListener('click', () => el.remove());\n  });\n\n  document.body.appendChild(el);\n  return el;\n}\n\n// ─── Public API ────────────────────────────────────────────────────────────\n\nexport function showInstallBanner(options: BannerOptions = {}): HTMLElement | null {\n  // SB-PRD-03 AC5: the ready toast is its OWN affordance and must fire even\n  // during the dismiss window — a returning, now-ready user should be welcomed,\n  // not silenced by an old \"Not now\". Only the install/guidance sheets honour dismiss.\n  if (options.state === 'active') return showReadyToast(options);\n  // SB-PRD-08 (AC3): the passive on-load banner honours the dismissal cooldown so\n  // it is not nagging; a user-initiated recovery gesture passes forceShow:true to\n  // re-open the flow without waiting out the timer (no localStorage clearing).\n  if (!options.forceShow && isDismissed()) return null;\n  // GH #448: `mode` is the caller's PRESENTATION choice — except where the chosen\n  // presentation cannot tell the state's story. `showBarBanner` renders one fixed\n  // thing and never reads `state`: the \"Enable Bluetooth\" title plus an install CTA\n  // into the App Store funnel. Inside a Home Screen web app that is precisely the\n  // install-again loop this state exists to break — the app may already be\n  // installed, and installing it again changes nothing, because extensions never\n  // load in a standalone web app. Only the sheet can carry the x-safari- escape\n  // hatch and the copyable address, so this one state overrides `mode`. An\n  // exhaustive switch, not a `!==` test, so a future BannerState has to decide for\n  // itself whether the lightweight bar can express its recovery.\n  const barCanTellTheStory = ((): boolean => {\n    switch (options.state ?? 'not-installed') {\n      case 'home-screen-web-app':\n        return false;\n      // No 'active' case: the early return above narrows it out of `options.state`\n      // here, so the compiler itself proves this switch exhaustive — adding it is\n      // a TS2678 error rather than dead code.\n      case 'not-installed':\n      case 'installed-inactive':\n      case 'denied':\n      case 'private-browsing':\n        return true;\n    }\n  })();\n  return options.mode === 'banner' && barCanTellTheStory\n    ? showBarBanner(options)\n    : showBottomSheet(options);\n}\n\nexport function removeInstallBanner(): void {\n  const el = document.getElementById('beacio-banner');\n  if (el) el.remove();\n}\n"]}