{"version":3,"file":"broadcast-BDfxMA8N.js","names":[],"sources":["../../node_modules/@tingdahl/tsutils/dist/cache/cache_sync.js","../../node_modules/@tingdahl/tsutils/dist/zip/zip.js","../../src/core/cache.ts","../../src/core/tenant_manager.ts","../../src/core/header.ts","../../src/broadcaster/audio-manager.ts","../../src/broadcaster/deepgram-streamer.ts","../../src/broadcaster/ws-publisher.ts","../../src/broadcaster/languages.ts","../../src/broadcaster/language-selector.ts","../../src/broadcaster/broadcaster-view.ts","../../src/features/broadcast/broadcast_panel.ts","../../src/features/channels/channels_panel.ts","../../src/features/tenant/tenant_panel.ts","../../src/features/tenant/new_tenant_panel.ts","../../src/features/legal/broadcaster_tc.ts","../../src/broadcast.ts"],"sourcesContent":["export class CacheSyncService {\n    channel = null;\n    tabId;\n    listeners = new Set();\n    channelName;\n    constructor(options) {\n        this.channelName = options?.channelName || 'app_cache_sync';\n        this.tabId = Math.random().toString(36).substring(2) + Date.now().toString(36);\n        if (typeof BroadcastChannel !== 'undefined') {\n            try {\n                this.channel = new BroadcastChannel(this.channelName);\n                this.channel.onmessage = (event) => {\n                    if (event.data && event.data.senderTabId !== this.tabId) {\n                        this.notifyListeners(event.data);\n                    }\n                };\n            }\n            catch (e) {\n                console.warn(`BroadcastChannel not supported or failed to initialize for channel \"${this.channelName}\":`, e);\n            }\n        }\n    }\n    broadcastInvalidation(domain, scopeId, metadata) {\n        const idStr = scopeId != null ? String(scopeId) : undefined;\n        const payload = {\n            domain,\n            scopeId: idStr,\n            companyId: idStr,\n            entityType: metadata?.entityType,\n            entityId: metadata?.entityId,\n            senderTabId: this.tabId,\n            timestamp: Date.now(),\n            extra: metadata?.extra,\n        };\n        if (this.channel) {\n            try {\n                this.channel.postMessage(payload);\n            }\n            catch (e) {\n                console.warn(`Failed to broadcast cache invalidation on channel \"${this.channelName}\":`, e);\n            }\n        }\n        this.notifyListeners(payload);\n    }\n    onInvalidate(listener) {\n        this.listeners.add(listener);\n        return () => this.listeners.delete(listener);\n    }\n    notifyListeners(payload) {\n        this.listeners.forEach(listener => {\n            try {\n                listener(payload);\n            }\n            catch (e) {\n                console.error('Error in cache invalidation listener:', e);\n            }\n        });\n    }\n    getTabId() {\n        return this.tabId;\n    }\n    getChannelName() {\n        return this.channelName;\n    }\n    close() {\n        if (this.channel) {\n            this.channel.close();\n            this.channel = null;\n        }\n        this.listeners.clear();\n    }\n}\n//# sourceMappingURL=cache_sync.js.map","/**\n * Zero-dependency ZIP archive builder using standard ZIP format specification.\n */\n// Pre-computed CRC32 lookup table\nconst CRC32_TABLE = new Uint32Array(256);\nfor (let i = 0; i < 256; i++) {\n    let c = i;\n    for (let k = 0; k < 8; k++) {\n        c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);\n    }\n    CRC32_TABLE[i] = c >>> 0;\n}\nexport function calculateCrc32(data) {\n    let crc = 0xFFFFFFFF;\n    for (let i = 0; i < data.length; i++) {\n        crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ data[i]) & 0xFF];\n    }\n    return (crc ^ 0xFFFFFFFF) >>> 0;\n}\nexport class ZipWriter {\n    entries = [];\n    totalUncompressedSize = 0;\n    /**\n     * Converts a JavaScript Date to MS-DOS date and time format.\n     */\n    static toDosDateTime(d) {\n        const year = d.getFullYear();\n        const dosYear = year >= 1980 ? year - 1980 : 0;\n        const dosMonth = d.getMonth() + 1;\n        const dosDay = d.getDate();\n        const dosDate = (dosYear << 9) | (dosMonth << 5) | dosDay;\n        const dosHours = d.getHours();\n        const dosMinutes = d.getMinutes();\n        const dosSeconds = Math.floor(d.getSeconds() / 2);\n        const dosTime = (dosHours << 11) | (dosMinutes << 5) | dosSeconds;\n        return { dosTime, dosDate };\n    }\n    /**\n     * Adds a file to the ZIP archive.\n     */\n    addFile(name, content, date = new Date()) {\n        let dataBytes;\n        if (typeof content === 'string') {\n            dataBytes = new TextEncoder().encode(content);\n        }\n        else if (content instanceof ArrayBuffer) {\n            dataBytes = new Uint8Array(content);\n        }\n        else {\n            dataBytes = content;\n        }\n        const nameBytes = new TextEncoder().encode(name);\n        const crc = calculateCrc32(dataBytes);\n        const { dosTime, dosDate } = ZipWriter.toDosDateTime(date);\n        this.entries.push({\n            name,\n            nameBytes,\n            data: dataBytes,\n            crc32: crc,\n            dosTime,\n            dosDate,\n            offset: 0 // populated on build\n        });\n        this.totalUncompressedSize += dataBytes.length;\n    }\n    /**\n     * Returns total raw bytes added so far.\n     */\n    getCurrentSize() {\n        return this.totalUncompressedSize;\n    }\n    /**\n     * Returns number of files added.\n     */\n    getFileCount() {\n        return this.entries.length;\n    }\n    /**\n     * Resets the writer to start a fresh volume.\n     */\n    reset() {\n        this.entries = [];\n        this.totalUncompressedSize = 0;\n    }\n    /**\n     * Assembles all entries into an application/zip Blob.\n     */\n    build() {\n        const chunks = [];\n        let currentOffset = 0;\n        // 1. Write Local File Headers + Data\n        for (const entry of this.entries) {\n            entry.offset = currentOffset;\n            const header = new Uint8Array(30 + entry.nameBytes.length);\n            const view = new DataView(header.buffer);\n            view.setUint32(0, 0x04034b50, true); // Local file header signature\n            view.setUint16(4, 20, true); // Version needed (2.0)\n            view.setUint16(6, 0x0800, true); // General purpose bit flag (UTF-8 filename flag)\n            view.setUint16(8, 0, true); // Compression method (0 = store)\n            view.setUint16(10, entry.dosTime, true);\n            view.setUint16(12, entry.dosDate, true);\n            view.setUint32(14, entry.crc32, true);\n            view.setUint32(18, entry.data.length, true); // Compressed size\n            view.setUint32(22, entry.data.length, true); // Uncompressed size\n            view.setUint16(26, entry.nameBytes.length, true); // Filename length\n            view.setUint16(28, 0, true); // Extra field length\n            header.set(entry.nameBytes, 30);\n            chunks.push(header);\n            chunks.push(entry.data);\n            currentOffset += header.length + entry.data.length;\n        }\n        const centralDirStartOffset = currentOffset;\n        // 2. Write Central Directory Headers\n        let centralDirSize = 0;\n        for (const entry of this.entries) {\n            const cdHeader = new Uint8Array(46 + entry.nameBytes.length);\n            const view = new DataView(cdHeader.buffer);\n            view.setUint32(0, 0x02014b50, true); // Central directory header signature\n            view.setUint16(4, 20, true); // Version made by (2.0)\n            view.setUint16(6, 20, true); // Version needed (2.0)\n            view.setUint16(8, 0x0800, true); // General purpose bit flag (UTF-8)\n            view.setUint16(10, 0, true); // Compression method (0 = store)\n            view.setUint16(12, entry.dosTime, true);\n            view.setUint16(14, entry.dosDate, true);\n            view.setUint32(16, entry.crc32, true);\n            view.setUint32(20, entry.data.length, true); // Compressed size\n            view.setUint32(24, entry.data.length, true); // Uncompressed size\n            view.setUint16(28, entry.nameBytes.length, true); // Filename length\n            view.setUint16(30, 0, true); // Extra field length\n            view.setUint16(32, 0, true); // File comment length\n            view.setUint16(34, 0, true); // Disk number start\n            view.setUint16(36, 0, true); // Internal file attributes\n            view.setUint32(38, 0, true); // External file attributes\n            view.setUint32(42, entry.offset, true); // Relative offset of local header\n            cdHeader.set(entry.nameBytes, 46);\n            chunks.push(cdHeader);\n            centralDirSize += cdHeader.length;\n        }\n        const eocd = new Uint8Array(22);\n        const eocdView = new DataView(eocd.buffer);\n        eocdView.setUint32(0, 0x06054b50, true);\n        eocdView.setUint16(8, this.entries.length, true);\n        eocdView.setUint16(10, this.entries.length, true);\n        eocdView.setUint32(12, centralDirSize, true);\n        eocdView.setUint32(16, centralDirStartOffset, true);\n        chunks.push(eocd);\n        return new Blob(chunks, { type: 'application/zip' });\n    }\n    /**\n     * Triggers a browser download of the assembled ZIP file.\n     */\n    download(filename) {\n        const blob = this.build();\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = filename;\n        document.body.appendChild(a);\n        a.click();\n        document.body.removeChild(a);\n        URL.revokeObjectURL(url);\n    }\n}\n/**\n * Unzips entries from a ZIP binary ArrayBuffer using standard DecompressionStream.\n * Zero external npm dependencies.\n */\nexport async function unzipEntries(rawBytes) {\n    const entries = new Map();\n    if (rawBytes.length < 22)\n        return entries;\n    const view = new DataView(rawBytes.buffer, rawBytes.byteOffset, rawBytes.byteLength);\n    // 1. Locate End of Central Directory (EOCD) record (0x06054b50)\n    let eocdOffset = -1;\n    for (let i = rawBytes.length - 22; i >= Math.max(0, rawBytes.length - 65557); i--) {\n        if (view.getUint32(i, true) === 0x06054b50) {\n            eocdOffset = i;\n            break;\n        }\n    }\n    if (eocdOffset >= 0) {\n        const totalEntries = view.getUint16(eocdOffset + 10, true);\n        const cdOffset = view.getUint32(eocdOffset + 16, true);\n        let cdPos = cdOffset;\n        for (let i = 0; i < totalEntries && cdPos + 46 <= rawBytes.length; i++) {\n            if (view.getUint32(cdPos, true) !== 0x02014b50) {\n                break;\n            }\n            const compressionMethod = view.getUint16(cdPos + 10, true);\n            const compressedSize = view.getUint32(cdPos + 20, true);\n            const fileNameLength = view.getUint16(cdPos + 28, true);\n            const extraFieldLength = view.getUint16(cdPos + 30, true);\n            const fileCommentLength = view.getUint16(cdPos + 32, true);\n            const localHeaderOffset = view.getUint32(cdPos + 42, true);\n            const fileNameBytes = rawBytes.subarray(cdPos + 46, cdPos + 46 + fileNameLength);\n            const fileName = new TextDecoder().decode(fileNameBytes);\n            cdPos += 46 + fileNameLength + extraFieldLength + fileCommentLength;\n            // Read data offset from local header\n            if (localHeaderOffset + 30 > rawBytes.length)\n                continue;\n            if (view.getUint32(localHeaderOffset, true) !== 0x04034b50)\n                continue;\n            const localNameLen = view.getUint16(localHeaderOffset + 26, true);\n            const localExtraLen = view.getUint16(localHeaderOffset + 28, true);\n            const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;\n            const dataEnd = dataOffset + compressedSize;\n            if (dataEnd > rawBytes.length)\n                continue;\n            const compressedData = rawBytes.subarray(dataOffset, dataEnd);\n            if (compressionMethod === 0) {\n                // Stored\n                entries.set(fileName, compressedData);\n            }\n            else if (compressionMethod === 8) {\n                // Deflate\n                try {\n                    const stream = new ReadableStream({\n                        start(controller) {\n                            controller.enqueue(compressedData);\n                            controller.close();\n                        }\n                    }).pipeThrough(new DecompressionStream('deflate-raw'));\n                    const uncompressedBuffer = await new Response(stream).arrayBuffer();\n                    entries.set(fileName, new Uint8Array(uncompressedBuffer));\n                }\n                catch (e) {\n                    console.warn(`Failed to decompress ZIP entry \"${fileName}\":`, e);\n                }\n            }\n        }\n        if (entries.size > 0) {\n            return entries;\n        }\n    }\n    // Fallback: Scan sequential local file headers if no valid Central Directory\n    let offset = 0;\n    while (offset + 30 <= rawBytes.length) {\n        const signature = view.getUint32(offset, true);\n        if (signature !== 0x04034b50) {\n            break;\n        }\n        const compressionMethod = view.getUint16(offset + 8, true);\n        const compressedSize = view.getUint32(offset + 18, true);\n        const fileNameLength = view.getUint16(offset + 26, true);\n        const extraFieldLength = view.getUint16(offset + 28, true);\n        const fileNameBytes = rawBytes.subarray(offset + 30, offset + 30 + fileNameLength);\n        const fileName = new TextDecoder().decode(fileNameBytes);\n        const dataOffset = offset + 30 + fileNameLength + extraFieldLength;\n        const dataEnd = dataOffset + compressedSize;\n        if (dataEnd > rawBytes.length || compressedSize === 0) {\n            break;\n        }\n        const compressedData = rawBytes.subarray(dataOffset, dataEnd);\n        if (compressionMethod === 0) {\n            entries.set(fileName, compressedData);\n        }\n        else if (compressionMethod === 8) {\n            try {\n                const stream = new ReadableStream({\n                    start(controller) {\n                        controller.enqueue(compressedData);\n                        controller.close();\n                    }\n                }).pipeThrough(new DecompressionStream('deflate-raw'));\n                const uncompressedBuffer = await new Response(stream).arrayBuffer();\n                entries.set(fileName, new Uint8Array(uncompressedBuffer));\n            }\n            catch (e) {\n                console.warn(`Failed to decompress ZIP entry \"${fileName}\":`, e);\n            }\n        }\n        offset = dataEnd;\n    }\n    return entries;\n}\n//# sourceMappingURL=zip.js.map","import { CacheSyncService } from '@tingdahl/tsutils';\n\nexport type CacheDomain = 'entitlements' | 'channels' | 'tenants';\n\nlet cacheSyncInstance: CacheSyncService<CacheDomain> | null = null;\n\nexport function getCacheSync(): CacheSyncService<CacheDomain> {\n  if (!cacheSyncInstance) {\n    cacheSyncInstance = new CacheSyncService<CacheDomain>({\n      channelName: 'fluentchurch_cache_sync',\n    });\n  }\n  return cacheSyncInstance;\n}\n","import { fetchWithAuth } from '../api-config';\nimport { tenants, channels, users, entitlement } from '../proto/compiled_proto';\nimport { DeviceSessionInfo } from './auth';\nimport {\n  computeEntitlementSummary,\n  fetchTenantEntitlements,\n  TenantEntitlementSummary,\n} from './entitlement_utils';\nimport { getCacheSync } from './cache';\n\nexport type TenantChangedCallback = (tenant: tenants.TenantProtoRow | null) => void;\nexport type ChannelChangedCallback = (channel: channels.ChannelProtoRow | null) => void;\nexport type SessionStatusChangedCallback = (status: any | null) => void;\n\nexport class TenantManager {\n  private userTenants: tenants.TenantProtoRow[] = [];\n  private currentTenant: tenants.TenantProtoRow | null = null;\n  private currentChannels: channels.ChannelProtoRow[] = [];\n  private currentChannel: channels.ChannelProtoRow | null = null;\n  private entitlementSummary: TenantEntitlementSummary | null = null;\n  private entitlementProto: entitlement.EntitlementProto | null = null;\n  private sessionStatus: any | null = null;\n  private getUser: () => users.UserProtoRow | null;\n  private isDeviceSession?: () => boolean;\n  private getDeviceInfo?: () => DeviceSessionInfo | null;\n\n  private onTenantChangedCallbacks: TenantChangedCallback[] = [];\n  private onChannelChangedCallbacks: ChannelChangedCallback[] = [];\n  private onSessionStatusChangedCallbacks: SessionStatusChangedCallback[] = [];\n\n  constructor(\n    getUser: () => users.UserProtoRow | null,\n    isDeviceSession?: () => boolean,\n    getDeviceInfo?: () => DeviceSessionInfo | null\n  ) {\n    this.getUser = getUser;\n    this.isDeviceSession = isDeviceSession;\n    this.getDeviceInfo = getDeviceInfo;\n\n    const cacheSync = getCacheSync();\n    cacheSync.onInvalidate(async (payload) => {\n      if (payload.domain === 'entitlements') {\n        if (!payload.scopeId || payload.scopeId === String(this.currentTenant?.id)) {\n          await this.updateSessionStatus();\n        }\n      } else if (payload.domain === 'channels') {\n        if (!payload.scopeId || payload.scopeId === String(this.currentTenant?.id)) {\n          if (this.currentTenant) {\n            await this.loadChannelsForCurrentTenant();\n            for (const cb of this.onChannelChangedCallbacks) {\n              cb(this.currentChannel);\n            }\n          }\n        }\n      } else if (payload.domain === 'tenants') {\n        await this.loadUserTenants();\n        for (const cb of this.onTenantChangedCallbacks) {\n          cb(this.currentTenant);\n        }\n      }\n    });\n  }\n\n  public onTenantChanged(cb: TenantChangedCallback): void {\n    this.onTenantChangedCallbacks.push(cb);\n  }\n\n  public onChannelChanged(cb: ChannelChangedCallback): void {\n    this.onChannelChangedCallbacks.push(cb);\n  }\n\n  public onSessionStatusChanged(cb: SessionStatusChangedCallback): void {\n    this.onSessionStatusChangedCallbacks.push(cb);\n  }\n\n  public getUserTenants(): tenants.TenantProtoRow[] {\n    return this.userTenants;\n  }\n\n  public getCurrentTenant(): tenants.TenantProtoRow | null {\n    return this.currentTenant;\n  }\n\n  public getChannels(): channels.ChannelProtoRow[] {\n    return this.currentChannels;\n  }\n\n  public getCurrentChannel(): channels.ChannelProtoRow | null {\n    return this.currentChannel;\n  }\n\n  public getEntitlementSummary(): TenantEntitlementSummary | null {\n    return this.entitlementSummary;\n  }\n\n  public getEntitlementProto(): entitlement.EntitlementProto | null {\n    return this.entitlementProto;\n  }\n\n  public getSessionStatus(): any | null {\n    return this.sessionStatus;\n  }\n\n  public async loadUserTenants(): Promise<tenants.TenantProtoRow[]> {\n    if (this.isDeviceSession?.()) {\n      const devInfo = this.getDeviceInfo?.();\n      if (devInfo) {\n        const devTenant = tenants.TenantProtoRow.create({\n          id: devInfo.tenantId,\n          name: devInfo.tenantName,\n        });\n        this.userTenants = [devTenant];\n        this.currentTenant = devTenant;\n        await this.loadChannelsForCurrentTenant();\n        if (devInfo.channelName) {\n          const matched = this.currentChannels.find(\n            c => c.name.toLowerCase() === devInfo.channelName!.toLowerCase()\n          );\n          if (matched) {\n            this.currentChannel = matched;\n          }\n        }\n        return this.userTenants;\n      }\n    }\n\n    try {\n      const resp = await fetchWithAuth('/api/tenants');\n      if (resp.ok) {\n        const buffer = await resp.arrayBuffer();\n        const decoded = tenants.TenantsProto.decode(new Uint8Array(buffer));\n        this.userTenants = (decoded.tenants as tenants.TenantProtoRow[]) || [];\n\n        if (this.currentTenant) {\n          const found = this.userTenants.find(t => String(t.id) === String(this.currentTenant!.id));\n          this.currentTenant = found || (this.userTenants.length > 0 ? this.userTenants[0] : null);\n        } else if (this.userTenants.length > 0) {\n          this.currentTenant = this.userTenants[0];\n        } else {\n          this.currentTenant = null;\n        }\n\n        if (this.currentTenant) {\n          await this.loadChannelsForCurrentTenant();\n        }\n\n        return this.userTenants;\n      }\n    } catch (e) {\n      console.warn('Could not load user tenants:', e);\n      this.userTenants = [];\n    }\n    return [];\n  }\n\n  public async selectTenant(tenantId: string | number | any): Promise<void> {\n    const found = this.userTenants.find(t => String(t.id) === String(tenantId));\n    if (found) {\n      this.currentTenant = found;\n      this.currentChannel = null;\n      await this.loadChannelsForCurrentTenant();\n      await this.updateSessionStatus();\n\n      for (const cb of this.onTenantChangedCallbacks) {\n        cb(this.currentTenant);\n      }\n    }\n  }\n\n  public async updateCurrentTenantName(name: string): Promise<void> {\n    if (this.currentTenant) {\n      this.currentTenant.name = name;\n      const found = this.userTenants.find(t => String(t.id) === String(this.currentTenant!.id));\n      if (found) {\n        found.name = name;\n      }\n      for (const cb of this.onTenantChangedCallbacks) {\n        cb(this.currentTenant);\n      }\n    }\n  }\n\n  public async loadChannelsForCurrentTenant(): Promise<channels.ChannelProtoRow[]> {\n    if (!this.currentTenant) {\n      this.currentChannels = [];\n      this.currentChannel = null;\n      return [];\n    }\n\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${this.currentTenant.id}/channels`);\n      if (resp.ok) {\n        const buffer = await resp.arrayBuffer();\n        const decoded = channels.ChannelsProto.decode(new Uint8Array(buffer));\n        this.currentChannels = (decoded.channels as channels.ChannelProtoRow[]) || [];\n\n        if (this.currentChannel) {\n          const found = this.currentChannels.find(ch => ch.name === this.currentChannel!.name);\n          this.currentChannel = found || (this.currentChannels.length > 0 ? this.currentChannels[0] : null);\n        } else if (this.currentChannels.length > 0) {\n          this.currentChannel = this.currentChannels[0];\n        } else {\n          this.currentChannel = null;\n        }\n\n        return this.currentChannels;\n      }\n    } catch (e) {\n      console.warn('Could not load channels:', e);\n      this.currentChannels = [];\n      this.currentChannel = null;\n    }\n    return [];\n  }\n\n  public selectChannel(channelName: string): void {\n    const found = this.currentChannels.find(ch => ch.name === channelName);\n    if (found) {\n      this.currentChannel = found;\n      this.updateSessionStatus();\n      for (const cb of this.onChannelChangedCallbacks) {\n        cb(this.currentChannel);\n      }\n    }\n  }\n\n  public async updateSessionStatus(): Promise<any | null> {\n    if (!this.currentTenant) {\n      this.sessionStatus = null;\n      this.entitlementSummary = null;\n      this.entitlementProto = null;\n      return null;\n    }\n\n    try {\n      const protoDoc = await fetchTenantEntitlements(this.currentTenant.id);\n      this.entitlementProto = protoDoc;\n      const summary = computeEntitlementSummary(protoDoc);\n      this.entitlementSummary = summary;\n\n      const chName = this.currentChannel?.name;\n      const activeSession = chName ? summary.activeSessions.get(chName) : undefined;\n\n      this.sessionStatus = {\n        tenantBalanceSeconds: summary.totalBalanceSeconds,\n        effectiveAvailableSeconds: summary.availableSeconds,\n        subscriptionValid: summary.isSubscribed,\n        subscribedUntilUnixMs: summary.subscribedUntilUnixMs,\n        hasActiveSession: activeSession != null,\n        sessionId: activeSession?.sessionId || '',\n        remainingSeconds: activeSession?.remainingSeconds || 0,\n      };\n\n      for (const cb of this.onSessionStatusChangedCallbacks) {\n        cb(this.sessionStatus);\n      }\n      return this.sessionStatus;\n    } catch (e) {\n      console.warn('Could not update entitlement summary:', e);\n    }\n    return null;\n  }\n\n  public isTenantAdmin(): boolean {\n    const user = this.getUser();\n    if (!user || !this.currentTenant || !user.tenantRoles) return false;\n    const role = user.tenantRoles[String(this.currentTenant.id)];\n    return role === users.Role.ROLE_ADMIN;\n  }\n\n  public isBroadcaster(): boolean {\n    if (this.isDeviceSession?.()) {\n      return true;\n    }\n    const user = this.getUser();\n    if (!user || !this.currentTenant || !user.tenantRoles) return false;\n    const role = user.tenantRoles[String(this.currentTenant.id)];\n    return role === users.Role.ROLE_ADMIN || role === users.Role.ROLE_BROADCASTER;\n  }\n}\n","import { PanelHandler, PageManager } from '@tingdahl/tsutils/ui';\nimport { AuthManager } from './auth';\nimport { TenantManager } from './tenant_manager';\nimport {\n  EVENT_CLICK,\n  EVENT_CHANGE,\n  DISPLAY_NONE,\n  DISPLAY_FLEX,\n  DISPLAY_INLINE_BLOCK,\n  CLASS_DANGER,\n  CLASS_EXPIRED,\n} from './strings';\n\nexport class HeaderHandler extends PanelHandler {\n  private authManager: AuthManager;\n  private tenantManager: TenantManager;\n  private pageManager: PageManager;\n\n  constructor(authManager: AuthManager, tenantManager: TenantManager, pageManager: PageManager) {\n    super('main-header');\n    this.authManager = authManager;\n    this.tenantManager = tenantManager;\n    this.pageManager = pageManager;\n\n    this.tenantManager.onSessionStatusChanged(() => {\n      this.updateLeaseStatsDisplay();\n    });\n  }\n\n  async init(): Promise<void> {\n    await super.init();\n\n    const switchTenantBtn = document.getElementById('header-switch-tenant-btn');\n    if (switchTenantBtn) {\n      switchTenantBtn.addEventListener(EVENT_CLICK, async () => {\n        await this.pageManager.showPanel('new-tenant-panel');\n      });\n    }\n\n    const tenantSelect = document.getElementById('tenant-select') as HTMLSelectElement | null;\n    if (tenantSelect) {\n      tenantSelect.addEventListener(EVENT_CHANGE, async () => {\n        const val = tenantSelect.value;\n        if (val === '__NEW_TENANT__') {\n          await this.pageManager.showPanel('new-tenant-panel');\n        } else if (val) {\n          await this.tenantManager.selectTenant(val);\n          await this.refresh();\n        }\n      });\n    }\n\n    const logoutBtn = document.getElementById('header-logout-btn');\n    if (logoutBtn) {\n      logoutBtn.addEventListener(EVENT_CLICK, () => {\n        this.authManager.logout();\n      });\n    }\n  }\n\n  async refresh(): Promise<void> {\n    await super.refresh();\n\n    const user = this.authManager.getUser();\n    const isDevice = this.authManager.isDeviceSession();\n    const deviceInfo = this.authManager.getDeviceInfo();\n    const userBadgeEl = document.getElementById('header-user-badge');\n    const userNameEl = document.getElementById('header-user-name');\n    const userRoleEl = document.getElementById('header-user-role');\n    const tenantGroupEl = document.getElementById('header-tenant-group');\n    const tenantNameEl = document.getElementById('header-tenant-name');\n    const switchTenantBtn = document.getElementById('header-switch-tenant-btn');\n    const tenantSelect = document.getElementById('tenant-select') as HTMLSelectElement | null;\n    const statsGroupEl = document.getElementById('header-session-stats') || document.getElementById('header-lease-stats');\n    const logoutBtn = document.getElementById('header-logout-btn');\n\n    if (user || isDevice) {\n      if (userBadgeEl) userBadgeEl.style.display = DISPLAY_FLEX;\n      if (userNameEl) {\n        userNameEl.textContent = isDevice\n          ? (deviceInfo?.label || 'Booth Device')\n          : (user?.name || user?.email || 'User');\n      }\n      if (logoutBtn) {\n        logoutBtn.style.display = DISPLAY_INLINE_BLOCK;\n        logoutBtn.textContent = isDevice ? 'Disconnect' : 'Log out';\n      }\n\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      const userTenants = this.tenantManager.getUserTenants();\n\n      if (currentTenant && userTenants.length > 0) {\n        if (tenantGroupEl) tenantGroupEl.style.display = DISPLAY_FLEX;\n        if (statsGroupEl) statsGroupEl.style.display = DISPLAY_FLEX;\n\n        if (tenantNameEl) {\n          tenantNameEl.textContent = currentTenant.name;\n        }\n\n        if (switchTenantBtn) {\n          switchTenantBtn.style.display = (!isDevice && userTenants.length > 1) ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n        }\n\n        if (tenantSelect) {\n          if (isDevice) {\n            tenantSelect.innerHTML = `<option value=\"${currentTenant.id}\" selected>${currentTenant.name}</option>`;\n            tenantSelect.disabled = true;\n          } else {\n            tenantSelect.disabled = false;\n            tenantSelect.innerHTML = `\n              ${userTenants.map(t => `<option value=\"${t.id}\" ${String(t.id) === String(currentTenant.id) ? 'selected' : ''}>${t.name}</option>`).join('')}\n              <option value=\"__NEW_TENANT__\" class=\"text-primary-var\">+ Create New Organization</option>\n            `;\n          }\n        }\n\n        if (userRoleEl) {\n          userRoleEl.style.display = DISPLAY_INLINE_BLOCK;\n          userRoleEl.textContent = isDevice ? 'Booth Device' : (this.tenantManager.isTenantAdmin() ? 'Admin' : 'Broadcaster');\n        }\n\n        this.updateLeaseStatsDisplay();\n      } else {\n        if (tenantGroupEl) tenantGroupEl.style.display = DISPLAY_NONE;\n        if (statsGroupEl) statsGroupEl.style.display = DISPLAY_NONE;\n        if (userRoleEl) userRoleEl.style.display = DISPLAY_NONE;\n      }\n\n    } else {\n      if (userBadgeEl) userBadgeEl.style.display = DISPLAY_NONE;\n      if (tenantGroupEl) tenantGroupEl.style.display = DISPLAY_NONE;\n      if (statsGroupEl) statsGroupEl.style.display = DISPLAY_NONE;\n      if (logoutBtn) logoutBtn.style.display = DISPLAY_NONE;\n    }\n  }\n\n  private updateLeaseStatsDisplay(): void {\n    const balanceEl = document.getElementById('header-stats-balance') || document.getElementById('header-stats-minutes');\n    const balancePill = document.getElementById('header-balance-pill');\n    const subscribedEl = document.getElementById('header-stats-subscribed');\n    const subscriptionPill = document.getElementById('header-subscription-pill');\n\n    const status = this.tenantManager.getSessionStatus();\n    if (!status) {\n      if (balanceEl) balanceEl.textContent = '--:--';\n      if (subscribedEl) subscribedEl.textContent = '--';\n      return;\n    }\n\n    const toNum = (v: any): number => {\n      if (v === null || v === undefined) return 0;\n      if (typeof v === 'number') return v;\n      if (typeof v === 'object') {\n        if (typeof v.toNumber === 'function') return v.toNumber();\n        if (typeof v.low === 'number') return v.low + (v.high || 0) * 4294967296;\n      }\n      return Number(v) || 0;\n    };\n\n    const availSec = Math.max(0, toNum(status.effectiveAvailableSeconds));\n    const hours = Math.floor(availSec / 3600);\n    const mins = Math.floor((availSec % 3600) / 60);\n    const formattedBalance = `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;\n\n    if (balanceEl) {\n      balanceEl.textContent = formattedBalance;\n    }\n\n    if (balancePill) {\n      if (availSec < 1800) {\n        balancePill.classList.add(CLASS_DANGER);\n      } else {\n        balancePill.classList.remove(CLASS_DANGER);\n      }\n    }\n\n    const subscribedUntilMs = toNum((status as any).subscribedUntilUnixMs);\n    const now = Date.now();\n    const isSubValid = Boolean(status.subscriptionValid) && (subscribedUntilMs === 0 || subscribedUntilMs > now);\n\n    if (subscribedEl) {\n      if (subscribedUntilMs > 0) {\n        const d = new Date(subscribedUntilMs);\n        subscribedEl.textContent = d.toLocaleDateString(undefined, {\n          month: 'short',\n          day: 'numeric',\n          year: 'numeric',\n        });\n      } else {\n        subscribedEl.textContent = isSubValid ? 'Active' : 'Expired';\n      }\n    }\n\n    if (subscriptionPill) {\n      if (!isSubValid && subscribedUntilMs > 0) {\n        subscriptionPill.classList.add(CLASS_EXPIRED);\n      } else {\n        subscriptionPill.classList.remove(CLASS_EXPIRED);\n      }\n    }\n  }\n}\n","import { VADTelemetry, VADState } from '../types';\nimport vadProcessorUrl from '../worklet/vad-processor.ts?worker&url';\n\nexport interface AudioManagerCallbacks {\n  onTelemetry?: (telemetry: VADTelemetry) => void;\n  onStateChange?: (state: VADState) => void;\n  onAudioData?: (data: ArrayBuffer, isBurst: boolean) => void;\n  onError?: (err: Error) => void;\n}\n\nexport class AudioManager {\n  private audioContext: AudioContext | null = null;\n  private mediaStream: MediaStream | null = null;\n  private sourceNode: MediaStreamAudioSourceNode | null = null;\n  private vadNode: AudioWorkletNode | null = null;\n  private callbacks: AudioManagerCallbacks;\n\n  public thresholdDb: number = -42;\n  public attackMs: number = 300;\n  public releaseMs: number = 5000;\n  public forceTranscription: boolean = false;\n  public currentVADState: VADState = 'MUTED';\n\n  get isRunning(): boolean {\n    return this.mediaStream !== null && this.mediaStream.active;\n  }\n\n  constructor(callbacks: AudioManagerCallbacks = {}) {\n    this.callbacks = callbacks;\n  }\n\n  static async getAudioDevices(): Promise<MediaDeviceInfo[]> {\n    try {\n      // Prompt for temporary permissions if device list lacks labels\n      const devices = await navigator.mediaDevices.enumerateDevices();\n      return devices.filter((d) => d.kind === 'audioinput');\n    } catch (e) {\n      console.error('Failed to enumerate audio devices:', e);\n      return [];\n    }\n  }\n\n  async start(deviceId?: string): Promise<void> {\n    await this.stop();\n\n    try {\n      const constraints: MediaStreamConstraints = {\n        audio: {\n          deviceId: deviceId ? { exact: deviceId } : undefined,\n          echoCancellation: false,\n          noiseSuppression: false,\n          autoGainControl: false,\n          channelCount: 1,\n        },\n        video: false,\n      };\n\n      this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);\n      this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 48000 });\n\n      if (this.audioContext.state === 'suspended') {\n        await this.audioContext.resume();\n      }\n\n      // Load VAD processor worklet (compiled by Vite with content checksum hash)\n      await this.audioContext.audioWorklet.addModule(vadProcessorUrl);\n\n      this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream);\n      this.vadNode = new AudioWorkletNode(this.audioContext, 'vad-processor', {\n        processorOptions: {\n          targetSampleRate: 16000,\n          thresholdDb: this.thresholdDb,\n          attackMs: this.attackMs,\n          releaseMs: this.releaseMs,\n          forceTranscription: this.forceTranscription,\n        },\n      });\n\n      this.vadNode.port.onmessage = (event: MessageEvent) => {\n        const msg = event.data;\n        if (!msg) return;\n\n        if (msg.type === 'meter') {\n          if (msg.state) {\n            this.currentVADState = msg.state;\n          }\n          this.callbacks.onTelemetry?.({\n            rms: msg.rms,\n            db: msg.db,\n            state: msg.state,\n          });\n        } else if (msg.type === 'state') {\n          this.currentVADState = msg.state;\n          this.callbacks.onStateChange?.(msg.state);\n        } else if (msg.type === 'burst') {\n          this.callbacks.onAudioData?.(msg.buffer, true);\n        } else if (msg.type === 'audio') {\n          this.callbacks.onAudioData?.(msg.buffer, false);\n        }\n      };\n\n      // Connect source to worklet (do NOT connect to destination to avoid speaker feedback)\n      this.sourceNode.connect(this.vadNode);\n    } catch (err: any) {\n      this.callbacks.onError?.(err);\n      throw err;\n    }\n  }\n\n  setThreshold(db: number): void {\n    this.thresholdDb = db;\n    if (this.vadNode) {\n      this.vadNode.port.postMessage({ type: 'setThreshold', thresholdDb: db });\n    }\n  }\n\n  setAttack(ms: number): void {\n    this.attackMs = ms;\n    if (this.vadNode) {\n      this.vadNode.port.postMessage({ type: 'setAttack', attackMs: ms });\n    }\n  }\n\n  setHold(ms: number): void {\n    this.releaseMs = ms;\n    if (this.vadNode) {\n      this.vadNode.port.postMessage({ type: 'setHold', holdMs: ms, releaseMs: ms });\n    }\n  }\n\n  setRelease(ms: number): void {\n    this.setHold(ms);\n  }\n\n  setForceTranscription(force: boolean): void {\n    this.forceTranscription = force;\n    if (this.vadNode) {\n      this.vadNode.port.postMessage({ type: 'setForceTranscription', forceTranscription: force });\n    }\n  }\n\n  async stop(): Promise<void> {\n    if (this.vadNode) {\n      this.vadNode.disconnect();\n      this.vadNode = null;\n    }\n    if (this.sourceNode) {\n      this.sourceNode.disconnect();\n      this.sourceNode = null;\n    }\n    if (this.mediaStream) {\n      this.mediaStream.getTracks().forEach((t) => t.stop());\n      this.mediaStream = null;\n    }\n    if (this.audioContext) {\n      await this.audioContext.close();\n      this.audioContext = null;\n    }\n    this.currentVADState = 'MUTED';\n  }\n}\n","import { DeepgramResult } from '../types';\nimport { WS_SUBPROTOCOL_TOKEN } from '../core/strings';\nimport { getClientContinent } from '../api-config';\n\n/**\n * Resolves the Deepgram regional hostname based on Cloudflare continent code (ip.src.continent).\n * Reference: https://developers.deepgram.com/reference/regional-endpoints\n * - EU (Europe), AF (Africa): api.eu.deepgram.com\n * - AS (Asia): api.in.deepgram.com (India)\n * - OC (Oceania): api.au.deepgram.com (Australia)\n * - Default / other: api.deepgram.com\n */\nexport function getDeepgramHost(continent?: string | null): string {\n  if (!continent) {\n    return 'api.deepgram.com';\n  }\n  switch (continent.trim().toUpperCase()) {\n    case 'EU':\n    case 'AF':\n      return 'api.eu.deepgram.com';\n    case 'AS':\n      return 'api.in.deepgram.com';\n    case 'OC':\n      return 'api.au.deepgram.com';\n    default:\n      return 'api.deepgram.com';\n  }\n}\n\nexport interface DeepgramStreamerCallbacks {\n  onTranscript?: (text: string, isFinal: boolean, speechFinal: boolean, confidence: number) => void;\n  onUtteranceEnd?: () => void;\n  onStatusChange?: (status: 'connected' | 'connecting' | 'disconnected' | 'error', error?: string) => void;\n  onBillingUpdate?: (activeSeconds: number, savedSeconds: number) => void;\n}\n\nexport class DeepgramStreamer {\n  private ws: WebSocket | null = null;\n  private apiKey: string = '';\n  private keepAliveTimer: number | null = null;\n  private callbacks: DeepgramStreamerCallbacks;\n\n  public isConnected: boolean = false;\n  public isMuted: boolean = true;\n  public isAudiencePaused: boolean = false;\n\n  // Billing & savings tracking\n  public activeSeconds: number = 0;\n  public savedSeconds: number = 0;\n  private statsTimer: number | null = null;\n\n  public language: string = 'en-US';\n  private forceTranscription: boolean = false;\n\n  constructor(callbacks: DeepgramStreamerCallbacks = {}) {\n    this.callbacks = callbacks;\n  }\n\n  setForceTranscription(force: boolean): void {\n    this.forceTranscription = force;\n    if (force) {\n      this.stopKeepAlive();\n    } else if (this.isMuted || this.isAudiencePaused) {\n      this.startKeepAlive();\n    }\n  }\n\n  async connect(\n    apiKey: string,\n    language: string = 'en-US',\n    sessionId?: string,\n    tag?: string,\n    continent?: string\n  ): Promise<void> {\n    this.disconnect();\n    this.apiKey = apiKey;\n    this.language = language;\n\n    this.callbacks.onStatusChange?.('connecting');\n\n    const params = new URLSearchParams({\n      model: 'nova-3',\n      language: this.language,\n      encoding: 'linear16',\n      sample_rate: '16000',\n      channels: '1',\n      interim_results: 'true',\n      smart_format: 'true',\n      endpointing: '300',\n      utterance_end_ms: '1000',\n      vad_events: 'true',\n    });\n\n    if (tag) {\n      params.set('tag', tag);\n    } else if (sessionId) {\n      params.set('tag', sessionId.startsWith('session:') ? sessionId : `session:${sessionId}`);\n    }\n\n    const resolvedContinent = continent || getClientContinent();\n    const host = getDeepgramHost(resolvedContinent);\n    const wsUrl = `wss://${host}/v1/listen?${params.toString()}`;\n\n    return new Promise((resolve, reject) => {\n      try {\n        // Deepgram accepts subprotocol: ['token', apiKey]\n        this.ws = new WebSocket(wsUrl, [WS_SUBPROTOCOL_TOKEN, this.apiKey]);\n        this.ws.binaryType = 'arraybuffer';\n\n        this.ws.onopen = () => {\n          this.isConnected = true;\n          this.callbacks.onStatusChange?.('connected');\n          this.startStatsTimer();\n          if (this.forceTranscription) {\n            this.stopKeepAlive();\n          } else if (this.isMuted || this.isAudiencePaused) {\n            this.startKeepAlive();\n          }\n          resolve();\n        };\n\n        this.ws.onmessage = (event: MessageEvent) => {\n          try {\n            const data: DeepgramResult = JSON.parse(event.data);\n            if (data.type === 'UtteranceEnd') {\n              this.callbacks.onUtteranceEnd?.();\n              return;\n            }\n            if (data.type === 'Results' && data.channel?.alternatives?.length) {\n              const alt = data.channel.alternatives[0];\n              const transcript = alt.transcript?.trim() || '';\n              if (transcript) {\n                const isFinal = Boolean(data.is_final);\n                const speechFinal = Boolean(data.speech_final);\n                const confidence = alt.confidence || 0.99;\n                this.callbacks.onTranscript?.(transcript, isFinal, speechFinal, confidence);\n              }\n            }\n          } catch (err) {\n            console.error('Error parsing Deepgram message:', err);\n          }\n        };\n\n        this.ws.onerror = (err) => {\n          console.error('Deepgram WebSocket error:', err);\n          this.callbacks.onStatusChange?.('error', 'WebSocket connection error');\n          reject(err);\n        };\n\n        this.ws.onclose = (event) => {\n          this.isConnected = false;\n          this.stopKeepAlive();\n          this.stopStatsTimer();\n          this.callbacks.onStatusChange?.('disconnected', `Closed (code ${event.code})`);\n        };\n      } catch (err: any) {\n        this.callbacks.onStatusChange?.('error', err.message);\n        reject(err);\n      }\n    });\n  }\n\n  /**\n   * Called by AudioManager when VAD transitions between ACTIVE and MUTED.\n   */\n  setVADState(state: 'ACTIVE' | 'MUTED'): void {\n    if (state === 'MUTED') {\n      this.isMuted = true;\n      if (!this.forceTranscription) {\n        this.startKeepAlive();\n      }\n    } else {\n      this.isMuted = false;\n      if (!this.isAudiencePaused || this.forceTranscription) {\n        this.stopKeepAlive();\n      }\n    }\n  }\n\n  /**\n   * Called by BroadcasterView when audience headcount is 0 (Option A).\n   */\n  setAudiencePaused(paused: boolean): void {\n    this.isAudiencePaused = paused;\n    if (paused && !this.forceTranscription) {\n      this.startKeepAlive();\n    } else if (!this.isMuted || this.forceTranscription) {\n      this.stopKeepAlive();\n    }\n  }\n\n  /**\n   * Send binary PCM audio frame to Deepgram.\n   * Sends whenever forceTranscription is active, OR when speech is active AND audience is present.\n   */\n  sendAudio(data: ArrayBuffer): void {\n    if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {\n      return;\n    }\n    if (this.forceTranscription || (!this.isMuted && !this.isAudiencePaused)) {\n      this.ws.send(data);\n    }\n  }\n\n  private startKeepAlive(): void {\n    if (this.keepAliveTimer !== null) return;\n\n    // Send immediately on entering silence or audience pause\n    this.sendKeepAlivePing();\n\n    // Repeat every 5 seconds to keep Deepgram socket warm without billing audio\n    this.keepAliveTimer = window.setInterval(() => {\n      this.sendKeepAlivePing();\n    }, 5000);\n  }\n\n  private stopKeepAlive(): void {\n    if (this.keepAliveTimer !== null) {\n      clearInterval(this.keepAliveTimer);\n      this.keepAliveTimer = null;\n    }\n  }\n\n  private sendKeepAlivePing(): void {\n    if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n      this.ws.send(JSON.stringify({ type: 'KeepAlive' }));\n    }\n  }\n\n  private startStatsTimer(): void {\n    this.stopStatsTimer();\n    this.statsTimer = window.setInterval(() => {\n      if (this.isConnected) {\n        if (this.isMuted || this.isAudiencePaused) {\n          this.savedSeconds += 1;\n        } else {\n          this.activeSeconds += 1;\n        }\n        this.callbacks.onBillingUpdate?.(this.activeSeconds, this.savedSeconds);\n      }\n    }, 1000);\n  }\n\n  private stopStatsTimer(): void {\n    if (this.statsTimer !== null) {\n      clearInterval(this.statsTimer);\n      this.statsTimer = null;\n    }\n  }\n\n  disconnect(): void {\n    this.stopKeepAlive();\n    this.stopStatsTimer();\n    if (this.ws) {\n      if (this.ws.readyState === WebSocket.OPEN) {\n        // Orderly close\n        this.ws.send(JSON.stringify({ type: 'CloseStream' }));\n        this.ws.close();\n      }\n      this.ws = null;\n    }\n    this.isConnected = false;\n  }\n}\n","import { fetchWithAuth, getWorkerUrl, normalizeWorkerUrl } from '../api-config';\nimport { tokens } from '../proto/compiled_proto.js';\nimport {\n  EVENT_OPEN,\n  EVENT_MESSAGE,\n  EVENT_CLOSE,\n  EVENT_ERROR,\n  PARAM_TOKEN,\n} from '../core/strings';\n\nexport interface WsPublisherCallbacks {\n  onStatusChange?: (status: 'connecting' | 'connected' | 'error' | 'disconnected', message?: string) => void;\n  onOccupancyChange?: (subscribers: number, byLang?: Record<string, number>) => void;\n  onError?: (err: Error) => void;\n}\n\nexport class WsPublisher {\n  private ws: WebSocket | null = null;\n  private channelName: string = '';\n  private callbacks: WsPublisherCallbacks;\n  private isExplicitDisconnect: boolean = false;\n  private reconnectAttempts: number = 0;\n  private reconnectTimer: number | null = null;\n\n  constructor(callbacks: WsPublisherCallbacks = {}) {\n    this.callbacks = callbacks;\n  }\n\n  async connect(channelName: string): Promise<void> {\n    this.disconnect();\n    this.channelName = channelName;\n    this.isExplicitDisconnect = false;\n    this.reconnectAttempts = 0;\n\n    await this.establishConnection();\n  }\n\n  private async establishConnection(): Promise<void> {\n    this.callbacks.onStatusChange?.('connecting');\n\n    try {\n      const tokenResp = await fetchWithAuth(\n        `/api/channels/${encodeURIComponent(this.channelName)}/token/worker?role=broadcaster`\n      );\n      if (!tokenResp.ok) {\n        throw new Error(`Failed to obtain broadcaster token (HTTP ${tokenResp.status})`);\n      }\n\n      const buffer = await tokenResp.arrayBuffer();\n      const connData = tokens.WorkerConnectionProto.decode(new Uint8Array(buffer));\n\n      let rawBase = getWorkerUrl() || connData.workerUrl || window.location.origin;\n      let workerBase = normalizeWorkerUrl(rawBase);\n      let wsBase = workerBase.replace(/^http/i, 'ws');\n      if (!wsBase.startsWith('ws://') && !wsBase.startsWith('wss://')) {\n        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n        wsBase = `${protocol}//${wsBase}`;\n      }\n      if (window.location.protocol === 'https:' && wsBase.startsWith('ws://')) {\n        wsBase = wsBase.replace(/^ws:\\/\\//i, 'wss://');\n      }\n\n      const wsUrl = `${wsBase.replace(/\\/+$/, '')}/ws/broadcast/${encodeURIComponent(this.channelName)}?${PARAM_TOKEN}=${encodeURIComponent(connData.token || '')}`;\n\n      const socket = new WebSocket(wsUrl);\n      this.ws = socket;\n\n      socket.addEventListener(EVENT_OPEN, () => {\n        if (this.ws !== socket) return;\n        this.reconnectAttempts = 0;\n        this.callbacks.onStatusChange?.('connected');\n      });\n\n      socket.addEventListener(EVENT_MESSAGE, (event) => {\n        if (this.ws !== socket) return;\n        try {\n          const data = JSON.parse(event.data);\n          if (data.type === 'occupancy') {\n            this.callbacks.onOccupancyChange?.(data.total ?? 0, data.byLang);\n          }\n        } catch (err) {\n          console.warn('[WsPublisher] Failed to parse message from worker:', err);\n        }\n      });\n\n      socket.addEventListener(EVENT_CLOSE, (event) => {\n        if (this.ws !== socket) return;\n        this.ws = null;\n        if (!this.isExplicitDisconnect) {\n          this.callbacks.onStatusChange?.('disconnected', `Connection closed (${event.code})`);\n          this.scheduleReconnect();\n        } else {\n          this.callbacks.onStatusChange?.('disconnected');\n        }\n      });\n\n      socket.addEventListener(EVENT_ERROR, () => {\n        if (this.ws !== socket) return;\n        const err = new Error('WebSocket connection error');\n        this.callbacks.onError?.(err);\n        this.callbacks.onStatusChange?.('error', 'WebSocket error');\n      });\n    } catch (err: any) {\n      this.callbacks.onError?.(err);\n      this.callbacks.onStatusChange?.('error', err.message);\n      if (!this.isExplicitDisconnect) {\n        this.scheduleReconnect();\n      }\n      throw err;\n    }\n  }\n\n  private scheduleReconnect(): void {\n    if (this.isExplicitDisconnect || this.reconnectTimer !== null) return;\n\n    const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 15000);\n    this.reconnectAttempts++;\n\n    this.reconnectTimer = window.setTimeout(async () => {\n      this.reconnectTimer = null;\n      if (!this.isExplicitDisconnect) {\n        try {\n          await this.establishConnection();\n        } catch {\n          // Reconnect attempt handled internally\n        }\n      }\n    }, delay);\n  }\n\n  async publish(\n    text: string,\n    isFinal: boolean,\n    speechFinal: boolean,\n    confidence: number = 0.99,\n    lang?: string\n  ): Promise<void> {\n    return this.publishCaption(text, isFinal, speechFinal, confidence, lang);\n  }\n\n  async publishCaption(\n    text: string,\n    isFinal: boolean,\n    speechFinal: boolean,\n    confidence: number = 0.99,\n    lang?: string\n  ): Promise<void> {\n    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n      console.warn('Cannot publish caption: WebSocket is not open');\n      return;\n    }\n\n    const event = {\n      type: 'caption',\n      id: `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`,\n      channelName: this.channelName,\n      text,\n      isFinal,\n      speechFinal,\n      confidence,\n      timestamp: Date.now(),\n      lang,\n    };\n\n    this.ws.send(JSON.stringify(event));\n  }\n\n  publishUtteranceEnd(): void {\n    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n      return;\n    }\n\n    const event = {\n      type: 'utterance_end',\n      channelName: this.channelName,\n      timestamp: Date.now(),\n    };\n\n    this.ws.send(JSON.stringify(event));\n  }\n\n  disconnect(): void {\n    this.isExplicitDisconnect = true;\n    if (this.reconnectTimer !== null) {\n      clearTimeout(this.reconnectTimer);\n      this.reconnectTimer = null;\n    }\n    if (this.ws) {\n      try {\n        this.ws.close(1000, 'Broadcaster disconnected');\n      } catch {\n        // Ignore\n      }\n      this.ws = null;\n    }\n  }\n}\n","import { apiUrl } from '../api-config';\nimport { tokens } from '../proto/compiled_proto.js';\n\nexport interface LanguageOption {\n  code: string;\n  name: string;\n  nativeName?: string;\n}\n\n/**\n * Fetches supported languages dynamically from the backend Deepgram API endpoint via binary Protobuf.\n * Throws an error if the API is down or unconfigured - no static fallback is used.\n */\nexport async function fetchSupportedLanguages(model: string = 'nova-3'): Promise<LanguageOption[]> {\n  const res = await fetch(apiUrl(`/api/languages?model=${encodeURIComponent(model)}`));\n  if (!res.ok) {\n    const errorBody = await res.text().catch(() => '');\n    throw new Error(`Failed to load languages from Deepgram API (HTTP ${res.status}): ${errorBody}`);\n  }\n  const buffer = await res.arrayBuffer();\n  const supported = tokens.SupportedLanguagesProto.decode(new Uint8Array(buffer));\n  const data: LanguageOption[] = (supported.languages || []).map((l) => ({\n    code: l.code || '',\n    name: l.name || '',\n    nativeName: l.nativeName || undefined,\n  }));\n  if (data.length === 0) {\n    throw new Error('Deepgram API returned an empty language list');\n  }\n  return data;\n}\n\nconst TIMEZONE_LANGUAGE_HINTS: Record<string, string> = {\n  'Europe/Stockholm': 'sv',\n  'Europe/Oslo': 'no',\n  'Europe/Copenhagen': 'da',\n  'Europe/Helsinki': 'fi',\n  'Europe/Berlin': 'de',\n  'Europe/Vienna': 'de',\n  'Europe/Zurich': 'de',\n  'Europe/Paris': 'fr',\n  'Europe/Rome': 'it',\n  'Europe/Madrid': 'es',\n  'Europe/Lisbon': 'pt',\n  'Europe/Amsterdam': 'nl',\n  'Europe/Brussels': 'fr',\n  'Europe/Warsaw': 'pl',\n  'Europe/Prague': 'cs',\n  'Europe/Budapest': 'hu',\n  'Europe/Athens': 'el',\n  'Europe/Bucharest': 'ro',\n  'Europe/Kyiv': 'uk',\n  'Europe/London': 'en-GB',\n  'Europe/Dublin': 'en',\n  'America/Sao_Paulo': 'pt-BR',\n  'America/Buenos_Aires': 'es',\n  'America/Santiago': 'es',\n  'America/Bogota': 'es',\n  'America/Lima': 'es',\n  'America/Mexico_City': 'es',\n  'Asia/Tokyo': 'ja',\n  'Asia/Seoul': 'ko',\n};\n\n/**\n * Detects the user's language based on browser environment (languages, locale, timezone).\n * If no matching language is detected from the environment, falls back to \"multi\" (Multilingual).\n */\nexport function detectDefaultLanguage(options: LanguageOption[]): LanguageOption | null {\n  if (!options || options.length === 0) return null;\n\n  const userLocales: string[] = [];\n  if (typeof navigator !== 'undefined') {\n    if (navigator.languages && navigator.languages.length > 0) {\n      userLocales.push(...navigator.languages);\n    }\n    if (navigator.language) {\n      userLocales.push(navigator.language);\n    }\n  }\n\n  for (const locale of userLocales) {\n    const cleanLocale = locale.toLowerCase().replace('_', '-');\n\n    // 1. Exact match (e.g. \"en-us\", \"sv-se\", \"pt-br\")\n    const exact = options.find((l) => l.code.toLowerCase() === cleanLocale);\n    if (exact) return exact;\n\n    // 2. Primary language subtag match (e.g. \"sv-SE\" -> \"sv\", \"de-DE\" -> \"de\")\n    const primary = cleanLocale.split('-')[0];\n    const primaryMatch = options.find((l) => {\n      const codePrimary = l.code.toLowerCase().split('-')[0];\n      return codePrimary === primary;\n    });\n    if (primaryMatch) return primaryMatch;\n  }\n\n  // 3. Try to infer from environment location (time zone)\n  try {\n    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n    if (tz && TIMEZONE_LANGUAGE_HINTS[tz]) {\n      const hint = TIMEZONE_LANGUAGE_HINTS[tz].toLowerCase();\n      const tzMatch = options.find(\n        (l) => l.code.toLowerCase() === hint || l.code.toLowerCase().split('-')[0] === hint.split('-')[0]\n      );\n      if (tzMatch) return tzMatch;\n    }\n  } catch {\n    // Ignore timezone detection errors\n  }\n\n  // 4. If environment cannot determine a language, default to \"multi\" (Multilingual)\n  const multi = options.find((l) => l.code.toLowerCase() === 'multi');\n  if (multi) return multi;\n\n  return options[0] || null;\n}\n","import { LanguageOption, detectDefaultLanguage, fetchSupportedLanguages } from './languages';\nimport {\n  EVENT_FOCUS,\n  EVENT_CLICK,\n  EVENT_INPUT,\n  EVENT_KEYDOWN,\n  EVENT_MOUSEENTER,\n  KEY_ARROW_DOWN,\n  KEY_ARROW_UP,\n  KEY_ENTER,\n  KEY_ESCAPE,\n  DISPLAY_BLOCK,\n  DISPLAY_NONE,\n  STORAGE_BROADCAST_LANGUAGE,\n} from '../core/strings';\n\nfunction getSavedLanguage(key: string): string | null {\n  try {\n    const val = localStorage.getItem(key);\n    if (val) return val;\n  } catch {}\n\n  try {\n    if (typeof document !== 'undefined' && document.cookie) {\n      const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]*)`));\n      if (match) return decodeURIComponent(match[1]);\n    }\n  } catch {}\n\n  return null;\n}\n\nfunction saveLanguage(key: string, code: string): void {\n  try {\n    localStorage.setItem(key, code);\n  } catch {}\n\n  try {\n    if (typeof document !== 'undefined') {\n      document.cookie = `${key}=${encodeURIComponent(code)}; path=/; max-age=31536000; SameSite=Lax`;\n    }\n  } catch {}\n}\n\nexport class LanguageSelector {\n  private container: HTMLElement;\n  private selectedLanguage: LanguageOption | null = null;\n  private onSelectCallback: (lang: LanguageOption) => void;\n  private availableLanguages: LanguageOption[] = [];\n\n  private inputEl!: HTMLInputElement;\n  private dropdownEl!: HTMLDivElement;\n  private isOpen: boolean = false;\n  private highlightedIndex: number = -1;\n  private filteredLanguages: LanguageOption[] = [];\n  private userManuallyChanged: boolean = false;\n  private isLoading: boolean = true;\n  private errorMessage: string = '';\n  private storageKey: string;\n\n  constructor(container: HTMLElement, onSelect: (lang: LanguageOption) => void, channelName?: string) {\n    this.container = container;\n    this.onSelectCallback = onSelect;\n    const channelClean = channelName ? channelName.replace(/^@+/, '') : '';\n    this.storageKey = channelClean\n      ? `${STORAGE_BROADCAST_LANGUAGE}_${channelClean}`\n      : STORAGE_BROADCAST_LANGUAGE;\n\n    this.render();\n    this.bindEvents();\n\n    // Fetch dynamic supported languages purely from Deepgram API (no static fallback)\n    this.loadLanguages();\n  }\n\n  public async loadLanguages(): Promise<void> {\n    this.isLoading = true;\n    this.errorMessage = '';\n    this.availableLanguages = [];\n    this.filteredLanguages = [];\n\n    if (this.inputEl) {\n      this.inputEl.value = '';\n      this.inputEl.placeholder = 'Fetching languages from Deepgram API...';\n      this.inputEl.disabled = true;\n      this.inputEl.style.borderColor = 'var(--border-color)';\n    }\n\n    try {\n      const langs = await fetchSupportedLanguages('nova-3');\n      this.isLoading = false;\n      this.availableLanguages = langs;\n      this.filteredLanguages = [...langs];\n\n      // Restore previously saved language (channel cookie/storage -> global cookie/storage -> environment -> multi)\n      if (!this.userManuallyChanged) {\n        let initialLang: LanguageOption | null = null;\n        const savedChannelLang = getSavedLanguage(this.storageKey);\n        const savedGlobalLang = getSavedLanguage(STORAGE_BROADCAST_LANGUAGE);\n        const targetCode = savedChannelLang || savedGlobalLang;\n\n        if (targetCode) {\n          initialLang = this.availableLanguages.find((l) => l.code.toLowerCase() === targetCode.toLowerCase()) || null;\n        }\n\n        if (!initialLang) {\n          initialLang = detectDefaultLanguage(this.availableLanguages);\n        }\n\n        this.selectedLanguage = initialLang;\n        if (this.selectedLanguage) {\n          this.onSelectCallback(this.selectedLanguage);\n        }\n      }\n\n      if (this.inputEl) {\n        this.inputEl.disabled = false;\n        this.inputEl.placeholder = 'Type to filter language...';\n        if (this.selectedLanguage) {\n          this.inputEl.value = `${this.selectedLanguage.name} (${this.selectedLanguage.code})`;\n        }\n      }\n\n      if (this.isOpen) {\n        this.renderDropdownItems();\n      }\n    } catch (err: any) {\n      this.isLoading = false;\n      this.errorMessage = err.message || 'Deepgram API unreachable';\n      this.selectedLanguage = null;\n\n      if (this.inputEl) {\n        this.inputEl.disabled = true;\n        this.inputEl.value = '⚠️ Deepgram API offline';\n        this.inputEl.style.borderColor = 'var(--accent-coral, #ef4444)';\n      }\n\n      this.renderDropdownItems();\n    }\n  }\n\n  public getSelected(): LanguageOption | null {\n    return this.selectedLanguage;\n  }\n\n  public setSelected(code: string): void {\n    const found = this.availableLanguages.find((l) => l.code === code);\n    if (found) {\n      this.userManuallyChanged = true;\n      this.selectedLanguage = found;\n      this.saveSelectedLanguage(found.code);\n      this.inputEl.value = `${found.name} (${found.code})`;\n      this.onSelectCallback(found);\n    }\n  }\n\n  private saveSelectedLanguage(code: string): void {\n    saveLanguage(this.storageKey, code);\n    saveLanguage(STORAGE_BROADCAST_LANGUAGE, code);\n  }\n\n  private render(): void {\n    this.container.innerHTML = `\n      <div class=\"lang-selector-wrapper\">\n        <div class=\"lang-selector-input-box\">\n          <input\n            id=\"lang-search-input\"\n            type=\"text\"\n            autocomplete=\"off\"\n            spellcheck=\"false\"\n            placeholder=\"Fetching languages from Deepgram API...\"\n            disabled\n            class=\"lang-selector-input\"\n          />\n          <span class=\"lang-selector-arrow\">\n            ▼\n          </span>\n        </div>\n        <div\n          id=\"lang-dropdown-list\"\n          class=\"custom-scrollbar lang-dropdown-list hidden\"\n        ></div>\n      </div>\n    `;\n\n    this.inputEl = this.container.querySelector('#lang-search-input') as HTMLInputElement;\n    this.dropdownEl = this.container.querySelector('#lang-dropdown-list') as HTMLDivElement;\n  }\n\n  private bindEvents(): void {\n    // Open dropdown on focus / click\n    this.inputEl.addEventListener(EVENT_FOCUS, () => {\n      if (this.isLoading || this.errorMessage) return;\n      this.inputEl.select();\n      const currentSelectedText = this.selectedLanguage ? `${this.selectedLanguage.name} (${this.selectedLanguage.code})` : '';\n      this.filter(this.inputEl.value === currentSelectedText ? '' : this.inputEl.value);\n      this.open();\n    });\n\n    this.inputEl.addEventListener(EVENT_CLICK, () => {\n      if (this.isLoading || this.errorMessage) return;\n      if (!this.isOpen) {\n        this.open();\n      }\n    });\n\n    // Type-to-filter\n    this.inputEl.addEventListener(EVENT_INPUT, () => {\n      if (this.isLoading || this.errorMessage) return;\n      this.filter(this.inputEl.value.trim());\n      if (!this.isOpen) this.open();\n    });\n\n    // Keyboard navigation\n    this.inputEl.addEventListener(EVENT_KEYDOWN, (e: KeyboardEvent) => {\n      if (this.isLoading || this.errorMessage) return;\n\n      if (!this.isOpen && (e.key === KEY_ARROW_DOWN || e.key === KEY_ENTER)) {\n        this.open();\n        e.preventDefault();\n        return;\n      }\n\n      if (!this.isOpen) return;\n\n      if (e.key === KEY_ARROW_DOWN) {\n        e.preventDefault();\n        this.highlightedIndex = Math.min(this.highlightedIndex + 1, this.filteredLanguages.length - 1);\n        this.updateHighlight();\n      } else if (e.key === KEY_ARROW_UP) {\n        e.preventDefault();\n        this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);\n        this.updateHighlight();\n      } else if (e.key === KEY_ENTER) {\n        e.preventDefault();\n        if (this.highlightedIndex >= 0 && this.highlightedIndex < this.filteredLanguages.length) {\n          this.select(this.filteredLanguages[this.highlightedIndex]);\n        }\n      } else if (e.key === KEY_ESCAPE) {\n        this.close();\n      }\n    });\n\n    // Close on click outside\n    document.addEventListener(EVENT_CLICK, (e: MouseEvent) => {\n      if (!this.container.contains(e.target as Node)) {\n        this.close();\n      }\n    });\n  }\n\n  private filter(query: string): void {\n    const q = query.toLowerCase();\n    if (!q) {\n      this.filteredLanguages = [...this.availableLanguages];\n    } else {\n      this.filteredLanguages = this.availableLanguages.filter(\n        (l) =>\n          (l.name && l.name.toLowerCase().includes(q)) ||\n          (l.code && l.code.toLowerCase().includes(q)) ||\n          (l.nativeName && l.nativeName.toLowerCase().includes(q))\n      );\n    }\n    this.highlightedIndex = this.filteredLanguages.length > 0 ? 0 : -1;\n    this.renderDropdownItems();\n  }\n\n  private renderDropdownItems(): void {\n    if (this.errorMessage) {\n      this.dropdownEl.innerHTML = `\n        <div class=\"lang-dropdown-error\">\n          <div>${this.errorMessage}</div>\n          <button id=\"retry-lang-btn\" class=\"btn btn-secondary lang-retry-btn\">\n            Retry Deepgram API\n          </button>\n        </div>\n      `;\n      const retryBtn = this.dropdownEl.querySelector('#retry-lang-btn');\n      retryBtn?.addEventListener(EVENT_CLICK, () => this.loadLanguages());\n      return;\n    }\n\n    if (this.isLoading) {\n      this.dropdownEl.innerHTML = `\n        <div class=\"lang-dropdown-status\">\n          Fetching supported languages from Deepgram...\n        </div>\n      `;\n      return;\n    }\n\n    if (this.filteredLanguages.length === 0) {\n      this.dropdownEl.innerHTML = `\n        <div class=\"lang-dropdown-empty\">\n          No matching languages found\n        </div>\n      `;\n      return;\n    }\n\n    this.dropdownEl.innerHTML = this.filteredLanguages\n      .map((l, index) => {\n        const isSelected = this.selectedLanguage && l.code === this.selectedLanguage.code;\n        const isHighlighted = index === this.highlightedIndex;\n        const classes = ['lang-item'];\n        if (isSelected) classes.push('lang-item-selected');\n        if (isHighlighted) classes.push('lang-item-highlighted');\n\n        return `\n          <div\n            class=\"${classes.join(' ')}\"\n            data-index=\"${index}\"\n          >\n            <div>\n              <span class=\"lang-item-name\">\n                ${l.name}\n              </span>\n              <span class=\"lang-item-native\">\n                ${l.nativeName && l.nativeName !== l.name ? `(${l.nativeName})` : ''}\n              </span>\n            </div>\n            <span class=\"lang-item-code\">\n              ${l.code}\n            </span>\n          </div>\n        `;\n      })\n      .join('');\n\n    // Attach click listeners to items\n    const items = this.dropdownEl.querySelectorAll('.lang-item');\n    items.forEach((item) => {\n      item.addEventListener(EVENT_CLICK, () => {\n        const index = parseInt(item.getAttribute('data-index') || '0', 10);\n        this.select(this.filteredLanguages[index]);\n      });\n      item.addEventListener(EVENT_MOUSEENTER, () => {\n        this.highlightedIndex = parseInt(item.getAttribute('data-index') || '0', 10);\n        this.updateHighlight();\n      });\n    });\n  }\n\n  private updateHighlight(): void {\n    const items = this.dropdownEl.querySelectorAll('.lang-item');\n    items.forEach((item, index) => {\n      const isSelected = Boolean(this.selectedLanguage && this.filteredLanguages[index].code === this.selectedLanguage.code);\n      const isHighlighted = index === this.highlightedIndex;\n      item.classList.toggle('lang-item-highlighted', isHighlighted);\n      item.classList.toggle('lang-item-selected', isSelected);\n\n      if (isHighlighted) {\n        (item as HTMLElement).scrollIntoView({ block: 'nearest' });\n      }\n    });\n  }\n\n  private select(lang: LanguageOption): void {\n    this.userManuallyChanged = true;\n    this.selectedLanguage = lang;\n    this.saveSelectedLanguage(lang.code);\n    this.inputEl.value = `${lang.name} (${lang.code})`;\n    this.close();\n    this.onSelectCallback(lang);\n  }\n\n  private open(): void {\n    this.isOpen = true;\n    this.dropdownEl.style.display = DISPLAY_BLOCK;\n    this.renderDropdownItems();\n  }\n\n  private close(): void {\n    this.isOpen = false;\n    this.dropdownEl.style.display = DISPLAY_NONE;\n    if (this.selectedLanguage) {\n      this.inputEl.value = `${this.selectedLanguage.name} (${this.selectedLanguage.code})`;\n    } else if (!this.errorMessage) {\n      this.inputEl.value = '';\n    }\n  }\n}\n","import { AudioManager } from './audio-manager';\nimport { DeepgramStreamer } from './deepgram-streamer';\nimport { WsPublisher } from './ws-publisher';\nimport { LanguageSelector } from './language-selector';\nimport { fetchWithAuth } from '../api-config';\nimport { VADTelemetry, VADState, SILENCE_TOKEN } from '../types';\nimport { createChannelBadgeElement } from '../channel-badge';\nimport { tokens } from '../proto/compiled_proto';\nimport { getCacheSync } from '../core/cache';\nimport QRCode from 'qrcode';\nimport { t } from '../i18n';\nimport { ScreenWakeLock } from '../core/screen-wake-lock';\nimport {\n  EVENT_CLICK,\n  EVENT_CHANGE,\n  EVENT_INPUT,\n  EVENT_BEFOREUNLOAD,\n  EVENT_DEVICECHANGE,\n  VALUE_TRUE,\n  VALUE_FALSE,\n  STORAGE_GATE_THRESHOLD,\n  STORAGE_GATE_ATTACK,\n  STORAGE_GATE_HOLD,\n  STORAGE_AUDIO_DEVICE_ID,\n  STORAGE_AUDIO_DEVICE_LABEL,\n} from '../core/strings';\n\nexport interface BroadcasterViewOptions {\n  onSessionChange?: () => void;\n}\n\n// Special tokens and tags\nconst TAG_SPAN = 'span';\nconst TAG_OPTION = 'option';\n\n// Storage keys\nconst STORAGE_FORCE_TRANSCRIPTION = 'lst_force_transcription';\nconst STORAGE_FORCE_TEST_MODE = 'lst_force_test_mode';\n\n// Selectors\nconst SELECTOR_TEMPLATE = 'broadcaster-console-template';\nconst SELECTOR_AUDIO_DEVICE = '#audio-device-select';\nconst SELECTOR_BTN_STOP = '#btn-broadcast-stop';\nconst SELECTOR_BTN_PAUSE = '#btn-broadcast-pause';\nconst SELECTOR_BTN_LIVE = '#btn-broadcast-live';\nconst SELECTOR_FORCE_TEST_MODE = '#force-test-mode-toggle';\nconst SELECTOR_MONITOR_FEED = '#monitor-feed';\nconst SELECTOR_MONITOR_PLACEHOLDER = '.live-monitor-placeholder';\nconst SELECTOR_MONITOR_INTERIM = '.monitor-interim';\nconst SELECTOR_AUDIENCE_PILL = '#audience-status-pill';\nconst SELECTOR_AUDIENCE_TEXT = '#audience-status-text';\nconst SELECTOR_GATE_BADGE = '#gate-badge';\nconst SELECTOR_GATE_BADGE_TEXT = '#gate-badge-text';\nconst SELECTOR_VU_BAR = '#vu-bar';\nconst SELECTOR_VU_DB_READOUT = '#vu-db-readout';\nconst SELECTOR_VU_THRESHOLD_LINE = '#vu-threshold-line';\nconst SELECTOR_THRESHOLD_SLIDER = '#threshold-slider';\nconst SELECTOR_THRESHOLD_VAL = '#threshold-val';\nconst SELECTOR_ATTACK_SLIDER = '#attack-slider';\nconst SELECTOR_ATTACK_VAL = '#attack-val';\nconst SELECTOR_HOLD_SLIDER = '#hold-slider, #release-slider';\nconst SELECTOR_HOLD_VAL = '#hold-val, #release-val';\nconst SELECTOR_SHARE_LINK_INPUT = '#share-link-input';\nconst SELECTOR_COPY_LINK_BTN = '#copy-link-btn';\nconst SELECTOR_QR_CANVAS = '#qr-canvas';\nconst SELECTOR_BADGE_MOUNT = '#broadcaster-channel-badge-mount';\nconst SELECTOR_LANGUAGE_MOUNT = '#language-selector-mount';\nconst SELECTOR_AUDIENCE_CHANNEL_NAME = '#broadcaster-audience-channel-name';\nconst SELECTOR_METER_CARD_TITLE = '.meter-card-title';\nconst SELECTOR_LIVE_MONITOR_TITLE = '.live-monitor-title';\nconst SELECTOR_AUDIENCE_ACCESS_TITLE = '.audience-access-title';\nconst SELECTOR_AUDIENCE_ACCESS_DESC = '.audience-access-desc';\n\n// CSS classes\nconst CLASS_STATUS_STOPPED = 'status-pill stopped';\nconst CLASS_STATUS_CONNECTED = 'status-pill connected';\nconst CLASS_STATUS_PAUSED = 'status-pill paused';\nconst CLASS_GATE_STOPPED = 'gate-badge stopped';\nconst CLASS_GATE_ACTIVE = 'gate-badge active';\nconst CLASS_GATE_PAUSED = 'gate-badge paused';\nconst CLASS_MONITOR_FINAL = 'monitor-final-item';\nconst CLASS_MONITOR_SILENCE = 'monitor-silence-item';\nconst CLASS_MONITOR_INTERIM = 'monitor-interim';\nconst CLASS_CASSETTE_ACTIVE = 'active';\n\nexport type BroadcasterState = 'stopped' | 'paused' | 'live';\n\n// Status labels & messages\nconst TEXT_FORCE_TRANSCRIPTION = 'FORCE TRANSCRIPTION (STREAMING AUDIO)';\nconst TEXT_GATE_OPEN = 'GATE OPEN (STREAMING AUDIO)';\nconst TEXT_GATE_CLOSED = 'GATE CLOSED (AUDIO SUPPRESSED)';\nconst TEXT_GATE_SPEECH_PAUSED = 'SPEECH DETECTED (NO AUDIENCE - PAUSED)';\nconst DEFAULT_BROADCAST_STOPPED_MSG = 'Broadcast has stopped';\nconst I18N_BROADCAST_STOPPED_KEY = 'broadcaster.broadcastStoppedMsg';\n\nexport class BroadcasterView {\n  private container: HTMLElement;\n  private channelName: string;\n  private options?: BroadcasterViewOptions;\n  private audioManager: AudioManager;\n  private deepgramStreamer: DeepgramStreamer;\n  private wsPublisher: WsPublisher;\n  private languageSelector!: LanguageSelector;\n  private wakeLock: ScreenWakeLock;\n\n  private isBroadcasting: boolean = false;\n  private cassetteState: BroadcasterState = 'stopped';\n  private forceTranscription: boolean = localStorage.getItem(STORAGE_FORCE_TRANSCRIPTION) === 'true' || localStorage.getItem(STORAGE_FORCE_TEST_MODE) === 'true';\n  private subscribersCount: number = 0;\n  private lastVADState: VADState = 'MUTED';\n  private lastBroadcastText: string = '';\n\n  private currentSessionId: string | null = null;\n  private currentTenantId: string | null = null;\n  private sessionExtensionTimer: number | null = null;\n  private sessionRemainingTimer: number | null = null;\n  private sessionExpiresAtMs: number = 0;\n  private smoothedDb: number = -60;\n  private lastDbReadoutTime: number = 0;\n\n  constructor(container: HTMLElement, channelName: string, options?: BroadcasterViewOptions) {\n    this.container = container;\n    this.channelName = channelName;\n    this.options = options;\n    this.wakeLock = new ScreenWakeLock();\n\n    // Initialize subsystems\n    this.audioManager = new AudioManager({\n      onTelemetry: (t) => this.handleTelemetry(t),\n      onStateChange: (s) => this.handleVADStateChange(s),\n      onAudioData: (data, isBurst) => this.handleAudioData(data, isBurst),\n      onError: (err) => alert('Audio error: ' + err.message),\n    });\n    this.audioManager.setForceTranscription(this.forceTranscription);\n\n    // Initialize saved noise gate parameters\n    try {\n      const savedThreshold = localStorage.getItem(STORAGE_GATE_THRESHOLD);\n      if (savedThreshold !== null) {\n        const parsed = parseInt(savedThreshold, 10);\n        if (!isNaN(parsed) && parsed >= -60 && parsed <= -10) {\n          this.audioManager.setThreshold(parsed);\n        }\n      }\n\n      const savedAttack = localStorage.getItem(STORAGE_GATE_ATTACK);\n      if (savedAttack !== null) {\n        const parsed = parseInt(savedAttack, 10);\n        if (!isNaN(parsed) && parsed >= 10 && parsed <= 1000) {\n          this.audioManager.setAttack(parsed === 25 ? 300 : parsed);\n        }\n      }\n\n      const savedHold = localStorage.getItem(STORAGE_GATE_HOLD);\n      if (savedHold !== null) {\n        const parsed = parseInt(savedHold, 10);\n        if (!isNaN(parsed) && parsed >= 500 && parsed <= 10000) {\n          this.audioManager.setHold(parsed);\n        }\n      }\n    } catch {\n      // Ignore storage errors in restricted contexts\n    }\n\n    this.deepgramStreamer = new DeepgramStreamer({\n      onTranscript: (text, isFinal, speechFinal, conf) => {\n        this.handleTranscript(text, isFinal, speechFinal, conf);\n      },\n      onUtteranceEnd: () => {\n        this.wsPublisher.publishUtteranceEnd();\n      },\n      onStatusChange: (status, detail) => {\n        this.updateDeepgramStatus(status, detail);\n      },\n    });\n    this.deepgramStreamer.setForceTranscription(this.forceTranscription);\n\n    this.wsPublisher = new WsPublisher({\n      onStatusChange: (status, msg) => {\n        this.updateRelayStatus(status, msg);\n      },\n      onOccupancyChange: (count) => {\n        this.handleOccupancyChange(count);\n      },\n    });\n\n    this.render();\n    this.loadAudioDevices();\n  }\n\n  private render(): void {\n    const channelClean = this.channelName.replace(/^@+/, '');\n    const audienceUrl = `${window.location.origin}/@${encodeURIComponent(channelClean)}`;\n\n    const template = document.getElementById(SELECTOR_TEMPLATE) as HTMLTemplateElement | null;\n    if (template) {\n      this.container.replaceChildren(template.content.cloneNode(true));\n    } else {\n      console.error(`${SELECTOR_TEMPLATE} not found`);\n    }\n\n    // Dynamic content initialization\n    const forceToggle = this.container.querySelector(SELECTOR_FORCE_TEST_MODE) as HTMLInputElement | null;\n    if (forceToggle) {\n      forceToggle.checked = this.forceTranscription;\n    }\n\n    const channelDisplay = this.container.querySelector(SELECTOR_AUDIENCE_CHANNEL_NAME);\n    if (channelDisplay) {\n      channelDisplay.textContent = `@${channelClean}`;\n    }\n\n    const shareInput = this.container.querySelector(SELECTOR_SHARE_LINK_INPUT) as HTMLInputElement | null;\n    if (shareInput) {\n      shareInput.value = audienceUrl;\n    }\n\n    // Dynamic localized labels\n    const audienceTxt = this.container.querySelector(SELECTOR_AUDIENCE_TEXT);\n    if (audienceTxt) audienceTxt.textContent = t('broadcaster.audienceOffline');\n\n    const stopLabel = this.container.querySelector(`${SELECTOR_BTN_STOP} .cassette-label`);\n    if (stopLabel) stopLabel.textContent = t('broadcaster.btnStop') || 'Stop';\n\n    const pauseLabel = this.container.querySelector(`${SELECTOR_BTN_PAUSE} .cassette-label`);\n    if (pauseLabel) pauseLabel.textContent = t('broadcaster.btnPause') || 'Pause';\n\n    const liveLabel = this.container.querySelector(`${SELECTOR_BTN_LIVE} .cassette-label`);\n    if (liveLabel) liveLabel.textContent = t('broadcaster.btnBroadcast') || 'Broadcast';\n\n    const meterTitle = this.container.querySelector(SELECTOR_METER_CARD_TITLE);\n    if (meterTitle) meterTitle.textContent = t('broadcaster.audioInputTitle');\n\n    const liveMonitorTitle = this.container.querySelector(SELECTOR_LIVE_MONITOR_TITLE);\n    if (liveMonitorTitle) liveMonitorTitle.textContent = t('broadcaster.liveMonitorTitle');\n\n    const liveMonitorPlaceholder = this.container.querySelector(SELECTOR_MONITOR_PLACEHOLDER);\n    if (liveMonitorPlaceholder) liveMonitorPlaceholder.textContent = t('broadcaster.liveMonitorPlaceholder');\n\n    const audienceTitle = this.container.querySelector(SELECTOR_AUDIENCE_ACCESS_TITLE);\n    if (audienceTitle) audienceTitle.textContent = t('broadcaster.audienceAccess');\n\n    const audienceDesc = this.container.querySelector(SELECTOR_AUDIENCE_ACCESS_DESC);\n    if (audienceDesc) audienceDesc.textContent = t('broadcaster.audienceAccessDesc');\n\n    this.bindEvents();\n\n    const badgeMount = this.container.querySelector(SELECTOR_BADGE_MOUNT);\n    if (badgeMount) {\n      const badge = createChannelBadgeElement({\n        channel: this.channelName,\n        showLogo: false,\n      });\n      badgeMount.appendChild(badge);\n    }\n\n    this.drawQRCode(audienceUrl);\n    // Cassette deck begins in \"stopped\" state: no audio recording or auto-start on page load\n    this.loadAudioDevices();\n    this.cassetteState = 'stopped';\n    this.updateCassetteButtonsUI('stopped');\n    this.evaluateAudienceState();\n    this.resetVUMeter();\n\n    const langMount = this.container.querySelector(SELECTOR_LANGUAGE_MOUNT) as HTMLElement;\n    if (langMount) {\n      this.languageSelector = new LanguageSelector(\n        langMount,\n        (lang) => {\n          if (this.isBroadcasting) {\n            this.reconnectDeepgramWithNewLanguage(lang.code);\n          }\n        },\n        this.channelName\n      );\n    }\n  }\n\n  private async reconnectDeepgramWithNewLanguage(langCode: string): Promise<void> {\n    try {\n      this.updateDeepgramStatus('connecting', `Switching language to ${langCode}...`);\n      const dgResp = await fetchWithAuth(`/api/channels/${encodeURIComponent(this.channelName)}/token/deepgram`);\n      if (dgResp.ok) {\n        const buf = new Uint8Array(await dgResp.arrayBuffer());\n        const dgData = tokens.DeepgramTokenProto.decode(buf);\n        this.currentSessionId = dgData.sessionId ? String(dgData.sessionId) : this.currentSessionId;\n        const continent = dgResp.headers.get('cf-ipcontinent') || dgResp.headers.get('x-continent') || undefined;\n        await this.deepgramStreamer.connect(\n          dgData.key,\n          langCode,\n          this.currentSessionId || undefined,\n          dgData.tag || undefined,\n          continent\n        );\n      }\n    } catch (err: any) {\n      console.error('Failed to switch language live:', err);\n      this.updateDeepgramStatus('error', err.message);\n    }\n  }\n\n  private async loadAudioDevices(): Promise<void> {\n    const devices = await AudioManager.getAudioDevices();\n    const select = this.container.querySelector(SELECTOR_AUDIO_DEVICE) as HTMLSelectElement;\n    if (!select) return;\n\n    let savedId: string | null = null;\n    let savedLabel: string | null = null;\n    try {\n      savedId = localStorage.getItem(STORAGE_AUDIO_DEVICE_ID);\n      savedLabel = localStorage.getItem(STORAGE_AUDIO_DEVICE_LABEL);\n    } catch {\n      // ignore in restricted environments\n    }\n\n    const currentVal = select.value;\n    select.replaceChildren();\n    const defaultOpt = document.createElement(TAG_OPTION);\n    defaultOpt.value = '';\n    defaultOpt.textContent = t('broadcaster.defaultMic') || 'Default Microphone';\n    select.appendChild(defaultOpt);\n    let virtualMicOption: HTMLOptionElement | null = null;\n    let savedMatchOption: HTMLOptionElement | null = null;\n\n    devices.forEach((d, i) => {\n      const opt = document.createElement(TAG_OPTION);\n      opt.value = d.deviceId;\n      const rawLabel = d.label || `Audio Input ${i + 1}`;\n      if (rawLabel.toLowerCase().includes('virtual')) {\n        opt.textContent = `🎤 ${rawLabel} (Test Feed)`;\n        virtualMicOption = opt;\n      } else {\n        opt.textContent = rawLabel;\n      }\n\n      // Check if this input matches the saved device ID or human-readable label\n      if (savedId && d.deviceId === savedId) {\n        savedMatchOption = opt;\n      } else if (!savedMatchOption && savedLabel && (rawLabel === savedLabel || opt.textContent === savedLabel)) {\n        savedMatchOption = opt;\n      }\n\n      select.appendChild(opt);\n    });\n\n    if (currentVal && Array.from(select.options).some((o) => o.value === currentVal)) {\n      select.value = currentVal;\n    } else if (savedMatchOption) {\n      select.value = (savedMatchOption as HTMLOptionElement).value;\n    } else if (savedId === '') {\n      select.value = '';\n    } else if (virtualMicOption && !currentVal) {\n      select.value = (virtualMicOption as HTMLOptionElement).value;\n    }\n  }\n\n  private bindEvents(): void {\n    const btnStop = this.container.querySelector(SELECTOR_BTN_STOP);\n    if (btnStop) {\n      btnStop.addEventListener(EVENT_CLICK, () => this.handleStopClick());\n    }\n\n    const btnPause = this.container.querySelector(SELECTOR_BTN_PAUSE);\n    if (btnPause) {\n      btnPause.addEventListener(EVENT_CLICK, () => this.handlePauseClick());\n    }\n\n    const btnLive = this.container.querySelector(SELECTOR_BTN_LIVE);\n    if (btnLive) {\n      btnLive.addEventListener(EVENT_CLICK, () => this.handleLiveClick());\n    }\n\n    const forceToggle = this.container.querySelector(SELECTOR_FORCE_TEST_MODE) as HTMLInputElement;\n    if (forceToggle) {\n      forceToggle.addEventListener(EVENT_CHANGE, () => {\n        this.forceTranscription = forceToggle.checked;\n        localStorage.setItem(STORAGE_FORCE_TRANSCRIPTION, this.forceTranscription ? VALUE_TRUE : VALUE_FALSE);\n        this.audioManager.setForceTranscription(this.forceTranscription);\n        this.deepgramStreamer.setForceTranscription(this.forceTranscription);\n        this.evaluateAudienceState();\n      });\n    }\n\n    // Switch device live while broadcasting or monitoring\n    const deviceSelect = this.container.querySelector(SELECTOR_AUDIO_DEVICE) as HTMLSelectElement;\n    if (deviceSelect) {\n      deviceSelect.addEventListener(EVENT_CHANGE, async () => {\n        try {\n          const selectedOpt = deviceSelect.selectedOptions ? deviceSelect.selectedOptions[0] : null;\n          localStorage.setItem(STORAGE_AUDIO_DEVICE_ID, deviceSelect.value || '');\n          localStorage.setItem(STORAGE_AUDIO_DEVICE_LABEL, selectedOpt?.textContent || '');\n        } catch {\n          // ignore in restricted environments\n        }\n\n        if (this.audioManager.isRunning) {\n          await this.audioManager.start(deviceSelect.value || undefined);\n          await this.loadAudioDevices();\n        }\n      });\n    }\n\n    // Auto-refresh audio devices if devices are plugged in or virtual mic starts/stops\n    if (navigator.mediaDevices && navigator.mediaDevices.addEventListener) {\n      navigator.mediaDevices.addEventListener(EVENT_DEVICECHANGE, () => {\n        this.loadAudioDevices();\n      });\n    }\n\n    const threshSlider = this.container.querySelector(SELECTOR_THRESHOLD_SLIDER) as HTMLInputElement | null;\n    const threshVal = this.container.querySelector(SELECTOR_THRESHOLD_VAL) as HTMLSpanElement | null;\n    if (threshSlider && threshVal) {\n      threshSlider.value = String(this.audioManager.thresholdDb);\n      threshVal.textContent = `${this.audioManager.thresholdDb} dB`;\n      threshSlider.addEventListener(EVENT_INPUT, () => {\n        const val = parseInt(threshSlider.value, 10);\n        threshVal.textContent = `${val} dB`;\n        this.audioManager.setThreshold(val);\n        this.updateThresholdLine(val);\n        try {\n          localStorage.setItem(STORAGE_GATE_THRESHOLD, String(val));\n        } catch {\n          // ignore\n        }\n      });\n    }\n\n    const attackSlider = this.container.querySelector(SELECTOR_ATTACK_SLIDER) as HTMLInputElement | null;\n    const attackVal = this.container.querySelector(SELECTOR_ATTACK_VAL) as HTMLSpanElement | null;\n    if (attackSlider && attackVal) {\n      attackSlider.value = String(this.audioManager.attackMs);\n      attackVal.textContent = `${this.audioManager.attackMs} ms`;\n      attackSlider.addEventListener(EVENT_INPUT, () => {\n        const val = parseInt(attackSlider.value, 10);\n        attackVal.textContent = `${val} ms`;\n        this.audioManager.setAttack(val);\n        try {\n          localStorage.setItem(STORAGE_GATE_ATTACK, String(val));\n        } catch {\n          // ignore\n        }\n      });\n    }\n\n    const holdSlider = this.container.querySelector(SELECTOR_HOLD_SLIDER) as HTMLInputElement | null;\n    const holdVal = this.container.querySelector(SELECTOR_HOLD_VAL) as HTMLSpanElement | null;\n    if (holdSlider && holdVal) {\n      holdSlider.value = String(this.audioManager.releaseMs);\n      holdVal.textContent = `${(this.audioManager.releaseMs / 1000).toFixed(1)} s`;\n      holdSlider.addEventListener(EVENT_INPUT, () => {\n        const val = parseInt(holdSlider.value, 10);\n        holdVal.textContent = `${(val / 1000).toFixed(1)} s`;\n        this.audioManager.setHold(val);\n        try {\n          localStorage.setItem(STORAGE_GATE_HOLD, String(val));\n        } catch {\n          // ignore\n        }\n      });\n    }\n\n    const copyBtn = this.container.querySelector(SELECTOR_COPY_LINK_BTN) as HTMLButtonElement;\n    copyBtn.addEventListener(EVENT_CLICK, () => {\n      const input = this.container.querySelector(SELECTOR_SHARE_LINK_INPUT) as HTMLInputElement;\n      navigator.clipboard.writeText(input.value);\n      copyBtn.textContent = t('common.copied');\n      setTimeout(() => (copyBtn.textContent = t('common.copy')), 2000);\n    });\n\n    // Initialize threshold visual line\n    this.updateThresholdLine(this.audioManager.thresholdDb);\n  }\n\n  private updateThresholdLine(db: number): void {\n    // Range -60 to 0 dB mapped to 0% - 100%\n    const pct = Math.max(0, Math.min(100, ((db + 60) / 60) * 100));\n    const line = this.container.querySelector(SELECTOR_VU_THRESHOLD_LINE) as HTMLElement;\n    if (line) {\n      line.style.left = `${pct}%`;\n    }\n  }\n\n  public isLive(): boolean {\n    return this.cassetteState === 'live';\n  }\n\n  private handleBeforeUnload = (e: BeforeUnloadEvent): string => {\n    if (this.isBroadcasting) {\n      e.preventDefault();\n      e.returnValue = '';\n      return '';\n    }\n    return '';\n  };\n\n  private updateCassetteButtonsUI(state: BroadcasterState): void {\n    const btnStop = this.container.querySelector(SELECTOR_BTN_STOP) as HTMLButtonElement | null;\n    const btnPause = this.container.querySelector(SELECTOR_BTN_PAUSE) as HTMLButtonElement | null;\n    const btnLive = this.container.querySelector(SELECTOR_BTN_LIVE) as HTMLButtonElement | null;\n\n    if (btnStop) {\n      btnStop.classList.toggle(CLASS_CASSETTE_ACTIVE, state === 'stopped');\n      btnStop.setAttribute('aria-pressed', state === 'stopped' ? 'true' : 'false');\n      btnStop.disabled = false;\n    }\n    if (btnPause) {\n      btnPause.classList.toggle(CLASS_CASSETTE_ACTIVE, state === 'paused');\n      btnPause.setAttribute('aria-pressed', state === 'paused' ? 'true' : 'false');\n      btnPause.disabled = false;\n    }\n    if (btnLive) {\n      btnLive.classList.toggle(CLASS_CASSETTE_ACTIVE, state === 'live');\n      btnLive.setAttribute('aria-pressed', state === 'live' ? 'true' : 'false');\n      btnLive.disabled = false;\n    }\n  }\n\n  private resetVUMeter(): void {\n    this.smoothedDb = -60;\n    const bar = this.container.querySelector(SELECTOR_VU_BAR) as HTMLElement | null;\n    const readout = this.container.querySelector(SELECTOR_VU_DB_READOUT) as HTMLElement | null;\n    if (bar) bar.style.width = '0%';\n    if (readout) readout.textContent = '-∞ dB';\n  }\n\n  private async handleStopClick(): Promise<void> {\n    if (this.cassetteState === 'stopped') return;\n    await this.setCassetteState('stopped');\n  }\n\n  private async handlePauseClick(): Promise<void> {\n    if (this.cassetteState === 'paused') return;\n    await this.setCassetteState('paused');\n  }\n\n  private async handleLiveClick(): Promise<void> {\n    if (this.cassetteState === 'live') return;\n    await this.setCassetteState('live');\n  }\n\n  private async setCassetteState(targetState: BroadcasterState): Promise<void> {\n    if (targetState === 'stopped') {\n      if (this.isBroadcasting) {\n        await this.stopLiveBroadcast();\n      }\n      try {\n        await this.audioManager.stop();\n      } catch (err) {\n        console.warn('[Broadcaster] Error stopping audio manager:', err);\n      }\n      this.resetVUMeter();\n      this.lastVADState = 'MUTED';\n      this.cassetteState = 'stopped';\n      this.updateCassetteButtonsUI('stopped');\n      this.evaluateAudienceState();\n      return;\n    }\n\n    if (targetState === 'paused') {\n      if (this.isBroadcasting) {\n        await this.stopLiveBroadcast();\n      }\n      if (!this.audioManager.isRunning) {\n        try {\n          const select = this.container.querySelector(SELECTOR_AUDIO_DEVICE) as HTMLSelectElement | null;\n          const deviceId = select ? select.value : undefined;\n          await this.audioManager.start(deviceId || undefined);\n          await this.loadAudioDevices();\n        } catch (err: any) {\n          alert('Could not access microphone: ' + (err?.message || err));\n          this.updateCassetteButtonsUI(this.cassetteState);\n          return;\n        }\n      }\n      this.cassetteState = 'paused';\n      this.lastVADState = this.audioManager.currentVADState;\n      this.updateCassetteButtonsUI('paused');\n      this.evaluateAudienceState();\n      return;\n    }\n\n    if (targetState === 'live') {\n      const btnLive = this.container.querySelector(SELECTOR_BTN_LIVE) as HTMLButtonElement | null;\n      if (btnLive) btnLive.disabled = true;\n\n      if (!this.audioManager.isRunning) {\n        try {\n          const select = this.container.querySelector(SELECTOR_AUDIO_DEVICE) as HTMLSelectElement | null;\n          const deviceId = select ? select.value : undefined;\n          await this.audioManager.start(deviceId || undefined);\n          await this.loadAudioDevices();\n        } catch (err: any) {\n          alert('Could not access microphone: ' + (err?.message || err));\n          if (btnLive) btnLive.disabled = false;\n          this.updateCassetteButtonsUI(this.cassetteState);\n          return;\n        }\n      }\n\n      try {\n        await this.startLiveBroadcast();\n        this.cassetteState = 'live';\n        this.updateCassetteButtonsUI('live');\n        this.evaluateAudienceState();\n      } catch (err: any) {\n        alert('Could not start broadcast: ' + (err?.message || err));\n        if (this.audioManager.isRunning) {\n          this.cassetteState = 'paused';\n          this.updateCassetteButtonsUI('paused');\n        } else {\n          this.cassetteState = 'stopped';\n          this.updateCassetteButtonsUI('stopped');\n        }\n        this.evaluateAudienceState();\n      } finally {\n        if (btnLive) btnLive.disabled = false;\n      }\n    }\n  }\n\n  private async stopLiveBroadcast(): Promise<void> {\n    if (!this.isBroadcasting) return;\n\n    this.isBroadcasting = false;\n    this.stopSessionTimers();\n    window.removeEventListener(EVENT_BEFOREUNLOAD, this.handleBeforeUnload);\n\n    // 1. Tell Hub to stop session and move to PENDING_SETTLEMENT\n    try {\n      await fetchWithAuth(`/api/channels/${encodeURIComponent(this.channelName)}/session/stop`, {\n        method: 'POST',\n      });\n    } catch (err) {\n      console.warn('Could not cleanly stop session on Hub:', err);\n    }\n    const prevTenantId = this.currentTenantId;\n    this.currentSessionId = null;\n    this.currentTenantId = null;\n    getCacheSync().broadcastInvalidation('entitlements', prevTenantId || undefined);\n    this.options?.onSessionChange?.();\n\n    // Transmit last message: \"Broadcast has stopped\", translated using the selected locale\n    const selectedLang = this.languageSelector?.getSelected()?.code;\n    const stopMessage = this.getBroadcastStoppedMessage(selectedLang);\n    try {\n      await this.wsPublisher.publish(stopMessage, true, true, 1.0, selectedLang);\n      this.renderFinalMonitorMessage(stopMessage);\n    } catch (err) {\n      console.error('Failed to publish broadcast-stopped message:', err);\n    }\n\n    // Disconnect streaming services (audio manager stays active if in paused mode)\n    this.deepgramStreamer.disconnect();\n    this.wsPublisher.disconnect();\n    this.subscribersCount = 0;\n    this.updateRelayStatus('disconnected');\n    this.updateDeepgramStatus('disconnected');\n    this.wakeLock.releaseLock();\n    this.lastVADState = this.audioManager.currentVADState;\n    this.updateGateBadge();\n  }\n\n  private async startLiveBroadcast(): Promise<void> {\n    // 1. Start Streaming Session\n    const sessionResp = await fetchWithAuth(`/api/channels/${encodeURIComponent(this.channelName)}/session/start`, {\n      method: 'POST',\n    });\n    if (!sessionResp.ok) {\n      if (sessionResp.status === 402) {\n        throw new Error('Subscription expired or insufficient streaming balance. Please check your church account.');\n      }\n      throw new Error('Failed to start streaming session.');\n    }\n    const sessionBuf = new Uint8Array(await sessionResp.arrayBuffer());\n    const sessionData = tokens.SessionResponseProto.decode(sessionBuf);\n    this.currentSessionId = sessionData.sessionId ? String(sessionData.sessionId) : null;\n    this.sessionExpiresAtMs = Number(sessionData.expiresAtUnixMs);\n    this.currentTenantId = sessionData.tenantId != null ? String(sessionData.tenantId) : null;\n    getCacheSync().broadcastInvalidation('entitlements', this.currentTenantId || undefined);\n    this.options?.onSessionChange?.();\n\n    // 2. Fetch Deepgram Token\n    const dgResp = await fetchWithAuth(`/api/channels/${encodeURIComponent(this.channelName)}/token/deepgram`);\n    if (!dgResp.ok) {\n      let errDetail = '';\n      try {\n        const errJson = await dgResp.json();\n        errDetail = errJson.error || errJson.message || JSON.stringify(errJson);\n      } catch {\n        errDetail = await dgResp.text().catch(() => '');\n      }\n      throw new Error(`Failed to obtain Deepgram temporary token from Hub${errDetail ? ': ' + errDetail : ''}`);\n    }\n    const dgBuf = new Uint8Array(await dgResp.arrayBuffer());\n    const dgData = tokens.DeepgramTokenProto.decode(dgBuf);\n\n    // 3. Connect Worker Publisher\n    await this.wsPublisher.connect(this.channelName);\n\n    // 4. Connect Deepgram Streamer with selected language and session_id\n    const selectedLang = this.languageSelector.getSelected();\n    if (!selectedLang || !selectedLang.code) {\n      throw new Error('No language selected. Deepgram API is unreachable.');\n    }\n    const continent = dgResp.headers.get('cf-ipcontinent') || dgResp.headers.get('x-continent') || undefined;\n    await this.deepgramStreamer.connect(\n      dgData.key,\n      selectedLang.code,\n      this.currentSessionId || undefined,\n      dgData.tag || undefined,\n      continent\n    );\n\n    // 5. Start keepalive/extension timer (checks every minute, extends when < 5 mins remain)\n    this.startSessionExtensionTimer();\n\n    this.isBroadcasting = true;\n    const initialVAD = this.forceTranscription ? 'ACTIVE' : this.audioManager.currentVADState;\n    this.lastVADState = initialVAD;\n    this.deepgramStreamer.setVADState(initialVAD);\n    this.lastBroadcastText = '';\n    this.updateGateBadge();\n    window.addEventListener(EVENT_BEFOREUNLOAD, this.handleBeforeUnload);\n    this.updateDeepgramStatus(this.deepgramStreamer.isConnected ? 'connected' : 'disconnected');\n    this.wakeLock.requestLock();\n  }\n\n  private startSessionExtensionTimer(): void {\n    this.stopSessionTimers();\n    // Check every 10 seconds to smoothly handle short sessions (e.g. 1 minute in dev)\n    this.sessionExtensionTimer = window.setInterval(async () => {\n      if (!this.isBroadcasting || !this.currentSessionId) return;\n\n      const now = Date.now();\n      const remainingMs = this.sessionExpiresAtMs - now;\n      // When less than 5 minutes remain (or less than 20 seconds for short dev sessions), extend it\n      const thresholdMs = remainingMs < 60 * 1000 ? 20 * 1000 : 5 * 60 * 1000;\n      if (remainingMs <= thresholdMs) {\n        try {\n          const extResp = await fetchWithAuth(`/api/channels/${encodeURIComponent(this.channelName)}/session/extend`, {\n            method: 'POST',\n          });\n          if (extResp.ok) {\n            const extBuf = new Uint8Array(await extResp.arrayBuffer());\n            const extData = tokens.SessionResponseProto.decode(extBuf);\n            this.sessionExpiresAtMs = Number(extData.expiresAtUnixMs);\n            this.currentTenantId = extData.tenantId != null ? String(extData.tenantId) : this.currentTenantId;\n            console.log('[Broadcaster] Session extended successfully until', new Date(this.sessionExpiresAtMs));\n            getCacheSync().broadcastInvalidation('entitlements', this.currentTenantId || undefined);\n            this.options?.onSessionChange?.();\n          } else if (extResp.status === 402) {\n            console.warn('[Broadcaster] Warning: Church streaming balance depleted during broadcast.');\n          }\n        } catch (err) {\n          console.error('[Broadcaster] Failed to extend session:', err);\n        }\n      }\n    }, 10000);\n  }\n\n  private stopSessionTimers(): void {\n    if (this.sessionExtensionTimer !== null) {\n      clearInterval(this.sessionExtensionTimer);\n      this.sessionExtensionTimer = null;\n    }\n    if (this.sessionRemainingTimer !== null) {\n      clearInterval(this.sessionRemainingTimer);\n      this.sessionRemainingTimer = null;\n    }\n  }\n\n  private handleOccupancyChange(count: number): void {\n    this.subscribersCount = count;\n    this.evaluateAudienceState();\n  }\n\n  private evaluateAudienceState(): void {\n    const isTranscribing = this.subscribersCount > 0 || this.forceTranscription;\n    this.deepgramStreamer.setAudiencePaused(!isTranscribing);\n\n    const pill = this.container.querySelector(SELECTOR_AUDIENCE_PILL);\n    const txt = this.container.querySelector(SELECTOR_AUDIENCE_TEXT);\n\n    if (pill && txt) {\n      if (!this.isBroadcasting) {\n        pill.className = CLASS_STATUS_STOPPED;\n        txt.textContent = t('broadcaster.audienceOffline');\n      } else {\n        if (this.subscribersCount > 0) {\n          pill.className = CLASS_STATUS_CONNECTED;\n        } else {\n          pill.className = CLASS_STATUS_PAUSED;\n        }\n        txt.textContent = `Audience: ${this.subscribersCount}`;\n      }\n    }\n\n    this.updateGateBadge();\n  }\n\n  private updateGateBadge(): void {\n    const badge = this.container.querySelector(SELECTOR_GATE_BADGE) as HTMLElement;\n    const badgeText = this.container.querySelector(SELECTOR_GATE_BADGE_TEXT) as HTMLElement;\n    if (!badge || !badgeText) return;\n\n    if (this.cassetteState === 'stopped') {\n      badge.className = CLASS_GATE_STOPPED;\n      badgeText.textContent = t('broadcaster.gateStopped');\n      return;\n    }\n\n    if (this.cassetteState === 'paused') {\n      if (this.lastVADState === 'ACTIVE') {\n        badge.className = CLASS_GATE_ACTIVE;\n        badgeText.textContent = 'Speech Detected (Monitoring)';\n      } else {\n        badge.className = CLASS_GATE_PAUSED;\n        badgeText.textContent = 'Gate Closed (Monitoring)';\n      }\n      return;\n    }\n\n    if (this.forceTranscription) {\n      badge.className = CLASS_GATE_ACTIVE;\n      badgeText.textContent = TEXT_FORCE_TRANSCRIPTION;\n      return;\n    }\n\n    const isAudiencePresent = this.subscribersCount > 0;\n\n    if (isAudiencePresent) {\n      if (this.lastVADState === 'ACTIVE') {\n        badge.className = CLASS_GATE_ACTIVE;\n        badgeText.textContent = TEXT_GATE_OPEN;\n      } else {\n        badge.className = CLASS_GATE_PAUSED;\n        badgeText.textContent = TEXT_GATE_CLOSED;\n      }\n    } else {\n      // Audience is 0 (Paused)\n      badge.className = CLASS_GATE_PAUSED;\n      if (this.lastVADState === 'ACTIVE') {\n        badgeText.textContent = TEXT_GATE_SPEECH_PAUSED;\n      } else {\n        badgeText.textContent = t('broadcaster.gatePaused');\n      }\n    }\n  }\n\n  private handleTelemetry(t: VADTelemetry): void {\n    const bar = this.container.querySelector(SELECTOR_VU_BAR) as HTMLElement | null;\n    const readout = this.container.querySelector(SELECTOR_VU_DB_READOUT) as HTMLElement | null;\n\n    const clampedDb = Math.max(-60, Math.min(0, t.db));\n    this.smoothedDb = clampedDb;\n\n    if (bar) {\n      const pct = Math.max(0, Math.min(100, ((this.smoothedDb + 60) / 60) * 100));\n      bar.style.width = `${pct}%`;\n    }\n\n    if (readout) {\n      const now = performance.now();\n      // Calm numeric display: update every 300ms with whole integer dB\n      if (now - this.lastDbReadoutTime >= 300) {\n        this.lastDbReadoutTime = now;\n        const roundedDb = Math.round(this.smoothedDb);\n        readout.textContent = roundedDb <= -58 ? '-∞ dB' : `${roundedDb} dB`;\n      }\n    }\n\n    // Heartbeat synchronization: reconcile VAD state with worklet processor\n    if (t.state && t.state !== this.lastVADState) {\n      this.handleVADStateChange(t.state);\n    }\n  }\n\n  private handleVADStateChange(state: VADState): void {\n    if (!this.isBroadcasting) {\n      this.deepgramStreamer.setVADState('MUTED');\n      this.lastVADState = state;\n      this.updateGateBadge();\n      return;\n    }\n\n    if (this.forceTranscription) {\n      this.deepgramStreamer.setVADState('ACTIVE');\n      this.lastVADState = 'ACTIVE';\n      this.updateGateBadge();\n      return;\n    }\n\n    const isAudiencePaused = this.subscribersCount === 0;\n\n    if (state === 'ACTIVE') {\n      this.deepgramStreamer.setVADState('ACTIVE');\n      this.lastVADState = 'ACTIVE';\n    } else {\n      this.deepgramStreamer.setVADState('MUTED');\n\n      // If the broadcast is paused due to lack of audio, broadcast [silence]\n      if (this.lastVADState === 'ACTIVE' && !isAudiencePaused) {\n        this.broadcastSilence();\n      }\n      this.lastVADState = 'MUTED';\n    }\n\n    this.updateGateBadge();\n  }\n\n  private handleAudioData(data: ArrayBuffer, _isBurst: boolean): void {\n    if (!this.isBroadcasting) return;\n    // Send PCM data directly to Deepgram WebSocket\n    this.deepgramStreamer.sendAudio(data);\n  }\n\n  private handleTranscript(text: string, isFinal: boolean, speechFinal: boolean, confidence: number): void {\n    if (isFinal) {\n      this.lastBroadcastText = text.trim();\n    }\n    // 1. Publish directly to Worker WebSocket\n    const selectedLang = this.languageSelector?.getSelected()?.code;\n    this.wsPublisher.publish(text, isFinal, speechFinal, confidence, selectedLang);\n\n    // 2. Render to local monitor feed\n    const monitor = this.container.querySelector(SELECTOR_MONITOR_FEED) as HTMLElement;\n    if (monitor) {\n      if (monitor.querySelector(SELECTOR_MONITOR_PLACEHOLDER) || monitor.querySelector('span[style*=\"font-style: italic\"]')) {\n        monitor.replaceChildren();\n      }\n      if (isFinal) {\n        const item = document.createElement(TAG_SPAN);\n        item.className = text === SILENCE_TOKEN ? CLASS_MONITOR_SILENCE : CLASS_MONITOR_FINAL;\n        item.textContent = ' ' + text;\n        monitor.appendChild(item);\n      } else {\n        let interim = monitor.querySelector(SELECTOR_MONITOR_INTERIM) as HTMLElement;\n        if (!interim) {\n          interim = document.createElement(TAG_SPAN);\n          interim.className = CLASS_MONITOR_INTERIM;\n          monitor.appendChild(interim);\n        }\n        interim.textContent = ' ' + text;\n      }\n      monitor.scrollTop = monitor.scrollHeight;\n    }\n  }\n\n  private updateDeepgramStatus(status: string, detail?: string): void {\n    if (status === 'error') {\n      console.warn('[Broadcaster] Deepgram error:', detail);\n    }\n  }\n\n  private updateRelayStatus(status: string, msg?: string): void {\n    if (status === 'error' || status === 'disconnected') {\n      console.log(`[Broadcaster] Relay status: ${status}${msg ? ` (${msg})` : ''}`);\n    }\n  }\n\n  private drawQRCode(url: string): void {\n    const canvas = this.container.querySelector(SELECTOR_QR_CANVAS) as HTMLCanvasElement;\n    if (!canvas) return;\n\n    QRCode.toCanvas(canvas, url, {\n      width: 220,\n      margin: 1,\n      color: {\n        dark: '#000000',\n        light: '#ffffff',\n      },\n    }).catch((err) => {\n      console.error('Failed to draw canvas QR code:', err);\n    });\n  }\n\n  private getBroadcastStoppedMessage(langCode?: string): string {\n    const rawCode = (langCode || navigator.language || 'en').toLowerCase().trim();\n    const primary = rawCode.split(/[-_]/)[0];\n\n    const translations: Record<string, string> = {\n      en: DEFAULT_BROADCAST_STOPPED_MSG,\n      sv: 'Sändningen har avslutats',\n      es: 'La transmisión se ha detenido',\n      de: 'Die Übertragung wurde beendet',\n      fr: 'La diffusion est arrêtée',\n      pt: 'A transmissão foi interrompida',\n      it: 'La trasmissione è terminata',\n      nl: 'De uitzending is gestopt',\n      no: 'Sendingen er stoppet',\n      nb: 'Sendingen er stoppet',\n      nn: 'Sendinga er stoppa',\n      da: 'Udsendelsen er stoppet',\n      fi: 'Lähetys on päättynyt',\n      uk: 'Трансляцію зупинено',\n      ru: 'Трансляция остановлена',\n      pl: 'Transmisja została zatrzymana',\n      cs: 'Vysílání bylo zastaveno',\n      el: 'Η μετάδοση διακόπηκε',\n      ro: 'Transmisiunea s-a oprit',\n      hu: 'A közvetítés leállt',\n      tr: 'Yayın durduruldu',\n      ja: '配信が終了しました',\n      ko: '방송이 종료되었습니다',\n      zh: '直播已停止',\n      hi: 'प्रसारण बंद हो गया है',\n      ar: 'تم إيقاف البث',\n      he: 'השידור הופסק',\n      id: 'Siaran telah berhenti',\n      ms: 'Siaran telah dihentikan',\n      vi: 'Phát sóng đã dừng',\n      th: 'การถ่ายทอดสดหยุดลงแล้ว',\n    };\n\n    if (translations[primary]) {\n      return translations[primary];\n    }\n\n    const i18nMsg = t(I18N_BROADCAST_STOPPED_KEY);\n    if (i18nMsg && i18nMsg !== I18N_BROADCAST_STOPPED_KEY) {\n      return i18nMsg;\n    }\n\n    return DEFAULT_BROADCAST_STOPPED_MSG;\n  }\n\n  private renderFinalMonitorMessage(text: string): void {\n    const monitor = this.container.querySelector(SELECTOR_MONITOR_FEED) as HTMLElement;\n    if (monitor) {\n      const item = document.createElement(TAG_SPAN);\n      item.className = CLASS_MONITOR_SILENCE;\n      item.textContent = ' ' + text;\n      monitor.appendChild(item);\n      monitor.scrollTop = monitor.scrollHeight;\n    }\n  }\n\n  private broadcastSilence(): void {\n    if (!this.isBroadcasting) return;\n    if (this.lastBroadcastText === SILENCE_TOKEN) return;\n    this.handleTranscript(SILENCE_TOKEN, true, true, 1.0);\n  }\n\n  async destroy(): Promise<void> {\n    if (this.isBroadcasting) {\n      window.removeEventListener(EVENT_BEFOREUNLOAD, this.handleBeforeUnload);\n      const selectedLang = this.languageSelector?.getSelected()?.code;\n      const stopMessage = this.getBroadcastStoppedMessage(selectedLang);\n      try {\n        await this.wsPublisher.publish(stopMessage, true, true, 1.0, selectedLang);\n      } catch (err) {\n        console.warn('Could not publish broadcast stopped message on destroy:', err);\n      }\n    }\n    this.isBroadcasting = false;\n    this.subscribersCount = 0;\n    this.wakeLock.destroy();\n    await this.audioManager.stop();\n    this.deepgramStreamer.disconnect();\n    this.wsPublisher.disconnect();\n    this.cassetteState = 'stopped';\n  }\n}\n","import { PanelHandler, PageManager } from '@tingdahl/tsutils/ui';\nimport { TenantManager } from '../../core/tenant_manager';\nimport { BroadcasterView } from '../../broadcaster/broadcaster-view';\nimport {\n  DISPLAY_NONE,\n  DISPLAY_BLOCK,\n  EVENT_CLICK,\n} from '../../core/strings';\n\n// DOM Tag names\nconst TAG_BUTTON = 'button';\nconst TAG_A = 'a';\n\n// Selectors\nconst SELECTOR_NO_TENANT_STATE = '#broadcast-no-tenant-state';\nconst SELECTOR_CONTENT = '#broadcast-content';\nconst SELECTOR_EMPTY_CREATE_BTN = '#broadcast-empty-create-btn';\nconst SELECTOR_CHANNEL_TABS_LIST = '#channel-tabs-list';\nconst SELECTOR_AUDIENCE_LINK_WRAPPER = '#channel-audience-link-wrapper';\nconst SELECTOR_BROADCASTER_MOUNT = '#broadcaster-mount';\nconst SELECTOR_NO_CHANNELS = '#broadcast-no-channels-placeholder';\n\n// CSS classes\nconst CLASS_BTN_PRIMARY = 'btn btn-primary';\nconst CLASS_BTN_OUTLINE = 'btn btn-outline';\nconst CLASS_CHANNEL_TAB_BTN = 'btn btn-sm channel-tab-btn';\nconst CLASS_AUDIENCE_LINK = 'channel-audience-link';\n\nexport class BroadcastPanel extends PanelHandler {\n  private tenantManager: TenantManager;\n  private pageManager: PageManager;\n  private broadcasterView: BroadcasterView | null = null;\n  private renderedChannelName: string | null = null;\n  private staticEventsBound: boolean = false;\n\n  constructor(tenantManager: TenantManager, pageManager: PageManager) {\n    super('broadcast-panel');\n    this.tenantManager = tenantManager;\n    this.pageManager = pageManager;\n\n    this.tenantManager.onTenantChanged(() => {\n      this.refresh();\n    });\n\n    this.tenantManager.onChannelChanged(() => {\n      this.refresh();\n    });\n  }\n\n  async init(): Promise<void> {\n    await super.init();\n    const el = document.getElementById(this.id);\n    if (el) {\n      this.bindStaticEvents(el);\n    }\n  }\n\n  async refresh(): Promise<void> {\n    await super.refresh();\n    const el = document.getElementById(this.id);\n    if (!el) return;\n\n    if (!this.staticEventsBound) {\n      this.bindStaticEvents(el);\n    }\n\n    const currentTenant = this.tenantManager.getCurrentTenant();\n    const noTenantState = el.querySelector<HTMLElement>(SELECTOR_NO_TENANT_STATE);\n    const content = el.querySelector<HTMLElement>(SELECTOR_CONTENT);\n\n    if (!currentTenant) {\n      if (this.broadcasterView) {\n        await this.broadcasterView.destroy();\n        this.broadcasterView = null;\n        this.renderedChannelName = null;\n      }\n      if (noTenantState) noTenantState.style.display = DISPLAY_BLOCK;\n      if (content) content.style.display = DISPLAY_NONE;\n      return;\n    }\n\n    if (noTenantState) noTenantState.style.display = DISPLAY_NONE;\n    if (content) content.style.display = DISPLAY_BLOCK;\n\n    const channels = this.tenantManager.getChannels();\n    const currentChannel = this.tenantManager.getCurrentChannel();\n\n    // Update channels navigation tabs\n    const tabsList = el.querySelector<HTMLElement>(SELECTOR_CHANNEL_TABS_LIST);\n    if (tabsList) {\n      tabsList.replaceChildren();\n\n      channels.forEach((ch) => {\n        const btn = document.createElement(TAG_BUTTON);\n        btn.type = 'button';\n        const isCurrent = currentChannel && currentChannel.name === ch.name;\n        btn.className = `${CLASS_CHANNEL_TAB_BTN} ${isCurrent ? CLASS_BTN_PRIMARY : CLASS_BTN_OUTLINE}`;\n        btn.textContent = `@${ch.name}`;\n        btn.addEventListener(EVENT_CLICK, () => {\n          this.tenantManager.selectChannel(ch.name);\n        });\n        tabsList.appendChild(btn);\n      });\n    }\n\n    // Update audience link\n    const linkWrapper = el.querySelector<HTMLElement>(SELECTOR_AUDIENCE_LINK_WRAPPER);\n    if (linkWrapper) {\n      linkWrapper.replaceChildren();\n      linkWrapper.append('Audience link: ');\n      if (currentChannel) {\n        const link = document.createElement(TAG_A);\n        link.href = `/@${encodeURIComponent(currentChannel.name)}`;\n        link.target = '_blank';\n        link.rel = 'noopener';\n        link.className = CLASS_AUDIENCE_LINK;\n        link.textContent = `fluent.church/@${currentChannel.name}`;\n        linkWrapper.appendChild(link);\n      } else {\n        linkWrapper.append('None');\n      }\n    }\n\n    // Mount or update BroadcasterView\n    const mount = el.querySelector<HTMLElement>(SELECTOR_BROADCASTER_MOUNT);\n    const noChannelsPlaceholder = el.querySelector<HTMLElement>(SELECTOR_NO_CHANNELS);\n\n    if (currentChannel && mount) {\n      if (noChannelsPlaceholder) noChannelsPlaceholder.style.display = DISPLAY_NONE;\n      mount.style.display = DISPLAY_BLOCK;\n\n      if (!this.broadcasterView || this.renderedChannelName !== currentChannel.name) {\n        if (this.broadcasterView) {\n          await this.broadcasterView.destroy();\n          this.broadcasterView = null;\n        }\n        mount.replaceChildren();\n        this.renderedChannelName = currentChannel.name;\n        this.broadcasterView = new BroadcasterView(mount, currentChannel.name, {\n          onSessionChange: () => {\n            this.tenantManager.updateSessionStatus();\n          },\n        });\n      }\n    } else {\n      if (this.broadcasterView) {\n        await this.broadcasterView.destroy();\n        this.broadcasterView = null;\n        this.renderedChannelName = null;\n      }\n      if (mount) mount.style.display = DISPLAY_NONE;\n      if (noChannelsPlaceholder) noChannelsPlaceholder.style.display = DISPLAY_BLOCK;\n    }\n  }\n\n  private bindStaticEvents(el: HTMLElement): void {\n    if (this.staticEventsBound) return;\n    this.staticEventsBound = true;\n\n    // Empty state create organization button\n    const emptyCreateBtn = el.querySelector(SELECTOR_EMPTY_CREATE_BTN);\n    emptyCreateBtn?.addEventListener(EVENT_CLICK, () => {\n      this.pageManager.showPanel('new-tenant-panel');\n    });\n  }\n\n  async destroy(): Promise<void> {\n    if (this.broadcasterView) {\n      await this.broadcasterView.destroy();\n      this.broadcasterView = null;\n      this.renderedChannelName = null;\n    }\n  }\n}\n","import { PanelHandler, PageManager } from '@tingdahl/tsutils/ui';\nimport { fetchSignedUrl } from '@tingdahl/tsutils/net';\nimport { bindImage } from '../../core/image_utils';\nimport { TenantManager } from '../../core/tenant_manager';\nimport { fetchWithAuth } from '../../api-config';\nimport { openQRCodeModal } from '../../channel-badge';\nimport { getCacheSync } from '../../core/cache';\nimport { t } from '../../i18n';\nimport {\n  EVENT_CLICK,\n  EVENT_CHANGE,\n  EVENT_INPUT,\n  EVENT_SUBMIT,\n  CLASS_HIDDEN,\n  DISPLAY_NONE,\n  DISPLAY_BLOCK,\n  DISPLAY_FLEX,\n  DISPLAY_INLINE_BLOCK,\n  HTTP_POST,\n  HTTP_PUT,\n  HTTP_DELETE,\n} from '../../core/strings';\n\nconst CHANNEL_NAME_REGEX = /^[a-zA-Z0-9_]+(-[a-zA-Z0-9_]+)*$/;\n\nexport class ChannelsPanel extends PanelHandler {\n  private tenantManager: TenantManager;\n  private pageManager: PageManager;\n  private staticEventsBound: boolean = false;\n\n  constructor(tenantManager: TenantManager, pageManager: PageManager) {\n    super('channels-panel');\n    this.tenantManager = tenantManager;\n    this.pageManager = pageManager;\n\n    this.tenantManager.onTenantChanged(() => {\n      this.refresh();\n    });\n\n    this.tenantManager.onChannelChanged(() => {\n      this.refresh();\n    });\n  }\n\n  async init(): Promise<void> {\n    await super.init();\n    const el = document.getElementById(this.id);\n    if (el) {\n      this.bindStaticEvents(el);\n    }\n  }\n\n  async refresh(): Promise<void> {\n    await super.refresh();\n    const el = document.getElementById(this.id);\n    if (!el) return;\n\n    if (!this.staticEventsBound) {\n      this.bindStaticEvents(el);\n    }\n\n    const currentTenant = this.tenantManager.getCurrentTenant();\n    const noTenantState = el.querySelector<HTMLElement>('#channels-no-tenant-state');\n    const channelsContent = el.querySelector<HTMLElement>('#channels-content');\n\n    if (!currentTenant) {\n      if (noTenantState) noTenantState.style.display = DISPLAY_BLOCK;\n      if (channelsContent) channelsContent.style.display = DISPLAY_NONE;\n      return;\n    }\n\n    if (noTenantState) noTenantState.style.display = DISPLAY_NONE;\n    if (channelsContent) channelsContent.style.display = DISPLAY_FLEX;\n\n    const tenantNameEl = el.querySelector<HTMLElement>('#channels-current-tenant-name');\n    if (tenantNameEl) tenantNameEl.textContent = currentTenant.name;\n\n    const channels = this.tenantManager.getChannels();\n    const currentChannel = this.tenantManager.getCurrentChannel();\n    const isAdmin = this.tenantManager.isTenantAdmin();\n\n    const createBtn = el.querySelector<HTMLElement>('#open-create-channel-modal-btn');\n    if (createBtn) createBtn.style.display = isAdmin ? DISPLAY_FLEX : DISPLAY_NONE;\n\n    // 1. Selector dropdown\n    const select = el.querySelector<HTMLSelectElement>('#channel-select');\n    if (select) {\n      select.replaceChildren();\n      for (const ch of channels) {\n        const opt = document.createElement('option');\n        opt.value = ch.name;\n        opt.textContent = `@${ch.name}`;\n        if (currentChannel?.name === ch.name) opt.selected = true;\n        select.appendChild(opt);\n      }\n    }\n\n    // 2. Channels List Card\n    const countEl = el.querySelector<HTMLElement>('#channels-count');\n    if (countEl) countEl.textContent = String(channels.length);\n\n    const listEmpty = el.querySelector<HTMLElement>('#channels-list-empty');\n    const listBody = el.querySelector<HTMLElement>('#channels-list-body');\n    const emptyAddBtn = el.querySelector<HTMLElement>('#channels-empty-add-btn');\n    if (emptyAddBtn) emptyAddBtn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n\n    if (channels.length === 0) {\n      if (listEmpty) listEmpty.style.display = DISPLAY_BLOCK;\n      if (listBody) {\n        listBody.style.display = DISPLAY_NONE;\n        listBody.replaceChildren();\n      }\n    } else {\n      if (listEmpty) listEmpty.style.display = DISPLAY_NONE;\n      if (listBody) {\n        listBody.style.display = DISPLAY_FLEX;\n        listBody.replaceChildren();\n\n        const template = document.getElementById('channel-row-template') as HTMLTemplateElement | null;\n        for (const ch of channels) {\n          const audienceUrl = `${window.location.origin}/@${ch.name}`;\n          if (template) {\n            const clone = template.content.cloneNode(true) as DocumentFragment;\n            const row = clone.querySelector('.channel-row') as HTMLElement;\n            const nameSpan = row.querySelector('.channel-name-text');\n            if (nameSpan) {\n              nameSpan.textContent = `@${ch.name}${ch.isPrimary ? ' (Primary)' : ''}`;\n            }\n            const link = row.querySelector('.channel-audience-link') as HTMLAnchorElement | null;\n            if (link) {\n              link.href = audienceUrl;\n              link.textContent = audienceUrl;\n            }\n            const copyBtn = row.querySelector('.copy-link-btn') as HTMLElement | null;\n            if (copyBtn) copyBtn.dataset.url = audienceUrl;\n            const qrBtn = row.querySelector('.qr-modal-btn') as HTMLElement | null;\n            if (qrBtn) {\n              qrBtn.dataset.url = audienceUrl;\n              qrBtn.dataset.channel = ch.name;\n            }\n            const delBtn = row.querySelector('.delete-channel-btn') as HTMLElement | null;\n            if (delBtn) {\n              delBtn.dataset.channel = ch.name;\n              delBtn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n            }\n            listBody.appendChild(row);\n          }\n        }\n        this.bindRowEvents(listBody);\n      }\n    }\n\n    // 3. Channel Settings Card\n    const settingsEmpty = el.querySelector<HTMLElement>('#channel-settings-empty');\n    const settingsContent = el.querySelector<HTMLElement>('#channel-settings-content');\n\n    if (!currentChannel) {\n      if (settingsEmpty) settingsEmpty.style.display = DISPLAY_BLOCK;\n      if (settingsContent) settingsContent.style.display = DISPLAY_NONE;\n    } else {\n      if (settingsEmpty) settingsEmpty.style.display = DISPLAY_NONE;\n      if (settingsContent) settingsContent.style.display = DISPLAY_BLOCK;\n\n      const nameEl = el.querySelector<HTMLElement>('#selected-channel-name');\n      if (nameEl) nameEl.textContent = currentChannel.name;\n\n      const audienceUrl = `${window.location.origin}/@${currentChannel.name}`;\n\n      const copyBtn = el.querySelector<HTMLButtonElement>('#selected-copy-link-btn');\n      if (copyBtn) copyBtn.setAttribute('data-url', audienceUrl);\n\n      const qrBtn = el.querySelector<HTMLButtonElement>('#selected-qr-modal-btn');\n      if (qrBtn) {\n        qrBtn.setAttribute('data-url', audienceUrl);\n        qrBtn.setAttribute('data-channel', currentChannel.name);\n      }\n\n\n      const deleteBtn = el.querySelector<HTMLButtonElement>('#selected-delete-channel-btn');\n      if (deleteBtn) {\n        deleteBtn.setAttribute('data-channel', currentChannel.name);\n        deleteBtn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n      }\n\n\n\n      const hasLogo = Boolean(currentChannel.logoFileId);\n      const logoImg = el.querySelector<HTMLImageElement>('#logo-img');\n      const logoPlaceholder = el.querySelector<HTMLElement>('#logo-placeholder');\n      const logoRemoveBtn = el.querySelector<HTMLElement>('#logo-remove-btn');\n      const logoInput = el.querySelector<HTMLInputElement>('#logo-upload');\n      const livePreviewLogo = el.querySelector<HTMLImageElement>('#live-preview-logo');\n      const livePreviewDefault = el.querySelector<HTMLElement>('#live-preview-default-logo');\n\n      if (logoInput) logoInput.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n      if (logoRemoveBtn) logoRemoveBtn.style.display = (isAdmin && hasLogo) ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n\n      if (hasLogo) {\n        fetchSignedUrl(`/api/channels/${encodeURIComponent(currentChannel.name)}/logo?v=${currentChannel.logoFileId}_${currentChannel.updatedAtUnixMs || 0}`)\n          .then(signedUrl => {\n            bindImage(logoImg, signedUrl, { placeholderEl: logoPlaceholder, removeBtnEl: logoRemoveBtn });\n            bindImage(livePreviewLogo, signedUrl, { fallbackEl: livePreviewDefault });\n          });\n      } else {\n        bindImage(logoImg, null, { placeholderEl: logoPlaceholder, removeBtnEl: logoRemoveBtn });\n        bindImage(livePreviewLogo, null, { fallbackEl: livePreviewDefault });\n      }\n\n      const bg = currentChannel.backgroundColor || '#0a0a0a';\n      const colorPicker = el.querySelector<HTMLInputElement>('#bgcolor-picker');\n      const colorText = el.querySelector<HTMLInputElement>('#bgcolor-text');\n      const resetColorBtn = el.querySelector<HTMLElement>('#bgcolor-reset-btn');\n      const livePreview = el.querySelector<HTMLElement>('#live-preview');\n      if (colorPicker) {\n        colorPicker.value = bg;\n        colorPicker.disabled = !isAdmin;\n      }\n      if (colorText) {\n        colorText.value = bg;\n        colorText.disabled = !isAdmin;\n      }\n      if (resetColorBtn) resetColorBtn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n      el.querySelectorAll<HTMLElement>('.color-swatch-btn').forEach(btn => {\n        btn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n      });\n      if (livePreview) livePreview.style.background = bg;\n\n      const primaryCheckbox = el.querySelector<HTMLInputElement>('#channel-primary-checkbox');\n      if (primaryCheckbox) {\n        primaryCheckbox.checked = Boolean(currentChannel.isPrimary);\n        primaryCheckbox.disabled = !isAdmin;\n      }\n\n      const saveBtn = el.querySelector<HTMLElement>('#save-settings-btn');\n      if (saveBtn) saveBtn.style.display = isAdmin ? DISPLAY_INLINE_BLOCK : DISPLAY_NONE;\n\n      const previewName = el.querySelector<HTMLElement>('#live-preview-channel-name');\n      if (previewName) previewName.textContent = currentChannel.name;\n    }\n  }\n\n  private bindRowEvents(container: HTMLElement): void {\n\n    container.querySelectorAll<HTMLButtonElement>('.copy-link-btn').forEach(btn => {\n      btn.addEventListener(EVENT_CLICK, async () => {\n        const url = btn.getAttribute('data-url');\n        if (url) {\n          try {\n            await navigator.clipboard.writeText(url);\n            const originalText = btn.textContent;\n            btn.textContent = 'Copied!';\n            setTimeout(() => {\n              btn.textContent = originalText;\n            }, 2000);\n          } catch (err) {\n            console.error('Failed to copy URL:', err);\n          }\n        }\n      });\n    });\n\n    container.querySelectorAll<HTMLButtonElement>('.qr-modal-btn').forEach(btn => {\n      btn.addEventListener(EVENT_CLICK, () => {\n        const url = btn.getAttribute('data-url');\n        const ch = btn.getAttribute('data-channel');\n        if (url && ch) {\n          openQRCodeModal(url, ch);\n        }\n      });\n    });\n\n    container.querySelectorAll<HTMLButtonElement>('.delete-channel-btn').forEach(btn => {\n      btn.addEventListener(EVENT_CLICK, async () => {\n        const chName = btn.getAttribute('data-channel');\n        if (chName) {\n          await this.handleDeleteChannel(chName, btn);\n        }\n      });\n    });\n  }\n\n  private async handleDeleteChannel(chName: string, triggerBtn?: HTMLButtonElement | null): Promise<void> {\n    const currentTenant = this.tenantManager.getCurrentTenant();\n    if (!currentTenant) return;\n\n    if (!confirm(`Are you sure you want to delete channel @${chName}? All active broadcasts to this channel will cease.`)) {\n      return;\n    }\n\n    if (triggerBtn) triggerBtn.disabled = true;\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/channels/${encodeURIComponent(chName)}`, {\n        method: HTTP_DELETE,\n      });\n\n      if (!resp.ok) {\n        const errData = await resp.json().catch(() => ({}));\n        throw new Error(errData.error || `Failed to delete channel (HTTP ${resp.status})`);\n      }\n\n      await this.tenantManager.loadChannelsForCurrentTenant();\n      await this.refresh();\n    } catch (err: any) {\n      alert(err.message || 'Failed to delete channel');\n      if (triggerBtn) triggerBtn.disabled = false;\n    }\n  }\n\n  private bindStaticEvents(el: HTMLElement): void {\n    if (this.staticEventsBound) return;\n    this.staticEventsBound = true;\n\n    // Create organization from empty state\n    el.querySelector('#channels-empty-create-btn')?.addEventListener(EVENT_CLICK, () => {\n      this.pageManager.showPanel('new-tenant-panel');\n    });\n\n    // Channel selector dropdown\n    const channelSelect = el.querySelector('#channel-select') as HTMLSelectElement | null;\n    channelSelect?.addEventListener(EVENT_CHANGE, () => {\n      const chName = channelSelect?.value;\n      if (chName) {\n        this.tenantManager.selectChannel(chName);\n        this.refresh();\n      }\n    });\n\n\n    el.querySelector('#selected-copy-link-btn')?.addEventListener(EVENT_CLICK, async () => {\n      const ch = this.tenantManager.getCurrentChannel()?.name;\n      if (ch) {\n        const url = `${window.location.origin}/@${ch}`;\n        await navigator.clipboard.writeText(url);\n        const btn = el.querySelector<HTMLButtonElement>('#selected-copy-link-btn');\n        if (btn) {\n          btn.textContent = 'Copied!';\n          setTimeout(() => { btn.textContent = 'Copy Link'; }, 2000);\n        }\n      }\n    });\n\n    el.querySelector('#selected-qr-modal-btn')?.addEventListener(EVENT_CLICK, () => {\n      const ch = this.tenantManager.getCurrentChannel()?.name;\n      if (ch) {\n        const url = `${window.location.origin}/@${ch}`;\n        openQRCodeModal(url, ch);\n      }\n    });\n\n    el.querySelector('#selected-delete-channel-btn')?.addEventListener(EVENT_CLICK, async () => {\n      const ch = this.tenantManager.getCurrentChannel()?.name;\n      if (ch) {\n        await this.handleDeleteChannel(ch, el.querySelector<HTMLButtonElement>('#selected-delete-channel-btn'));\n      }\n    });\n\n\n\n    // Logo upload handling\n    const logoInput = el.querySelector('#logo-upload') as HTMLInputElement | null;\n    logoInput?.addEventListener(EVENT_CHANGE, async () => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      const file = logoInput.files?.[0];\n      if (!file || !currentTenant) return;\n      const chName = this.tenantManager.getCurrentChannel()?.name;\n      if (!chName) return;\n\n      const isPng = file.type === 'image/png' || file.name.toLowerCase().endsWith('.png');\n      const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg');\n      if (!isPng && !isSvg) {\n        alert('Unsupported image format. Logo must be PNG or SVG.');\n        logoInput.value = '';\n        return;\n      }\n      if (file.size > 64 * 1024) {\n        alert('Image exceeds the 64KB size limit. Please upload a smaller logo.');\n        logoInput.value = '';\n        return;\n      }\n\n      const contentType = isSvg ? 'image/svg+xml' : 'image/png';\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/channels/${encodeURIComponent(chName)}/logo`, {\n          method: HTTP_POST,\n          body: file,\n          headers: { 'Content-Type': contentType },\n        });\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          alert(errData.error || 'Failed to upload logo');\n          return;\n        }\n        await this.tenantManager.loadChannelsForCurrentTenant();\n        getCacheSync().broadcastInvalidation('channels', currentTenant.id);\n        await this.refresh();\n      } catch (e: any) {\n        alert(e.message || 'Failed to upload logo');\n      } finally {\n        logoInput.value = '';\n      }\n    });\n\n    // Logo removal handling\n    const logoRemoveBtn = el.querySelector('#logo-remove-btn') as HTMLButtonElement | null;\n    logoRemoveBtn?.addEventListener(EVENT_CLICK, async () => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      const chName = this.tenantManager.getCurrentChannel()?.name;\n      if (!currentTenant || !chName) return;\n      try {\n        await fetchWithAuth(`/api/tenants/${currentTenant.id}/channels/${encodeURIComponent(chName)}/logo`, { method: HTTP_DELETE });\n        await this.tenantManager.loadChannelsForCurrentTenant();\n        getCacheSync().broadcastInvalidation('channels', currentTenant.id);\n        await this.refresh();\n      } catch (e) {\n        alert('Failed to remove logo');\n      }\n    });\n\n    // Live color picker and swatches handling\n    const colorPicker = el.querySelector('#bgcolor-picker') as HTMLInputElement | null;\n    const colorText = el.querySelector('#bgcolor-text') as HTMLInputElement | null;\n    const previewBox = el.querySelector('#live-preview') as HTMLElement | null;\n    const resetColorBtn = el.querySelector('#bgcolor-reset-btn') as HTMLButtonElement | null;\n\n    const setPreviewColor = (hex: string) => {\n      if (colorPicker) colorPicker.value = hex;\n      if (colorText) colorText.value = hex;\n      if (previewBox) previewBox.style.background = hex;\n    };\n\n    colorPicker?.addEventListener(EVENT_INPUT, () => {\n      setPreviewColor(colorPicker.value);\n    });\n\n    colorText?.addEventListener(EVENT_CHANGE, () => {\n      const val = colorText.value.trim();\n      if (/^#([0-9a-fA-F]{3}){1,2}$/.test(val)) {\n        setPreviewColor(val);\n      }\n    });\n\n    resetColorBtn?.addEventListener(EVENT_CLICK, () => {\n      setPreviewColor('#0a0a0a');\n    });\n\n    el.querySelectorAll<HTMLButtonElement>('.color-swatch-btn').forEach(btn => {\n      btn.addEventListener(EVENT_CLICK, () => {\n        const color = btn.getAttribute('data-color');\n        if (color) setPreviewColor(color);\n      });\n    });\n\n    // Save settings button (background color)\n    const saveBtn = el.querySelector('#save-settings-btn') as HTMLButtonElement | null;\n    saveBtn?.addEventListener(EVENT_CLICK, async () => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      const chName = this.tenantManager.getCurrentChannel()?.name;\n      if (!currentTenant || !chName) return;\n      const colorTextInput = el.querySelector('#bgcolor-text') as HTMLInputElement | null;\n      const primaryCheckbox = el.querySelector('#channel-primary-checkbox') as HTMLInputElement | null;\n      const payload: any = {};\n      if (colorTextInput?.value) {\n        payload.background_color = colorTextInput.value.trim();\n      } else if (colorPicker?.value) {\n        payload.background_color = colorPicker.value;\n      }\n      if (primaryCheckbox) {\n        payload.is_primary = primaryCheckbox.checked;\n      }\n      saveBtn.disabled = true;\n      saveBtn.textContent = 'Saving...';\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/channels/${encodeURIComponent(chName)}/settings`, {\n          method: HTTP_PUT,\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify(payload),\n        });\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || 'Failed to save settings');\n        }\n        await this.tenantManager.loadChannelsForCurrentTenant();\n        getCacheSync().broadcastInvalidation('channels', currentTenant.id);\n        await this.refresh();\n        saveBtn.textContent = 'Saved!';\n        setTimeout(() => {\n          saveBtn.textContent = 'Save Settings';\n          saveBtn.disabled = false;\n        }, 1500);\n      } catch (e: any) {\n        alert(e.message || 'Failed to save channel settings');\n        saveBtn.disabled = false;\n        saveBtn.textContent = 'Save Settings';\n      }\n    });\n\n    // Modal logic\n    const modal = el.querySelector('#create-channel-modal') as HTMLElement | null;\n    const openBtn = el.querySelector('#open-create-channel-modal-btn');\n    const emptyAddBtn = el.querySelector('#channels-empty-add-btn');\n    const closeBtn = el.querySelector('#close-create-channel-modal');\n    const cancelBtn = el.querySelector('#cancel-create-channel-btn');\n    const form = el.querySelector('#create-channel-form') as HTMLFormElement | null;\n    const input = el.querySelector('#create-channel-name-input') as HTMLInputElement | null;\n    const errorDiv = el.querySelector('#create-channel-error') as HTMLElement | null;\n    const submitBtn = el.querySelector('#submit-create-channel-btn') as HTMLButtonElement | null;\n\n    const openModal = () => {\n      if (modal) {\n        modal.classList.remove(CLASS_HIDDEN);\n        modal.style.display = DISPLAY_FLEX;\n        if (input) {\n          input.value = '';\n          input.focus();\n        }\n        if (errorDiv) errorDiv.style.display = DISPLAY_NONE;\n      }\n    };\n\n    const closeModal = () => {\n      if (modal) {\n        modal.classList.add(CLASS_HIDDEN);\n        modal.style.display = DISPLAY_NONE;\n      }\n    };\n\n    openBtn?.addEventListener(EVENT_CLICK, openModal);\n    emptyAddBtn?.addEventListener(EVENT_CLICK, openModal);\n    closeBtn?.addEventListener(EVENT_CLICK, closeModal);\n    cancelBtn?.addEventListener(EVENT_CLICK, closeModal);\n\n    input?.addEventListener(EVENT_INPUT, () => {\n      input.value = input.value\n        .replace(/^@+/, '')\n        .replace(/[^a-zA-Z0-9_-]/g, '')\n        .replace(/--+/g, '-')\n        .replace(/^-/, '');\n    });\n\n    form?.addEventListener(EVENT_SUBMIT, async (e) => {\n      e.preventDefault();\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!currentTenant || !input || !submitBtn || !errorDiv) return;\n\n      const chName = input.value.trim().replace(/^@+/, '');\n      if (chName.length < 6 || chName.length > 20 || !CHANNEL_NAME_REGEX.test(chName)) {\n        errorDiv.textContent = t('studio.invalidChannelError');\n        errorDiv.style.display = DISPLAY_BLOCK;\n        return;\n      }\n\n      submitBtn.disabled = true;\n      errorDiv.style.display = DISPLAY_NONE;\n\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/channels`, {\n          method: HTTP_POST,\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ name: chName }),\n        });\n\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || `Failed to create channel (HTTP ${resp.status})`);\n        }\n\n        closeModal();\n        await this.tenantManager.loadChannelsForCurrentTenant();\n        getCacheSync().broadcastInvalidation('channels', currentTenant.id);\n        await this.refresh();\n      } catch (err: any) {\n        errorDiv.textContent = err.message || 'Failed to create channel';\n        errorDiv.style.display = DISPLAY_BLOCK;\n      } finally {\n        submitBtn.disabled = false;\n      }\n    });\n  }\n}\n","import { PanelHandler, PageManager } from '@tingdahl/tsutils/ui';\nimport { TenantManager } from '../../core/tenant_manager';\nimport { AuthManager } from '../../core/auth';\nimport { fetchWithAuth } from '../../api-config';\nimport { entitlement, users, devices } from '../../proto/compiled_proto';\nimport {\n  computeEntitlementSummary,\n  DIM_KEY_STREAMING_SECONDS,\n  DIM_KEY_PAID_UNTIL,\n  toNumeric,\n  getDimensionValue,\n  formatDurationDelta,\n  formatDateTime,\n} from '../../core/entitlement_utils';\nimport QRCode from 'qrcode';\nimport {\n  DISPLAY_NONE,\n  DISPLAY_BLOCK,\n  DISPLAY_FLEX,\n  DISPLAY_INLINE_FLEX,\n  DISPLAY_INLINE_BLOCK,\n  CLASS_HIDDEN,\n  EVENT_CLICK,\n  EVENT_SUBMIT,\n  HTTP_PUT,\n} from '../../core/strings';\n\n\nexport class TenantPanel extends PanelHandler {\n  private tenantManager: TenantManager;\n  private authManager: AuthManager;\n  private pageManager: PageManager;\n  private staticEventsBound: boolean = false;\n  private isRefreshing: boolean = false;\n\n  constructor(tenantManager: TenantManager, authManager: AuthManager, pageManager: PageManager) {\n    super('tenant-panel');\n    this.tenantManager = tenantManager;\n    this.authManager = authManager;\n    this.pageManager = pageManager;\n\n    this.tenantManager.onTenantChanged(() => {\n      this.refresh();\n    });\n\n    this.tenantManager.onSessionStatusChanged(() => {\n      this.refresh();\n    });\n  }\n\n  async init(): Promise<void> {\n    await super.init();\n    const el = document.getElementById(this.id);\n    if (el) {\n      this.bindStaticEvents(el);\n    }\n  }\n\n  async refresh(): Promise<void> {\n    await super.refresh();\n    const el = document.getElementById(this.id);\n    if (!el) return;\n\n    // Only fetch and render organization details when the Organization panel is currently active\n    if (this.pageManager.getActivePanelId() !== this.id) {\n      return;\n    }\n\n    if (this.isRefreshing) return;\n    this.isRefreshing = true;\n\n    try {\n      if (!this.staticEventsBound) {\n        this.bindStaticEvents(el);\n      }\n\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      const noTenantState = el.querySelector<HTMLElement>('#tenant-no-tenant-state');\n      const tenantContent = el.querySelector<HTMLElement>('#tenant-content');\n\n      if (!currentTenant) {\n        if (noTenantState) noTenantState.style.display = DISPLAY_BLOCK;\n        if (tenantContent) tenantContent.style.display = DISPLAY_NONE;\n        return;\n      }\n\n      if (noTenantState) noTenantState.style.display = DISPLAY_NONE;\n      if (tenantContent) tenantContent.style.display = DISPLAY_FLEX;\n\n      const isAdmin = this.tenantManager.isTenantAdmin();\n      const currentUser = this.authManager.getUser();\n\n      // Header values\n      const nameEl = el.querySelector<HTMLElement>('#tenant-name');\n      if (nameEl) nameEl.textContent = currentTenant.name;\n\n      const renameBtn = el.querySelector<HTMLElement>('#tenant-rename-btn');\n      if (renameBtn) {\n        renameBtn.style.display = (isAdmin && !this.authManager.isDeviceSession())\n          ? DISPLAY_INLINE_BLOCK\n          : DISPLAY_NONE;\n      }\n      const renameModal = el.querySelector<HTMLElement>('#tenant-rename-modal');\n      if (renameModal) {\n        renameModal.classList.add(CLASS_HIDDEN);\n        renameModal.style.display = DISPLAY_NONE;\n      }\n\n      const roleBadge = el.querySelector<HTMLElement>('#tenant-role-badge');\n      if (roleBadge) roleBadge.textContent = isAdmin ? 'Organization Admin' : 'Broadcaster';\n\n      const switchOrgBtn = el.querySelector<HTMLElement>('#tenant-switch-org-btn');\n      if (switchOrgBtn) {\n        switchOrgBtn.style.display = (!this.authManager.isDeviceSession() && this.tenantManager.getUserTenants().length > 1)\n          ? DISPLAY_INLINE_BLOCK\n          : DISPLAY_NONE;\n      }\n\n      // Fetch and interpret signed EntitlementProto through TenantManager to sync header and panels\n      await this.tenantManager.updateSessionStatus();\n      const protoDoc = this.tenantManager.getEntitlementProto();\n      const now = Date.now();\n      const summary = computeEntitlementSummary(protoDoc, now);\n\n      // Dynamically resolve dimension IDs by key name for table view\n      let streamingDimId: number | null = null;\n      let paidUntilDimId: number | null = null;\n\n      if (protoDoc?.dimensions) {\n        for (const [idStr, dim] of Object.entries(protoDoc.dimensions)) {\n          if (dim?.name === DIM_KEY_STREAMING_SECONDS) {\n            streamingDimId = Number(idStr);\n          } else if (dim?.name === DIM_KEY_PAID_UNTIL) {\n            paidUntilDimId = Number(idStr);\n          }\n        }\n      }\n      if (streamingDimId === null) streamingDimId = 1;\n      if (paidUntilDimId === null) paidUntilDimId = 2;\n\n      const availableSeconds = summary.availableSeconds;\n      const paidUntilMs = summary.subscribedUntilUnixMs;\n      const isSubValid = summary.isSubscribed;\n\n      const activeSessions = Array.from(summary.activeSessions.values());\n      const availableMinutes = Math.floor(availableSeconds / 60);\n      const availableHours = (availableMinutes / 60).toFixed(1);\n      const hasActiveSession = activeSessions.length > 0;\n\n      // Streaming balance card\n      const balanceMinEl = el.querySelector<HTMLElement>('#tenant-balance-minutes-val');\n      if (balanceMinEl) balanceMinEl.textContent = String(availableMinutes);\n\n      const balanceCard = el.querySelector<HTMLElement>('#tenant-balance-minutes');\n      if (balanceCard) {\n        balanceCard.className = `tenant-stat-value ${availableMinutes < 30 ? 'danger' : ''}`;\n      }\n\n      const balanceHoursEl = el.querySelector<HTMLElement>('#tenant-balance-hours');\n      if (balanceHoursEl) balanceHoursEl.textContent = `Approx. ${availableHours} hours available`;\n\n      const buyCreditsContainer = el.querySelector<HTMLElement>('#tenant-buy-credits-container');\n      if (buyCreditsContainer) {\n        if (isAdmin) {\n          buyCreditsContainer.classList.remove(CLASS_HIDDEN);\n          buyCreditsContainer.style.display = DISPLAY_INLINE_BLOCK;\n        } else {\n          buyCreditsContainer.classList.add(CLASS_HIDDEN);\n          buyCreditsContainer.style.display = DISPLAY_NONE;\n        }\n      }\n\n      // Active streams card\n      const streamsStatusEl = el.querySelector<HTMLElement>('#tenant-streams-status');\n      if (streamsStatusEl) {\n        streamsStatusEl.textContent = hasActiveSession ? `${activeSessions.length} Active` : '0 Active';\n        streamsStatusEl.className = `tenant-stat-value ${hasActiveSession ? 'success' : ''}`;\n      }\n\n      const streamsDescEl = el.querySelector<HTMLElement>('#tenant-streams-desc');\n      if (streamsDescEl) {\n        streamsDescEl.textContent = hasActiveSession\n          ? `${activeSessions.length} stream session${activeSessions.length > 1 ? 's' : ''} in progress`\n          : 'No active broadcast';\n      }\n\n      // Subscription status card\n      const subStatusEl = el.querySelector<HTMLElement>('#tenant-sub-status');\n      if (subStatusEl) {\n        subStatusEl.textContent = isSubValid ? 'Active' : 'Past Due';\n        subStatusEl.className = `tenant-stat-value ${isSubValid ? 'success' : 'danger'}`;\n      }\n\n      const subDescEl = el.querySelector<HTMLElement>('#tenant-sub-desc');\n      if (subDescEl) {\n        if (paidUntilMs > 0) {\n          subDescEl.textContent = `Valid until ${new Date(paidUntilMs).toLocaleDateString()}`;\n        } else {\n          subDescEl.textContent = isSubValid ? 'Active entitlement' : 'Action required';\n        }\n      }\n\n      // Render Active Streams Card\n      const activeStreamsCard = el.querySelector<HTMLElement>('#tenant-active-streams-card');\n      const activeStreamsList = el.querySelector<HTMLElement>('#tenant-active-streams-list');\n      const activeStreamsCount = el.querySelector<HTMLElement>('#tenant-active-streams-count');\n\n      if (activeStreamsCard && activeStreamsList) {\n        if (hasActiveSession) {\n          activeStreamsCard.style.display = DISPLAY_BLOCK;\n          if (activeStreamsCount) {\n            activeStreamsCount.textContent = `${activeSessions.length} Active Stream${activeSessions.length > 1 ? 's' : ''}`;\n          }\n          activeStreamsList.innerHTML = '';\n          for (const session of activeSessions) {\n            const itemEl = document.createElement('div');\n            itemEl.className = 'tenant-active-stream-item';\n            const timeStr = session.remainingSeconds < 60 ? `${session.remainingSeconds}s` : `${Math.ceil(session.remainingSeconds / 60)}m`;\n\n            itemEl.innerHTML = `\n            <div>\n              <div class=\"items-center gap-sm mb-1\">\n                <span class=\"badge-channel\">@${session.channelName}</span>\n                <span class=\"font-semibold text-sm\">Active Live Broadcast</span>\n              </div>\n              <div class=\"text-secondary text-xs\">\n                Started: ${formatDateTime(session.effectiveAtUnixMs)}\n              </div>\n            </div>\n            <div class=\"text-right\">\n              <div class=\"font-bold text-accent-emerald text-sm\">\n                ${timeStr}\n              </div>\n              <div class=\"text-muted text-xs\">\n                Expires: ${new Date(session.expiresAtUnixMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}\n              </div>\n            </div>\n          `;\n            activeStreamsList.appendChild(itemEl);\n          }\n        } else {\n          activeStreamsCard.style.display = DISPLAY_NONE;\n          activeStreamsList.innerHTML = '';\n        }\n      }\n\n      // Render Streams & Entitlements Ledger Table\n      const tbody = el.querySelector<HTMLElement>('#tenant-streams-tbody');\n      const emptyEl = el.querySelector<HTMLElement>('#tenant-streams-empty');\n\n      if (tbody) {\n        tbody.innerHTML = '';\n        const txs = [...(protoDoc?.transactions || [])].sort((a, b) => {\n          const aTime = toNumeric(a.effectiveAtUnixMs);\n          const bTime = toNumeric(b.effectiveAtUnixMs);\n          return bTime - aTime;\n        });\n\n        if (txs.length === 0) {\n          if (emptyEl) emptyEl.style.display = DISPLAY_BLOCK;\n        } else {\n          if (emptyEl) emptyEl.style.display = DISPLAY_NONE;\n\n          for (const tx of txs) {\n            const tr = document.createElement('tr');\n            const timeMs = toNumeric(tx.effectiveAtUnixMs);\n            const txType = tx.transactionType;\n            const isSessionTx = txType === entitlement.EntitlementTransactionType.ENTITLEMENT_TRANSACTION_TYPE_LEASE;\n            const streamSec = getDimensionValue(tx.dimensionValues, streamingDimId);\n            const paidUntilVal = getDimensionValue(tx.dimensionValues, paidUntilDimId);\n\n            let descContent = tx.description || 'Entitlement transaction';\n            const labelChannel = tx.labels?.channel;\n            if (labelChannel) {\n              let actionText = '';\n              if (tx.description?.startsWith(`@${labelChannel}`)) {\n                actionText = tx.description.slice(`@${labelChannel}`.length);\n              } else if (tx.description) {\n                actionText = ` ${tx.description}`;\n              }\n              descContent = `<span class=\"badge-channel\">@${labelChannel}</span>${actionText}`;\n            } else {\n              const atMatch = tx.description?.match(/@([^\\s,]+)(.*)/);\n              const channelMatch = tx.description?.match(/channel:\\s*([^\\s,]+)/i);\n              const sessionMatch = tx.description?.match(/session:([^\\s,:]+)/i);\n              if (atMatch) {\n                descContent = `<span class=\"badge-channel\">@${atMatch[1]}</span>${atMatch[2] || ''}`;\n              } else if (channelMatch) {\n                descContent = `<span class=\"badge-channel\">@${channelMatch[1]}</span> Live Broadcast`;\n              } else if (sessionMatch) {\n                descContent = `<span class=\"badge-channel\">@${sessionMatch[1]}</span> stream started`;\n              }\n            }\n\n            let descHtml = descContent;\n            if (isSessionTx) {\n              const clockIcon = `<svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"mr-1\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16 14\"></polyline></svg>`;\n              descHtml = `<div class=\"items-center gap-sm\"><span class=\"badge-lease\">${clockIcon}Preliminary</span> <span>${descContent}</span></div>`;\n            }\n\n            let impactHtml = '—';\n            if (streamSec !== 0) {\n              const isPos = streamSec > 0;\n              const colorClass = isPos ? 'text-accent-emerald' : 'text-accent-coral';\n              impactHtml = `<span class=\"${colorClass} font-semibold\">${formatDurationDelta(streamSec)}</span>`;\n            }\n\n            let paidUntilHtml = '—';\n            if (paidUntilVal > 0) {\n              paidUntilHtml = `<span class=\"font-medium\">${new Date(paidUntilVal).toLocaleDateString()}</span>`;\n            }\n\n            tr.innerHTML = `\n            <td class=\"nowrap\">${formatDateTime(timeMs)}</td>\n            <td>${descHtml}</td>\n            <td class=\"text-right nowrap tabular-nums\">${impactHtml}</td>\n            <td>${paidUntilHtml}</td>\n          `;\n            tbody.appendChild(tr);\n          }\n        }\n      }\n\n      // Billing Receipts Ledger (Admin only)\n      const billingLedgerCard = el.querySelector<HTMLElement>('#tenant-billing-ledger-card');\n      if (billingLedgerCard) {\n        if (isAdmin) {\n          billingLedgerCard.classList.remove(CLASS_HIDDEN);\n          billingLedgerCard.style.display = DISPLAY_BLOCK;\n          await this.loadAndRenderBillingReceipts(el, currentTenant.id);\n        } else {\n          billingLedgerCard.classList.add(CLASS_HIDDEN);\n          billingLedgerCard.style.display = DISPLAY_NONE;\n        }\n      }\n\n      // Team section\n      const teamTenantName = el.querySelector<HTMLElement>('#tenant-team-tenant-name');\n      if (teamTenantName) teamTenantName.textContent = currentTenant.name;\n\n      const openInviteBtn = el.querySelector<HTMLElement>('#open-invite-team-btn');\n      if (openInviteBtn) openInviteBtn.style.display = isAdmin ? DISPLAY_INLINE_FLEX : DISPLAY_NONE;\n\n      const openEnrollDeviceBtn = el.querySelector<HTMLElement>('#open-enroll-device-btn');\n      if (openEnrollDeviceBtn) openEnrollDeviceBtn.style.display = isAdmin ? DISPLAY_INLINE_FLEX : DISPLAY_NONE;\n\n      await this.loadAndRenderTeamMembers(el, currentTenant.id, isAdmin, currentUser);\n      await this.loadAndRenderDevices(el, currentTenant.id, isAdmin);\n      await this.loadAndRenderCustomDomain(el, currentTenant.id, isAdmin);\n    } finally {\n      this.isRefreshing = false;\n    }\n  }\n\n  private async loadAndRenderTeamMembers(\n    el: HTMLElement,\n    tenantId: any,\n    isAdmin: boolean,\n    currentUser: users.UserProtoRow | null\n  ): Promise<void> {\n    const listEl = el.querySelector<HTMLElement>('#tenant-team-list');\n    if (!listEl) return;\n\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${tenantId}/users`);\n      if (!resp.ok) {\n        throw new Error('Failed to load team members');\n      }\n      const members: Array<{\n        id: string;\n        email: string;\n        name: string;\n        role: string;\n        role_value: number;\n        pending: boolean;\n      }> = await resp.json();\n\n      listEl.innerHTML = '';\n\n      if (!members || members.length === 0) {\n        listEl.innerHTML = '<div class=\"list-empty-muted\">No team members found.</div>';\n        return;\n      }\n\n      for (const m of members) {\n        const item = document.createElement('div');\n        item.className = 'tenant-team-item';\n\n        const isSelf = Boolean(\n          currentUser &&\n          (m.id === currentUser.id || (currentUser.email && m.email.toLowerCase() === currentUser.email.toLowerCase()))\n        );\n        const displayName = m.name || m.email.split('@')[0] || 'Member';\n        const roleBadgeText = m.role === 'admin' ? 'Admin' : 'Broadcaster';\n        const pendingBadge = m.pending\n          ? `<span class=\"badge-pending-member\">Pending</span>`\n          : '';\n\n        const removeBtnHtml =\n          isAdmin && !isSelf\n            ? `<button type=\"button\" class=\"btn btn-sm btn-outline btn-danger-outline remove-member-btn\" data-user-id=\"${m.id}\" data-email=\"${m.email}\">Remove</button>`\n            : '';\n\n        item.innerHTML = `\n          <div class=\"flex-col gap-xs\">\n            <div class=\"font-semibold text-sm items-center\">\n              <span>${displayName}${isSelf ? ' <span class=\"text-muted font-normal text-xs\">(You)</span>' : ''}</span>\n              ${pendingBadge}\n            </div>\n            <div class=\"text-xs text-muted\">${m.email}</div>\n          </div>\n          <div class=\"items-center gap-sm\">\n            <span class=\"tenant-role-badge\">${roleBadgeText}</span>\n            ${removeBtnHtml}\n          </div>\n        `;\n\n        const removeBtn = item.querySelector<HTMLButtonElement>('.remove-member-btn');\n        if (removeBtn) {\n          removeBtn.addEventListener('click', async () => {\n            if (!confirm(`Are you sure you want to remove ${m.email} from this organization?`)) {\n              return;\n            }\n            try {\n              const delResp = await fetchWithAuth(`/api/tenants/${tenantId}/users/${m.id}`, {\n                method: 'DELETE',\n              });\n              if (!delResp.ok) {\n                const err = await delResp.json().catch(() => ({}));\n                throw new Error(err.error || 'Failed to remove member');\n              }\n              await this.loadAndRenderTeamMembers(el, tenantId, isAdmin, currentUser);\n            } catch (err: any) {\n              alert(err.message || 'Error removing member');\n            }\n          });\n        }\n\n        listEl.appendChild(item);\n      }\n    } catch (e) {\n      console.warn('Failed to load team members:', e);\n      listEl.innerHTML = '<div class=\"list-empty-muted\">Could not load team members.</div>';\n    }\n  }\n\n  private async loadAndRenderDevices(\n    el: HTMLElement,\n    tenantId: any,\n    isAdmin: boolean\n  ): Promise<void> {\n    const listEl = el.querySelector<HTMLElement>('#tenant-devices-list');\n    if (!listEl) return;\n\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${tenantId}/devices`);\n      if (!resp.ok) {\n        throw new Error('Failed to load booth devices');\n      }\n      const buffer = await resp.arrayBuffer();\n      const decoded = devices.DeviceListProto.decode(new Uint8Array(buffer));\n      const devicesList = (decoded.devices as devices.DeviceProtoRow[]) || [];\n\n      listEl.innerHTML = '';\n\n      if (!devicesList || devicesList.length === 0) {\n        listEl.innerHTML = '<div class=\"list-empty-muted\">No sound booth devices enrolled yet. Click \"+ Enroll Booth Device\" to authorize equipment.</div>';\n        return;\n      }\n\n      for (const d of devicesList) {\n        const item = document.createElement('div');\n        item.className = 'tenant-team-item';\n\n        const channelBadge = d.channelName\n          ? `<span class=\"badge-channel\">@${d.channelName}</span>`\n          : `<span class=\"badge-neutral badge-channel-all\">All Channels</span>`;\n\n        const lastUsedMs = Number(d.lastUsedUnixMs || 0);\n        const lastUsedText = lastUsedMs > 0\n          ? `• Last used ${new Date(lastUsedMs).toLocaleDateString()}`\n          : '• Never used';\n\n        const revokeBtnHtml = isAdmin\n          ? `<button type=\"button\" class=\"btn btn-sm btn-outline btn-danger-outline revoke-device-btn\" data-device-id=\"${d.id}\">Revoke</button>`\n          : '';\n\n        const createdAtMs = Number(d.createdAtUnixMs || 0);\n        item.innerHTML = `\n          <div class=\"flex-col gap-xs\">\n            <div class=\"font-semibold text-sm items-center gap-sm\">\n              <span>${d.label || 'Booth Device'}</span>\n              ${channelBadge}\n              <span class=\"badge-success badge-active-device\">Active</span>\n            </div>\n            <div class=\"text-xs text-muted items-center gap-xs\">\n              <span class=\"device-id-mono\">ID: ${(d.id || '').slice(0, 8)}...</span>\n              <span>Enrolled ${new Date(createdAtMs).toLocaleDateString()}</span>\n              <span>${lastUsedText}</span>\n            </div>\n          </div>\n          <div class=\"items-center gap-sm\">\n            ${revokeBtnHtml}\n          </div>\n        `;\n\n        const revokeBtn = item.querySelector<HTMLButtonElement>('.revoke-device-btn');\n        if (revokeBtn) {\n          revokeBtn.addEventListener('click', async () => {\n            if (!confirm(`Revoke authorization for booth device \"${d.label || d.id}\"? It will be immediately disconnected.`)) {\n              return;\n            }\n            try {\n              const delResp = await fetchWithAuth(`/api/tenants/${tenantId}/devices/${d.id}`, {\n                method: 'DELETE',\n              });\n              if (!delResp.ok) {\n                const err = await delResp.json().catch(() => ({}));\n                throw new Error(err.error || 'Failed to revoke booth device');\n              }\n              await this.loadAndRenderDevices(el, tenantId, isAdmin);\n            } catch (err: any) {\n              alert(err.message || 'Error revoking booth device');\n            }\n          });\n        }\n\n        listEl.appendChild(item);\n      }\n    } catch (e) {\n      console.warn('Failed to load booth devices:', e);\n      listEl.innerHTML = '<div class=\"list-empty-muted\">Could not load booth devices.</div>';\n    }\n  }\n\n  private bindStaticEvents(el: HTMLElement): void {\n    if (this.staticEventsBound) return;\n    this.staticEventsBound = true;\n\n    // Create org button in empty state\n    el.querySelector('#tenant-panel-create-btn')?.addEventListener(EVENT_CLICK, () => {\n      this.pageManager.showPanel('new-tenant-panel');\n    });\n\n    // Rename organization modal events\n    const renameBtn = el.querySelector<HTMLButtonElement>('#tenant-rename-btn');\n    const renameModal = el.querySelector<HTMLElement>('#tenant-rename-modal');\n    const renameCloseBtn = el.querySelector<HTMLButtonElement>('#tenant-rename-close-btn');\n    const renameCancelBtn = el.querySelector<HTMLButtonElement>('#tenant-rename-cancel-btn');\n    const renameForm = el.querySelector<HTMLFormElement>('#tenant-rename-form');\n    const renameInput = el.querySelector<HTMLInputElement>('#tenant-rename-input');\n    const renameError = el.querySelector<HTMLElement>('#tenant-rename-error');\n    const renameSubmitBtn = el.querySelector<HTMLButtonElement>('#tenant-rename-submit-btn');\n\n    const openRenameModal = () => {\n      if (!renameModal || !renameInput) return;\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      renameModal.classList.remove(CLASS_HIDDEN);\n      renameModal.style.display = DISPLAY_FLEX;\n      renameInput.value = currentTenant?.name || '';\n      if (renameError) {\n        renameError.style.display = DISPLAY_NONE;\n        renameError.classList.add(CLASS_HIDDEN);\n      }\n      renameInput.focus();\n      renameInput.select();\n    };\n\n    const closeRenameModal = () => {\n      if (renameModal) {\n        renameModal.classList.add(CLASS_HIDDEN);\n        renameModal.style.display = DISPLAY_NONE;\n      }\n      if (renameError) {\n        renameError.style.display = DISPLAY_NONE;\n        renameError.classList.add(CLASS_HIDDEN);\n      }\n    };\n\n    renameBtn?.addEventListener(EVENT_CLICK, openRenameModal);\n    renameCloseBtn?.addEventListener(EVENT_CLICK, closeRenameModal);\n    renameCancelBtn?.addEventListener(EVENT_CLICK, closeRenameModal);\n\n    renameModal?.addEventListener(EVENT_CLICK, (e) => {\n      if (e.target === renameModal) closeRenameModal();\n    });\n\n    window.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape' && renameModal && !renameModal.classList.contains(CLASS_HIDDEN)) {\n        closeRenameModal();\n      }\n    });\n\n    renameForm?.addEventListener(EVENT_SUBMIT, async (e) => {\n      e.preventDefault();\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!currentTenant || !renameInput || !renameSubmitBtn) return;\n\n      const newName = renameInput.value.trim();\n      if (!newName) {\n        if (renameError) {\n          renameError.textContent = 'Organization name cannot be empty';\n          renameError.classList.remove(CLASS_HIDDEN);\n          renameError.style.display = DISPLAY_BLOCK;\n        }\n        return;\n      }\n\n      renameSubmitBtn.disabled = true;\n      if (renameError) {\n        renameError.style.display = DISPLAY_NONE;\n        renameError.classList.add(CLASS_HIDDEN);\n      }\n\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}`, {\n          method: HTTP_PUT,\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ name: newName }),\n        });\n\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || `Failed to update name (HTTP ${resp.status})`);\n        }\n\n        const data = await resp.json();\n        const updatedName = data.name || newName;\n        await this.tenantManager.updateCurrentTenantName(updatedName);\n        const nameEl = el.querySelector<HTMLElement>('#tenant-name');\n        if (nameEl) nameEl.textContent = updatedName;\n\n        closeRenameModal();\n      } catch (err: any) {\n        if (renameError) {\n          renameError.textContent = err.message || 'Failed to update organization name';\n          renameError.classList.remove(CLASS_HIDDEN);\n          renameError.style.display = DISPLAY_BLOCK;\n        }\n      } finally {\n        renameSubmitBtn.disabled = false;\n      }\n    });\n\n    // Switch account button in header\n    el.querySelector('#tenant-switch-org-btn')?.addEventListener(EVENT_CLICK, () => {\n      this.pageManager.showPanel('new-tenant-panel');\n    });\n\n    // Buy credits dropdown and options\n    const buyCreditsBtn = el.querySelector<HTMLButtonElement>('#tenant-buy-credits-btn');\n    const buyCreditsMenu = el.querySelector<HTMLElement>('#tenant-buy-credits-menu');\n    const buyCredits10hBtn = el.querySelector<HTMLButtonElement>('#buy-credits-10h-btn');\n    const buyCredits100hBtn = el.querySelector<HTMLButtonElement>('#buy-credits-100h-btn');\n\n    const toggleBuyMenu = (open?: boolean) => {\n      if (!buyCreditsMenu || !buyCreditsBtn) return;\n      const shouldOpen = open !== undefined ? open : buyCreditsMenu.classList.contains(CLASS_HIDDEN);\n      if (shouldOpen) {\n        buyCreditsMenu.classList.remove(CLASS_HIDDEN);\n        buyCreditsBtn.setAttribute('aria-expanded', 'true');\n      } else {\n        buyCreditsMenu.classList.add(CLASS_HIDDEN);\n        buyCreditsBtn.setAttribute('aria-expanded', 'false');\n      }\n    };\n\n    buyCreditsBtn?.addEventListener(EVENT_CLICK, (e) => {\n      e.stopPropagation();\n      toggleBuyMenu();\n    });\n\n    document.addEventListener(EVENT_CLICK, (e) => {\n      if (buyCreditsMenu && !buyCreditsMenu.classList.contains(CLASS_HIDDEN)) {\n        if (!el.querySelector('#tenant-buy-credits-container')?.contains(e.target as Node)) {\n          toggleBuyMenu(false);\n        }\n      }\n    });\n\n    const handleBuyPackage = async (btn: HTMLButtonElement | null, pkg: '10h' | '100h') => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!currentTenant) return;\n\n      toggleBuyMenu(false);\n      if (btn) btn.disabled = true;\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/payments/checkout`, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ package: pkg }),\n        });\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || 'Payment configuration unavailable');\n        }\n        const data = await resp.json();\n        const link = data.checkout_url || data.url;\n        if (!link) {\n          alert('Online credit purchases for this package are not configured on this server.');\n          return;\n        }\n        window.open(link, '_blank');\n      } catch (err: any) {\n        console.error('Failed to open checkout session:', err);\n        alert(err.message || 'Failed to open checkout session');\n      } finally {\n        if (btn) btn.disabled = false;\n      }\n    };\n\n    buyCredits10hBtn?.addEventListener(EVENT_CLICK, () => handleBuyPackage(buyCredits10hBtn, '10h'));\n    buyCredits100hBtn?.addEventListener(EVENT_CLICK, () => handleBuyPackage(buyCredits100hBtn, '100h'));\n\n    window.addEventListener('message', (ev) => {\n      if (ev.data?.type === 'fluent_payment_success') {\n        this.tenantManager.updateSessionStatus().then(() => this.refresh());\n      }\n    });\n\n    window.addEventListener('focus', () => {\n      if (this.tenantManager.getCurrentTenant()) {\n        this.tenantManager.updateSessionStatus().then(() => this.refresh());\n      }\n    });\n\n    // Invite modal handlers\n    const inviteModal = el.querySelector<HTMLElement>('#invite-member-modal');\n    const openInviteBtn = el.querySelector('#open-invite-team-btn');\n    const closeInviteBtn = el.querySelector('#close-invite-modal-btn');\n    const cancelInviteBtn = el.querySelector('#cancel-invite-btn');\n    const inviteForm = el.querySelector<HTMLFormElement>('#invite-member-form');\n    const emailInput = el.querySelector<HTMLInputElement>('#invite-email-input');\n    const errorDiv = el.querySelector<HTMLElement>('#invite-status-error');\n    const successDiv = el.querySelector<HTMLElement>('#invite-status-success');\n    const submitInviteBtn = el.querySelector<HTMLButtonElement>('#submit-invite-btn');\n\n    const openModal = () => {\n      if (inviteModal) {\n        inviteModal.classList.remove(CLASS_HIDDEN);\n        inviteModal.style.display = DISPLAY_FLEX;\n        if (emailInput) {\n          emailInput.value = '';\n          emailInput.focus();\n        }\n        if (errorDiv) errorDiv.style.display = DISPLAY_NONE;\n        if (successDiv) successDiv.style.display = DISPLAY_NONE;\n      }\n    };\n\n    const closeModal = () => {\n      if (inviteModal) {\n        inviteModal.classList.add(CLASS_HIDDEN);\n        inviteModal.style.display = DISPLAY_NONE;\n      }\n    };\n\n    openInviteBtn?.addEventListener(EVENT_CLICK, openModal);\n    closeInviteBtn?.addEventListener(EVENT_CLICK, closeModal);\n    cancelInviteBtn?.addEventListener(EVENT_CLICK, closeModal);\n\n    inviteForm?.addEventListener(EVENT_SUBMIT, async (e) => {\n      e.preventDefault();\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!emailInput || !submitInviteBtn || !errorDiv || !successDiv || !currentTenant) return;\n\n      const email = emailInput.value.trim();\n      if (!email) return;\n\n      submitInviteBtn.disabled = true;\n      submitInviteBtn.textContent = 'Sending...';\n      errorDiv.style.display = DISPLAY_NONE;\n      successDiv.style.display = DISPLAY_NONE;\n\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/invitations`, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ email }),\n        });\n\n        if (!resp.ok) {\n          const err = await resp.json().catch(() => ({}));\n          throw new Error(err.error || 'Failed to send invitation');\n        }\n\n        successDiv.textContent = `Invitation sent to ${email}!`;\n        successDiv.style.display = DISPLAY_BLOCK;\n        emailInput.value = '';\n        const panel = document.getElementById(this.id);\n        if (panel) {\n          await this.loadAndRenderTeamMembers(\n            panel,\n            currentTenant.id,\n            this.tenantManager.isTenantAdmin(),\n            this.authManager.getUser()\n          );\n        }\n        setTimeout(() => {\n          closeModal();\n        }, 1200);\n      } catch (err: any) {\n        errorDiv.textContent = err.message || 'Failed to send invitation';\n        errorDiv.style.display = DISPLAY_BLOCK;\n      } finally {\n        submitInviteBtn.disabled = false;\n        submitInviteBtn.textContent = 'Send Invitation';\n      }\n    });\n\n    // Enroll device modal handlers\n    const enrollModal = el.querySelector<HTMLElement>('#enroll-device-modal');\n    const openEnrollBtn = el.querySelector<HTMLElement>('#open-enroll-device-btn');\n    const closeEnrollBtn = el.querySelector<HTMLElement>('#close-enroll-device-modal-btn');\n    const cancelEnrollBtn = el.querySelector<HTMLElement>('#cancel-enroll-device-btn');\n    const doneEnrollBtn = el.querySelector<HTMLElement>('#done-enroll-device-btn');\n    const enrollForm = el.querySelector<HTMLFormElement>('#enroll-device-form');\n    const labelInput = el.querySelector<HTMLInputElement>('#enroll-device-label-input');\n    const channelSelect = el.querySelector<HTMLSelectElement>('#enroll-device-channel-select');\n    const enrollErrorDiv = el.querySelector<HTMLElement>('#enroll-device-status-error');\n    const enrollResultDiv = el.querySelector<HTMLElement>('#enroll-device-result');\n    const enrollInputsDiv = el.querySelector<HTMLElement>('#enroll-device-inputs');\n    const linkInput = el.querySelector<HTMLInputElement>('#enroll-device-link-input');\n    const copyLinkBtn = el.querySelector<HTMLButtonElement>('#enroll-device-copy-btn');\n    const qrContainer = el.querySelector<HTMLElement>('#enroll-device-qr');\n    const submitEnrollBtn = el.querySelector<HTMLButtonElement>('#submit-enroll-device-btn');\n\n    const openEnrollModal = () => {\n      if (enrollModal) {\n        enrollModal.classList.remove(CLASS_HIDDEN);\n        enrollModal.style.display = DISPLAY_FLEX;\n        if (labelInput) {\n          labelInput.value = '';\n          labelInput.focus();\n        }\n        if (channelSelect) {\n          channelSelect.innerHTML = '<option value=\"\">All Channels (Select on device)</option>';\n          const currentChannels = this.tenantManager.getChannels();\n          for (const ch of currentChannels) {\n            const opt = document.createElement('option');\n            opt.value = ch.name;\n            opt.textContent = `@${ch.name}`;\n            channelSelect.appendChild(opt);\n          }\n        }\n        if (enrollInputsDiv) enrollInputsDiv.style.display = DISPLAY_BLOCK;\n        if (enrollErrorDiv) enrollErrorDiv.style.display = DISPLAY_NONE;\n        if (enrollResultDiv) enrollResultDiv.style.display = DISPLAY_NONE;\n        if (submitEnrollBtn) {\n          submitEnrollBtn.style.display = DISPLAY_INLINE_BLOCK;\n          submitEnrollBtn.disabled = false;\n          submitEnrollBtn.textContent = 'Generate Enrollment Link';\n        }\n        if (cancelEnrollBtn) cancelEnrollBtn.style.display = DISPLAY_INLINE_BLOCK;\n        if (doneEnrollBtn) doneEnrollBtn.style.display = DISPLAY_NONE;\n      }\n    };\n\n    const closeEnrollModal = () => {\n      if (enrollModal) {\n        enrollModal.classList.add(CLASS_HIDDEN);\n        enrollModal.style.display = DISPLAY_NONE;\n      }\n    };\n\n    openEnrollBtn?.addEventListener(EVENT_CLICK, openEnrollModal);\n    closeEnrollBtn?.addEventListener(EVENT_CLICK, closeEnrollModal);\n    cancelEnrollBtn?.addEventListener(EVENT_CLICK, closeEnrollModal);\n    doneEnrollBtn?.addEventListener(EVENT_CLICK, async () => {\n      closeEnrollModal();\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (currentTenant) {\n        await this.loadAndRenderDevices(el, currentTenant.id, this.tenantManager.isTenantAdmin());\n      }\n    });\n\n    copyLinkBtn?.addEventListener(EVENT_CLICK, async () => {\n      if (!linkInput?.value) return;\n      try {\n        await navigator.clipboard.writeText(linkInput.value);\n        copyLinkBtn.textContent = 'Copied!';\n        setTimeout(() => {\n          copyLinkBtn.textContent = 'Copy Link';\n        }, 2000);\n      } catch (err) {\n        console.error('Failed to copy enrollment link:', err);\n      }\n    });\n\n    enrollForm?.addEventListener(EVENT_SUBMIT, async (e) => {\n      e.preventDefault();\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!labelInput || !submitEnrollBtn || !enrollErrorDiv || !currentTenant) return;\n\n      const label = labelInput.value.trim();\n      const channelName = channelSelect?.value || undefined;\n      if (!label) return;\n\n      submitEnrollBtn.disabled = true;\n      submitEnrollBtn.textContent = 'Generating...';\n      enrollErrorDiv.style.display = DISPLAY_NONE;\n\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/devices/enrollment-link`, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ label, channel_name: channelName }),\n        });\n\n        if (!resp.ok) {\n          const err = await resp.json().catch(() => ({}));\n          throw new Error(err.error || 'Failed to generate enrollment link');\n        }\n\n        const buffer = await resp.arrayBuffer();\n        const data = devices.DeviceEnrollmentLinkProto.decode(new Uint8Array(buffer));\n        const fullUrl = data.enrollmentUrl || `${window.location.origin}/broadcast.html?device_enroll=${encodeURIComponent(data.enrollmentToken || '')}`;\n\n        if (linkInput) linkInput.value = fullUrl;\n\n        if (qrContainer) {\n          qrContainer.replaceChildren();\n          const svgString = await QRCode.toString(fullUrl, {\n            type: 'svg',\n            margin: 2,\n            width: 180,\n            color: { dark: '#000000', light: '#ffffff' },\n          });\n          const parser = new DOMParser();\n          const doc = parser.parseFromString(svgString, 'image/svg+xml');\n          qrContainer.appendChild(doc.documentElement);\n        }\n\n        if (enrollInputsDiv) enrollInputsDiv.style.display = DISPLAY_NONE;\n        if (enrollResultDiv) enrollResultDiv.style.display = DISPLAY_FLEX;\n        submitEnrollBtn.style.display = DISPLAY_NONE;\n        if (cancelEnrollBtn) cancelEnrollBtn.style.display = DISPLAY_NONE;\n        if (doneEnrollBtn) doneEnrollBtn.style.display = DISPLAY_INLINE_BLOCK;\n\n        // Auto refresh list in the background\n        await this.loadAndRenderDevices(el, currentTenant.id, this.tenantManager.isTenantAdmin());\n      } catch (err: any) {\n        enrollErrorDiv.textContent = err.message || 'Failed to generate enrollment link';\n        enrollErrorDiv.style.display = DISPLAY_BLOCK;\n        submitEnrollBtn.disabled = false;\n        submitEnrollBtn.textContent = 'Generate Enrollment Link';\n      }\n    });\n\n    // Custom domain save & remove handlers\n    const cnameInput = el.querySelector<HTMLInputElement>('#tenant-cname-input');\n    const cnameSaveBtn = el.querySelector<HTMLButtonElement>('#tenant-cname-save-btn');\n    const cnameRemoveBtn = el.querySelector<HTMLButtonElement>('#tenant-cname-remove-btn');\n    const cnameFeedback = el.querySelector<HTMLElement>('#tenant-cname-feedback');\n\n    const showCnameFeedback = (msg: string, isError: boolean = false) => {\n      if (!cnameFeedback) return;\n      cnameFeedback.textContent = msg;\n      cnameFeedback.className = `cname-feedback-box ${isError ? 'error' : 'success'}`;\n      cnameFeedback.classList.remove(CLASS_HIDDEN);\n      cnameFeedback.style.display = DISPLAY_BLOCK;\n      setTimeout(() => {\n        cnameFeedback.classList.add(CLASS_HIDDEN);\n        cnameFeedback.style.display = DISPLAY_NONE;\n      }, 5000);\n    };\n\n    cnameSaveBtn?.addEventListener(EVENT_CLICK, async () => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!currentTenant || !cnameInput) return;\n\n      const domain = cnameInput.value.trim();\n\n      cnameSaveBtn.disabled = true;\n      cnameSaveBtn.textContent = 'Saving...';\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/custom-domain`, {\n          method: 'PUT',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ custom_domain: domain }),\n        });\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || 'Failed to save custom domain');\n        }\n        showCnameFeedback('Custom domain saved. Verification will occur automatically once DNS is detected.');\n        await this.loadAndRenderCustomDomain(el, currentTenant.id, this.tenantManager.isTenantAdmin());\n      } catch (err: any) {\n        showCnameFeedback(err.message || 'Error saving custom domain', true);\n      } finally {\n        cnameSaveBtn.disabled = false;\n        cnameSaveBtn.textContent = 'Save Domain';\n      }\n    });\n\n    cnameRemoveBtn?.addEventListener(EVENT_CLICK, async () => {\n      const currentTenant = this.tenantManager.getCurrentTenant();\n      if (!currentTenant) return;\n      if (!confirm('Are you sure you want to remove the custom domain for this organization?')) {\n        return;\n      }\n\n      cnameRemoveBtn.disabled = true;\n      cnameRemoveBtn.textContent = 'Removing...';\n      try {\n        const resp = await fetchWithAuth(`/api/tenants/${currentTenant.id}/custom-domain`, {\n          method: 'DELETE',\n        });\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || 'Failed to remove custom domain');\n        }\n        showCnameFeedback('Custom domain removed successfully.');\n        if (cnameInput) cnameInput.value = '';\n        await this.loadAndRenderCustomDomain(el, currentTenant.id, this.tenantManager.isTenantAdmin());\n      } catch (err: any) {\n        showCnameFeedback(err.message || 'Error removing custom domain', true);\n      } finally {\n        cnameRemoveBtn.disabled = false;\n        cnameRemoveBtn.textContent = 'Remove';\n      }\n    });\n  }\n\n  private async loadAndRenderCustomDomain(el: HTMLElement, tenantId: any, isAdmin: boolean): Promise<void> {\n    const inputEl = el.querySelector<HTMLInputElement>('#tenant-cname-input');\n    const targetDomainEl = el.querySelector<HTMLElement>('#tenant-cname-target-domain');\n    const statusEl = el.querySelector<HTMLElement>('#tenant-cname-status');\n    const removeBtn = el.querySelector<HTMLButtonElement>('#tenant-cname-remove-btn');\n    const saveBtn = el.querySelector<HTMLButtonElement>('#tenant-cname-save-btn');\n    const feedbackEl = el.querySelector<HTMLElement>('#tenant-cname-feedback');\n\n    if (feedbackEl) feedbackEl.classList.add(CLASS_HIDDEN);\n\n    if (saveBtn) saveBtn.disabled = !isAdmin;\n    if (removeBtn) removeBtn.disabled = !isAdmin;\n    if (inputEl) inputEl.disabled = !isAdmin;\n\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${tenantId}/custom-domain`);\n      if (!resp.ok) return;\n\n      const data = await resp.json();\n      if (targetDomainEl && data.cname_target) {\n        targetDomainEl.textContent = data.cname_target;\n      }\n      if (inputEl) {\n        inputEl.value = data.custom_domain || '';\n      }\n\n      // Status badge\n      if (statusEl) {\n        if (!data.custom_domain) {\n          statusEl.textContent = 'Not Configured';\n          statusEl.className = 'badge cname-status-badge badge-neutral';\n          if (removeBtn) removeBtn.classList.add(CLASS_HIDDEN);\n        } else if (data.dns_verified) {\n          statusEl.textContent = 'Active / Verified';\n          statusEl.className = 'badge cname-status-badge badge-verified';\n          if (removeBtn) removeBtn.classList.remove(CLASS_HIDDEN);\n        } else {\n          statusEl.textContent = 'Pending Verification';\n          statusEl.className = 'badge cname-status-badge badge-pending';\n          if (removeBtn) removeBtn.classList.remove(CLASS_HIDDEN);\n        }\n      }\n    } catch (e) {\n      console.warn('Failed to load tenant custom domain:', e);\n    }\n  }\n\n  private async loadAndRenderBillingReceipts(el: HTMLElement, tenantId: any): Promise<void> {\n    const tbody = el.querySelector<HTMLElement>('#tenant-receipts-tbody');\n    const emptyEl = el.querySelector<HTMLElement>('#tenant-receipts-empty');\n    if (!tbody) return;\n\n    try {\n      const resp = await fetchWithAuth(`/api/tenants/${tenantId}/payments/receipts`);\n      if (!resp.ok) {\n        tbody.innerHTML = '';\n        if (emptyEl) {\n          emptyEl.classList.remove(CLASS_HIDDEN);\n          emptyEl.style.display = DISPLAY_BLOCK;\n        }\n        return;\n      }\n\n      const receipts: Array<{\n        id: string;\n        invoice_id?: string;\n        amount_cents?: number;\n        currency?: string;\n        created_at_unix_ms?: number;\n        description?: string;\n        pdf_filename?: string;\n      }> = await resp.json();\n\n      tbody.innerHTML = '';\n      if (!Array.isArray(receipts) || receipts.length === 0) {\n        if (emptyEl) {\n          emptyEl.classList.remove(CLASS_HIDDEN);\n          emptyEl.style.display = DISPLAY_BLOCK;\n        }\n        return;\n      }\n\n      if (emptyEl) {\n        emptyEl.classList.add(CLASS_HIDDEN);\n        emptyEl.style.display = DISPLAY_NONE;\n      }\n\n      for (const r of receipts) {\n        const tr = document.createElement('tr');\n        const timeMs = toNumeric(r.created_at_unix_ms);\n        const amountStr = r.amount_cents != null\n          ? `${(r.amount_cents / 100).toFixed(2)} ${r.currency || 'USD'}`\n          : '—';\n        const invStr = r.invoice_id || r.id || '—';\n\n        tr.innerHTML = `\n          <td class=\"nowrap\">${formatDateTime(timeMs)}</td>\n          <td class=\"font-mono text-sm\">${invStr}</td>\n          <td>${r.description || 'Streaming Credits'}</td>\n          <td class=\"text-right nowrap tabular-nums font-semibold\">${amountStr}</td>\n          <td class=\"text-right nowrap\">\n            <button type=\"button\" class=\"btn btn-outline btn-sm download-receipt-btn\" data-receipt-id=\"${r.id}\">\n              Download PDF\n            </button>\n          </td>\n        `;\n\n        const dlBtn = tr.querySelector<HTMLButtonElement>('.download-receipt-btn');\n        dlBtn?.addEventListener(EVENT_CLICK, async () => {\n          dlBtn.disabled = true;\n          dlBtn.textContent = 'Downloading...';\n          try {\n            const dlResp = await fetchWithAuth(`/api/tenants/${tenantId}/payments/receipts/${r.id}/download?download=true`, {\n              headers: { 'Accept': 'application/pdf, application/json' },\n            });\n            if (!dlResp.ok) throw new Error('Receipt PDF not ready or not found');\n\n            let blob: Blob;\n            let filename = `invoice-${r.invoice_id || r.id}.pdf`;\n\n            const contentType = dlResp.headers.get('content-type') || '';\n            if (contentType.includes('application/pdf')) {\n              const disposition = dlResp.headers.get('content-disposition') || '';\n              const match = disposition.match(/filename=\"?([^\";]+)\"?/);\n              if (match && match[1]) filename = match[1];\n              blob = await dlResp.blob();\n            } else {\n              const data = await dlResp.json();\n              if (data.filename) filename = data.filename;\n              const fileResp = await fetch(data.url);\n              if (!fileResp.ok) throw new Error('Failed to retrieve PDF file');\n              blob = await fileResp.blob();\n            }\n\n            const blobUrl = URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = blobUrl;\n            a.download = filename;\n            document.body.appendChild(a);\n            a.click();\n            document.body.removeChild(a);\n            setTimeout(() => URL.revokeObjectURL(blobUrl), 2000);\n          } catch (err: any) {\n            alert(err.message || 'Could not download receipt PDF');\n          } finally {\n            dlBtn.disabled = false;\n            dlBtn.textContent = 'Download';\n          }\n        });\n\n        tbody.appendChild(tr);\n      }\n    } catch (err) {\n      console.error('Failed to load billing receipts:', err);\n      if (emptyEl) {\n        emptyEl.classList.remove(CLASS_HIDDEN);\n        emptyEl.style.display = DISPLAY_BLOCK;\n      }\n    }\n  }\n}\n","import { PanelHandler, PageManager } from '@tingdahl/tsutils/ui';\nimport { fetchWithAuth } from '../../api-config';\nimport { TenantManager } from '../../core/tenant_manager';\nimport { AuthManager } from '../../core/auth';\nimport { getCacheSync } from '../../core/cache';\nimport { t } from '../../i18n';\nimport {\n  EVENT_CLICK,\n  EVENT_INPUT,\n  EVENT_SUBMIT,\n  DISPLAY_NONE,\n  DISPLAY_BLOCK,\n  HTTP_POST,\n  STORAGE_LAST_TENANT_ID,\n} from '../../core/strings';\n\nconst CHANNEL_NAME_REGEX = /^[a-zA-Z0-9_]+(-[a-zA-Z0-9_]+)*$/;\n\nexport class NewTenantPanel extends PanelHandler {\n  private tenantManager: TenantManager;\n  private authManager: AuthManager;\n  private pageManager: PageManager;\n\n  constructor(tenantManager: TenantManager, authManager: AuthManager, pageManager: PageManager) {\n    super('new-tenant-panel');\n    this.tenantManager = tenantManager;\n    this.authManager = authManager;\n    this.pageManager = pageManager;\n  }\n\n  async refresh(): Promise<void> {\n    await super.refresh();\n    const el = document.getElementById(this.id);\n    if (!el) return;\n\n    const user = this.authManager.getUser();\n    const userTenants = this.tenantManager.getUserTenants();\n    const currentTenant = this.tenantManager.getCurrentTenant();\n    const hasTenants = userTenants.length > 0;\n\n    el.innerHTML = `\n      <div class=\"new-tenant-container\">\n        <div class=\"card new-tenant-card\">\n          <div class=\"brand new-tenant-brand\">\n            <span>${t('common.appName')}</span>\n            <span class=\"brand-badge\">${hasTenants ? 'Accounts' : 'Setup'}</span>\n          </div>\n          <h2 class=\"new-tenant-title\">\n            ${hasTenants ? 'Select Account' : t('studio.welcome', { name: user?.name || 'Broadcaster' })}\n          </h2>\n          <p class=\"new-tenant-subtitle\">\n            ${hasTenants\n              ? 'Select an account to broadcast audio and manage settings, or create a new one.'\n              : t('studio.setupDesc')}\n          </p>\n\n          ${hasTenants ? `\n            <div id=\"tenant-selection-list\" class=\"tenant-selection-list\">\n              ${userTenants.map(t => {\n                const isCurrent = currentTenant && String(currentTenant.id) === String(t.id);\n                return `\n                  <button type=\"button\" class=\"tenant-select-card-btn\" data-tenant-id=\"${t.id}\">\n                    <div class=\"tenant-select-card-left\">\n                      <div class=\"tenant-avatar-badge\">\n                        ${(t.name || 'A').charAt(0).toUpperCase()}\n                      </div>\n                      <div>\n                        <div class=\"tenant-select-card-name\">${t.name}</div>\n                        <div class=\"tenant-select-card-role\">Admin</div>\n                      </div>\n                    </div>\n                    <div class=\"tenant-select-card-right\">\n                      ${isCurrent ? '<span class=\"badge-neutral badge-current\">Current</span>' : ''}\n                      <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"text-muted\">\n                        <polyline points=\"9 18 15 12 9 6\"></polyline>\n                      </svg>\n                    </div>\n                  </button>\n                `;\n              }).join('')}\n            </div>\n\n            <div class=\"divider-container\">\n              <div class=\"divider-line\"></div>\n              <span class=\"divider-text\">or</span>\n              <div class=\"divider-line\"></div>\n            </div>\n\n            <button type=\"button\" id=\"toggle-create-org-btn\" class=\"btn btn-outline toggle-create-org-btn\">\n              <span>+</span> Create New Organization\n            </button>\n          ` : ''}\n\n          <div id=\"create-org-form-container\" class=\"${hasTenants ? 'hidden' : ''}\">\n            <form id=\"new-tenant-form\" class=\"new-tenant-form\">\n              <div>\n                <label for=\"new-tenant-name\" class=\"form-label-bold\">\n                  ${t('studio.orgNameLabel')}\n                </label>\n                <input\n                  type=\"text\"\n                  id=\"new-tenant-name\"\n                  class=\"form-control form-control-full\"\n                  value=\"${user?.name || ''}\"\n                  placeholder=\"${user?.name || 'Organization name'}\"\n                  required\n                />\n              </div>\n\n              <div>\n                <label for=\"initial-channel-name\" class=\"form-label-bold\">\n                  ${t('studio.firstChannelLabel')}\n                </label>\n                <div class=\"channel-input-wrapper\">\n                  <span class=\"channel-prefix\">@</span>\n                  <input\n                    type=\"text\"\n                    id=\"initial-channel-name\"\n                    class=\"form-control channel-input-field\"\n                    required\n                    minlength=\"6\"\n                    maxlength=\"20\"\n                    pattern=\"[a-zA-Z0-9_]+(-[a-zA-Z0-9_]+)*\"\n                    placeholder=\"${t('studio.firstChannelPlaceholder')}\"\n                  />\n                </div>\n\n                <small class=\"channel-help-text\">\n                  ${t('studio.channelHelp')}\n                </small>\n              </div>\n\n              <div id=\"new-tenant-error\" class=\"form-error-text hidden\"></div>\n\n              <div class=\"form-actions-row\">\n                <button type=\"submit\" id=\"submit-tenant-btn\" class=\"btn btn-primary submit-tenant-btn\">\n                  ${t('studio.createOrgBtn')}\n                </button>\n                ${hasTenants ? `\n                  <button type=\"button\" id=\"cancel-new-tenant-btn\" class=\"btn btn-outline cancel-tenant-btn\">\n                    Cancel\n                  </button>\n                ` : ''}\n              </div>\n            </form>\n          </div>\n\n          ${currentTenant ? `\n            <div class=\"cancel-selection-box\">\n              <button type=\"button\" id=\"cancel-selection-btn\" class=\"cancel-selection-btn\">\n                Back to Broadcast\n              </button>\n            </div>\n          ` : ''}\n        </div>\n      </div>\n    `;\n\n    // Bind account selection buttons\n    el.querySelectorAll<HTMLButtonElement>('.tenant-select-card-btn').forEach(btn => {\n      btn.addEventListener(EVENT_CLICK, async () => {\n        const tenantId = btn.getAttribute('data-tenant-id');\n        if (tenantId) {\n          sessionStorage.setItem('current_tenant_id', tenantId);\n          localStorage.setItem(STORAGE_LAST_TENANT_ID, tenantId);\n          await this.tenantManager.selectTenant(tenantId);\n          await this.pageManager.showPanel('broadcast-panel');\n        }\n      });\n    });\n\n    const form = el.querySelector('#new-tenant-form') as HTMLFormElement | null;\n    const tenantNameInput = el.querySelector('#new-tenant-name') as HTMLInputElement | null;\n    const channelInput = el.querySelector('#initial-channel-name') as HTMLInputElement | null;\n    const submitBtn = el.querySelector('#submit-tenant-btn') as HTMLButtonElement | null;\n    const cancelBtn = el.querySelector('#cancel-new-tenant-btn') as HTMLButtonElement | null;\n    const cancelSelectionBtn = el.querySelector('#cancel-selection-btn') as HTMLButtonElement | null;\n    const errorDiv = el.querySelector('#new-tenant-error') as HTMLElement | null;\n    const toggleCreateBtn = el.querySelector('#toggle-create-org-btn') as HTMLButtonElement | null;\n    const createContainer = el.querySelector<HTMLElement>('#create-org-form-container');\n\n    toggleCreateBtn?.addEventListener(EVENT_CLICK, () => {\n      if (createContainer) {\n        const isHidden = createContainer.style.display === DISPLAY_NONE;\n        createContainer.style.display = isHidden ? DISPLAY_BLOCK : DISPLAY_NONE;\n        toggleCreateBtn.textContent = isHidden ? '− Hide Create Form' : '+ Create New Organization';\n        if (isHidden) {\n          if (tenantNameInput && !tenantNameInput.value && user?.name) {\n            tenantNameInput.value = user.name;\n          }\n          tenantNameInput?.focus();\n        }\n      }\n    });\n\n    cancelSelectionBtn?.addEventListener(EVENT_CLICK, () => {\n      this.pageManager.showPanel('broadcast-panel');\n    });\n\n    cancelBtn?.addEventListener(EVENT_CLICK, () => {\n      if (hasTenants && createContainer && toggleCreateBtn) {\n        createContainer.style.display = DISPLAY_NONE;\n        toggleCreateBtn.textContent = '+ Create New Organization';\n      } else {\n        this.pageManager.showPanel('broadcast-panel');\n      }\n    });\n\n    channelInput?.addEventListener(EVENT_INPUT, () => {\n      channelInput.value = channelInput.value\n        .replace(/^@+/, '')\n        .replace(/[^a-zA-Z0-9_-]/g, '')\n        .replace(/--+/g, '-')\n        .replace(/^-/, '');\n    });\n\n    form?.addEventListener(EVENT_SUBMIT, async (e) => {\n      e.preventDefault();\n      if (!tenantNameInput || !channelInput || !submitBtn || !errorDiv) return;\n\n      const tenantName = tenantNameInput.value.trim();\n      const channelName = channelInput.value.trim().replace(/^@+/, '');\n\n      if (!tenantName || !channelName) return;\n\n      if (channelName.length < 6 || channelName.length > 20 || !CHANNEL_NAME_REGEX.test(channelName)) {\n        errorDiv.textContent = t('studio.invalidChannelError');\n        errorDiv.style.display = DISPLAY_BLOCK;\n        return;\n      }\n\n      submitBtn.disabled = true;\n      submitBtn.textContent = t('studio.creatingOrg');\n      errorDiv.style.display = DISPLAY_NONE;\n\n      try {\n        const resp = await fetchWithAuth('/api/tenants', {\n          method: HTTP_POST,\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ name: tenantName, initial_channel_name: channelName }),\n        });\n\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.error || `Failed to create organization (HTTP ${resp.status})`);\n        }\n\n        // Re-fetch profile to update roles\n        await this.authManager.fetchCurrentUser();\n\n        // Refresh tenants list and switch to the new tenant\n        const updatedTenants = await this.tenantManager.loadUserTenants();\n        if (updatedTenants.length > 0) {\n          const newlyCreated = updatedTenants[updatedTenants.length - 1];\n          sessionStorage.setItem('current_tenant_id', String(newlyCreated.id));\n          localStorage.setItem(STORAGE_LAST_TENANT_ID, String(newlyCreated.id));\n          await this.tenantManager.selectTenant(String(newlyCreated.id));\n        }\n        getCacheSync().broadcastInvalidation('tenants');\n\n        await this.pageManager.showPanel('broadcast-panel');\n      } catch (err: any) {\n        errorDiv.textContent = err.message || 'An error occurred while creating organization.';\n        errorDiv.style.display = DISPLAY_BLOCK;\n      } finally {\n        submitBtn.disabled = false;\n        submitBtn.textContent = t('studio.createOrgBtn');\n      }\n    });\n  }\n}\n","import { AuthManager } from '../../core/auth';\nimport { renderMarkdown } from '../../core/markdown';\n\nlet activeTermsPrompt: Promise<boolean> | null = null;\n\n/**\n * Ensures that the authenticated user has accepted the latest Broadcaster Terms and Conditions.\n * If terms are not accepted, displays a modal requiring acceptance before proceeding.\n * Deduplicates concurrent calls so multiple callers share the same modal session.\n */\nexport async function ensureBroadcasterTermsAccepted(authManager: AuthManager): Promise<boolean> {\n  if (activeTermsPrompt) {\n    return activeTermsPrompt;\n  }\n\n  activeTermsPrompt = doEnsureBroadcasterTermsAccepted(authManager).finally(() => {\n    activeTermsPrompt = null;\n  });\n\n  return activeTermsPrompt;\n}\n\nasync function doEnsureBroadcasterTermsAccepted(authManager: AuthManager): Promise<boolean> {\n  const user = authManager.getUser();\n  if (!user) return true; // Unauthenticated or device session\n\n  const terms = await authManager.getBroadcasterTerms();\n  if (!terms || !terms.version) return true; // Offline or unavailable\n\n  // Check if user already accepted this version\n  if (user.tcAcceptedVersion === terms.version) {\n    return true;\n  }\n\n  const modal = document.getElementById('broadcaster-tc-modal');\n  const viewBtn = document.getElementById('broadcaster-tc-view-btn') as HTMLButtonElement | null;\n  const checkbox = document.getElementById('broadcaster-tc-checkbox') as HTMLInputElement | null;\n  const label = document.getElementById('broadcaster-tc-checkbox-label') as HTMLLabelElement | null;\n  const acceptBtn = document.getElementById('broadcaster-tc-accept-btn') as HTMLButtonElement | null;\n\n  const readerModal = document.getElementById('broadcaster-tc-reader-modal');\n  const contentEl = document.getElementById('broadcaster-tc-content');\n  const readerCloseBtn = document.getElementById('broadcaster-tc-reader-close-btn');\n  const readerDismissBtn = document.getElementById('broadcaster-tc-reader-dismiss-btn');\n  const readHint = document.getElementById('broadcaster-tc-read-hint');\n\n  if (!modal || !checkbox || !acceptBtn) {\n    console.warn('Broadcaster terms modal elements not found');\n    return true;\n  }\n\n  // Populate reader content with parsed markdown (including tables)\n  if (contentEl) {\n    contentEl.innerHTML = renderMarkdown(terms.content);\n  }\n\n  let hasReadTerms = false;\n\n  const updateState = () => {\n    if (hasReadTerms) {\n      checkbox.disabled = false;\n      checkbox.removeAttribute('disabled');\n      checkbox.style.cursor = 'pointer';\n      if (label) {\n        label.style.cursor = 'pointer';\n        label.style.opacity = '1';\n      }\n      if (readHint) readHint.classList.add('hidden');\n      acceptBtn.disabled = !checkbox.checked;\n      acceptBtn.title = checkbox.checked ? '' : 'Please check the box to agree to the Terms & Conditions';\n    } else {\n      checkbox.disabled = true;\n      checkbox.setAttribute('disabled', 'disabled');\n      checkbox.checked = false;\n      checkbox.style.cursor = 'not-allowed';\n      if (label) {\n        label.style.cursor = 'not-allowed';\n        label.style.opacity = '0.7';\n      }\n      if (readHint) readHint.classList.remove('hidden');\n      acceptBtn.disabled = true;\n      acceptBtn.title = 'Please read the Terms & Conditions before accepting';\n    }\n  };\n\n  const closeReader = () => {\n    if (readerModal) readerModal.classList.add('hidden');\n    updateState();\n  };\n\n  if (viewBtn && readerModal) {\n    viewBtn.onclick = (e) => {\n      e.preventDefault();\n      hasReadTerms = true;\n      viewBtn.classList.add('read-completed');\n      updateState();\n      readerModal.classList.remove('hidden');\n    };\n  }\n\n  if (readerCloseBtn) readerCloseBtn.onclick = closeReader;\n  if (readerDismissBtn) readerDismissBtn.onclick = closeReader;\n\n  if (readerModal) {\n    readerModal.onclick = (e: MouseEvent) => {\n      if (e.target === readerModal) {\n        closeReader();\n      }\n    };\n  }\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Escape' && readerModal && !readerModal.classList.contains('hidden')) {\n      closeReader();\n    }\n  };\n  if (typeof window !== 'undefined') {\n    window.addEventListener('keydown', onKeyDown);\n  }\n\n  checkbox.checked = false;\n  updateState();\n  modal.classList.remove('hidden');\n\n  return new Promise<boolean>((resolve) => {\n    const onCheckboxChange = () => {\n      updateState();\n    };\n    checkbox.onchange = onCheckboxChange;\n\n    acceptBtn.onclick = async () => {\n      if (!hasReadTerms || !checkbox.checked) {\n        return;\n      }\n      acceptBtn.disabled = true;\n      const originalText = acceptBtn.textContent;\n      acceptBtn.textContent = 'Saving...';\n      try {\n        const updated = await authManager.acceptTerms(terms.version);\n        if (updated) {\n          modal.classList.add('hidden');\n          closeReader();\n          if (typeof window !== 'undefined') {\n            window.removeEventListener('keydown', onKeyDown);\n          }\n          checkbox.onchange = null;\n          acceptBtn.onclick = null;\n          acceptBtn.textContent = originalText;\n          resolve(true);\n          return;\n        }\n      } catch (err) {\n        console.error('Terms acceptance error:', err);\n      }\n      acceptBtn.disabled = false;\n      acceptBtn.textContent = originalText;\n      alert('Failed to record Terms & Conditions acceptance. Please try again.');\n    };\n  });\n}\n","import './broadcast.css';\nimport './proto-inspector';\nimport { PageManager } from '@tingdahl/tsutils/ui';\nimport { AuthManager, SignInPanelHandler } from './core/auth';\nimport { TenantManager } from './core/tenant_manager';\nimport { HeaderHandler } from './core/header';\nimport { MenuManager } from './core/menu';\nimport { BroadcastPanel } from './features/broadcast/broadcast_panel';\nimport { ChannelsPanel } from './features/channels/channels_panel';\nimport { TenantPanel } from './features/tenant/tenant_panel';\nimport { NewTenantPanel } from './features/tenant/new_tenant_panel';\nimport { users } from './proto/compiled_proto';\nimport { initTheme } from './theme-manager';\nimport { enforceBrowserCompatibility } from './core/browser_compat';\nimport { setOnUnauthorized } from './api-config';\nimport { ensureBroadcasterTermsAccepted } from './features/legal/broadcaster_tc';\nimport {\n  fetchStatusConfig,\n  checkBackendReachable,\n  displayAnnouncementIfConfigured,\n  renderMaintenanceView,\n  renderServiceUnavailableView,\n} from './core/status';\nimport {\n  EVENT_HASHCHANGE,\n  EVENT_DOM_CONTENT_LOADED,\n  STORAGE_LAST_TENANT_ID,\n  PARAM_CHANNEL,\n} from './core/strings';\n\nclass App {\n  private authManager: AuthManager;\n  private tenantManager: TenantManager;\n  private pageManager: PageManager;\n  private headerHandler: HeaderHandler;\n  private menuManager: MenuManager;\n\n  constructor() {\n    this.authManager = new AuthManager();\n    this.tenantManager = new TenantManager(\n      () => this.authManager.getUser(),\n      () => this.authManager.isDeviceSession(),\n      () => this.authManager.getDeviceInfo()\n    );\n    this.pageManager = new PageManager('main-content');\n    this.headerHandler = new HeaderHandler(this.authManager, this.tenantManager, this.pageManager);\n    this.menuManager = new MenuManager();\n  }\n\n  async init(): Promise<void> {\n    initTheme();\n    enforceBrowserCompatibility();\n\n    const statusConfig = await fetchStatusConfig();\n    if (statusConfig?.maintenance?.enabled) {\n      renderMaintenanceView(document.body, statusConfig.maintenance);\n      return;\n    }\n\n    const backendHealthy = await checkBackendReachable();\n    if (!backendHealthy) {\n      renderServiceUnavailableView(document.body, () => window.location.reload());\n      return;\n    }\n\n    displayAnnouncementIfConfigured(statusConfig, 'broadcast');\n\n    // Setup Auth and Token handling\n    const hasJustLoggedIn = this.authManager.checkUrlForToken();\n    if (hasJustLoggedIn) {\n      sessionStorage.removeItem('current_tenant_id');\n    }\n    const enrollToken = this.authManager.checkUrlForEnrollmentToken();\n    if (enrollToken) {\n      try {\n        await this.authManager.enrollDevice(enrollToken);\n      } catch (err: any) {\n        console.error('Device enrollment failed:', err);\n        alert(err.message || 'Failed to enroll device');\n      }\n    }\n\n    // Register Panels\n    const signInPanel = new SignInPanelHandler(this.authManager);\n    const newTenantPanel = new NewTenantPanel(this.tenantManager, this.authManager, this.pageManager);\n    const broadcastPanel = new BroadcastPanel(this.tenantManager, this.pageManager);\n    const channelsPanel = new ChannelsPanel(this.tenantManager, this.pageManager);\n    const tenantPanel = new TenantPanel(this.tenantManager, this.authManager, this.pageManager);\n\n    this.pageManager.addPanel(signInPanel);\n    this.pageManager.addPanel(newTenantPanel);\n    this.pageManager.addPanel(broadcastPanel);\n    this.pageManager.addPanel(channelsPanel);\n    this.pageManager.addPanel(tenantPanel);\n\n    // Initialize Menu & Header\n    this.menuManager.init();\n    await this.headerHandler.init();\n    await this.pageManager.init();\n\n    this.menuManager.setMenuItemClickedCallback(async (pageId: string) => {\n      await this.showPage(pageId);\n    });\n\n    this.pageManager.onPanelShown = (panelId: string) => {\n      if (panelId === 'sign-in-panel' || (!this.tenantManager.getCurrentTenant() && panelId === 'new-tenant-panel')) {\n        this.menuManager.disableMenus(true);\n      } else {\n        this.menuManager.disableMenus(false);\n      }\n      this.menuManager.setSelectedItem(panelId);\n      this.syncUrlHash(panelId);\n    };\n\n    // Watch tenant changes\n    this.tenantManager.onTenantChanged(async (tenant) => {\n      if (tenant) {\n        this.menuManager.disableMenus(false);\n      }\n      await this.headerHandler.refresh();\n    });\n\n    // Watch auth state changes\n    this.authManager.onAuthChanged(async (user: users.UserProtoRow | null) => {\n      await this.headerHandler.refresh();\n      if (!user && !this.authManager.isDeviceSession()) {\n        this.menuManager.disableMenus(true);\n        await this.pageManager.showPanel('sign-in-panel');\n      } else if (user) {\n        await ensureBroadcasterTermsAccepted(this.authManager);\n      }\n    });\n\n    setOnUnauthorized(async () => {\n      this.authManager.handleSessionExpired();\n      this.menuManager.disableMenus(true);\n      await this.pageManager.showPanel('sign-in-panel');\n    });\n\n    // Check device session first to avoid /api/me 401 Unauthorized\n    if (this.authManager.isDeviceSession()) {\n      const userTenants = await this.tenantManager.loadUserTenants();\n      if (userTenants.length > 0) {\n        this.menuManager.disableMenus(false);\n        const tenantMenuItem = document.querySelector('a[data-page=\"tenant-panel\"], a[page=\"tenant-panel\"]')?.parentElement;\n        if (tenantMenuItem) tenantMenuItem.style.display = 'none';\n\n        await this.tenantManager.selectTenant(userTenants[0].id);\n        const devInfo = this.authManager.getDeviceInfo();\n        if (devInfo?.channelName) {\n          this.tenantManager.selectChannel(devInfo.channelName);\n        }\n        await this.headerHandler.refresh();\n        await this.routeInitialHash();\n        window.addEventListener(EVENT_HASHCHANGE, async () => {\n          await this.routeInitialHash();\n        });\n        return;\n      }\n    }\n\n    // Check user authentication\n    const user = await this.authManager.fetchCurrentUser();\n    await this.headerHandler.refresh();\n\n    if (!user) {\n      this.menuManager.disableMenus(true);\n      await this.pageManager.showPanel('sign-in-panel');\n      return;\n    }\n\n    // Ensure broadcaster terms & conditions accepted\n    await ensureBroadcasterTermsAccepted(this.authManager);\n\n    // Load user tenants\n    const userTenants = await this.tenantManager.loadUserTenants();\n    if (userTenants.length === 0) {\n      this.menuManager.disableMenus(true);\n      await this.pageManager.showPanel('new-tenant-panel');\n      return;\n    }\n\n    const sessionTenantId = sessionStorage.getItem('current_tenant_id');\n\n    // If user has access to multiple accounts, show selector when logging in (or if no account selected for this session yet)\n    if (userTenants.length > 1 && (hasJustLoggedIn || !sessionTenantId || !userTenants.some(t => String(t.id) === sessionTenantId))) {\n      this.menuManager.disableMenus(true);\n      await this.pageManager.showPanel('new-tenant-panel');\n      return;\n    }\n\n    // Tenant exists: select chosen or saved tenant\n    this.menuManager.disableMenus(false);\n    const chosenTenantId = (sessionTenantId && userTenants.find(t => String(t.id) === sessionTenantId)?.id) ||\n      (localStorage.getItem(STORAGE_LAST_TENANT_ID) && userTenants.find(t => String(t.id) === localStorage.getItem(STORAGE_LAST_TENANT_ID))?.id) ||\n      userTenants[0].id;\n\n    sessionStorage.setItem('current_tenant_id', String(chosenTenantId));\n    localStorage.setItem(STORAGE_LAST_TENANT_ID, String(chosenTenantId));\n    await this.tenantManager.selectTenant(chosenTenantId);\n    await this.headerHandler.refresh();\n\n    // Check URL query parameters for channel selection (e.g. ?channel=xxx or ?@xxx)\n    const urlParams = new URLSearchParams(window.location.search);\n    const channelParam = urlParams.get(PARAM_CHANNEL) || (window.location.search.startsWith('?@') ? window.location.search.slice(2) : null);\n    if (channelParam) {\n      this.tenantManager.selectChannel(channelParam);\n    }\n\n    // Handle initial route based on hash\n    await this.routeInitialHash();\n\n    // Listen for hash changes\n    window.addEventListener(EVENT_HASHCHANGE, async () => {\n      await this.routeInitialHash();\n    });\n  }\n\n  private async showPage(pageId: string): Promise<void> {\n    await this.pageManager.showPanel(pageId);\n    this.syncUrlHash(pageId);\n  }\n\n  private syncUrlHash(panelId: string): void {\n    let hash = '';\n    switch (panelId) {\n      case 'broadcast-panel':\n        hash = '#broadcast';\n        break;\n      case 'channels-panel':\n        hash = '#channels';\n        break;\n      case 'tenant-panel':\n        hash = '#tenant';\n        break;\n      case 'new-tenant-panel':\n        hash = '#new-tenant';\n        break;\n      default:\n        break;\n    }\n    if (hash && window.location.hash !== hash) {\n      window.history.replaceState(null, '', hash);\n    }\n  }\n\n  private async routeInitialHash(): Promise<void> {\n    const isDevice = this.authManager.isDeviceSession();\n    if (!this.authManager.getUser() && !isDevice) return;\n    if (this.tenantManager.getUserTenants().length === 0) return;\n\n    const rawHash = window.location.hash.replace(/^#/, '');\n    switch (rawHash) {\n      case 'channels':\n        await this.pageManager.showPanel('channels-panel');\n        break;\n      case 'tenant':\n        if (isDevice) {\n          await this.pageManager.showPanel('broadcast-panel');\n        } else {\n          await this.pageManager.showPanel('tenant-panel');\n        }\n        break;\n      case 'new-tenant':\n        if (isDevice) {\n          await this.pageManager.showPanel('broadcast-panel');\n        } else {\n          await this.pageManager.showPanel('new-tenant-panel');\n        }\n        break;\n      case 'broadcast':\n      default:\n        await this.pageManager.showPanel('broadcast-panel');\n        break;\n    }\n  }\n}\n\nlet appInitialized = false;\n\nfunction bootstrap(): void {\n  if (appInitialized) return;\n  appInitialized = true;\n  const app = new App();\n  app.init().catch(err => {\n    console.error('Failed to initialize application:', err);\n  });\n}\n\nif (document.readyState === 'loading') {\n  document.addEventListener(EVENT_DOM_CONTENT_LOADED, bootstrap);\n} else {\n  bootstrap();\n}\n"],"x_google_ignoreList":[0,1],"mappings":"uvBAAA,IAAa,GAAb,KAA8B,CAC1B,QAAU,KACV,MACA,UAAY,IAAI,IAChB,YACA,YAAY,EAAS,CAGjB,GAFA,KAAK,YAAc,GAAS,aAAe,iBAC3C,KAAK,MAAQ,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC,EAAI,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EACzE,OAAO,iBAAqB,IAC5B,GAAI,CACA,KAAK,QAAU,IAAI,iBAAiB,KAAK,WAAW,EACpD,KAAK,QAAQ,UAAa,GAAU,CAC5B,EAAM,MAAQ,EAAM,KAAK,cAAgB,KAAK,OAC9C,KAAK,gBAAgB,EAAM,IAAI,CAEvC,CACJ,OACO,EAAG,CACN,QAAQ,KAAK,uEAAuE,KAAK,YAAY,IAAK,CAAC,CAC/G,CAER,CACA,sBAAsB,EAAQ,EAAS,EAAU,CAC7C,IAAM,EAAQ,GAAW,KAAyB,IAAA,GAAlB,OAAO,CAAO,EACxC,EAAU,CACZ,SACA,QAAS,EACT,UAAW,EACX,WAAY,GAAU,WACtB,SAAU,GAAU,SACpB,YAAa,KAAK,MAClB,UAAW,KAAK,IAAI,EACpB,MAAO,GAAU,KACrB,EACA,GAAI,KAAK,QACL,GAAI,CACA,KAAK,QAAQ,YAAY,CAAO,CACpC,OACO,EAAG,CACN,QAAQ,KAAK,sDAAsD,KAAK,YAAY,IAAK,CAAC,CAC9F,CAEJ,KAAK,gBAAgB,CAAO,CAChC,CACA,aAAa,EAAU,CAEnB,OADA,KAAK,UAAU,IAAI,CAAQ,MACd,KAAK,UAAU,OAAO,CAAQ,CAC/C,CACA,gBAAgB,EAAS,CACrB,KAAK,UAAU,QAAQ,GAAY,CAC/B,GAAI,CACA,EAAS,CAAO,CACpB,OACO,EAAG,CACN,QAAQ,MAAM,wCAAyC,CAAC,CAC5D,CACJ,CAAC,CACL,CACA,UAAW,CACP,OAAO,KAAK,KAChB,CACA,gBAAiB,CACb,OAAO,KAAK,WAChB,CACA,OAAQ,CACJ,AAEI,KAAK,WADL,KAAK,QAAQ,MAAM,EACJ,MAEnB,KAAK,UAAU,MAAM,CACzB,CACJ,ECnEM,GAAc,IAAI,YAAY,GAAG,EACvC,IAAK,IAAI,EAAI,EAAG,EAAI,IAAK,IAAK,CAC1B,IAAI,EAAI,EACR,IAAK,IAAI,EAAI,EAAG,EAAI,EAAG,IACnB,EAAK,EAAI,EAAM,WAAc,IAAM,EAAO,IAAM,EAEpD,GAAY,GAAK,IAAM,CAC3B,CCPA,IAAI,GAA0D,KAE9D,SAAgB,GAA8C,CAM5D,MALA,CACE,KAAoB,IAAI,GAA8B,CACpD,YAAa,yBACf,CAAC,EAEI,EACT,CCCA,IAAa,GAAb,KAA2B,CACzB,YAAgD,CAAC,EACjD,cAAuD,KACvD,gBAAsD,CAAC,EACvD,eAA0D,KAC1D,mBAA8D,KAC9D,iBAAgE,KAChE,cAAoC,KACpC,QACA,gBACA,cAEA,yBAA4D,CAAC,EAC7D,0BAA8D,CAAC,EAC/D,gCAA0E,CAAC,EAE3E,YACE,EACA,EACA,EACA,CACA,KAAK,QAAU,EACf,KAAK,gBAAkB,EACvB,KAAK,cAAgB,EAGrB,EAAA,CAAA,CAAU,aAAa,KAAO,IAAY,CACxC,GAAI,EAAQ,SAAW,gBACjB,CAAC,EAAQ,SAAW,EAAQ,UAAY,OAAO,KAAK,eAAe,EAAE,IACvE,MAAM,KAAK,oBAAoB,OAE5B,GAAI,EAAQ,SAAW,WACxB,KAAA,CAAC,EAAQ,SAAW,EAAQ,UAAY,OAAO,KAAK,eAAe,EAAE,IACnE,KAAK,cAAe,CACtB,MAAM,KAAK,6BAA6B,EACxC,IAAK,IAAM,KAAM,KAAK,0BACpB,EAAG,KAAK,cAAc,CAE1B,OAEG,GAAI,EAAQ,SAAW,UAAW,CACvC,MAAM,KAAK,gBAAgB,EAC3B,IAAK,IAAM,KAAM,KAAK,yBACpB,EAAG,KAAK,aAAa,CAEzB,CACF,CAAC,CACH,CAEA,gBAAuB,EAAiC,CACtD,KAAK,yBAAyB,KAAK,CAAE,CACvC,CAEA,iBAAwB,EAAkC,CACxD,KAAK,0BAA0B,KAAK,CAAE,CACxC,CAEA,uBAA8B,EAAwC,CACpE,KAAK,gCAAgC,KAAK,CAAE,CAC9C,CAEA,gBAAkD,CAChD,OAAO,KAAK,WACd,CAEA,kBAAyD,CACvD,OAAO,KAAK,aACd,CAEA,aAAiD,CAC/C,OAAO,KAAK,eACd,CAEA,mBAA4D,CAC1D,OAAO,KAAK,cACd,CAEA,uBAAgE,CAC9D,OAAO,KAAK,kBACd,CAEA,qBAAkE,CAChE,OAAO,KAAK,gBACd,CAEA,kBAAsC,CACpC,OAAO,KAAK,aACd,CAEA,MAAa,iBAAqD,CAChE,GAAI,KAAK,kBAAkB,EAAG,CAC5B,IAAM,EAAU,KAAK,gBAAgB,EACrC,GAAI,EAAS,CACX,IAAM,EAAY,EAAQ,eAAe,OAAO,CAC9C,GAAI,EAAQ,SACZ,KAAM,EAAQ,UAChB,CAAC,EAID,GAHA,KAAK,YAAc,CAAC,CAAS,EAC7B,KAAK,cAAgB,EACrB,MAAM,KAAK,6BAA6B,EACpC,EAAQ,YAAa,CACvB,IAAM,EAAU,KAAK,gBAAgB,KACnC,GAAK,EAAE,KAAK,YAAY,IAAM,EAAQ,YAAa,YAAY,CACjE,EACI,IACF,KAAK,eAAiB,EAE1B,CACA,OAAO,KAAK,WACd,CACF,CAEA,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,cAAc,EAC/C,GAAI,EAAK,GAAI,CACX,IAAM,EAAS,MAAM,EAAK,YAAY,EAChC,EAAU,EAAQ,aAAa,OAAO,IAAI,WAAW,CAAM,CAAC,EAGlE,GAFA,KAAK,YAAe,EAAQ,SAAwC,CAAC,EAEjE,KAAK,cAAe,CACtB,IAAM,EAAQ,KAAK,YAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,OAAO,KAAK,cAAe,EAAE,CAAC,EACxF,KAAK,cAAgB,IAAU,KAAK,YAAY,OAAS,EAAI,KAAK,YAAY,GAAK,KACrF,KAAO,CAGL,KAAK,cAHI,KAAK,YAAY,OAAS,EACd,KAAK,YAAY,GAEjB,KAOvB,OAJI,KAAK,eACP,MAAM,KAAK,6BAA6B,EAGnC,KAAK,WACd,CACF,OAAS,EAAG,CACV,QAAQ,KAAK,+BAAgC,CAAC,EAC9C,KAAK,YAAc,CAAC,CACtB,CACA,MAAO,CAAC,CACV,CAEA,MAAa,aAAa,EAAgD,CACxE,IAAM,EAAQ,KAAK,YAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,OAAO,CAAQ,CAAC,EAC1E,GAAI,EAAO,CACT,KAAK,cAAgB,EACrB,KAAK,eAAiB,KACtB,MAAM,KAAK,6BAA6B,EACxC,MAAM,KAAK,oBAAoB,EAE/B,IAAK,IAAM,KAAM,KAAK,yBACpB,EAAG,KAAK,aAAa,CAEzB,CACF,CAEA,MAAa,wBAAwB,EAA6B,CAChE,GAAI,KAAK,cAAe,CACtB,KAAK,cAAc,KAAO,EAC1B,IAAM,EAAQ,KAAK,YAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,OAAO,KAAK,cAAe,EAAE,CAAC,EACpF,IACF,EAAM,KAAO,GAEf,IAAK,IAAM,KAAM,KAAK,yBACpB,EAAG,KAAK,aAAa,CAEzB,CACF,CAEA,MAAa,8BAAoE,CAC/E,GAAI,CAAC,KAAK,cAGR,MAFA,MAAK,gBAAkB,CAAC,EACxB,KAAK,eAAiB,KACf,CAAC,EAGV,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,KAAK,cAAc,GAAG,UAAU,EACjF,GAAI,EAAK,GAAI,CACX,IAAM,EAAS,MAAM,EAAK,YAAY,EAChC,EAAU,EAAS,cAAc,OAAO,IAAI,WAAW,CAAM,CAAC,EAGpE,GAFA,KAAK,gBAAmB,EAAQ,UAA2C,CAAC,EAExE,KAAK,eAAgB,CACvB,IAAM,EAAQ,KAAK,gBAAgB,KAAK,GAAM,EAAG,OAAS,KAAK,eAAgB,IAAI,EACnF,KAAK,eAAiB,IAAU,KAAK,gBAAgB,OAAS,EAAI,KAAK,gBAAgB,GAAK,KAC9F,KAAO,CAGL,KAAK,eAHI,KAAK,gBAAgB,OAAS,EACjB,KAAK,gBAAgB,GAErB,KAGxB,OAAO,KAAK,eACd,CACF,OAAS,EAAG,CACV,QAAQ,KAAK,2BAA4B,CAAC,EAC1C,KAAK,gBAAkB,CAAC,EACxB,KAAK,eAAiB,IACxB,CACA,MAAO,CAAC,CACV,CAEA,cAAqB,EAA2B,CAC9C,IAAM,EAAQ,KAAK,gBAAgB,KAAK,GAAM,EAAG,OAAS,CAAW,EACrE,GAAI,EAAO,CACT,KAAK,eAAiB,EACtB,KAAK,oBAAoB,EACzB,IAAK,IAAM,KAAM,KAAK,0BACpB,EAAG,KAAK,cAAc,CAE1B,CACF,CAEA,MAAa,qBAA2C,CACtD,GAAI,CAAC,KAAK,cAIR,MAHA,MAAK,cAAgB,KACrB,KAAK,mBAAqB,KAC1B,KAAK,iBAAmB,KACjB,KAGT,GAAI,CACF,IAAM,EAAW,MAAM,GAAwB,KAAK,cAAc,EAAE,EACpE,KAAK,iBAAmB,EACxB,IAAM,EAAU,GAA0B,CAAQ,EAClD,KAAK,mBAAqB,EAE1B,IAAM,EAAS,KAAK,gBAAgB,KAC9B,EAAgB,EAAS,EAAQ,eAAe,IAAI,CAAM,EAAI,IAAA,GAEpE,KAAK,cAAgB,CACnB,qBAAsB,EAAQ,oBAC9B,0BAA2B,EAAQ,iBACnC,kBAAmB,EAAQ,aAC3B,sBAAuB,EAAQ,sBAC/B,iBAAkB,GAAiB,KACnC,UAAW,GAAe,WAAa,GACvC,iBAAkB,GAAe,kBAAoB,CACvD,EAEA,IAAK,IAAM,KAAM,KAAK,gCACpB,EAAG,KAAK,aAAa,EAEvB,OAAO,KAAK,aACd,OAAS,EAAG,CACV,QAAQ,KAAK,wCAAyC,CAAC,CACzD,CACA,OAAO,IACT,CAEA,eAAgC,CAC9B,IAAM,EAAO,KAAK,QAAQ,EAG1B,MAFI,CAAC,GAAQ,CAAC,KAAK,eAAiB,CAAC,EAAK,YAAoB,GACjD,EAAK,YAAY,OAAO,KAAK,cAAc,EAAE,KAC1C,EAAM,KAAK,UAC7B,CAEA,eAAgC,CAC9B,GAAI,KAAK,kBAAkB,EACzB,MAAO,GAET,IAAM,EAAO,KAAK,QAAQ,EAC1B,GAAI,CAAC,GAAQ,CAAC,KAAK,eAAiB,CAAC,EAAK,YAAa,MAAO,GAC9D,IAAM,EAAO,EAAK,YAAY,OAAO,KAAK,cAAc,EAAE,GAC1D,OAAO,IAAS,EAAM,KAAK,YAAc,IAAS,EAAM,KAAK,gBAC/D,CACF,EC1Qa,GAAb,cAAmC,EAAa,CAC9C,YACA,cACA,YAEA,YAAY,EAA0B,EAA8B,EAA0B,CAC5F,MAAM,aAAa,EACnB,KAAK,YAAc,EACnB,KAAK,cAAgB,EACrB,KAAK,YAAc,EAEnB,KAAK,cAAc,2BAA6B,CAC9C,KAAK,wBAAwB,CAC/B,CAAC,CACH,CAEA,MAAM,MAAsB,CAC1B,MAAM,MAAM,KAAK,EAEjB,IAAM,EAAkB,SAAS,eAAe,0BAA0B,EACtE,GACF,EAAgB,iBAAiB,EAAa,SAAY,CACxD,MAAM,KAAK,YAAY,UAAU,kBAAkB,CACrD,CAAC,EAGH,IAAM,EAAe,SAAS,eAAe,eAAe,EACxD,GACF,EAAa,iBAAiB,EAAc,SAAY,CACtD,IAAM,EAAM,EAAa,MACrB,IAAQ,iBACV,MAAM,KAAK,YAAY,UAAU,kBAAkB,EAC1C,IACT,MAAM,KAAK,cAAc,aAAa,CAAG,EACzC,MAAM,KAAK,QAAQ,EAEvB,CAAC,EAGH,IAAM,EAAY,SAAS,eAAe,mBAAmB,EACzD,GACF,EAAU,iBAAiB,MAAmB,CAC5C,KAAK,YAAY,OAAO,CAC1B,CAAC,CAEL,CAEA,MAAM,SAAyB,CAC7B,MAAM,MAAM,QAAQ,EAEpB,IAAM,EAAO,KAAK,YAAY,QAAQ,EAChC,EAAW,KAAK,YAAY,gBAAgB,EAC5C,EAAa,KAAK,YAAY,cAAc,EAC5C,EAAc,SAAS,eAAe,mBAAmB,EACzD,EAAa,SAAS,eAAe,kBAAkB,EACvD,EAAa,SAAS,eAAe,kBAAkB,EACvD,EAAgB,SAAS,eAAe,qBAAqB,EAC7D,EAAe,SAAS,eAAe,oBAAoB,EAC3D,EAAkB,SAAS,eAAe,0BAA0B,EACpE,EAAe,SAAS,eAAe,eAAe,EACtD,EAAe,SAAS,eAAe,sBAAsB,GAAK,SAAS,eAAe,oBAAoB,EAC9G,EAAY,SAAS,eAAe,mBAAmB,EAE7D,GAAI,GAAQ,EAAU,CAChB,IAAa,EAAY,MAAM,QAAU,GACzC,IACF,EAAW,YAAc,EACpB,GAAY,OAAS,eACrB,GAAM,MAAQ,GAAM,OAAS,QAEhC,IACF,EAAU,MAAM,QAAU,EAC1B,EAAU,YAAc,EAAW,aAAe,WAGpD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAc,KAAK,cAAc,eAAe,EAElD,GAAiB,EAAY,OAAS,GACpC,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAc,EAAa,MAAM,QAAU,GAE3C,IACF,EAAa,YAAc,EAAc,MAGvC,IACF,EAAgB,MAAM,QAAW,CAAC,GAAY,EAAY,OAAS,EAAK,EAAuB,GAG7F,IACE,GACF,EAAa,UAAY,kBAAkB,EAAc,GAAG,aAAa,EAAc,KAAK,WAC5F,EAAa,SAAW,KAExB,EAAa,SAAW,GACxB,EAAa,UAAY;gBACrB,EAAY,IAAI,GAAK,kBAAkB,EAAE,GAAG,IAAI,OAAO,EAAE,EAAE,IAAM,OAAO,EAAc,EAAE,EAAI,WAAa,GAAG,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC,KAAK,EAAE,EAAA;;gBAM7I,IACF,EAAW,MAAM,QAAU,EAC3B,EAAW,YAAc,EAAW,eAAkB,KAAK,cAAc,cAAc,EAAI,QAAU,eAGvG,KAAK,wBAAwB,IAEzB,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAc,EAAa,MAAM,QAAU,GAC3C,IAAY,EAAW,MAAM,QAAU,GAG/C,MACM,IAAa,EAAY,MAAM,QAAU,GACzC,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAc,EAAa,MAAM,QAAU,GAC3C,IAAW,EAAU,MAAM,QAAU,EAE7C,CAEA,yBAAwC,CACtC,IAAM,EAAY,SAAS,eAAe,sBAAsB,GAAK,SAAS,eAAe,sBAAsB,EAC7G,EAAc,SAAS,eAAe,qBAAqB,EAC3D,EAAe,SAAS,eAAe,yBAAyB,EAChE,EAAmB,SAAS,eAAe,0BAA0B,EAErE,EAAS,KAAK,cAAc,iBAAiB,EACnD,GAAI,CAAC,EAAQ,CACP,IAAW,EAAU,YAAc,SACnC,IAAc,EAAa,YAAc,MAC7C,MACF,CAEA,IAAM,EAAS,GAAmB,CAChC,GAAI,GAAM,KAAyB,MAAO,GAC1C,GAAI,OAAO,GAAM,SAAU,OAAO,EAClC,GAAI,OAAO,GAAM,SAAU,CACzB,GAAI,OAAO,EAAE,UAAa,WAAY,OAAO,EAAE,SAAS,EACxD,GAAI,OAAO,EAAE,KAAQ,SAAU,OAAO,EAAE,KAAO,EAAE,MAAQ,GAAK,UAChE,CACA,OAAO,OAAO,CAAC,GAAK,CACtB,EAEM,EAAW,KAAK,IAAI,EAAG,EAAM,EAAO,yBAAyB,CAAC,EAC9D,EAAQ,KAAK,MAAM,EAAW,IAAI,EAClC,EAAO,KAAK,MAAO,EAAW,KAAQ,EAAE,EACxC,EAAmB,GAAG,OAAO,CAAK,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GAAG,OAAO,CAAI,CAAC,CAAC,SAAS,EAAG,GAAG,IAEtF,IACF,EAAU,YAAc,GAGtB,IACE,EAAW,KACb,EAAY,UAAU,IAAI,CAAY,EAEtC,EAAY,UAAU,OAAO,CAAY,GAI7C,IAAM,EAAoB,EAAO,EAAe,qBAAqB,EAC/D,EAAM,KAAK,IAAI,EACf,EAAa,EAAQ,EAAO,oBAAuB,IAAsB,GAAK,EAAoB,GAEpG,IACF,AAQE,EAAa,YARX,EAAoB,EAEK,IADb,KAAK,CACQ,CAAA,CAAE,mBAAmB,IAAA,GAAW,CACzD,MAAO,QACP,IAAK,UACL,KAAM,SACR,CAAC,EAE0B,EAAa,SAAW,WAInD,IACE,CAAC,GAAc,EAAoB,EACrC,EAAiB,UAAU,IAAI,CAAa,EAE5C,EAAiB,UAAU,OAAO,CAAa,EAGrD,CACF,8DC/La,GAAb,KAA0B,CACxB,aAA4C,KAC5C,YAA0C,KAC1C,WAAwD,KACxD,QAA2C,KAC3C,UAEA,YAA6B,IAC7B,SAA0B,IAC1B,UAA2B,IAC3B,mBAAqC,GACrC,gBAAmC,QAEnC,IAAI,WAAqB,CACvB,OAAO,KAAK,cAAgB,MAAQ,KAAK,YAAY,MACvD,CAEA,YAAY,EAAmC,CAAC,EAAG,CACjD,KAAK,UAAY,CACnB,CAEA,aAAa,iBAA8C,CACzD,GAAI,CAGF,OAAO,MADe,UAAU,aAAa,iBAAiB,EAAA,CAC/C,OAAQ,GAAM,EAAE,OAAS,YAAY,CACtD,OAAS,EAAG,CAEV,OADA,QAAQ,MAAM,qCAAsC,CAAC,EAC9C,CAAC,CACV,CACF,CAEA,MAAM,MAAM,EAAkC,CAC5C,MAAM,KAAK,KAAK,EAEhB,GAAI,CACF,IAAM,EAAsC,CAC1C,MAAO,CACL,SAAU,EAAW,CAAE,MAAO,CAAS,EAAI,IAAA,GAC3C,iBAAkB,GAClB,iBAAkB,GAClB,gBAAiB,GACjB,aAAc,CAChB,EACA,MAAO,EACT,EAEA,KAAK,YAAc,MAAM,UAAU,aAAa,aAAa,CAAW,EACxE,KAAK,aAAe,IAAK,OAAO,cAAiB,OAAe,oBAAoB,CAAE,WAAY,IAAM,CAAC,EAErG,KAAK,aAAa,QAAU,aAC9B,MAAM,KAAK,aAAa,OAAO,EAIjC,MAAM,KAAK,aAAa,aAAa,UAAU,EAAe,EAE9D,KAAK,WAAa,KAAK,aAAa,wBAAwB,KAAK,WAAW,EAC5E,KAAK,QAAU,IAAI,iBAAiB,KAAK,aAAc,gBAAiB,CACtE,iBAAkB,CAChB,iBAAkB,KAClB,YAAa,KAAK,YAClB,SAAU,KAAK,SACf,UAAW,KAAK,UAChB,mBAAoB,KAAK,kBAC3B,CACF,CAAC,EAED,KAAK,QAAQ,KAAK,UAAa,GAAwB,CACrD,IAAM,EAAM,EAAM,KACb,IAED,EAAI,OAAS,SACX,EAAI,QACN,KAAK,gBAAkB,EAAI,OAE7B,KAAK,UAAU,cAAc,CAC3B,IAAK,EAAI,IACT,GAAI,EAAI,GACR,MAAO,EAAI,KACb,CAAC,GACQ,EAAI,OAAS,SACtB,KAAK,gBAAkB,EAAI,MAC3B,KAAK,UAAU,gBAAgB,EAAI,KAAK,GAC/B,EAAI,OAAS,QACtB,KAAK,UAAU,cAAc,EAAI,OAAQ,EAAI,EACpC,EAAI,OAAS,SACtB,KAAK,UAAU,cAAc,EAAI,OAAQ,EAAK,EAElD,EAGA,KAAK,WAAW,QAAQ,KAAK,OAAO,CACtC,OAAS,EAAU,CAEjB,MADA,KAAK,UAAU,UAAU,CAAG,EACtB,CACR,CACF,CAEA,aAAa,EAAkB,CAC7B,KAAK,YAAc,EACf,KAAK,SACP,KAAK,QAAQ,KAAK,YAAY,CAAE,KAAM,eAAgB,YAAa,CAAG,CAAC,CAE3E,CAEA,UAAU,EAAkB,CAC1B,KAAK,SAAW,EACZ,KAAK,SACP,KAAK,QAAQ,KAAK,YAAY,CAAE,KAAM,YAAa,SAAU,CAAG,CAAC,CAErE,CAEA,QAAQ,EAAkB,CACxB,KAAK,UAAY,EACb,KAAK,SACP,KAAK,QAAQ,KAAK,YAAY,CAAE,KAAM,UAAW,OAAQ,EAAI,UAAW,CAAG,CAAC,CAEhF,CAEA,WAAW,EAAkB,CAC3B,KAAK,QAAQ,CAAE,CACjB,CAEA,sBAAsB,EAAsB,CAC1C,KAAK,mBAAqB,EACtB,KAAK,SACP,KAAK,QAAQ,KAAK,YAAY,CAAE,KAAM,wBAAyB,mBAAoB,CAAM,CAAC,CAE9F,CAEA,MAAM,MAAsB,CAC1B,AAEE,KAAK,WADL,KAAK,QAAQ,WAAW,EACT,MAEjB,AAEE,KAAK,cADL,KAAK,WAAW,WAAW,EACT,MAEpB,AAEE,KAAK,eADL,KAAK,YAAY,UAAU,CAAC,CAAC,QAAS,GAAM,EAAE,KAAK,CAAC,EACjC,MAErB,AAEE,KAAK,gBADL,MAAM,KAAK,aAAa,MAAM,EACV,MAEtB,KAAK,gBAAkB,OACzB,CACF,ECpJA,SAAgB,GAAgB,EAAmC,CACjE,GAAI,CAAC,EACH,MAAO,mBAET,OAAQ,EAAU,KAAK,CAAC,CAAC,YAAY,EAArC,CACE,IAAK,KACL,IAAK,KACH,MAAO,sBACT,IAAK,KACH,MAAO,sBACT,IAAK,KACH,MAAO,sBACT,QACE,MAAO,kBACX,CACF,CASA,IAAa,GAAb,KAA8B,CAC5B,GAA+B,KAC/B,OAAyB,GACzB,eAAwC,KACxC,UAEA,YAA8B,GAC9B,QAA0B,GAC1B,iBAAmC,GAGnC,cAA+B,EAC/B,aAA8B,EAC9B,WAAoC,KAEpC,SAA0B,QAC1B,mBAAsC,GAEtC,YAAY,EAAuC,CAAC,EAAG,CACrD,KAAK,UAAY,CACnB,CAEA,sBAAsB,EAAsB,CAC1C,KAAK,mBAAqB,EACtB,EACF,KAAK,cAAc,GACV,KAAK,SAAW,KAAK,mBAC9B,KAAK,eAAe,CAExB,CAEA,MAAM,QACJ,EACA,EAAmB,QACnB,EACA,EACA,EACe,CACf,KAAK,WAAW,EAChB,KAAK,OAAS,EACd,KAAK,SAAW,EAEhB,KAAK,UAAU,iBAAiB,YAAY,EAE5C,IAAM,EAAS,IAAI,gBAAgB,CACjC,MAAO,SACP,SAAU,KAAK,SACf,SAAU,WACV,YAAa,QACb,SAAU,IACV,gBAAiB,OACjB,aAAc,OACd,YAAa,MACb,iBAAkB,OAClB,WAAY,MACd,CAAC,EAEG,EACF,EAAO,IAAI,MAAO,CAAG,EACZ,GACT,EAAO,IAAI,MAAO,EAAU,WAAW,UAAU,EAAI,EAAY,WAAW,GAAW,EAKzF,IAAM,EAAQ,SADD,GADa,GAAa,EAAmB,CAEnC,EAAK,aAAa,EAAO,SAAS,IAEzD,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,GAAI,CAEF,KAAK,GAAK,IAAI,UAAU,EAAO,CAAC,EAAsB,KAAK,MAAM,CAAC,EAClE,KAAK,GAAG,WAAa,cAErB,KAAK,GAAG,WAAe,CACrB,KAAK,YAAc,GACnB,KAAK,UAAU,iBAAiB,WAAW,EAC3C,KAAK,gBAAgB,EACjB,KAAK,mBACP,KAAK,cAAc,GACV,KAAK,SAAW,KAAK,mBAC9B,KAAK,eAAe,EAEtB,EAAQ,CACV,EAEA,KAAK,GAAG,UAAa,GAAwB,CAC3C,GAAI,CACF,IAAM,EAAuB,KAAK,MAAM,EAAM,IAAI,EAClD,GAAI,EAAK,OAAS,eAAgB,CAChC,KAAK,UAAU,iBAAiB,EAChC,MACF,CACA,GAAI,EAAK,OAAS,WAAa,EAAK,SAAS,cAAc,OAAQ,CACjE,IAAM,EAAM,EAAK,QAAQ,aAAa,GAChC,EAAa,EAAI,YAAY,KAAK,GAAK,GAC7C,GAAI,EAAY,CACd,IAAM,EAAU,EAAQ,EAAK,SACvB,EAAc,EAAQ,EAAK,aAC3B,EAAa,EAAI,YAAc,IACrC,KAAK,UAAU,eAAe,EAAY,EAAS,EAAa,CAAU,CAC5E,CACF,CACF,OAAS,EAAK,CACZ,QAAQ,MAAM,kCAAmC,CAAG,CACtD,CACF,EAEA,KAAK,GAAG,QAAW,GAAQ,CACzB,QAAQ,MAAM,4BAA6B,CAAG,EAC9C,KAAK,UAAU,iBAAiB,QAAS,4BAA4B,EACrE,EAAO,CAAG,CACZ,EAEA,KAAK,GAAG,QAAW,GAAU,CAC3B,KAAK,YAAc,GACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,UAAU,iBAAiB,eAAgB,gBAAgB,EAAM,KAAK,EAAE,CAC/E,CACF,OAAS,EAAU,CACjB,KAAK,UAAU,iBAAiB,QAAS,EAAI,OAAO,EACpD,EAAO,CAAG,CACZ,CACF,CAAC,CACH,CAKA,YAAY,EAAiC,CACvC,IAAU,SACZ,KAAK,QAAU,GACV,KAAK,oBACR,KAAK,eAAe,IAGtB,KAAK,QAAU,IACX,CAAC,KAAK,kBAAoB,KAAK,qBACjC,KAAK,cAAc,EAGzB,CAKA,kBAAkB,EAAuB,CACvC,KAAK,iBAAmB,EACpB,GAAU,CAAC,KAAK,mBAClB,KAAK,eAAe,GACX,CAAC,KAAK,SAAW,KAAK,qBAC/B,KAAK,cAAc,CAEvB,CAMA,UAAU,EAAyB,CAC5B,KAAK,aAAgB,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,OAGlE,KAAK,oBAAuB,CAAC,KAAK,SAAW,CAAC,KAAK,mBACrD,KAAK,GAAG,KAAK,CAAI,CAErB,CAEA,gBAA+B,CACzB,KAAK,iBAAmB,OAG5B,KAAK,kBAAkB,EAGvB,KAAK,eAAiB,OAAO,gBAAkB,CAC7C,KAAK,kBAAkB,CACzB,EAAG,GAAI,EACT,CAEA,eAA8B,CACxB,KAAK,iBAAmB,OAC1B,cAAc,KAAK,cAAc,EACjC,KAAK,eAAiB,KAE1B,CAEA,mBAAkC,CAC5B,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,MAC9C,KAAK,GAAG,KAAK,KAAK,UAAU,CAAE,KAAM,WAAY,CAAC,CAAC,CAEtD,CAEA,iBAAgC,CAC9B,KAAK,eAAe,EACpB,KAAK,WAAa,OAAO,gBAAkB,CACrC,KAAK,cACH,KAAK,SAAW,KAAK,iBACvB,KAAK,cAAgB,EAErB,KAAK,eAAiB,EAExB,KAAK,UAAU,kBAAkB,KAAK,cAAe,KAAK,YAAY,EAE1E,EAAG,GAAI,CACT,CAEA,gBAA+B,CACzB,KAAK,aAAe,OACtB,cAAc,KAAK,UAAU,EAC7B,KAAK,WAAa,KAEtB,CAEA,YAAmB,CACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,AAME,KAAK,MALD,KAAK,GAAG,aAAe,UAAU,OAEnC,KAAK,GAAG,KAAK,KAAK,UAAU,CAAE,KAAM,aAAc,CAAC,CAAC,EACpD,KAAK,GAAG,MAAM,GAEN,MAEZ,KAAK,YAAc,EACrB,CACF,ECvPa,GAAb,KAAyB,CACvB,GAA+B,KAC/B,YAA8B,GAC9B,UACA,qBAAwC,GACxC,kBAAoC,EACpC,eAAwC,KAExC,YAAY,EAAkC,CAAC,EAAG,CAChD,KAAK,UAAY,CACnB,CAEA,MAAM,QAAQ,EAAoC,CAChD,KAAK,WAAW,EAChB,KAAK,YAAc,EACnB,KAAK,qBAAuB,GAC5B,KAAK,kBAAoB,EAEzB,MAAM,KAAK,oBAAoB,CACjC,CAEA,MAAc,qBAAqC,CACjD,KAAK,UAAU,iBAAiB,YAAY,EAE5C,GAAI,CACF,IAAM,EAAY,MAAM,EACtB,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,+BACxD,EACA,GAAI,CAAC,EAAU,GACb,MAAU,MAAM,4CAA4C,EAAU,OAAO,EAAE,EAGjF,IAAM,EAAS,MAAM,EAAU,YAAY,EACrC,EAAW,EAAO,sBAAsB,OAAO,IAAI,WAAW,CAAM,CAAC,EAEvE,EAAU,EAAa,GAAK,EAAS,WAAa,OAAO,SAAS,OAElE,EADa,EAAmB,CACvB,CAAA,CAAW,QAAQ,SAAU,IAAI,EAC1C,CAAC,EAAO,WAAW,OAAO,GAAK,CAAC,EAAO,WAAW,QAAQ,IAE5D,EAAS,GADQ,OAAO,SAAS,WAAa,SAAW,OAAS,MAC7C,IAAI,KAEvB,OAAO,SAAS,WAAa,UAAY,EAAO,WAAW,OAAO,IACpE,EAAS,EAAO,QAAQ,YAAa,QAAQ,GAG/C,IAAM,EAAQ,GAAG,EAAO,QAAQ,OAAQ,EAAE,EAAE,gBAAgB,mBAAmB,KAAK,WAAW,EAAE,GAAG,EAAY,GAAG,mBAAmB,EAAS,OAAS,EAAE,IAEpJ,EAAS,IAAI,UAAU,CAAK,EAClC,KAAK,GAAK,EAEV,EAAO,iBAAiB,MAAkB,CACpC,KAAK,KAAO,IAChB,KAAK,kBAAoB,EACzB,KAAK,UAAU,iBAAiB,WAAW,EAC7C,CAAC,EAED,EAAO,iBAAiB,EAAgB,GAAU,CAC5C,QAAK,KAAO,EAChB,GAAI,CACF,IAAM,EAAO,KAAK,MAAM,EAAM,IAAI,EAC9B,EAAK,OAAS,aAChB,KAAK,UAAU,oBAAoB,EAAK,OAAS,EAAG,EAAK,MAAM,CAEnE,OAAS,EAAK,CACZ,QAAQ,KAAK,qDAAsD,CAAG,CACxE,CACF,CAAC,EAED,EAAO,iBAAiB,EAAc,GAAU,CAC1C,KAAK,KAAO,IAChB,KAAK,GAAK,KACL,KAAK,qBAIR,KAAK,UAAU,iBAAiB,cAAc,GAH9C,KAAK,UAAU,iBAAiB,eAAgB,sBAAsB,EAAM,KAAK,EAAE,EACnF,KAAK,kBAAkB,GAI3B,CAAC,EAED,EAAO,iBAAiB,MAAmB,CACzC,GAAI,KAAK,KAAO,EAAQ,OACxB,IAAM,EAAU,MAAM,4BAA4B,EAClD,KAAK,UAAU,UAAU,CAAG,EAC5B,KAAK,UAAU,iBAAiB,QAAS,iBAAiB,CAC5D,CAAC,CACH,OAAS,EAAU,CAMjB,MALA,KAAK,UAAU,UAAU,CAAG,EAC5B,KAAK,UAAU,iBAAiB,QAAS,EAAI,OAAO,EAC/C,KAAK,sBACR,KAAK,kBAAkB,EAEnB,CACR,CACF,CAEA,mBAAkC,CAChC,GAAI,KAAK,sBAAwB,KAAK,iBAAmB,KAAM,OAE/D,IAAM,EAAQ,KAAK,IAAI,IAAgB,KAAK,KAAK,kBAAoB,IAAK,EAC1E,KAAK,oBAEL,KAAK,eAAiB,OAAO,WAAW,SAAY,CAElD,GADA,KAAK,eAAiB,KAClB,CAAC,KAAK,qBACR,GAAI,CACF,MAAM,KAAK,oBAAoB,CACjC,MAAQ,CAER,CAEJ,EAAG,CAAK,CACV,CAEA,MAAM,QACJ,EACA,EACA,EACA,EAAqB,IACrB,EACe,CACf,OAAO,KAAK,eAAe,EAAM,EAAS,EAAa,EAAY,CAAI,CACzE,CAEA,MAAM,eACJ,EACA,EACA,EACA,EAAqB,IACrB,EACe,CACf,GAAI,CAAC,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,KAAM,CACrD,QAAQ,KAAK,+CAA+C,EAC5D,MACF,CAEA,IAAM,EAAQ,CACZ,KAAM,UACN,GAAI,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,EAAG,CAAC,IAC9D,YAAa,KAAK,YAClB,OACA,UACA,cACA,aACA,UAAW,KAAK,IAAI,EACpB,MACF,EAEA,KAAK,GAAG,KAAK,KAAK,UAAU,CAAK,CAAC,CACpC,CAEA,qBAA4B,CAC1B,GAAI,CAAC,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,KAC/C,OAGF,IAAM,EAAQ,CACZ,KAAM,gBACN,YAAa,KAAK,YAClB,UAAW,KAAK,IAAI,CACtB,EAEA,KAAK,GAAG,KAAK,KAAK,UAAU,CAAK,CAAC,CACpC,CAEA,YAAmB,CAMjB,GALA,KAAK,qBAAuB,GACxB,KAAK,iBAAmB,OAC1B,aAAa,KAAK,cAAc,EAChC,KAAK,eAAiB,MAEpB,KAAK,GAAI,CACX,GAAI,CACF,KAAK,GAAG,MAAM,IAAM,0BAA0B,CAChD,MAAQ,CAER,CACA,KAAK,GAAK,IACZ,CACF,CACF,ECvLA,eAAsB,GAAwB,EAAgB,SAAqC,CACjG,IAAM,EAAM,MAAM,MAAM,EAAO,wBAAwB,mBAAmB,CAAK,GAAG,CAAC,EACnF,GAAI,CAAC,EAAI,GAAI,CACX,IAAM,EAAY,MAAM,EAAI,KAAK,CAAC,CAAC,UAAY,EAAE,EACjD,MAAU,MAAM,oDAAoD,EAAI,OAAO,KAAK,GAAW,CACjG,CACA,IAAM,EAAS,MAAM,EAAI,YAAY,EAE/B,GADY,EAAO,wBAAwB,OAAO,IAAI,WAAW,CAAM,CAC7C,CAAA,CAAU,WAAa,CAAC,EAAA,CAAG,IAAK,IAAO,CACrE,KAAM,EAAE,MAAQ,GAChB,KAAM,EAAE,MAAQ,GAChB,WAAY,EAAE,YAAc,IAAA,EAC9B,EAAE,EACF,GAAI,EAAK,SAAW,EAClB,MAAU,MAAM,8CAA8C,EAEhE,OAAO,CACT,CAEA,IAAM,GAAkD,CACtD,mBAAoB,KACpB,cAAe,KACf,oBAAqB,KACrB,kBAAmB,KACnB,gBAAiB,KACjB,gBAAiB,KACjB,gBAAiB,KACjB,eAAgB,KAChB,cAAe,KACf,gBAAiB,KACjB,gBAAiB,KACjB,mBAAoB,KACpB,kBAAmB,KACnB,gBAAiB,KACjB,gBAAiB,KACjB,kBAAmB,KACnB,gBAAiB,KACjB,mBAAoB,KACpB,cAAe,KACf,gBAAiB,QACjB,gBAAiB,KACjB,oBAAqB,QACrB,uBAAwB,KACxB,mBAAoB,KACpB,iBAAkB,KAClB,eAAgB,KAChB,sBAAuB,KACvB,aAAc,KACd,aAAc,IAChB,EAMA,SAAgB,GAAsB,EAAkD,CACtF,GAAI,CAAC,GAAW,EAAQ,SAAW,EAAG,OAAO,KAE7C,IAAM,EAAwB,CAAC,EAC3B,OAAO,UAAc,MACnB,UAAU,WAAa,UAAU,UAAU,OAAS,GACtD,EAAY,KAAK,GAAG,UAAU,SAAS,EAErC,UAAU,UACZ,EAAY,KAAK,UAAU,QAAQ,GAIvC,IAAK,IAAM,KAAU,EAAa,CAChC,IAAM,EAAc,EAAO,YAAY,CAAC,CAAC,QAAQ,IAAK,GAAG,EAGnD,EAAQ,EAAQ,KAAM,GAAM,EAAE,KAAK,YAAY,IAAM,CAAW,EACtE,GAAI,EAAO,OAAO,EAGlB,IAAM,EAAU,EAAY,MAAM,GAAG,CAAC,CAAC,GACjC,EAAe,EAAQ,KAAM,GACb,EAAE,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAC7B,CACxB,EACD,GAAI,EAAc,OAAO,CAC3B,CAGA,GAAI,CACF,IAAM,EAAK,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC,SACnD,GAAI,GAAM,GAAwB,GAAK,CACrC,IAAM,EAAO,GAAwB,EAAG,CAAC,YAAY,EAC/C,EAAU,EAAQ,KACrB,GAAM,EAAE,KAAK,YAAY,IAAM,GAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAO,EAAK,MAAM,GAAG,CAAC,CAAC,EACjG,EACA,GAAI,EAAS,OAAO,CACtB,CACF,MAAQ,CAER,CAMA,OAHc,EAAQ,KAAM,GAAM,EAAE,KAAK,YAAY,IAAM,OACvD,GAEG,EAAQ,IAAM,IACvB,CCpGA,SAAS,GAAiB,EAA4B,CACpD,GAAI,CACF,IAAM,EAAM,aAAa,QAAQ,CAAG,EACpC,GAAI,EAAK,OAAO,CAClB,MAAQ,CAAC,CAET,GAAI,CACF,GAAI,OAAO,SAAa,KAAe,SAAS,OAAQ,CACtD,IAAM,EAAQ,SAAS,OAAO,MAAU,OAAO,WAAW,EAAI,SAAS,CAAC,EACxE,GAAI,EAAO,OAAO,mBAAmB,EAAM,EAAE,CAC/C,CACF,MAAQ,CAAC,CAET,OAAO,IACT,CAEA,SAAS,GAAa,EAAa,EAAoB,CACrD,GAAI,CACF,aAAa,QAAQ,EAAK,CAAI,CAChC,MAAQ,CAAC,CAET,GAAI,CACE,OAAO,SAAa,MACtB,SAAS,OAAS,GAAG,EAAI,GAAG,mBAAmB,CAAI,EAAE,0CAEzD,MAAQ,CAAC,CACX,CAEA,IAAa,GAAb,KAA8B,CAC5B,UACA,iBAAkD,KAClD,iBACA,mBAA+C,CAAC,EAEhD,QACA,WACA,OAA0B,GAC1B,iBAAmC,GACnC,kBAA8C,CAAC,EAC/C,oBAAuC,GACvC,UAA6B,GAC7B,aAA+B,GAC/B,WAEA,YAAY,EAAwB,EAA0C,EAAsB,CAClG,KAAK,UAAY,EACjB,KAAK,iBAAmB,EACxB,IAAM,EAAe,EAAc,EAAY,QAAQ,MAAO,EAAE,EAAI,GACpE,KAAK,WAAa,EACd,GAAG,EAA2B,GAAG,IACjC,EAEJ,KAAK,OAAO,EACZ,KAAK,WAAW,EAGhB,KAAK,cAAc,CACrB,CAEA,MAAa,eAA+B,CAC1C,KAAK,UAAY,GACjB,KAAK,aAAe,GACpB,KAAK,mBAAqB,CAAC,EAC3B,KAAK,kBAAoB,CAAC,EAEtB,KAAK,UACP,KAAK,QAAQ,MAAQ,GACrB,KAAK,QAAQ,YAAc,0CAC3B,KAAK,QAAQ,SAAW,GACxB,KAAK,QAAQ,MAAM,YAAc,uBAGnC,GAAI,CACF,IAAM,EAAQ,MAAM,GAAwB,QAAQ,EAMpD,GALA,KAAK,UAAY,GACjB,KAAK,mBAAqB,EAC1B,KAAK,kBAAoB,CAAC,GAAG,CAAK,EAG9B,CAAC,KAAK,oBAAqB,CAC7B,IAAI,EAAqC,KACnC,EAAmB,GAAiB,KAAK,UAAU,EACnD,EAAkB,GAAiB,CAA0B,EAC7D,EAAa,GAAoB,EAEnC,IACF,EAAc,KAAK,mBAAmB,KAAM,GAAM,EAAE,KAAK,YAAY,IAAM,EAAW,YAAY,CAAC,GAAK,MAG1G,AACE,IAAc,GAAsB,KAAK,kBAAkB,EAG7D,KAAK,iBAAmB,EACpB,KAAK,kBACP,KAAK,iBAAiB,KAAK,gBAAgB,CAE/C,CAEI,KAAK,UACP,KAAK,QAAQ,SAAW,GACxB,KAAK,QAAQ,YAAc,6BACvB,KAAK,mBACP,KAAK,QAAQ,MAAQ,GAAG,KAAK,iBAAiB,KAAK,IAAI,KAAK,iBAAiB,KAAK,KAIlF,KAAK,QACP,KAAK,oBAAoB,CAE7B,OAAS,EAAU,CACjB,KAAK,UAAY,GACjB,KAAK,aAAe,EAAI,SAAW,2BACnC,KAAK,iBAAmB,KAEpB,KAAK,UACP,KAAK,QAAQ,SAAW,GACxB,KAAK,QAAQ,MAAQ,0BACrB,KAAK,QAAQ,MAAM,YAAc,gCAGnC,KAAK,oBAAoB,CAC3B,CACF,CAEA,aAA4C,CAC1C,OAAO,KAAK,gBACd,CAEA,YAAmB,EAAoB,CACrC,IAAM,EAAQ,KAAK,mBAAmB,KAAM,GAAM,EAAE,OAAS,CAAI,EAC7D,IACF,KAAK,oBAAsB,GAC3B,KAAK,iBAAmB,EACxB,KAAK,qBAAqB,EAAM,IAAI,EACpC,KAAK,QAAQ,MAAQ,GAAG,EAAM,KAAK,IAAI,EAAM,KAAK,GAClD,KAAK,iBAAiB,CAAK,EAE/B,CAEA,qBAA6B,EAAoB,CAC/C,GAAa,KAAK,WAAY,CAAI,EAClC,GAAa,EAA4B,CAAI,CAC/C,CAEA,QAAuB,CACrB,KAAK,UAAU,UAAY;;;;;;;;;;;;;;;;;;;;;MAuB3B,KAAK,QAAU,KAAK,UAAU,cAAc,oBAAoB,EAChE,KAAK,WAAa,KAAK,UAAU,cAAc,qBAAqB,CACtE,CAEA,YAA2B,CAEzB,KAAK,QAAQ,iBAAiB,MAAmB,CAC/C,GAAI,KAAK,WAAa,KAAK,aAAc,OACzC,KAAK,QAAQ,OAAO,EACpB,IAAM,EAAsB,KAAK,iBAAmB,GAAG,KAAK,iBAAiB,KAAK,IAAI,KAAK,iBAAiB,KAAK,GAAK,GACtH,KAAK,OAAO,KAAK,QAAQ,QAAU,EAAsB,GAAK,KAAK,QAAQ,KAAK,EAChF,KAAK,KAAK,CACZ,CAAC,EAED,KAAK,QAAQ,iBAAiB,MAAmB,CAC3C,KAAK,WAAa,KAAK,cACtB,KAAK,QACR,KAAK,KAAK,CAEd,CAAC,EAGD,KAAK,QAAQ,iBAAiB,MAAmB,CAC3C,KAAK,WAAa,KAAK,eAC3B,KAAK,OAAO,KAAK,QAAQ,MAAM,KAAK,CAAC,EAChC,KAAK,QAAQ,KAAK,KAAK,EAC9B,CAAC,EAGD,KAAK,QAAQ,iBAAiB,EAAgB,GAAqB,CAC7D,UAAK,WAAa,KAAK,cAE3B,IAAI,CAAC,KAAK,SAAW,EAAE,MAAA,aAA0B,EAAE,MAAA,SAAoB,CACrE,KAAK,KAAK,EACV,EAAE,eAAe,EACjB,MACF,CAEK,KAAK,SAEN,EAAE,MAAA,aACJ,EAAE,eAAe,EACjB,KAAK,iBAAmB,KAAK,IAAI,KAAK,iBAAmB,EAAG,KAAK,kBAAkB,OAAS,CAAC,EAC7F,KAAK,gBAAgB,GACZ,EAAE,MAAA,WACX,EAAE,eAAe,EACjB,KAAK,iBAAmB,KAAK,IAAI,KAAK,iBAAmB,EAAG,CAAC,EAC7D,KAAK,gBAAgB,GACZ,EAAE,MAAA,SACX,EAAE,eAAe,EACb,KAAK,kBAAoB,GAAK,KAAK,iBAAmB,KAAK,kBAAkB,QAC/E,KAAK,OAAO,KAAK,kBAAkB,KAAK,iBAAiB,GAElD,EAAE,MAAA,UACX,KAAK,MAAM,EAlBb,CAoBF,CAAC,EAGD,SAAS,iBAAiB,EAAc,GAAkB,CACnD,KAAK,UAAU,SAAS,EAAE,MAAc,GAC3C,KAAK,MAAM,CAEf,CAAC,CACH,CAEA,OAAe,EAAqB,CAClC,IAAM,EAAI,EAAM,YAAY,EAC5B,AACE,KAAK,kBADF,EAGsB,KAAK,mBAAmB,OAC9C,GACE,EAAE,MAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,SAAS,CAAC,GACzC,EAAE,MAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,SAAS,CAAC,GACzC,EAAE,YAAc,EAAE,WAAW,YAAY,CAAC,CAAC,SAAS,CAAC,CAC1D,EAPyB,CAAC,GAAG,KAAK,kBAAkB,EAStD,KAAK,iBAAmB,KAAK,kBAAkB,OAAS,EAAI,EAAI,GAChE,KAAK,oBAAoB,CAC3B,CAEA,qBAAoC,CAClC,GAAI,KAAK,aAAc,CACrB,KAAK,WAAW,UAAY;;iBAEjB,KAAK,aAAa;;;;;QAO7B,KADsB,WAAW,cAAc,iBAC/C,CAAA,EAAU,iBAAiB,MAAmB,KAAK,cAAc,CAAC,EAClE,MACF,CAEA,GAAI,KAAK,UAAW,CAClB,KAAK,WAAW,UAAY;;;;QAK5B,MACF,CAEA,GAAI,KAAK,kBAAkB,SAAW,EAAG,CACvC,KAAK,WAAW,UAAY;;;;QAK5B,MACF,CAEA,KAAK,WAAW,UAAY,KAAK,kBAC9B,KAAK,EAAG,IAAU,CACjB,IAAM,EAAa,KAAK,kBAAoB,EAAE,OAAS,KAAK,iBAAiB,KACvE,EAAgB,IAAU,KAAK,iBAC/B,EAAU,CAAC,WAAW,EAI5B,OAHI,GAAY,EAAQ,KAAK,oBAAoB,EAC7C,GAAe,EAAQ,KAAK,uBAAuB,EAEhD;;qBAEM,EAAQ,KAAK,GAAG,EAAE;0BACb,EAAM;;;;kBAId,EAAE,KAAA;;;kBAGF,EAAE,YAAc,EAAE,aAAe,EAAE,KAAO,IAAI,EAAE,WAAW,GAAK,GAAA;;;;gBAIlE,EAAE,KAAA;;;SAIZ,CAAC,CAAC,CACD,KAAK,EAAE,EAIV,KADmB,WAAW,iBAAiB,YAC/C,CAAA,CAAM,QAAS,GAAS,CACtB,EAAK,iBAAiB,MAAmB,CACvC,IAAM,EAAQ,SAAS,EAAK,aAAa,YAAY,GAAK,IAAK,EAAE,EACjE,KAAK,OAAO,KAAK,kBAAkB,EAAM,CAC3C,CAAC,EACD,EAAK,iBAAiB,MAAwB,CAC5C,KAAK,iBAAmB,SAAS,EAAK,aAAa,YAAY,GAAK,IAAK,EAAE,EAC3E,KAAK,gBAAgB,CACvB,CAAC,CACH,CAAC,CACH,CAEA,iBAAgC,CAE9B,KADmB,WAAW,iBAAiB,YAC/C,CAAA,CAAM,SAAS,EAAM,IAAU,CAC7B,IAAM,EAAa,GAAQ,KAAK,kBAAoB,KAAK,kBAAkB,EAAM,CAAC,OAAS,KAAK,iBAAiB,MAC3G,EAAgB,IAAU,KAAK,iBACrC,EAAK,UAAU,OAAO,wBAAyB,CAAa,EAC5D,EAAK,UAAU,OAAO,qBAAsB,CAAU,EAElD,GACF,EAAsB,eAAe,CAAE,MAAO,SAAU,CAAC,CAE7D,CAAC,CACH,CAEA,OAAe,EAA4B,CACzC,KAAK,oBAAsB,GAC3B,KAAK,iBAAmB,EACxB,KAAK,qBAAqB,EAAK,IAAI,EACnC,KAAK,QAAQ,MAAQ,GAAG,EAAK,KAAK,IAAI,EAAK,KAAK,GAChD,KAAK,MAAM,EACX,KAAK,iBAAiB,CAAI,CAC5B,CAEA,MAAqB,CACnB,KAAK,OAAS,GACd,KAAK,WAAW,MAAM,QAAU,EAChC,KAAK,oBAAoB,CAC3B,CAEA,OAAsB,CACpB,KAAK,OAAS,GACd,KAAK,WAAW,MAAM,QAAU,EAC5B,KAAK,iBACP,KAAK,QAAQ,MAAQ,GAAG,KAAK,iBAAiB,KAAK,IAAI,KAAK,iBAAiB,KAAK,GACxE,KAAK,eACf,KAAK,QAAQ,MAAQ,GAEzB,CACF,cC7VM,GAAW,OACX,GAAa,SAGb,GAA8B,0BAC9B,GAA0B,sBAG1B,GAAoB,+BACpB,GAAwB,uBACxB,GAAoB,sBACpB,GAAqB,uBACrB,GAAoB,sBACpB,GAA2B,0BAC3B,GAAwB,gBACxB,GAA+B,4BAC/B,GAA2B,mBAC3B,GAAyB,wBACzB,GAAyB,wBACzB,GAAsB,cACtB,GAA2B,mBAC3B,GAAkB,UAClB,GAAyB,iBACzB,GAA6B,qBAC7B,GAA4B,oBAC5B,GAAyB,iBACzB,GAAyB,iBACzB,GAAsB,cACtB,GAAuB,gCACvB,GAAoB,0BACpB,GAA4B,oBAC5B,GAAyB,iBACzB,GAAqB,aACrB,GAAuB,mCACvB,GAA0B,2BAC1B,GAAiC,qCACjC,GAA4B,oBAC5B,GAA8B,sBAC9B,GAAiC,yBACjC,GAAgC,wBAGhC,GAAuB,sBACvB,GAAyB,wBACzB,GAAsB,qBACtB,GAAqB,qBACrB,GAAoB,oBACpB,GAAoB,oBACpB,GAAsB,qBACtB,GAAwB,uBACxB,GAAwB,kBACxB,GAAwB,SAKxB,GAA2B,wCAC3B,GAAiB,8BACjB,GAAmB,iCACnB,GAA0B,yCAC1B,GAAgC,wBAChC,GAA6B,kCAEtB,GAAb,KAA6B,CAC3B,UACA,YACA,QACA,aACA,iBACA,YACA,iBACA,SAEA,eAAkC,GAClC,cAA0C,UAC1C,mBAAsC,aAAa,QAAQ,EAA2B,IAAM,QAAU,aAAa,QAAQ,EAAuB,IAAM,OACxJ,iBAAmC,EACnC,aAAiC,QACjC,kBAAoC,GAEpC,iBAA0C,KAC1C,gBAAyC,KACzC,sBAA+C,KAC/C,sBAA+C,KAC/C,mBAAqC,EACrC,WAA6B,IAC7B,kBAAoC,EAEpC,YAAY,EAAwB,EAAqB,EAAkC,CACzF,KAAK,UAAY,EACjB,KAAK,YAAc,EACnB,KAAK,QAAU,EACf,KAAK,SAAW,IAAI,GAGpB,KAAK,aAAe,IAAI,GAAa,CACnC,YAAc,GAAM,KAAK,gBAAgB,CAAC,EAC1C,cAAgB,GAAM,KAAK,qBAAqB,CAAC,EACjD,aAAc,EAAM,IAAY,KAAK,gBAAgB,EAAM,CAAO,EAClE,QAAU,GAAQ,MAAM,gBAAkB,EAAI,OAAO,CACvD,CAAC,EACD,KAAK,aAAa,sBAAsB,KAAK,kBAAkB,EAG/D,GAAI,CACF,IAAM,EAAiB,aAAa,QAAQ,CAAsB,EAClE,GAAI,IAAmB,KAAM,CAC3B,IAAM,EAAS,SAAS,EAAgB,EAAE,EACtC,CAAC,MAAM,CAAM,GAAK,GAAU,KAAO,GAAU,KAC/C,KAAK,aAAa,aAAa,CAAM,CAEzC,CAEA,IAAM,EAAc,aAAa,QAAQ,CAAmB,EAC5D,GAAI,IAAgB,KAAM,CACxB,IAAM,EAAS,SAAS,EAAa,EAAE,EACnC,CAAC,MAAM,CAAM,GAAK,GAAU,IAAM,GAAU,KAC9C,KAAK,aAAa,UAAU,IAAW,GAAK,IAAM,CAAM,CAE5D,CAEA,IAAM,EAAY,aAAa,QAAQ,CAAiB,EACxD,GAAI,IAAc,KAAM,CACtB,IAAM,EAAS,SAAS,EAAW,EAAE,EACjC,CAAC,MAAM,CAAM,GAAK,GAAU,KAAO,GAAU,KAC/C,KAAK,aAAa,QAAQ,CAAM,CAEpC,CACF,MAAQ,CAER,CAEA,KAAK,iBAAmB,IAAI,GAAiB,CAC3C,cAAe,EAAM,EAAS,EAAa,IAAS,CAClD,KAAK,iBAAiB,EAAM,EAAS,EAAa,CAAI,CACxD,EACA,mBAAsB,CACpB,KAAK,YAAY,oBAAoB,CACvC,EACA,gBAAiB,EAAQ,IAAW,CAClC,KAAK,qBAAqB,EAAQ,CAAM,CAC1C,CACF,CAAC,EACD,KAAK,iBAAiB,sBAAsB,KAAK,kBAAkB,EAEnE,KAAK,YAAc,IAAI,GAAY,CACjC,gBAAiB,EAAQ,IAAQ,CAC/B,KAAK,kBAAkB,EAAQ,CAAG,CACpC,EACA,kBAAoB,GAAU,CAC5B,KAAK,sBAAsB,CAAK,CAClC,CACF,CAAC,EAED,KAAK,OAAO,EACZ,KAAK,iBAAiB,CACxB,CAEA,QAAuB,CACrB,IAAM,EAAe,KAAK,YAAY,QAAQ,MAAO,EAAE,EACjD,EAAc,GAAG,OAAO,SAAS,OAAO,IAAI,mBAAmB,CAAY,IAE3E,EAAW,SAAS,eAAe,EAAiB,EACtD,EACF,KAAK,UAAU,gBAAgB,EAAS,QAAQ,UAAU,EAAI,CAAC,EAE/D,QAAQ,MAAM,GAAG,GAAkB,WAAW,EAIhD,IAAM,EAAc,KAAK,UAAU,cAAc,EAAwB,EACrE,IACF,EAAY,QAAU,KAAK,oBAG7B,IAAM,EAAiB,KAAK,UAAU,cAAc,EAA8B,EAC9E,IACF,EAAe,YAAc,IAAI,KAGnC,IAAM,EAAa,KAAK,UAAU,cAAc,EAAyB,EACrE,IACF,EAAW,MAAQ,GAIrB,IAAM,EAAc,KAAK,UAAU,cAAc,EAAsB,EACnE,IAAa,EAAY,YAAc,EAAE,6BAA6B,GAE1E,IAAM,EAAY,KAAK,UAAU,cAAc,GAAG,GAAkB,iBAAiB,EACjF,IAAW,EAAU,YAAc,EAAE,qBAAqB,GAAK,QAEnE,IAAM,EAAa,KAAK,UAAU,cAAc,GAAG,GAAmB,iBAAiB,EACnF,IAAY,EAAW,YAAc,EAAE,sBAAsB,GAAK,SAEtE,IAAM,EAAY,KAAK,UAAU,cAAc,GAAG,GAAkB,iBAAiB,EACjF,IAAW,EAAU,YAAc,EAAE,0BAA0B,GAAK,aAExE,IAAM,EAAa,KAAK,UAAU,cAAc,EAAyB,EACrE,IAAY,EAAW,YAAc,EAAE,6BAA6B,GAExE,IAAM,EAAmB,KAAK,UAAU,cAAc,EAA2B,EAC7E,IAAkB,EAAiB,YAAc,EAAE,8BAA8B,GAErF,IAAM,EAAyB,KAAK,UAAU,cAAc,EAA4B,EACpF,IAAwB,EAAuB,YAAc,EAAE,oCAAoC,GAEvG,IAAM,EAAgB,KAAK,UAAU,cAAc,EAA8B,EAC7E,IAAe,EAAc,YAAc,EAAE,4BAA4B,GAE7E,IAAM,EAAe,KAAK,UAAU,cAAc,EAA6B,EAC3E,IAAc,EAAa,YAAc,EAAE,gCAAgC,GAE/E,KAAK,WAAW,EAEhB,IAAM,EAAa,KAAK,UAAU,cAAc,EAAoB,EACpE,GAAI,EAAY,CACd,IAAM,EAAQ,GAA0B,CACtC,QAAS,KAAK,YACd,SAAU,EACZ,CAAC,EACD,EAAW,YAAY,CAAK,CAC9B,CAEA,KAAK,WAAW,CAAW,EAE3B,KAAK,iBAAiB,EACtB,KAAK,cAAgB,UACrB,KAAK,wBAAwB,SAAS,EACtC,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAElB,IAAM,EAAY,KAAK,UAAU,cAAc,EAAuB,EAClE,IACF,KAAK,iBAAmB,IAAI,GAC1B,EACC,GAAS,CACJ,KAAK,gBACP,KAAK,iCAAiC,EAAK,IAAI,CAEnD,EACA,KAAK,WACP,EAEJ,CAEA,MAAc,iCAAiC,EAAiC,CAC9E,GAAI,CACF,KAAK,qBAAqB,aAAc,yBAAyB,EAAS,IAAI,EAC9E,IAAM,EAAS,MAAM,EAAc,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,gBAAgB,EACzG,GAAI,EAAO,GAAI,CACb,IAAM,EAAM,IAAI,WAAW,MAAM,EAAO,YAAY,CAAC,EAC/C,EAAS,EAAO,mBAAmB,OAAO,CAAG,EACnD,KAAK,iBAAmB,EAAO,UAAY,OAAO,EAAO,SAAS,EAAI,KAAK,iBAC3E,IAAM,EAAY,EAAO,QAAQ,IAAI,gBAAgB,GAAK,EAAO,QAAQ,IAAI,aAAa,GAAK,IAAA,GAC/F,MAAM,KAAK,iBAAiB,QAC1B,EAAO,IACP,EACA,KAAK,kBAAoB,IAAA,GACzB,EAAO,KAAO,IAAA,GACd,CACF,CACF,CACF,OAAS,EAAU,CACjB,QAAQ,MAAM,kCAAmC,CAAG,EACpD,KAAK,qBAAqB,QAAS,EAAI,OAAO,CAChD,CACF,CAEA,MAAc,kBAAkC,CAC9C,IAAM,EAAU,MAAM,GAAa,gBAAgB,EAC7C,EAAS,KAAK,UAAU,cAAc,EAAqB,EACjE,GAAI,CAAC,EAAQ,OAEb,IAAI,EAAyB,KACzB,EAA4B,KAChC,GAAI,CACF,EAAU,aAAa,QAAQ,CAAuB,EACtD,EAAa,aAAa,QAAQ,CAA0B,CAC9D,MAAQ,CAER,CAEA,IAAM,EAAa,EAAO,MAC1B,EAAO,gBAAgB,EACvB,IAAM,EAAa,SAAS,cAAc,EAAU,EACpD,EAAW,MAAQ,GACnB,EAAW,YAAc,EAAE,wBAAwB,GAAK,qBACxD,EAAO,YAAY,CAAU,EAC7B,IAAI,EAA6C,KAC7C,EAA6C,KAEjD,EAAQ,SAAS,EAAG,IAAM,CACxB,IAAM,EAAM,SAAS,cAAc,EAAU,EAC7C,EAAI,MAAQ,EAAE,SACd,IAAM,EAAW,EAAE,OAAS,eAAe,EAAI,IAC3C,EAAS,YAAY,CAAC,CAAC,SAAS,SAAS,GAC3C,EAAI,YAAc,MAAM,EAAS,cACjC,EAAmB,GAEnB,EAAI,YAAc,GAIhB,GAAW,EAAE,WAAa,GAEnB,CAAC,GAAoB,IAAe,IAAa,GAAc,EAAI,cAAgB,MAD5F,EAAmB,GAKrB,EAAO,YAAY,CAAG,CACxB,CAAC,EAEG,GAAc,MAAM,KAAK,EAAO,OAAO,CAAC,CAAC,KAAM,GAAM,EAAE,QAAU,CAAU,EAC7E,EAAO,MAAQ,EACN,EACT,EAAO,MAAS,EAAuC,MAC9C,IAAY,GACrB,EAAO,MAAQ,GACN,GAAoB,CAAC,IAC9B,EAAO,MAAS,EAAuC,MAE3D,CAEA,YAA2B,CACzB,IAAM,EAAU,KAAK,UAAU,cAAc,EAAiB,EAC1D,GACF,EAAQ,iBAAiB,MAAmB,KAAK,gBAAgB,CAAC,EAGpE,IAAM,EAAW,KAAK,UAAU,cAAc,EAAkB,EAC5D,GACF,EAAS,iBAAiB,MAAmB,KAAK,iBAAiB,CAAC,EAGtE,IAAM,EAAU,KAAK,UAAU,cAAc,EAAiB,EAC1D,GACF,EAAQ,iBAAiB,MAAmB,KAAK,gBAAgB,CAAC,EAGpE,IAAM,EAAc,KAAK,UAAU,cAAc,EAAwB,EACrE,GACF,EAAY,iBAAiB,MAAoB,CAC/C,KAAK,mBAAqB,EAAY,QACtC,aAAa,QAAQ,GAA6B,KAAK,mBAAqB,GAAa,CAAW,EACpG,KAAK,aAAa,sBAAsB,KAAK,kBAAkB,EAC/D,KAAK,iBAAiB,sBAAsB,KAAK,kBAAkB,EACnE,KAAK,sBAAsB,CAC7B,CAAC,EAIH,IAAM,EAAe,KAAK,UAAU,cAAc,EAAqB,EACnE,GACF,EAAa,iBAAiB,EAAc,SAAY,CACtD,GAAI,CACF,IAAM,EAAc,EAAa,gBAAkB,EAAa,gBAAgB,GAAK,KACrF,aAAa,QAAQ,EAAyB,EAAa,OAAS,EAAE,EACtE,aAAa,QAAQ,EAA4B,GAAa,aAAe,EAAE,CACjF,MAAQ,CAER,CAEI,KAAK,aAAa,YACpB,MAAM,KAAK,aAAa,MAAM,EAAa,OAAS,IAAA,EAAS,EAC7D,MAAM,KAAK,iBAAiB,EAEhC,CAAC,EAIC,UAAU,cAAgB,UAAU,aAAa,kBACnD,UAAU,aAAa,iBAAiB,MAA0B,CAChE,KAAK,iBAAiB,CACxB,CAAC,EAGH,IAAM,EAAe,KAAK,UAAU,cAAc,EAAyB,EACrE,EAAY,KAAK,UAAU,cAAc,EAAsB,EACjE,GAAgB,IAClB,EAAa,MAAQ,OAAO,KAAK,aAAa,WAAW,EACzD,EAAU,YAAc,GAAG,KAAK,aAAa,YAAY,KACzD,EAAa,iBAAiB,MAAmB,CAC/C,IAAM,EAAM,SAAS,EAAa,MAAO,EAAE,EAC3C,EAAU,YAAc,GAAG,EAAI,KAC/B,KAAK,aAAa,aAAa,CAAG,EAClC,KAAK,oBAAoB,CAAG,EAC5B,GAAI,CACF,aAAa,QAAQ,EAAwB,OAAO,CAAG,CAAC,CAC1D,MAAQ,CAER,CACF,CAAC,GAGH,IAAM,EAAe,KAAK,UAAU,cAAc,EAAsB,EAClE,EAAY,KAAK,UAAU,cAAc,EAAmB,EAC9D,GAAgB,IAClB,EAAa,MAAQ,OAAO,KAAK,aAAa,QAAQ,EACtD,EAAU,YAAc,GAAG,KAAK,aAAa,SAAS,KACtD,EAAa,iBAAiB,MAAmB,CAC/C,IAAM,EAAM,SAAS,EAAa,MAAO,EAAE,EAC3C,EAAU,YAAc,GAAG,EAAI,KAC/B,KAAK,aAAa,UAAU,CAAG,EAC/B,GAAI,CACF,aAAa,QAAQ,EAAqB,OAAO,CAAG,CAAC,CACvD,MAAQ,CAER,CACF,CAAC,GAGH,IAAM,EAAa,KAAK,UAAU,cAAc,EAAoB,EAC9D,EAAU,KAAK,UAAU,cAAc,EAAiB,EAC1D,GAAc,IAChB,EAAW,MAAQ,OAAO,KAAK,aAAa,SAAS,EACrD,EAAQ,YAAc,IAAI,KAAK,aAAa,UAAY,IAAA,CAAM,QAAQ,CAAC,EAAE,IACzE,EAAW,iBAAiB,MAAmB,CAC7C,IAAM,EAAM,SAAS,EAAW,MAAO,EAAE,EACzC,EAAQ,YAAc,IAAI,EAAM,IAAA,CAAM,QAAQ,CAAC,EAAE,IACjD,KAAK,aAAa,QAAQ,CAAG,EAC7B,GAAI,CACF,aAAa,QAAQ,EAAmB,OAAO,CAAG,CAAC,CACrD,MAAQ,CAER,CACF,CAAC,GAGH,IAAM,EAAU,KAAK,UAAU,cAAc,EAAsB,EACnE,EAAQ,iBAAiB,MAAmB,CAC1C,IAAM,EAAQ,KAAK,UAAU,cAAc,EAAyB,EACpE,UAAU,UAAU,UAAU,EAAM,KAAK,EACzC,EAAQ,YAAc,EAAE,eAAe,EACvC,eAAkB,EAAQ,YAAc,EAAE,aAAa,EAAI,GAAI,CACjE,CAAC,EAGD,KAAK,oBAAoB,KAAK,aAAa,WAAW,CACxD,CAEA,oBAA4B,EAAkB,CAE5C,IAAM,EAAM,KAAK,IAAI,EAAG,KAAK,IAAI,KAAO,EAAK,IAAM,GAAM,GAAG,CAAC,EACvD,EAAO,KAAK,UAAU,cAAc,EAA0B,EAChE,IACF,EAAK,MAAM,KAAO,GAAG,EAAI,GAE7B,CAEA,QAAyB,CACvB,OAAO,KAAK,gBAAkB,MAChC,CAEA,mBAA8B,GACxB,KAAK,gBACP,EAAE,eAAe,EACjB,EAAE,YAAc,GACT,IAEF,GAGT,wBAAgC,EAA+B,CAC7D,IAAM,EAAU,KAAK,UAAU,cAAc,EAAiB,EACxD,EAAW,KAAK,UAAU,cAAc,EAAkB,EAC1D,EAAU,KAAK,UAAU,cAAc,EAAiB,EAE1D,IACF,EAAQ,UAAU,OAAO,GAAuB,IAAU,SAAS,EACnE,EAAQ,aAAa,eAAgB,IAAU,UAAY,OAAS,OAAO,EAC3E,EAAQ,SAAW,IAEjB,IACF,EAAS,UAAU,OAAO,GAAuB,IAAU,QAAQ,EACnE,EAAS,aAAa,eAAgB,IAAU,SAAW,OAAS,OAAO,EAC3E,EAAS,SAAW,IAElB,IACF,EAAQ,UAAU,OAAO,GAAuB,IAAU,MAAM,EAChE,EAAQ,aAAa,eAAgB,IAAU,OAAS,OAAS,OAAO,EACxE,EAAQ,SAAW,GAEvB,CAEA,cAA6B,CAC3B,KAAK,WAAa,IAClB,IAAM,EAAM,KAAK,UAAU,cAAc,EAAe,EAClD,EAAU,KAAK,UAAU,cAAc,EAAsB,EAC/D,IAAK,EAAI,MAAM,MAAQ,MACvB,IAAS,EAAQ,YAAc,QACrC,CAEA,MAAc,iBAAiC,CACzC,KAAK,gBAAkB,WAC3B,MAAM,KAAK,iBAAiB,SAAS,CACvC,CAEA,MAAc,kBAAkC,CAC1C,KAAK,gBAAkB,UAC3B,MAAM,KAAK,iBAAiB,QAAQ,CACtC,CAEA,MAAc,iBAAiC,CACzC,KAAK,gBAAkB,QAC3B,MAAM,KAAK,iBAAiB,MAAM,CACpC,CAEA,MAAc,iBAAiB,EAA8C,CAC3E,GAAI,IAAgB,UAAW,CACzB,KAAK,gBACP,MAAM,KAAK,kBAAkB,EAE/B,GAAI,CACF,MAAM,KAAK,aAAa,KAAK,CAC/B,OAAS,EAAK,CACZ,QAAQ,KAAK,8CAA+C,CAAG,CACjE,CACA,KAAK,aAAa,EAClB,KAAK,aAAe,QACpB,KAAK,cAAgB,UACrB,KAAK,wBAAwB,SAAS,EACtC,KAAK,sBAAsB,EAC3B,MACF,CAEA,GAAI,IAAgB,SAAU,CAI5B,GAHI,KAAK,gBACP,MAAM,KAAK,kBAAkB,EAE3B,CAAC,KAAK,aAAa,UACrB,GAAI,CACF,IAAM,EAAS,KAAK,UAAU,cAAc,EAAqB,EAC3D,EAAW,EAAS,EAAO,MAAQ,IAAA,GACzC,MAAM,KAAK,aAAa,MAAM,GAAY,IAAA,EAAS,EACnD,MAAM,KAAK,iBAAiB,CAC9B,OAAS,EAAU,CACjB,MAAM,iCAAmC,GAAK,SAAW,EAAI,EAC7D,KAAK,wBAAwB,KAAK,aAAa,EAC/C,MACF,CAEF,KAAK,cAAgB,SACrB,KAAK,aAAe,KAAK,aAAa,gBACtC,KAAK,wBAAwB,QAAQ,EACrC,KAAK,sBAAsB,EAC3B,MACF,CAEA,GAAI,IAAgB,OAAQ,CAC1B,IAAM,EAAU,KAAK,UAAU,cAAc,EAAiB,EAG9D,GAFI,IAAS,EAAQ,SAAW,IAE5B,CAAC,KAAK,aAAa,UACrB,GAAI,CACF,IAAM,EAAS,KAAK,UAAU,cAAc,EAAqB,EAC3D,EAAW,EAAS,EAAO,MAAQ,IAAA,GACzC,MAAM,KAAK,aAAa,MAAM,GAAY,IAAA,EAAS,EACnD,MAAM,KAAK,iBAAiB,CAC9B,OAAS,EAAU,CACjB,MAAM,iCAAmC,GAAK,SAAW,EAAI,EACzD,IAAS,EAAQ,SAAW,IAChC,KAAK,wBAAwB,KAAK,aAAa,EAC/C,MACF,CAGF,GAAI,CACF,MAAM,KAAK,mBAAmB,EAC9B,KAAK,cAAgB,OACrB,KAAK,wBAAwB,MAAM,EACnC,KAAK,sBAAsB,CAC7B,OAAS,EAAU,CACjB,MAAM,+BAAiC,GAAK,SAAW,EAAI,EACvD,KAAK,aAAa,WACpB,KAAK,cAAgB,SACrB,KAAK,wBAAwB,QAAQ,IAErC,KAAK,cAAgB,UACrB,KAAK,wBAAwB,SAAS,GAExC,KAAK,sBAAsB,CAC7B,QAAU,CACJ,IAAS,EAAQ,SAAW,GAClC,CACF,CACF,CAEA,MAAc,mBAAmC,CAC/C,GAAI,CAAC,KAAK,eAAgB,OAE1B,KAAK,eAAiB,GACtB,KAAK,kBAAkB,EACvB,OAAO,oBAAoB,EAAoB,KAAK,kBAAkB,EAGtE,GAAI,CACF,MAAM,EAAc,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,eAAgB,CACxF,OAAQ,MACV,CAAC,CACH,OAAS,EAAK,CACZ,QAAQ,KAAK,yCAA0C,CAAG,CAC5D,CACA,IAAM,EAAe,KAAK,gBAC1B,KAAK,iBAAmB,KACxB,KAAK,gBAAkB,KACvB,EAAa,CAAC,CAAC,sBAAsB,eAAgB,GAAgB,IAAA,EAAS,EAC9E,KAAK,SAAS,kBAAkB,EAGhC,IAAM,EAAe,KAAK,kBAAkB,YAAY,CAAC,EAAE,KACrD,EAAc,KAAK,2BAA2B,CAAY,EAChE,GAAI,CACF,MAAM,KAAK,YAAY,QAAQ,EAAa,GAAM,GAAM,EAAK,CAAY,EACzE,KAAK,0BAA0B,CAAW,CAC5C,OAAS,EAAK,CACZ,QAAQ,MAAM,+CAAgD,CAAG,CACnE,CAGA,KAAK,iBAAiB,WAAW,EACjC,KAAK,YAAY,WAAW,EAC5B,KAAK,iBAAmB,EACxB,KAAK,kBAAkB,cAAc,EACrC,KAAK,qBAAqB,cAAc,EACxC,KAAK,SAAS,YAAY,EAC1B,KAAK,aAAe,KAAK,aAAa,gBACtC,KAAK,gBAAgB,CACvB,CAEA,MAAc,oBAAoC,CAEhD,IAAM,EAAc,MAAM,EAAc,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,gBAAiB,CAC7G,OAAQ,MACV,CAAC,EACD,GAAI,CAAC,EAAY,GAIf,MAHI,EAAY,SAAW,IACf,MAAM,2FAA2F,EAEnG,MAAM,oCAAoC,EAEtD,IAAM,EAAa,IAAI,WAAW,MAAM,EAAY,YAAY,CAAC,EAC3D,EAAc,EAAO,qBAAqB,OAAO,CAAU,EACjE,KAAK,iBAAmB,EAAY,UAAY,OAAO,EAAY,SAAS,EAAI,KAChF,KAAK,mBAAqB,OAAO,EAAY,eAAe,EAC5D,KAAK,gBAAkB,EAAY,UAAY,KAAsC,KAA/B,OAAO,EAAY,QAAQ,EACjF,EAAa,CAAC,CAAC,sBAAsB,eAAgB,KAAK,iBAAmB,IAAA,EAAS,EACtF,KAAK,SAAS,kBAAkB,EAGhC,IAAM,EAAS,MAAM,EAAc,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,gBAAgB,EACzG,GAAI,CAAC,EAAO,GAAI,CACd,IAAI,EAAY,GAChB,GAAI,CACF,IAAM,EAAU,MAAM,EAAO,KAAK,EAClC,EAAY,EAAQ,OAAS,EAAQ,SAAW,KAAK,UAAU,CAAO,CACxE,MAAQ,CACN,EAAY,MAAM,EAAO,KAAK,CAAC,CAAC,UAAY,EAAE,CAChD,CACA,MAAU,MAAM,qDAAqD,EAAY,KAAO,EAAY,IAAI,CAC1G,CACA,IAAM,EAAQ,IAAI,WAAW,MAAM,EAAO,YAAY,CAAC,EACjD,EAAS,EAAO,mBAAmB,OAAO,CAAK,EAGrD,MAAM,KAAK,YAAY,QAAQ,KAAK,WAAW,EAG/C,IAAM,EAAe,KAAK,iBAAiB,YAAY,EACvD,GAAI,CAAC,GAAgB,CAAC,EAAa,KACjC,MAAU,MAAM,oDAAoD,EAEtE,IAAM,EAAY,EAAO,QAAQ,IAAI,gBAAgB,GAAK,EAAO,QAAQ,IAAI,aAAa,GAAK,IAAA,GAC/F,MAAM,KAAK,iBAAiB,QAC1B,EAAO,IACP,EAAa,KACb,KAAK,kBAAoB,IAAA,GACzB,EAAO,KAAO,IAAA,GACd,CACF,EAGA,KAAK,2BAA2B,EAEhC,KAAK,eAAiB,GACtB,IAAM,EAAa,KAAK,mBAAqB,SAAW,KAAK,aAAa,gBAC1E,KAAK,aAAe,EACpB,KAAK,iBAAiB,YAAY,CAAU,EAC5C,KAAK,kBAAoB,GACzB,KAAK,gBAAgB,EACrB,OAAO,iBAAiB,EAAoB,KAAK,kBAAkB,EACnE,KAAK,qBAAqB,KAAK,iBAAiB,YAAc,YAAc,cAAc,EAC1F,KAAK,SAAS,YAAY,CAC5B,CAEA,4BAA2C,CACzC,KAAK,kBAAkB,EAEvB,KAAK,sBAAwB,OAAO,YAAY,SAAY,CAC1D,GAAI,CAAC,KAAK,gBAAkB,CAAC,KAAK,iBAAkB,OAEpD,IAAM,EAAM,KAAK,IAAI,EACf,EAAc,KAAK,mBAAqB,EAG9C,GAAI,IADgB,EAAc,IAAY,IAAY,KAExD,GAAI,CACF,IAAM,EAAU,MAAM,EAAc,iBAAiB,mBAAmB,KAAK,WAAW,EAAE,iBAAkB,CAC1G,OAAQ,MACV,CAAC,EACD,GAAI,EAAQ,GAAI,CACd,IAAM,EAAS,IAAI,WAAW,MAAM,EAAQ,YAAY,CAAC,EACnD,EAAU,EAAO,qBAAqB,OAAO,CAAM,EACzD,KAAK,mBAAqB,OAAO,EAAQ,eAAe,EACxD,KAAK,gBAAkB,EAAQ,UAAY,KAAkC,KAAK,gBAAhC,OAAO,EAAQ,QAAQ,EACzE,QAAQ,IAAI,oDAAqD,IAAI,KAAK,KAAK,kBAAkB,CAAC,EAClG,EAAa,CAAC,CAAC,sBAAsB,eAAgB,KAAK,iBAAmB,IAAA,EAAS,EACtF,KAAK,SAAS,kBAAkB,CAClC,MAAW,EAAQ,SAAW,KAC5B,QAAQ,KAAK,4EAA4E,CAE7F,OAAS,EAAK,CACZ,QAAQ,MAAM,0CAA2C,CAAG,CAC9D,CAEJ,EAAG,GAAK,CACV,CAEA,mBAAkC,CAC5B,KAAK,wBAA0B,OACjC,cAAc,KAAK,qBAAqB,EACxC,KAAK,sBAAwB,MAE3B,KAAK,wBAA0B,OACjC,cAAc,KAAK,qBAAqB,EACxC,KAAK,sBAAwB,KAEjC,CAEA,sBAA8B,EAAqB,CACjD,KAAK,iBAAmB,EACxB,KAAK,sBAAsB,CAC7B,CAEA,uBAAsC,CACpC,IAAM,EAAiB,KAAK,iBAAmB,GAAK,KAAK,mBACzD,KAAK,iBAAiB,kBAAkB,CAAC,CAAc,EAEvD,IAAM,EAAO,KAAK,UAAU,cAAc,EAAsB,EAC1D,EAAM,KAAK,UAAU,cAAc,EAAsB,EAE3D,GAAQ,IACL,KAAK,gBAIR,AAGE,EAAK,UAHH,KAAK,iBAAmB,EACT,GAEA,GAEnB,EAAI,YAAc,aAAa,KAAK,qBARpC,EAAK,UAAY,GACjB,EAAI,YAAc,EAAE,6BAA6B,IAWrD,KAAK,gBAAgB,CACvB,CAEA,iBAAgC,CAC9B,IAAM,EAAQ,KAAK,UAAU,cAAc,EAAmB,EACxD,EAAY,KAAK,UAAU,cAAc,EAAwB,EACnE,GAAC,GAAU,EAEf,IAAI,KAAK,gBAAkB,UAAW,CACpC,EAAM,UAAY,GAClB,EAAU,YAAc,EAAE,yBAAyB,EACnD,MACF,CAEA,GAAI,KAAK,gBAAkB,SAAU,CAC/B,KAAK,eAAiB,UACxB,EAAM,UAAY,GAClB,EAAU,YAAc,iCAExB,EAAM,UAAY,GAClB,EAAU,YAAc,4BAE1B,MACF,CAEA,GAAI,KAAK,mBAAoB,CAC3B,EAAM,UAAY,GAClB,EAAU,YAAc,GACxB,MACF,CAE0B,KAAK,iBAAmB,EAG5C,KAAK,eAAiB,UACxB,EAAM,UAAY,GAClB,EAAU,YAAc,KAExB,EAAM,UAAY,GAClB,EAAU,YAAc,KAI1B,EAAM,UAAY,GAClB,AAGE,EAAU,YAHR,KAAK,eAAiB,SACA,GAEA,EAAE,wBAAwB,EAnCtD,CAsCF,CAEA,gBAAwB,EAAuB,CAC7C,IAAM,EAAM,KAAK,UAAU,cAAc,EAAe,EAClD,EAAU,KAAK,UAAU,cAAc,EAAsB,EAE7D,EAAY,KAAK,IAAI,IAAK,KAAK,IAAI,EAAG,EAAE,EAAE,CAAC,EAGjD,GAFA,KAAK,WAAa,EAEd,EAAK,CACP,IAAM,EAAM,KAAK,IAAI,EAAG,KAAK,IAAI,KAAO,KAAK,WAAa,IAAM,GAAM,GAAG,CAAC,EAC1E,EAAI,MAAM,MAAQ,GAAG,EAAI,EAC3B,CAEA,GAAI,EAAS,CACX,IAAM,EAAM,YAAY,IAAI,EAE5B,GAAI,EAAM,KAAK,mBAAqB,IAAK,CACvC,KAAK,kBAAoB,EACzB,IAAM,EAAY,KAAK,MAAM,KAAK,UAAU,EAC5C,EAAQ,YAAc,GAAa,IAAM,QAAU,GAAG,EAAU,IAClE,CACF,CAGI,EAAE,OAAS,EAAE,QAAU,KAAK,cAC9B,KAAK,qBAAqB,EAAE,KAAK,CAErC,CAEA,qBAA6B,EAAuB,CAClD,GAAI,CAAC,KAAK,eAAgB,CACxB,KAAK,iBAAiB,YAAY,OAAO,EACzC,KAAK,aAAe,EACpB,KAAK,gBAAgB,EACrB,MACF,CAEA,GAAI,KAAK,mBAAoB,CAC3B,KAAK,iBAAiB,YAAY,QAAQ,EAC1C,KAAK,aAAe,SACpB,KAAK,gBAAgB,EACrB,MACF,CAEA,IAAM,EAAmB,KAAK,mBAAqB,EAE/C,IAAU,UACZ,KAAK,iBAAiB,YAAY,QAAQ,EAC1C,KAAK,aAAe,WAEpB,KAAK,iBAAiB,YAAY,OAAO,EAGrC,KAAK,eAAiB,UAAY,CAAC,GACrC,KAAK,iBAAiB,EAExB,KAAK,aAAe,SAGtB,KAAK,gBAAgB,CACvB,CAEA,gBAAwB,EAAmB,EAAyB,CAC7D,KAAK,gBAEV,KAAK,iBAAiB,UAAU,CAAI,CACtC,CAEA,iBAAyB,EAAc,EAAkB,EAAsB,EAA0B,CACnG,IACF,KAAK,kBAAoB,EAAK,KAAK,GAGrC,IAAM,EAAe,KAAK,kBAAkB,YAAY,CAAC,EAAE,KAC3D,KAAK,YAAY,QAAQ,EAAM,EAAS,EAAa,EAAY,CAAY,EAG7E,IAAM,EAAU,KAAK,UAAU,cAAc,EAAqB,EAClE,GAAI,EAAS,CAIX,IAHI,EAAQ,cAAc,EAA4B,GAAK,EAAQ,cAAc,mCAAmC,IAClH,EAAQ,gBAAgB,EAEtB,EAAS,CACX,IAAM,EAAO,SAAS,cAAc,EAAQ,EAC5C,EAAK,UAAY,IAAA,YAAyB,GAAwB,GAClE,EAAK,YAAc,IAAM,EACzB,EAAQ,YAAY,CAAI,CAC1B,KAAO,CACL,IAAI,EAAU,EAAQ,cAAc,EAAwB,EACvD,IACH,EAAU,SAAS,cAAc,EAAQ,EACzC,EAAQ,UAAY,GACpB,EAAQ,YAAY,CAAO,GAE7B,EAAQ,YAAc,IAAM,CAC9B,CACA,EAAQ,UAAY,EAAQ,YAC9B,CACF,CAEA,qBAA6B,EAAgB,EAAuB,CAC9D,IAAW,SACb,QAAQ,KAAK,gCAAiC,CAAM,CAExD,CAEA,kBAA0B,EAAgB,EAAoB,EACxD,IAAW,SAAW,IAAW,iBACnC,QAAQ,IAAI,+BAA+B,IAAS,EAAM,KAAK,EAAI,GAAK,IAAI,CAEhF,CAEA,WAAmB,EAAmB,CACpC,IAAM,EAAS,KAAK,UAAU,cAAc,EAAkB,EACzD,GAEL,GAAO,SAAS,EAAQ,EAAK,CAC3B,MAAO,IACP,OAAQ,EACR,MAAO,CACL,KAAM,UACN,MAAO,SACT,CACF,CAAC,CAAC,CAAC,MAAO,GAAQ,CAChB,QAAQ,MAAM,iCAAkC,CAAG,CACrD,CAAC,CACH,CAEA,2BAAmC,EAA2B,CAE5D,IAAM,GADW,GAAY,UAAU,UAAY,KAAA,CAAM,YAAY,CAAC,CAAC,KACvD,CAAA,CAAQ,MAAM,MAAM,CAAC,CAAC,GAEhC,EAAuC,CAC3C,GAAI,GACJ,GAAI,2BACJ,GAAI,gCACJ,GAAI,gCACJ,GAAI,2BACJ,GAAI,iCACJ,GAAI,8BACJ,GAAI,2BACJ,GAAI,uBACJ,GAAI,uBACJ,GAAI,qBACJ,GAAI,yBACJ,GAAI,uBACJ,GAAI,sBACJ,GAAI,yBACJ,GAAI,gCACJ,GAAI,0BACJ,GAAI,uBACJ,GAAI,0BACJ,GAAI,sBACJ,GAAI,mBACJ,GAAI,YACJ,GAAI,cACJ,GAAI,QACJ,GAAI,wBACJ,GAAI,gBACJ,GAAI,eACJ,GAAI,wBACJ,GAAI,0BACJ,GAAI,oBACJ,GAAI,wBACN,EAEA,GAAI,EAAa,GACf,OAAO,EAAa,GAGtB,IAAM,EAAU,EAAE,EAA0B,EAK5C,OAJI,GAAW,IAAY,GAClB,EAGF,EACT,CAEA,0BAAkC,EAAoB,CACpD,IAAM,EAAU,KAAK,UAAU,cAAc,EAAqB,EAClE,GAAI,EAAS,CACX,IAAM,EAAO,SAAS,cAAc,EAAQ,EAC5C,EAAK,UAAY,GACjB,EAAK,YAAc,IAAM,EACzB,EAAQ,YAAY,CAAI,EACxB,EAAQ,UAAY,EAAQ,YAC9B,CACF,CAEA,kBAAiC,CAC1B,KAAK,gBACN,KAAK,oBAAA,aACT,KAAK,iBAAiB,GAAe,GAAM,GAAM,CAAG,CACtD,CAEA,MAAM,SAAyB,CAC7B,GAAI,KAAK,eAAgB,CACvB,OAAO,oBAAoB,EAAoB,KAAK,kBAAkB,EACtE,IAAM,EAAe,KAAK,kBAAkB,YAAY,CAAC,EAAE,KACrD,EAAc,KAAK,2BAA2B,CAAY,EAChE,GAAI,CACF,MAAM,KAAK,YAAY,QAAQ,EAAa,GAAM,GAAM,EAAK,CAAY,CAC3E,OAAS,EAAK,CACZ,QAAQ,KAAK,0DAA2D,CAAG,CAC7E,CACF,CACA,KAAK,eAAiB,GACtB,KAAK,iBAAmB,EACxB,KAAK,SAAS,QAAQ,EACtB,MAAM,KAAK,aAAa,KAAK,EAC7B,KAAK,iBAAiB,WAAW,EACjC,KAAK,YAAY,WAAW,EAC5B,KAAK,cAAgB,SACvB,CACF,EC5hCM,GAAa,SACb,GAAQ,IAGR,GAA2B,6BAC3B,GAAmB,qBACnB,GAA4B,8BAC5B,GAA6B,qBAC7B,GAAiC,iCACjC,GAA6B,qBAC7B,GAAuB,qCAGvB,GAAoB,kBACpB,GAAoB,kBACpB,GAAwB,6BACxB,GAAsB,wBAEf,GAAb,cAAoC,EAAa,CAC/C,cACA,YACA,gBAAkD,KAClD,oBAA6C,KAC7C,kBAAqC,GAErC,YAAY,EAA8B,EAA0B,CAClE,MAAM,iBAAiB,EACvB,KAAK,cAAgB,EACrB,KAAK,YAAc,EAEnB,KAAK,cAAc,oBAAsB,CACvC,KAAK,QAAQ,CACf,CAAC,EAED,KAAK,cAAc,qBAAuB,CACxC,KAAK,QAAQ,CACf,CAAC,CACH,CAEA,MAAM,MAAsB,CAC1B,MAAM,MAAM,KAAK,EACjB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EACtC,GACF,KAAK,iBAAiB,CAAE,CAE5B,CAEA,MAAM,SAAyB,CAC7B,MAAM,MAAM,QAAQ,EACpB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EAC1C,GAAI,CAAC,EAAI,OAEJ,KAAK,mBACR,KAAK,iBAAiB,CAAE,EAG1B,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAgB,EAAG,cAA2B,EAAwB,EACtE,EAAU,EAAG,cAA2B,EAAgB,EAE9D,GAAI,CAAC,EAAe,CACd,KAAK,kBACP,MAAM,KAAK,gBAAgB,QAAQ,EACnC,KAAK,gBAAkB,KACvB,KAAK,oBAAsB,MAEzB,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAS,EAAQ,MAAM,QAAU,GACrC,MACF,CAEI,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAS,EAAQ,MAAM,QAAU,GAErC,IAAM,EAAW,KAAK,cAAc,YAAY,EAC1C,EAAiB,KAAK,cAAc,kBAAkB,EAGtD,EAAW,EAAG,cAA2B,EAA0B,EACrE,IACF,EAAS,gBAAgB,EAEzB,EAAS,QAAS,GAAO,CACvB,IAAM,EAAM,SAAS,cAAc,EAAU,EAC7C,EAAI,KAAO,SAEX,EAAI,UAAY,GAAG,GAAsB,GADvB,GAAkB,EAAe,OAAS,EAAG,KACP,GAAoB,KAC5E,EAAI,YAAc,IAAI,EAAG,OACzB,EAAI,iBAAiB,MAAmB,CACtC,KAAK,cAAc,cAAc,EAAG,IAAI,CAC1C,CAAC,EACD,EAAS,YAAY,CAAG,CAC1B,CAAC,GAIH,IAAM,EAAc,EAAG,cAA2B,EAA8B,EAChF,GAAI,EAAa,CAGf,GAFA,EAAY,gBAAgB,EAC5B,EAAY,OAAO,iBAAiB,EAChC,EAAgB,CAClB,IAAM,EAAO,SAAS,cAAc,EAAK,EACzC,EAAK,KAAO,KAAK,mBAAmB,EAAe,IAAI,IACvD,EAAK,OAAS,SACd,EAAK,IAAM,WACX,EAAK,UAAY,GACjB,EAAK,YAAc,kBAAkB,EAAe,OACpD,EAAY,YAAY,CAAI,CAC9B,MACE,EAAY,OAAO,MAAM,CAE7B,CAGA,IAAM,EAAQ,EAAG,cAA2B,EAA0B,EAChE,EAAwB,EAAG,cAA2B,EAAoB,EAE5E,GAAkB,GAChB,IAAuB,EAAsB,MAAM,QAAU,GACjE,EAAM,MAAM,QAAU,GAElB,CAAC,KAAK,iBAAmB,KAAK,sBAAwB,EAAe,QACvE,AAEE,KAAK,mBADL,MAAM,KAAK,gBAAgB,QAAQ,EACZ,MAEzB,EAAM,gBAAgB,EACtB,KAAK,oBAAsB,EAAe,KAC1C,KAAK,gBAAkB,IAAI,GAAgB,EAAO,EAAe,KAAM,CACrE,oBAAuB,CACrB,KAAK,cAAc,oBAAoB,CACzC,CACF,CAAC,KAGC,KAAK,kBACP,MAAM,KAAK,gBAAgB,QAAQ,EACnC,KAAK,gBAAkB,KACvB,KAAK,oBAAsB,MAEzB,IAAO,EAAM,MAAM,QAAU,GAC7B,IAAuB,EAAsB,MAAM,QAAU,GAErE,CAEA,iBAAyB,EAAuB,CAC1C,KAAK,oBACT,KAAK,kBAAoB,GAIzB,EAD0B,cAAc,EACxC,CAAA,EAAgB,iBAAiB,MAAmB,CAClD,KAAK,YAAY,UAAU,kBAAkB,CAC/C,CAAC,EACH,CAEA,MAAM,SAAyB,CACzB,KAAK,kBACP,MAAM,KAAK,gBAAgB,QAAQ,EACnC,KAAK,gBAAkB,KACvB,KAAK,oBAAsB,KAE/B,CACF,ECtJM,GAAqB,mCAEd,GAAb,cAAmC,EAAa,CAC9C,cACA,YACA,kBAAqC,GAErC,YAAY,EAA8B,EAA0B,CAClE,MAAM,gBAAgB,EACtB,KAAK,cAAgB,EACrB,KAAK,YAAc,EAEnB,KAAK,cAAc,oBAAsB,CACvC,KAAK,QAAQ,CACf,CAAC,EAED,KAAK,cAAc,qBAAuB,CACxC,KAAK,QAAQ,CACf,CAAC,CACH,CAEA,MAAM,MAAsB,CAC1B,MAAM,MAAM,KAAK,EACjB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EACtC,GACF,KAAK,iBAAiB,CAAE,CAE5B,CAEA,MAAM,SAAyB,CAC7B,MAAM,MAAM,QAAQ,EACpB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EAC1C,GAAI,CAAC,EAAI,OAEJ,KAAK,mBACR,KAAK,iBAAiB,CAAE,EAG1B,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAgB,EAAG,cAA2B,2BAA2B,EACzE,EAAkB,EAAG,cAA2B,mBAAmB,EAEzE,GAAI,CAAC,EAAe,CACd,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAiB,EAAgB,MAAM,QAAU,GACrD,MACF,CAEI,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAiB,EAAgB,MAAM,QAAU,GAErD,IAAM,EAAe,EAAG,cAA2B,+BAA+B,EAC9E,IAAc,EAAa,YAAc,EAAc,MAE3D,IAAM,EAAW,KAAK,cAAc,YAAY,EAC1C,EAAiB,KAAK,cAAc,kBAAkB,EACtD,EAAU,KAAK,cAAc,cAAc,EAE3C,EAAY,EAAG,cAA2B,gCAAgC,EAC5E,IAAW,EAAU,MAAM,QAAU,EAAU,EAAe,GAGlE,IAAM,EAAS,EAAG,cAAiC,iBAAiB,EACpE,GAAI,EAAQ,CACV,EAAO,gBAAgB,EACvB,IAAK,IAAM,KAAM,EAAU,CACzB,IAAM,EAAM,SAAS,cAAc,QAAQ,EAC3C,EAAI,MAAQ,EAAG,KACf,EAAI,YAAc,IAAI,EAAG,OACrB,GAAgB,OAAS,EAAG,OAAM,EAAI,SAAW,IACrD,EAAO,YAAY,CAAG,CACxB,CACF,CAGA,IAAM,EAAU,EAAG,cAA2B,iBAAiB,EAC3D,IAAS,EAAQ,YAAc,OAAO,EAAS,MAAM,GAEzD,IAAM,EAAY,EAAG,cAA2B,sBAAsB,EAChE,EAAW,EAAG,cAA2B,qBAAqB,EAC9D,EAAc,EAAG,cAA2B,yBAAyB,EAG3E,GAFI,IAAa,EAAY,MAAM,QAAU,EAAU,EAAuB,GAE1E,EAAS,SAAW,EAClB,IAAW,EAAU,MAAM,QAAU,GACrC,IACF,EAAS,MAAM,QAAU,EACzB,EAAS,gBAAgB,QAI3B,GADI,IAAW,EAAU,MAAM,QAAU,GACrC,EAAU,CACZ,EAAS,MAAM,QAAU,EACzB,EAAS,gBAAgB,EAEzB,IAAM,EAAW,SAAS,eAAe,sBAAsB,EAC/D,IAAK,IAAM,KAAM,EAAU,CACzB,IAAM,EAAc,GAAG,OAAO,SAAS,OAAO,IAAI,EAAG,OACrD,GAAI,EAAU,CAEZ,IAAM,EADQ,EAAS,QAAQ,UAAU,EAC7B,CAAA,CAAM,cAAc,cAAc,EACxC,EAAW,EAAI,cAAc,oBAAoB,EACnD,IACF,EAAS,YAAc,IAAI,EAAG,OAAO,EAAG,UAAY,aAAe,MAErE,IAAM,EAAO,EAAI,cAAc,wBAAwB,EACnD,IACF,EAAK,KAAO,EACZ,EAAK,YAAc,GAErB,IAAM,EAAU,EAAI,cAAc,gBAAgB,EAC9C,IAAS,EAAQ,QAAQ,IAAM,GACnC,IAAM,EAAQ,EAAI,cAAc,eAAe,EAC3C,IACF,EAAM,QAAQ,IAAM,EACpB,EAAM,QAAQ,QAAU,EAAG,MAE7B,IAAM,EAAS,EAAI,cAAc,qBAAqB,EAClD,IACF,EAAO,QAAQ,QAAU,EAAG,KAC5B,EAAO,MAAM,QAAU,EAAU,EAAuB,GAE1D,EAAS,YAAY,CAAG,CAC1B,CACF,CACA,KAAK,cAAc,CAAQ,CAC7B,CAIF,IAAM,EAAgB,EAAG,cAA2B,yBAAyB,EACvE,EAAkB,EAAG,cAA2B,2BAA2B,EAEjF,GAAI,CAAC,EACC,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAiB,EAAgB,MAAM,QAAU,OAChD,CACD,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAiB,EAAgB,MAAM,QAAU,GAErD,IAAM,EAAS,EAAG,cAA2B,wBAAwB,EACjE,IAAQ,EAAO,YAAc,EAAe,MAEhD,IAAM,EAAc,GAAG,OAAO,SAAS,OAAO,IAAI,EAAe,OAE3D,EAAU,EAAG,cAAiC,yBAAyB,EACzE,GAAS,EAAQ,aAAa,WAAY,CAAW,EAEzD,IAAM,EAAQ,EAAG,cAAiC,wBAAwB,EACtE,IACF,EAAM,aAAa,WAAY,CAAW,EAC1C,EAAM,aAAa,eAAgB,EAAe,IAAI,GAIxD,IAAM,EAAY,EAAG,cAAiC,8BAA8B,EAChF,IACF,EAAU,aAAa,eAAgB,EAAe,IAAI,EAC1D,EAAU,MAAM,QAAU,EAAU,EAAuB,GAK7D,IAAM,EAAU,EAAQ,EAAe,WACjC,EAAU,EAAG,cAAgC,WAAW,EACxD,EAAkB,EAAG,cAA2B,mBAAmB,EACnE,EAAgB,EAAG,cAA2B,kBAAkB,EAChE,EAAY,EAAG,cAAgC,cAAc,EAC7D,EAAkB,EAAG,cAAgC,oBAAoB,EACzE,EAAqB,EAAG,cAA2B,4BAA4B,EAEjF,IAAW,EAAU,MAAM,QAAU,EAAU,EAAuB,GACtE,IAAe,EAAc,MAAM,QAAW,GAAW,EAAW,EAAuB,GAE3F,EACF,GAAe,iBAAiB,mBAAmB,EAAe,IAAI,EAAE,UAAU,EAAe,WAAW,GAAG,EAAe,iBAAmB,GAAG,CAAC,CAClJ,KAAK,GAAa,CACjB,EAAU,EAAS,EAAW,CAAE,cAAe,EAAiB,YAAa,CAAc,CAAC,EAC5F,EAAU,EAAiB,EAAW,CAAE,WAAY,CAAmB,CAAC,CAC1E,CAAC,GAEH,EAAU,EAAS,KAAM,CAAE,cAAe,EAAiB,YAAa,CAAc,CAAC,EACvF,EAAU,EAAiB,KAAM,CAAE,WAAY,CAAmB,CAAC,GAGrE,IAAM,EAAK,EAAe,iBAAmB,UACvC,EAAc,EAAG,cAAgC,iBAAiB,EAClE,EAAY,EAAG,cAAgC,eAAe,EAC9D,EAAgB,EAAG,cAA2B,oBAAoB,EAClE,EAAc,EAAG,cAA2B,eAAe,EAC7D,IACF,EAAY,MAAQ,EACpB,EAAY,SAAW,CAAC,GAEtB,IACF,EAAU,MAAQ,EAClB,EAAU,SAAW,CAAC,GAEpB,IAAe,EAAc,MAAM,QAAU,EAAU,EAAuB,GAClF,EAAG,iBAA8B,mBAAmB,CAAC,CAAC,QAAQ,GAAO,CACnE,EAAI,MAAM,QAAU,EAAU,EAAuB,CACvD,CAAC,EACG,IAAa,EAAY,MAAM,WAAa,GAEhD,IAAM,EAAkB,EAAG,cAAgC,2BAA2B,EAClF,IACF,EAAgB,QAAU,EAAQ,EAAe,UACjD,EAAgB,SAAW,CAAC,GAG9B,IAAM,EAAU,EAAG,cAA2B,oBAAoB,EAC9D,IAAS,EAAQ,MAAM,QAAU,EAAU,EAAuB,GAEtE,IAAM,EAAc,EAAG,cAA2B,4BAA4B,EAC1E,IAAa,EAAY,YAAc,EAAe,KAC5D,CACF,CAEA,cAAsB,EAA8B,CAElD,EAAU,iBAAoC,gBAAgB,CAAC,CAAC,QAAQ,GAAO,CAC7E,EAAI,iBAAiB,EAAa,SAAY,CAC5C,IAAM,EAAM,EAAI,aAAa,UAAU,EACvC,GAAI,EACF,GAAI,CACF,MAAM,UAAU,UAAU,UAAU,CAAG,EACvC,IAAM,EAAe,EAAI,YACzB,EAAI,YAAc,UAClB,eAAiB,CACf,EAAI,YAAc,CACpB,EAAG,GAAI,CACT,OAAS,EAAK,CACZ,QAAQ,MAAM,sBAAuB,CAAG,CAC1C,CAEJ,CAAC,CACH,CAAC,EAED,EAAU,iBAAoC,eAAe,CAAC,CAAC,QAAQ,GAAO,CAC5E,EAAI,iBAAiB,MAAmB,CACtC,IAAM,EAAM,EAAI,aAAa,UAAU,EACjC,EAAK,EAAI,aAAa,cAAc,EACtC,GAAO,GACT,EAAgB,EAAK,CAAE,CAE3B,CAAC,CACH,CAAC,EAED,EAAU,iBAAoC,qBAAqB,CAAC,CAAC,QAAQ,GAAO,CAClF,EAAI,iBAAiB,EAAa,SAAY,CAC5C,IAAM,EAAS,EAAI,aAAa,cAAc,EAC1C,GACF,MAAM,KAAK,oBAAoB,EAAQ,CAAG,CAE9C,CAAC,CACH,CAAC,CACH,CAEA,MAAc,oBAAoB,EAAgB,EAAsD,CACtG,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACrD,MAEA,QAAQ,4CAA4C,EAAO,oDAAoD,EAIpH,CAAI,IAAY,EAAW,SAAW,IACtC,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,YAAY,mBAAmB,CAAM,IAAK,CAC1G,OAAQ,CACV,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,kCAAkC,EAAK,OAAO,EAAE,CACnF,CAEA,MAAM,KAAK,cAAc,6BAA6B,EACtD,MAAM,KAAK,QAAQ,CACrB,OAAS,EAAU,CACjB,MAAM,EAAI,SAAW,0BAA0B,EAC3C,IAAY,EAAW,SAAW,GACxC,CAhBsC,CAiBxC,CAEA,iBAAyB,EAAuB,CAC9C,GAAI,KAAK,kBAAmB,OAC5B,KAAK,kBAAoB,GAGzB,EAAG,cAAc,4BAA4B,CAAC,EAAE,iBAAiB,MAAmB,CAClF,KAAK,YAAY,UAAU,kBAAkB,CAC/C,CAAC,EAGD,IAAM,EAAgB,EAAG,cAAc,iBAAiB,EACxD,GAAe,iBAAiB,MAAoB,CAClD,IAAM,EAAS,GAAe,MAC1B,IACF,KAAK,cAAc,cAAc,CAAM,EACvC,KAAK,QAAQ,EAEjB,CAAC,EAGD,EAAG,cAAc,yBAAyB,CAAC,EAAE,iBAAiB,EAAa,SAAY,CACrF,IAAM,EAAK,KAAK,cAAc,kBAAkB,CAAC,EAAE,KACnD,GAAI,EAAI,CACN,IAAM,EAAM,GAAG,OAAO,SAAS,OAAO,IAAI,IAC1C,MAAM,UAAU,UAAU,UAAU,CAAG,EACvC,IAAM,EAAM,EAAG,cAAiC,yBAAyB,EACrE,IACF,EAAI,YAAc,UAClB,eAAiB,CAAE,EAAI,YAAc,WAAa,EAAG,GAAI,EAE7D,CACF,CAAC,EAED,EAAG,cAAc,wBAAwB,CAAC,EAAE,iBAAiB,MAAmB,CAC9E,IAAM,EAAK,KAAK,cAAc,kBAAkB,CAAC,EAAE,KACnD,GAAI,EAAI,CACN,IAAM,EAAM,GAAG,OAAO,SAAS,OAAO,IAAI,IAC1C,EAAgB,EAAK,CAAE,CACzB,CACF,CAAC,EAED,EAAG,cAAc,8BAA8B,CAAC,EAAE,iBAAiB,EAAa,SAAY,CAC1F,IAAM,EAAK,KAAK,cAAc,kBAAkB,CAAC,EAAE,KAC/C,GACF,MAAM,KAAK,oBAAoB,EAAI,EAAG,cAAiC,8BAA8B,CAAC,CAE1G,CAAC,EAKD,IAAM,EAAY,EAAG,cAAc,cAAc,EACjD,GAAW,iBAAiB,EAAc,SAAY,CACpD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAO,EAAU,QAAQ,GAC/B,GAAI,CAAC,GAAQ,CAAC,EAAe,OAC7B,IAAM,EAAS,KAAK,cAAc,kBAAkB,CAAC,EAAE,KACvD,GAAI,CAAC,EAAQ,OAEb,IAAM,EAAQ,EAAK,OAAS,aAAe,EAAK,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,EAC5E,EAAQ,EAAK,OAAS,iBAAmB,EAAK,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,EACtF,GAAI,CAAC,GAAS,CAAC,EAAO,CACpB,MAAM,oDAAoD,EAC1D,EAAU,MAAQ,GAClB,MACF,CACA,GAAI,EAAK,KAAO,MAAW,CACzB,MAAM,kEAAkE,EACxE,EAAU,MAAQ,GAClB,MACF,CAEA,IAAM,EAAc,EAAQ,gBAAkB,YAC9C,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,YAAY,mBAAmB,CAAM,EAAE,OAAQ,CAC/G,OAAQ,EACR,KAAM,EACN,QAAS,CAAE,eAAgB,CAAY,CACzC,CAAC,EACD,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAM,EAAQ,OAAS,uBAAuB,EAC9C,MACF,CACA,MAAM,KAAK,cAAc,6BAA6B,EACtD,EAAa,CAAC,CAAC,sBAAsB,WAAY,EAAc,EAAE,EACjE,MAAM,KAAK,QAAQ,CACrB,OAAS,EAAQ,CACf,MAAM,EAAE,SAAW,uBAAuB,CAC5C,QAAU,CACR,EAAU,MAAQ,EACpB,CACF,CAAC,EAID,EADyB,cAAc,kBACvC,CAAA,EAAe,iBAAiB,EAAa,SAAY,CACvD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAS,KAAK,cAAc,kBAAkB,CAAC,EAAE,KACnD,GAAC,GAAkB,EACvB,GAAI,CACF,MAAM,EAAc,gBAAgB,EAAc,GAAG,YAAY,mBAAmB,CAAM,EAAE,OAAQ,CAAE,OAAQ,CAAY,CAAC,EAC3H,MAAM,KAAK,cAAc,6BAA6B,EACtD,EAAa,CAAC,CAAC,sBAAsB,WAAY,EAAc,EAAE,EACjE,MAAM,KAAK,QAAQ,CACrB,MAAY,CACV,MAAM,uBAAuB,CAC/B,CACF,CAAC,EAGD,IAAM,EAAc,EAAG,cAAc,iBAAiB,EAChD,EAAY,EAAG,cAAc,eAAe,EAC5C,EAAa,EAAG,cAAc,eAAe,EAC7C,EAAgB,EAAG,cAAc,oBAAoB,EAErD,EAAmB,GAAgB,CACnC,IAAa,EAAY,MAAQ,GACjC,IAAW,EAAU,MAAQ,GAC7B,IAAY,EAAW,MAAM,WAAa,EAChD,EAEA,GAAa,iBAAiB,MAAmB,CAC/C,EAAgB,EAAY,KAAK,CACnC,CAAC,EAED,GAAW,iBAAiB,MAAoB,CAC9C,IAAM,EAAM,EAAU,MAAM,KAAK,EAC7B,2BAA2B,KAAK,CAAG,GACrC,EAAgB,CAAG,CAEvB,CAAC,EAED,GAAe,iBAAiB,MAAmB,CACjD,EAAgB,SAAS,CAC3B,CAAC,EAED,EAAG,iBAAoC,mBAAmB,CAAC,CAAC,QAAQ,GAAO,CACzE,EAAI,iBAAiB,MAAmB,CACtC,IAAM,EAAQ,EAAI,aAAa,YAAY,EACvC,GAAO,EAAgB,CAAK,CAClC,CAAC,CACH,CAAC,EAGD,IAAM,EAAU,EAAG,cAAc,oBAAoB,EACrD,GAAS,iBAAiB,EAAa,SAAY,CACjD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAS,KAAK,cAAc,kBAAkB,CAAC,EAAE,KACvD,GAAI,CAAC,GAAiB,CAAC,EAAQ,OAC/B,IAAM,EAAiB,EAAG,cAAc,eAAe,EACjD,EAAkB,EAAG,cAAc,2BAA2B,EAC9D,EAAe,CAAC,EAClB,GAAgB,MAClB,EAAQ,iBAAmB,EAAe,MAAM,KAAK,EAC5C,GAAa,QACtB,EAAQ,iBAAmB,EAAY,OAErC,IACF,EAAQ,WAAa,EAAgB,SAEvC,EAAQ,SAAW,GACnB,EAAQ,YAAc,YACtB,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,YAAY,mBAAmB,CAAM,EAAE,WAAY,CACnH,OAAA,MACA,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAO,CAC9B,CAAC,EACD,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,yBAAyB,CAC5D,CACA,MAAM,KAAK,cAAc,6BAA6B,EACtD,EAAa,CAAC,CAAC,sBAAsB,WAAY,EAAc,EAAE,EACjE,MAAM,KAAK,QAAQ,EACnB,EAAQ,YAAc,SACtB,eAAiB,CACf,EAAQ,YAAc,gBACtB,EAAQ,SAAW,EACrB,EAAG,IAAI,CACT,OAAS,EAAQ,CACf,MAAM,EAAE,SAAW,iCAAiC,EACpD,EAAQ,SAAW,GACnB,EAAQ,YAAc,eACxB,CACF,CAAC,EAGD,IAAM,EAAQ,EAAG,cAAc,uBAAuB,EAChD,EAAU,EAAG,cAAc,gCAAgC,EAC3D,EAAc,EAAG,cAAc,yBAAyB,EACxD,EAAW,EAAG,cAAc,6BAA6B,EACzD,EAAY,EAAG,cAAc,4BAA4B,EACzD,EAAO,EAAG,cAAc,sBAAsB,EAC9C,EAAQ,EAAG,cAAc,4BAA4B,EACrD,EAAW,EAAG,cAAc,uBAAuB,EACnD,EAAY,EAAG,cAAc,4BAA4B,EAEzD,MAAkB,CAClB,IACF,EAAM,UAAU,OAAO,CAAY,EACnC,EAAM,MAAM,QAAU,EAClB,IACF,EAAM,MAAQ,GACd,EAAM,MAAM,GAEV,IAAU,EAAS,MAAM,QAAU,GAE3C,EAEM,MAAmB,CACnB,IACF,EAAM,UAAU,IAAI,CAAY,EAChC,EAAM,MAAM,QAAU,EAE1B,EAEA,GAAS,iBAAiB,EAAa,CAAS,EAChD,GAAa,iBAAiB,EAAa,CAAS,EACpD,GAAU,iBAAiB,EAAa,CAAU,EAClD,GAAW,iBAAiB,EAAa,CAAU,EAEnD,GAAO,iBAAiB,MAAmB,CACzC,EAAM,MAAQ,EAAM,MACjB,QAAQ,MAAO,EAAE,CAAC,CAClB,QAAQ,kBAAmB,EAAE,CAAC,CAC9B,QAAQ,OAAQ,GAAG,CAAC,CACpB,QAAQ,KAAM,EAAE,CACrB,CAAC,EAED,GAAM,iBAAiB,EAAc,KAAO,IAAM,CAChD,EAAE,eAAe,EACjB,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,GAAI,CAAC,GAAiB,CAAC,GAAS,CAAC,GAAa,CAAC,EAAU,OAEzD,IAAM,EAAS,EAAM,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAO,EAAE,EACnD,GAAI,EAAO,OAAS,GAAK,EAAO,OAAS,IAAM,CAAC,GAAmB,KAAK,CAAM,EAAG,CAC/E,EAAS,YAAc,EAAE,4BAA4B,EACrD,EAAS,MAAM,QAAU,EACzB,MACF,CAEA,EAAU,SAAW,GACrB,EAAS,MAAM,QAAU,EAEzB,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,WAAY,CAC5E,OAAQ,EACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,KAAM,CAAO,CAAC,CACvC,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,kCAAkC,EAAK,OAAO,EAAE,CACnF,CAEA,EAAW,EACX,MAAM,KAAK,cAAc,6BAA6B,EACtD,EAAa,CAAC,CAAC,sBAAsB,WAAY,EAAc,EAAE,EACjE,MAAM,KAAK,QAAQ,CACrB,OAAS,EAAU,CACjB,EAAS,YAAc,EAAI,SAAW,2BACtC,EAAS,MAAM,QAAU,CAC3B,QAAU,CACR,EAAU,SAAW,EACvB,CACF,CAAC,CACH,CACF,ECtiBa,GAAb,cAAiC,EAAa,CAC5C,cACA,YACA,YACA,kBAAqC,GACrC,aAAgC,GAEhC,YAAY,EAA8B,EAA0B,EAA0B,CAC5F,MAAM,cAAc,EACpB,KAAK,cAAgB,EACrB,KAAK,YAAc,EACnB,KAAK,YAAc,EAEnB,KAAK,cAAc,oBAAsB,CACvC,KAAK,QAAQ,CACf,CAAC,EAED,KAAK,cAAc,2BAA6B,CAC9C,KAAK,QAAQ,CACf,CAAC,CACH,CAEA,MAAM,MAAsB,CAC1B,MAAM,MAAM,KAAK,EACjB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EACtC,GACF,KAAK,iBAAiB,CAAE,CAE5B,CAEA,MAAM,SAAyB,CAC7B,MAAM,MAAM,QAAQ,EACpB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EACrC,MAGD,KAAK,YAAY,iBAAiB,IAAM,KAAK,IAI7C,MAAK,aACT,MAAK,aAAe,GAEpB,GAAI,CACG,KAAK,mBACR,KAAK,iBAAiB,CAAE,EAG1B,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAgB,EAAG,cAA2B,yBAAyB,EACvE,EAAgB,EAAG,cAA2B,iBAAiB,EAErE,GAAI,CAAC,EAAe,CACd,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAe,EAAc,MAAM,QAAU,GACjD,MACF,CAEI,IAAe,EAAc,MAAM,QAAU,GAC7C,IAAe,EAAc,MAAM,QAAU,GAEjD,IAAM,EAAU,KAAK,cAAc,cAAc,EAC3C,EAAc,KAAK,YAAY,QAAQ,EAGvC,EAAS,EAAG,cAA2B,cAAc,EACvD,IAAQ,EAAO,YAAc,EAAc,MAE/C,IAAM,EAAY,EAAG,cAA2B,oBAAoB,EAChE,IACF,EAAU,MAAM,QAAW,GAAW,CAAC,KAAK,YAAY,gBAAgB,EACpE,EACA,GAEN,IAAM,EAAc,EAAG,cAA2B,sBAAsB,EACpE,IACF,EAAY,UAAU,IAAI,CAAY,EACtC,EAAY,MAAM,QAAU,GAG9B,IAAM,EAAY,EAAG,cAA2B,oBAAoB,EAChE,IAAW,EAAU,YAAc,EAAU,qBAAuB,eAExE,IAAM,EAAe,EAAG,cAA2B,wBAAwB,EACvE,IACF,EAAa,MAAM,QAAW,CAAC,KAAK,YAAY,gBAAgB,GAAK,KAAK,cAAc,eAAe,CAAC,CAAC,OAAS,EAC9G,EACA,GAIN,MAAM,KAAK,cAAc,oBAAoB,EAC7C,IAAM,EAAW,KAAK,cAAc,oBAAoB,EAElD,EAAU,GAA0B,EAD9B,KAAK,IACmC,CAAG,EAGnD,EAAgC,KAChC,EAAgC,KAEpC,GAAI,GAAU,WACP,IAAA,GAAM,CAAC,EAAO,KAAQ,OAAO,QAAQ,EAAS,UAAU,EACvD,GAAK,OAAA,oBACP,EAAiB,OAAO,CAAK,EACpB,GAAK,OAAA,eACd,EAAiB,OAAO,CAAK,GAI/B,IAAmB,OAAM,EAAiB,GAC1C,IAAmB,OAAM,EAAiB,GAE9C,IAAM,EAAmB,EAAQ,iBAC3B,EAAc,EAAQ,sBACtB,EAAa,EAAQ,aAErB,EAAiB,MAAM,KAAK,EAAQ,eAAe,OAAO,CAAC,EAC3D,EAAmB,KAAK,MAAM,EAAmB,EAAE,EACnD,GAAkB,EAAmB,GAAA,CAAI,QAAQ,CAAC,EAClD,EAAmB,EAAe,OAAS,EAG3C,EAAe,EAAG,cAA2B,6BAA6B,EAC5E,IAAc,EAAa,YAAc,OAAO,CAAgB,GAEpE,IAAM,EAAc,EAAG,cAA2B,yBAAyB,EACvE,IACF,EAAY,UAAY,qBAAqB,EAAmB,GAAK,SAAW,MAGlF,IAAM,EAAiB,EAAG,cAA2B,uBAAuB,EACxE,IAAgB,EAAe,YAAc,WAAW,EAAe,mBAE3E,IAAM,EAAsB,EAAG,cAA2B,+BAA+B,EACrF,IACE,GACF,EAAoB,UAAU,OAAO,CAAY,EACjD,EAAoB,MAAM,QAAU,IAEpC,EAAoB,UAAU,IAAI,CAAY,EAC9C,EAAoB,MAAM,QAAU,IAKxC,IAAM,EAAkB,EAAG,cAA2B,wBAAwB,EAC1E,IACF,EAAgB,YAAc,EAAmB,GAAG,EAAe,OAAO,SAAW,WACrF,EAAgB,UAAY,qBAAqB,EAAmB,UAAY,MAGlF,IAAM,GAAgB,EAAG,cAA2B,sBAAsB,EACtE,KACF,GAAc,YAAc,EACxB,GAAG,EAAe,OAAO,iBAAiB,EAAe,OAAS,EAAI,IAAM,GAAG,cAC/E,uBAIN,IAAM,EAAc,EAAG,cAA2B,oBAAoB,EAClE,IACF,EAAY,YAAc,EAAa,SAAW,WAClD,EAAY,UAAY,qBAAqB,EAAa,UAAY,YAGxE,IAAM,EAAY,EAAG,cAA2B,kBAAkB,EAC9D,IACF,AAGE,EAAU,YAHR,EAAc,EACQ,eAAe,IAAI,KAAK,CAAW,CAAC,CAAC,mBAAmB,IAExD,EAAa,qBAAuB,mBAKhE,IAAM,EAAoB,EAAG,cAA2B,6BAA6B,EAC/E,EAAoB,EAAG,cAA2B,6BAA6B,EAC/E,EAAqB,EAAG,cAA2B,8BAA8B,EAEvF,GAAI,GAAqB,EAAmB,CAC1C,GAAI,EAAkB,CACpB,EAAkB,MAAM,QAAU,EAC9B,IACF,EAAmB,YAAc,GAAG,EAAe,OAAO,gBAAgB,EAAe,OAAS,EAAI,IAAM,MAE9G,EAAkB,UAAY,GAC9B,IAAK,IAAM,KAAW,EAAgB,CACpC,IAAM,EAAS,SAAS,cAAc,KAAK,EAC3C,EAAO,UAAY,4BACnB,IAAM,EAAU,EAAQ,iBAAmB,GAAK,GAAG,EAAQ,iBAAiB,GAAK,GAAG,KAAK,KAAK,EAAQ,iBAAmB,EAAE,EAAE,GAE7H,EAAO,UAAY;;;+CAGgB,EAAQ,YAAY;;;;2BAIxC,GAAe,EAAQ,iBAAiB,EAAA;;;;;kBAKjD,EAAA;;;2BAGS,IAAI,KAAK,EAAQ,eAAe,CAAC,CAAC,mBAAmB,CAAC,EAAG,CAAE,KAAM,UAAW,OAAQ,SAAU,CAAC,EAAA;;;YAI9G,EAAkB,YAAY,CAAM,CACtC,CACF,KACE,GAAkB,MAAM,QAAU,EAClC,EAAkB,UAAY,EAElC,CAGA,IAAM,EAAQ,EAAG,cAA2B,uBAAuB,EAC7D,EAAU,EAAG,cAA2B,uBAAuB,EAErE,GAAI,EAAO,CACT,EAAM,UAAY,GAClB,IAAM,EAAM,CAAC,GAAI,GAAU,cAAgB,CAAC,CAAE,CAAC,CAAC,MAAM,EAAG,IAAM,CAC7D,IAAM,EAAQ,GAAU,EAAE,iBAAiB,EAE3C,OADc,GAAU,EAAE,iBACnB,EAAQ,CACjB,CAAC,EAED,GAAI,EAAI,SAAW,EACb,IAAS,EAAQ,MAAM,QAAU,OAChC,CACD,IAAS,EAAQ,MAAM,QAAU,GAErC,IAAK,IAAM,KAAM,EAAK,CACpB,IAAM,EAAK,SAAS,cAAc,IAAI,EAChC,EAAS,GAAU,EAAG,iBAAiB,EAEvC,EADS,EAAG,kBACa,EAAY,2BAA2B,mCAChE,EAAY,GAAkB,EAAG,gBAAiB,CAAc,EAChE,EAAe,GAAkB,EAAG,gBAAiB,CAAc,EAErE,EAAc,EAAG,aAAe,0BAC9B,EAAe,EAAG,QAAQ,QAChC,GAAI,EAAc,CAChB,IAAI,EAAa,GACb,EAAG,aAAa,WAAW,IAAI,GAAc,EAC/C,EAAa,EAAG,YAAY,MAAM,IAAI,IAAe,MAAM,EAClD,EAAG,cACZ,EAAa,IAAI,EAAG,eAEtB,EAAc,gCAAgC,EAAa,SAAS,GACtE,KAAO,CACL,IAAM,EAAU,EAAG,aAAa,MAAM,gBAAgB,EAChD,EAAe,EAAG,aAAa,MAAM,uBAAuB,EAC5D,EAAe,EAAG,aAAa,MAAM,qBAAqB,EAC5D,EACF,EAAc,gCAAgC,EAAQ,GAAG,SAAS,EAAQ,IAAM,KACvE,EACT,EAAc,gCAAgC,EAAa,GAAG,wBACrD,IACT,EAAc,gCAAgC,EAAa,GAAG,wBAElE,CAEA,IAAI,EAAW,EACX,IAEF,EAAW,mVAAmG,EAAY,gBAG5H,IAAI,EAAa,IACb,IAAc,IAGhB,EAAa,gBAFC,EAAY,EACC,sBAAwB,oBACX,kBAAkB,GAAoB,CAAS,EAAE,UAG3F,IAAI,EAAgB,IAChB,EAAe,IACjB,EAAgB,6BAA6B,IAAI,KAAK,CAAY,CAAC,CAAC,mBAAmB,EAAE,UAG3F,EAAG,UAAY;iCACM,GAAe,CAAM,EAAE;kBACtC,EAAS;yDAC8B,EAAW;kBAClD,EAAc;YAEpB,EAAM,YAAY,CAAE,CACtB,CACF,CACF,CAGA,IAAM,EAAoB,EAAG,cAA2B,6BAA6B,EACjF,IACE,GACF,EAAkB,UAAU,OAAO,CAAY,EAC/C,EAAkB,MAAM,QAAU,EAClC,MAAM,KAAK,6BAA6B,EAAI,EAAc,EAAE,IAE5D,EAAkB,UAAU,IAAI,CAAY,EAC5C,EAAkB,MAAM,QAAU,IAKtC,IAAM,EAAiB,EAAG,cAA2B,0BAA0B,EAC3E,IAAgB,EAAe,YAAc,EAAc,MAE/D,IAAM,EAAgB,EAAG,cAA2B,uBAAuB,EACvE,IAAe,EAAc,MAAM,QAAU,EAAU,EAAsB,GAEjF,IAAM,EAAsB,EAAG,cAA2B,yBAAyB,EAC/E,IAAqB,EAAoB,MAAM,QAAU,EAAU,EAAsB,GAE7F,MAAM,KAAK,yBAAyB,EAAI,EAAc,GAAI,EAAS,CAAW,EAC9E,MAAM,KAAK,qBAAqB,EAAI,EAAc,GAAI,CAAO,EAC7D,MAAM,KAAK,0BAA0B,EAAI,EAAc,GAAI,CAAO,CACpE,QAAU,CACR,KAAK,aAAe,EACtB,CA1RoB,CA2RtB,CAEA,MAAc,yBACZ,EACA,EACA,EACA,EACe,CACf,IAAM,EAAS,EAAG,cAA2B,mBAAmB,EAC3D,KAEL,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAS,OAAO,EACjE,GAAI,CAAC,EAAK,GACR,MAAU,MAAM,6BAA6B,EAE/C,IAAM,EAOD,MAAM,EAAK,KAAK,EAIrB,GAFA,EAAO,UAAY,GAEf,CAAC,GAAW,EAAQ,SAAW,EAAG,CACpC,EAAO,UAAY,6DACnB,MACF,CAEA,IAAK,IAAM,KAAK,EAAS,CACvB,IAAM,EAAO,SAAS,cAAc,KAAK,EACzC,EAAK,UAAY,mBAEjB,IAAM,EAAS,GACb,IACC,EAAE,KAAO,EAAY,IAAO,EAAY,OAAS,EAAE,MAAM,YAAY,IAAM,EAAY,MAAM,YAAY,IAEtG,EAAc,EAAE,MAAQ,EAAE,MAAM,MAAM,GAAG,CAAC,CAAC,IAAM,SACjD,EAAgB,EAAE,OAAS,QAAU,QAAU,cAC/C,EAAe,EAAE,QACnB,oDACA,GAEE,EACJ,GAAW,CAAC,EACR,2GAA2G,EAAE,GAAG,gBAAgB,EAAE,MAAM,mBACxI,GAEN,EAAK,UAAY;;;sBAGH,IAAc,EAAS,6DAA+D,GAAG;gBAC/F,EAAA;;8CAE8B,EAAE,MAAM;;;8CAGR,EAAc;cAC9C,EAAA;;UAIN,IAAM,EAAY,EAAK,cAAiC,oBAAoB,EACxE,GACF,EAAU,iBAAiB,QAAS,SAAY,CACzC,WAAQ,mCAAmC,EAAE,MAAM,yBAAyB,EAGjF,GAAI,CACF,IAAM,EAAU,MAAM,EAAc,gBAAgB,EAAS,SAAS,EAAE,KAAM,CAC5E,OAAQ,QACV,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,IAAM,EAAM,MAAM,EAAQ,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EACjD,MAAU,MAAM,EAAI,OAAS,yBAAyB,CACxD,CACA,MAAM,KAAK,yBAAyB,EAAI,EAAU,EAAS,CAAW,CACxE,OAAS,EAAU,CACjB,MAAM,EAAI,SAAW,uBAAuB,CAC9C,CACF,CAAC,EAGH,EAAO,YAAY,CAAI,CACzB,CACF,OAAS,EAAG,CACV,QAAQ,KAAK,+BAAgC,CAAC,EAC9C,EAAO,UAAY,kEACrB,CACF,CAEA,MAAc,qBACZ,EACA,EACA,EACe,CACf,IAAM,EAAS,EAAG,cAA2B,sBAAsB,EAC9D,KAEL,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAS,SAAS,EACnE,GAAI,CAAC,EAAK,GACR,MAAU,MAAM,8BAA8B,EAEhD,IAAM,EAAS,MAAM,EAAK,YAAY,EAEhC,EADU,GAAQ,gBAAgB,OAAO,IAAI,WAAW,CAAM,CAC/C,CAAA,CAAQ,SAAwC,CAAC,EAItE,GAFA,EAAO,UAAY,GAEf,CAAC,GAAe,EAAY,SAAW,EAAG,CAC5C,EAAO,UAAY,iIACnB,MACF,CAEA,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAO,SAAS,cAAc,KAAK,EACzC,EAAK,UAAY,mBAEjB,IAAM,EAAe,EAAE,YACnB,gCAAgC,EAAE,YAAY,SAC9C,oEAEE,EAAa,OAAO,EAAE,gBAAkB,CAAC,EACzC,EAAe,EAAa,EAC9B,eAAe,IAAI,KAAK,CAAU,CAAC,CAAC,mBAAmB,IACvD,eAEE,EAAgB,EAClB,6GAA6G,EAAE,GAAG,mBAClH,GAEE,EAAc,OAAO,EAAE,iBAAmB,CAAC,EACjD,EAAK,UAAY;;;sBAGH,EAAE,OAAS,eAAe;gBAChC,EAAA;;;;kDAIkC,EAAE,IAAM,GAAA,CAAI,MAAM,EAAG,CAAC,EAAE;+BAC3C,IAAI,KAAK,CAAW,CAAC,CAAC,mBAAmB,EAAE;sBACpD,EAAa;;;;cAIrB,EAAA;;UAIN,IAAM,EAAY,EAAK,cAAiC,oBAAoB,EACxE,GACF,EAAU,iBAAiB,QAAS,SAAY,CACzC,WAAQ,0CAA0C,EAAE,OAAS,EAAE,GAAG,wCAAwC,EAG/G,GAAI,CACF,IAAM,EAAU,MAAM,EAAc,gBAAgB,EAAS,WAAW,EAAE,KAAM,CAC9E,OAAQ,QACV,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,IAAM,EAAM,MAAM,EAAQ,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EACjD,MAAU,MAAM,EAAI,OAAS,+BAA+B,CAC9D,CACA,MAAM,KAAK,qBAAqB,EAAI,EAAU,CAAO,CACvD,OAAS,EAAU,CACjB,MAAM,EAAI,SAAW,6BAA6B,CACpD,CACF,CAAC,EAGH,EAAO,YAAY,CAAI,CACzB,CACF,OAAS,EAAG,CACV,QAAQ,KAAK,gCAAiC,CAAC,EAC/C,EAAO,UAAY,mEACrB,CACF,CAEA,iBAAyB,EAAuB,CAC9C,GAAI,KAAK,kBAAmB,OAC5B,KAAK,kBAAoB,GAGzB,EAAG,cAAc,0BAA0B,CAAC,EAAE,iBAAiB,MAAmB,CAChF,KAAK,YAAY,UAAU,kBAAkB,CAC/C,CAAC,EAGD,IAAM,EAAY,EAAG,cAAiC,oBAAoB,EACpE,EAAc,EAAG,cAA2B,sBAAsB,EAClE,EAAiB,EAAG,cAAiC,0BAA0B,EAC/E,EAAkB,EAAG,cAAiC,2BAA2B,EACjF,EAAa,EAAG,cAA+B,qBAAqB,EACpE,EAAc,EAAG,cAAgC,sBAAsB,EACvE,EAAc,EAAG,cAA2B,sBAAsB,EAClE,EAAkB,EAAG,cAAiC,2BAA2B,EAEjF,MAAwB,CAC5B,GAAI,CAAC,GAAe,CAAC,EAAa,OAClC,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,EAAY,UAAU,OAAO,CAAY,EACzC,EAAY,MAAM,QAAU,EAC5B,EAAY,MAAQ,GAAe,MAAQ,GACvC,IACF,EAAY,MAAM,QAAU,EAC5B,EAAY,UAAU,IAAI,CAAY,GAExC,EAAY,MAAM,EAClB,EAAY,OAAO,CACrB,EAEM,MAAyB,CACzB,IACF,EAAY,UAAU,IAAI,CAAY,EACtC,EAAY,MAAM,QAAU,GAE1B,IACF,EAAY,MAAM,QAAU,EAC5B,EAAY,UAAU,IAAI,CAAY,EAE1C,EAEA,GAAW,iBAAiB,EAAa,CAAe,EACxD,GAAgB,iBAAiB,EAAa,CAAgB,EAC9D,GAAiB,iBAAiB,EAAa,CAAgB,EAE/D,GAAa,iBAAiB,EAAc,GAAM,CAC5C,EAAE,SAAW,GAAa,EAAiB,CACjD,CAAC,EAED,OAAO,iBAAiB,UAAY,GAAM,CACpC,EAAE,MAAQ,UAAY,GAAe,CAAC,EAAY,UAAU,SAAA,QAAqB,GACnF,EAAiB,CAErB,CAAC,EAED,GAAY,iBAAiB,EAAc,KAAO,IAAM,CACtD,EAAE,eAAe,EACjB,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,GAAI,CAAC,GAAiB,CAAC,GAAe,CAAC,EAAiB,OAExD,IAAM,EAAU,EAAY,MAAM,KAAK,EACvC,GAAI,CAAC,EAAS,CACR,IACF,EAAY,YAAc,oCAC1B,EAAY,UAAU,OAAO,CAAY,EACzC,EAAY,MAAM,QAAU,GAE9B,MACF,CAEA,EAAgB,SAAW,GACvB,IACF,EAAY,MAAM,QAAU,EAC5B,EAAY,UAAU,IAAI,CAAY,GAGxC,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,KAAM,CACnE,OAAA,MACA,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,KAAM,CAAQ,CAAC,CACxC,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,+BAA+B,EAAK,OAAO,EAAE,CAChF,CAGA,IAAM,GAAc,MADD,EAAK,KAAK,EAAA,CACJ,MAAQ,EACjC,MAAM,KAAK,cAAc,wBAAwB,CAAW,EAC5D,IAAM,EAAS,EAAG,cAA2B,cAAc,EACvD,IAAQ,EAAO,YAAc,GAEjC,EAAiB,CACnB,OAAS,EAAU,CACb,IACF,EAAY,YAAc,EAAI,SAAW,qCACzC,EAAY,UAAU,OAAO,CAAY,EACzC,EAAY,MAAM,QAAU,EAEhC,QAAU,CACR,EAAgB,SAAW,EAC7B,CACF,CAAC,EAGD,EAAG,cAAc,wBAAwB,CAAC,EAAE,iBAAiB,MAAmB,CAC9E,KAAK,YAAY,UAAU,kBAAkB,CAC/C,CAAC,EAGD,IAAM,EAAgB,EAAG,cAAiC,yBAAyB,EAC7E,EAAiB,EAAG,cAA2B,0BAA0B,EACzE,EAAmB,EAAG,cAAiC,sBAAsB,EAC7E,EAAoB,EAAG,cAAiC,uBAAuB,EAE/E,EAAiB,GAAmB,CACnC,GAAmB,KACL,IAAS,IAAA,GAAmB,EAAe,UAAU,SAAA,QAAqB,EAArD,IAEtC,EAAe,UAAU,OAAO,CAAY,EAC5C,EAAc,aAAa,gBAAiB,MAAM,IAElD,EAAe,UAAU,IAAI,CAAY,EACzC,EAAc,aAAa,gBAAiB,OAAO,GAEvD,EAEA,GAAe,iBAAiB,EAAc,GAAM,CAClD,EAAE,gBAAgB,EAClB,EAAc,CAChB,CAAC,EAED,SAAS,iBAAiB,EAAc,GAAM,CACxC,GAAkB,CAAC,EAAe,UAAU,SAAA,QAAqB,IAC9D,EAAG,cAAc,+BAA+B,CAAC,EAAE,SAAS,EAAE,MAAc,GAC/E,EAAc,EAAK,EAGzB,CAAC,EAED,IAAM,EAAmB,MAAO,EAA+B,IAAwB,CACrF,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACrD,KAGL,CADA,EAAc,EAAK,EACf,IAAK,EAAI,SAAW,IACxB,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,oBAAqB,CACrF,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,QAAS,CAAI,CAAC,CACvC,CAAC,EACD,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,mCAAmC,CACtE,CACA,IAAM,EAAO,MAAM,EAAK,KAAK,EACvB,EAAO,EAAK,cAAgB,EAAK,IACvC,GAAI,CAAC,EAAM,CACT,MAAM,6EAA6E,EACnF,MACF,CACA,OAAO,KAAK,EAAM,QAAQ,CAC5B,OAAS,EAAU,CACjB,QAAQ,MAAM,mCAAoC,CAAG,EACrD,MAAM,EAAI,SAAW,iCAAiC,CACxD,QAAU,CACJ,IAAK,EAAI,SAAW,GAC1B,CAvBwB,CAwB1B,EAEA,GAAkB,iBAAiB,MAAmB,EAAiB,EAAkB,KAAK,CAAC,EAC/F,GAAmB,iBAAiB,MAAmB,EAAiB,EAAmB,MAAM,CAAC,EAElG,OAAO,iBAAiB,UAAY,GAAO,CACrC,EAAG,MAAM,OAAS,0BACpB,KAAK,cAAc,oBAAoB,CAAC,CAAC,SAAW,KAAK,QAAQ,CAAC,CAEtE,CAAC,EAED,OAAO,iBAAiB,YAAe,CACjC,KAAK,cAAc,iBAAiB,GACtC,KAAK,cAAc,oBAAoB,CAAC,CAAC,SAAW,KAAK,QAAQ,CAAC,CAEtE,CAAC,EAGD,IAAM,EAAc,EAAG,cAA2B,sBAAsB,EAClE,EAAgB,EAAG,cAAc,uBAAuB,EACxD,EAAiB,EAAG,cAAc,yBAAyB,EAC3D,EAAkB,EAAG,cAAc,oBAAoB,EACvD,EAAa,EAAG,cAA+B,qBAAqB,EACpE,EAAa,EAAG,cAAgC,qBAAqB,EACrE,EAAW,EAAG,cAA2B,sBAAsB,EAC/D,EAAa,EAAG,cAA2B,wBAAwB,EACnE,EAAkB,EAAG,cAAiC,oBAAoB,EAE1E,OAAkB,CAClB,IACF,EAAY,UAAU,OAAO,CAAY,EACzC,EAAY,MAAM,QAAU,EACxB,IACF,EAAW,MAAQ,GACnB,EAAW,MAAM,GAEf,IAAU,EAAS,MAAM,QAAU,GACnC,IAAY,EAAW,MAAM,QAAU,GAE/C,EAEM,MAAmB,CACnB,IACF,EAAY,UAAU,IAAI,CAAY,EACtC,EAAY,MAAM,QAAU,EAEhC,EAEA,GAAe,iBAAiB,EAAa,EAAS,EACtD,GAAgB,iBAAiB,EAAa,CAAU,EACxD,GAAiB,iBAAiB,EAAa,CAAU,EAEzD,GAAY,iBAAiB,EAAc,KAAO,IAAM,CACtD,EAAE,eAAe,EACjB,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,GAAI,CAAC,GAAc,CAAC,GAAmB,CAAC,GAAY,CAAC,GAAc,CAAC,EAAe,OAEnF,IAAM,EAAQ,EAAW,MAAM,KAAK,EAC/B,KAKL,CAHA,EAAgB,SAAW,GAC3B,EAAgB,YAAc,aAC9B,EAAS,MAAM,QAAU,EACzB,EAAW,MAAM,QAAU,EAE3B,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,cAAe,CAC/E,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,OAAM,CAAC,CAChC,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAM,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAC9C,MAAU,MAAM,EAAI,OAAS,2BAA2B,CAC1D,CAEA,EAAW,YAAc,sBAAsB,EAAM,GACrD,EAAW,MAAM,QAAU,EAC3B,EAAW,MAAQ,GACnB,IAAM,EAAQ,SAAS,eAAe,KAAK,EAAE,EACzC,GACF,MAAM,KAAK,yBACT,EACA,EAAc,GACd,KAAK,cAAc,cAAc,EACjC,KAAK,YAAY,QAAQ,CAC3B,EAEF,eAAiB,CACf,EAAW,CACb,EAAG,IAAI,CACT,OAAS,EAAU,CACjB,EAAS,YAAc,EAAI,SAAW,4BACtC,EAAS,MAAM,QAAU,CAC3B,QAAU,CACR,EAAgB,SAAW,GAC3B,EAAgB,YAAc,iBAChC,CAnC2B,CAoC7B,CAAC,EAGD,IAAM,EAAc,EAAG,cAA2B,sBAAsB,EAClE,EAAgB,EAAG,cAA2B,yBAAyB,EACvE,EAAiB,EAAG,cAA2B,gCAAgC,EAC/E,EAAkB,EAAG,cAA2B,2BAA2B,EAC3E,EAAgB,EAAG,cAA2B,yBAAyB,EACvE,EAAa,EAAG,cAA+B,qBAAqB,EACpE,EAAa,EAAG,cAAgC,4BAA4B,EAC5E,EAAgB,EAAG,cAAiC,+BAA+B,EACnF,EAAiB,EAAG,cAA2B,6BAA6B,EAC5E,EAAkB,EAAG,cAA2B,uBAAuB,EACvE,EAAkB,EAAG,cAA2B,uBAAuB,EACvE,EAAY,EAAG,cAAgC,2BAA2B,EAC1E,EAAc,EAAG,cAAiC,yBAAyB,EAC3E,GAAc,EAAG,cAA2B,mBAAmB,EAC/D,EAAkB,EAAG,cAAiC,2BAA2B,EAEjF,OAAwB,CAC5B,GAAI,EAAa,CAOf,GANA,EAAY,UAAU,OAAO,CAAY,EACzC,EAAY,MAAM,QAAU,EACxB,IACF,EAAW,MAAQ,GACnB,EAAW,MAAM,GAEf,EAAe,CACjB,EAAc,UAAY,4DAC1B,IAAM,EAAkB,KAAK,cAAc,YAAY,EACvD,IAAK,IAAM,KAAM,EAAiB,CAChC,IAAM,EAAM,SAAS,cAAc,QAAQ,EAC3C,EAAI,MAAQ,EAAG,KACf,EAAI,YAAc,IAAI,EAAG,OACzB,EAAc,YAAY,CAAG,CAC/B,CACF,CACI,IAAiB,EAAgB,MAAM,QAAU,GACjD,IAAgB,EAAe,MAAM,QAAU,GAC/C,IAAiB,EAAgB,MAAM,QAAU,GACjD,IACF,EAAgB,MAAM,QAAU,EAChC,EAAgB,SAAW,GAC3B,EAAgB,YAAc,4BAE5B,IAAiB,EAAgB,MAAM,QAAU,GACjD,IAAe,EAAc,MAAM,QAAU,EACnD,CACF,EAEM,OAAyB,CACzB,IACF,EAAY,UAAU,IAAI,CAAY,EACtC,EAAY,MAAM,QAAU,EAEhC,EAEA,GAAe,iBAAiB,EAAa,EAAe,EAC5D,GAAgB,iBAAiB,EAAa,EAAgB,EAC9D,GAAiB,iBAAiB,EAAa,EAAgB,EAC/D,GAAe,iBAAiB,EAAa,SAAY,CACvD,GAAiB,EACjB,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACtD,GACF,MAAM,KAAK,qBAAqB,EAAI,EAAc,GAAI,KAAK,cAAc,cAAc,CAAC,CAE5F,CAAC,EAED,GAAa,iBAAiB,EAAa,SAAY,CAChD,MAAW,MAChB,GAAI,CACF,MAAM,UAAU,UAAU,UAAU,EAAU,KAAK,EACnD,EAAY,YAAc,UAC1B,eAAiB,CACf,EAAY,YAAc,WAC5B,EAAG,GAAI,CACT,OAAS,EAAK,CACZ,QAAQ,MAAM,kCAAmC,CAAG,CACtD,CACF,CAAC,EAED,GAAY,iBAAiB,EAAc,KAAO,IAAM,CACtD,EAAE,eAAe,EACjB,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,GAAI,CAAC,GAAc,CAAC,GAAmB,CAAC,GAAkB,CAAC,EAAe,OAE1E,IAAM,EAAQ,EAAW,MAAM,KAAK,EAC9B,EAAc,GAAe,OAAS,IAAA,GACvC,KAIL,CAFA,EAAgB,SAAW,GAC3B,EAAgB,YAAc,gBAC9B,EAAe,MAAM,QAAU,EAE/B,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,0BAA2B,CAC3F,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,QAAO,aAAc,CAAY,CAAC,CAC3D,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAM,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAC9C,MAAU,MAAM,EAAI,OAAS,oCAAoC,CACnE,CAEA,IAAM,EAAS,MAAM,EAAK,YAAY,EAChC,EAAO,GAAQ,0BAA0B,OAAO,IAAI,WAAW,CAAM,CAAC,EACtE,EAAU,EAAK,eAAiB,GAAG,OAAO,SAAS,OAAO,gCAAgC,mBAAmB,EAAK,iBAAmB,EAAE,IAI7I,GAFI,IAAW,EAAU,MAAQ,GAE7B,GAAa,CACf,GAAY,gBAAgB,EAC5B,IAAM,EAAY,MAAA,GAAa,SAAS,EAAS,CAC/C,KAAM,MACN,OAAQ,EACR,MAAO,IACP,MAAO,CAAE,KAAM,UAAW,MAAO,SAAU,CAC7C,CAAC,EAEK,EAAM,IADO,UACP,CAAA,CAAO,gBAAgB,EAAW,eAAe,EAC7D,GAAY,YAAY,EAAI,eAAe,CAC7C,CAEI,IAAiB,EAAgB,MAAM,QAAU,GACjD,IAAiB,EAAgB,MAAM,QAAU,GACrD,EAAgB,MAAM,QAAU,EAC5B,IAAiB,EAAgB,MAAM,QAAU,GACjD,IAAe,EAAc,MAAM,QAAU,GAGjD,MAAM,KAAK,qBAAqB,EAAI,EAAc,GAAI,KAAK,cAAc,cAAc,CAAC,CAC1F,OAAS,EAAU,CACjB,EAAe,YAAc,EAAI,SAAW,qCAC5C,EAAe,MAAM,QAAU,EAC/B,EAAgB,SAAW,GAC3B,EAAgB,YAAc,0BAChC,CA9C+B,CA+CjC,CAAC,EAGD,IAAM,EAAa,EAAG,cAAgC,qBAAqB,EACrE,EAAe,EAAG,cAAiC,wBAAwB,EAC3E,EAAiB,EAAG,cAAiC,0BAA0B,EAC/E,EAAgB,EAAG,cAA2B,wBAAwB,EAEtE,IAAqB,EAAa,EAAmB,KAAU,CAC9D,IACL,EAAc,YAAc,EAC5B,EAAc,UAAY,sBAAsB,EAAU,QAAU,YACpE,EAAc,UAAU,OAAO,CAAY,EAC3C,EAAc,MAAM,QAAU,EAC9B,eAAiB,CACf,EAAc,UAAU,IAAI,CAAY,EACxC,EAAc,MAAM,QAAU,CAChC,EAAG,GAAI,EACT,EAEA,GAAc,iBAAiB,EAAa,SAAY,CACtD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EAC1D,GAAI,CAAC,GAAiB,CAAC,EAAY,OAEnC,IAAM,EAAS,EAAW,MAAM,KAAK,EAErC,EAAa,SAAW,GACxB,EAAa,YAAc,YAC3B,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,gBAAiB,CACjF,OAAQ,MACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,cAAe,CAAO,CAAC,CAChD,CAAC,EACD,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,8BAA8B,CACjE,CACA,GAAkB,kFAAkF,EACpG,MAAM,KAAK,0BAA0B,EAAI,EAAc,GAAI,KAAK,cAAc,cAAc,CAAC,CAC/F,OAAS,EAAU,CACjB,GAAkB,EAAI,SAAW,6BAA8B,EAAI,CACrE,QAAU,CACR,EAAa,SAAW,GACxB,EAAa,YAAc,aAC7B,CACF,CAAC,EAED,GAAgB,iBAAiB,EAAa,SAAY,CACxD,IAAM,EAAgB,KAAK,cAAc,iBAAiB,EACrD,MACA,QAAQ,0EAA0E,EAKvF,CADA,EAAe,SAAW,GAC1B,EAAe,YAAc,cAC7B,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAc,GAAG,gBAAiB,CACjF,OAAQ,QACV,CAAC,EACD,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,gCAAgC,CACnE,CACA,GAAkB,qCAAqC,EACnD,IAAY,EAAW,MAAQ,IACnC,MAAM,KAAK,0BAA0B,EAAI,EAAc,GAAI,KAAK,cAAc,cAAc,CAAC,CAC/F,OAAS,EAAU,CACjB,GAAkB,EAAI,SAAW,+BAAgC,EAAI,CACvE,QAAU,CACR,EAAe,SAAW,GAC1B,EAAe,YAAc,QAC/B,CAjB6B,CAkB/B,CAAC,CACH,CAEA,MAAc,0BAA0B,EAAiB,EAAe,EAAiC,CACvG,IAAM,EAAU,EAAG,cAAgC,qBAAqB,EAClE,EAAiB,EAAG,cAA2B,6BAA6B,EAC5E,EAAW,EAAG,cAA2B,sBAAsB,EAC/D,EAAY,EAAG,cAAiC,0BAA0B,EAC1E,EAAU,EAAG,cAAiC,wBAAwB,EACtE,EAAa,EAAG,cAA2B,wBAAwB,EAErE,GAAY,EAAW,UAAU,IAAI,CAAY,EAEjD,IAAS,EAAQ,SAAW,CAAC,GAC7B,IAAW,EAAU,SAAW,CAAC,GACjC,IAAS,EAAQ,SAAW,CAAC,GAEjC,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAS,eAAe,EACzE,GAAI,CAAC,EAAK,GAAI,OAEd,IAAM,EAAO,MAAM,EAAK,KAAK,EACzB,GAAkB,EAAK,eACzB,EAAe,YAAc,EAAK,cAEhC,IACF,EAAQ,MAAQ,EAAK,eAAiB,IAIpC,IACG,EAAK,cAIC,EAAK,cACd,EAAS,YAAc,oBACvB,EAAS,UAAY,0CACjB,GAAW,EAAU,UAAU,OAAO,CAAY,IAEtD,EAAS,YAAc,uBACvB,EAAS,UAAY,yCACjB,GAAW,EAAU,UAAU,OAAO,CAAY,IAVtD,EAAS,YAAc,iBACvB,EAAS,UAAY,yCACjB,GAAW,EAAU,UAAU,IAAI,CAAY,GAWzD,OAAS,EAAG,CACV,QAAQ,KAAK,uCAAwC,CAAC,CACxD,CACF,CAEA,MAAc,6BAA6B,EAAiB,EAA8B,CACxF,IAAM,EAAQ,EAAG,cAA2B,wBAAwB,EAC9D,EAAU,EAAG,cAA2B,wBAAwB,EACjE,KAEL,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,gBAAgB,EAAS,mBAAmB,EAC7E,GAAI,CAAC,EAAK,GAAI,CACZ,EAAM,UAAY,GACd,IACF,EAAQ,UAAU,OAAO,CAAY,EACrC,EAAQ,MAAM,QAAU,GAE1B,MACF,CAEA,IAAM,EAQD,MAAM,EAAK,KAAK,EAGrB,GADA,EAAM,UAAY,GACd,CAAC,MAAM,QAAQ,CAAQ,GAAK,EAAS,SAAW,EAAG,CACjD,IACF,EAAQ,UAAU,OAAO,CAAY,EACrC,EAAQ,MAAM,QAAU,GAE1B,MACF,CAEI,IACF,EAAQ,UAAU,IAAI,CAAY,EAClC,EAAQ,MAAM,QAAU,GAG1B,IAAK,IAAM,KAAK,EAAU,CACxB,IAAM,EAAK,SAAS,cAAc,IAAI,EAChC,EAAS,GAAU,EAAE,kBAAkB,EACvC,EAAY,EAAE,cAAgB,KAEhC,IADA,IAAI,EAAE,aAAe,IAAA,CAAK,QAAQ,CAAC,EAAE,GAAG,EAAE,UAAY,QAEpD,EAAS,EAAE,YAAc,EAAE,IAAM,IAEvC,EAAG,UAAY;+BACQ,GAAe,CAAM,EAAE;0CACZ,EAAO;gBACjC,EAAE,aAAe,oBAAoB;qEACgB,EAAU;;yGAE0B,EAAE,GAAG;;;;UAMtG,IAAM,EAAQ,EAAG,cAAiC,uBAAuB,EACzE,GAAO,iBAAiB,EAAa,SAAY,CAC/C,EAAM,SAAW,GACjB,EAAM,YAAc,iBACpB,GAAI,CACF,IAAM,EAAS,MAAM,EAAc,gBAAgB,EAAS,qBAAqB,EAAE,GAAG,yBAA0B,CAC9G,QAAS,CAAE,OAAU,mCAAoC,CAC3D,CAAC,EACD,GAAI,CAAC,EAAO,GAAI,MAAU,MAAM,oCAAoC,EAEpE,IAAI,EACA,EAAW,WAAW,EAAE,YAAc,EAAE,GAAG,MAG/C,IADoB,EAAO,QAAQ,IAAI,cAAc,GAAK,GAAA,CAC1C,SAAS,iBAAiB,EAAG,CAE3C,IAAM,GADc,EAAO,QAAQ,IAAI,qBAAqB,GAAK,GAAA,CACvC,MAAM,uBAAuB,EACnD,GAAS,EAAM,KAAI,EAAW,EAAM,IACxC,EAAO,MAAM,EAAO,KAAK,CAC3B,KAAO,CACL,IAAM,EAAO,MAAM,EAAO,KAAK,EAC3B,EAAK,WAAU,EAAW,EAAK,UACnC,IAAM,EAAW,MAAM,MAAM,EAAK,GAAG,EACrC,GAAI,CAAC,EAAS,GAAI,MAAU,MAAM,6BAA6B,EAC/D,EAAO,MAAM,EAAS,KAAK,CAC7B,CAEA,IAAM,EAAU,IAAI,gBAAgB,CAAI,EAClC,EAAI,SAAS,cAAc,GAAG,EACpC,EAAE,KAAO,EACT,EAAE,SAAW,EACb,SAAS,KAAK,YAAY,CAAC,EAC3B,EAAE,MAAM,EACR,SAAS,KAAK,YAAY,CAAC,EAC3B,eAAiB,IAAI,gBAAgB,CAAO,EAAG,GAAI,CACrD,OAAS,EAAU,CACjB,MAAM,EAAI,SAAW,gCAAgC,CACvD,QAAU,CACR,EAAM,SAAW,GACjB,EAAM,YAAc,UACtB,CACF,CAAC,EAED,EAAM,YAAY,CAAE,CACtB,CACF,OAAS,EAAK,CACZ,QAAQ,MAAM,mCAAoC,CAAG,EACjD,IACF,EAAQ,UAAU,OAAO,CAAY,EACrC,EAAQ,MAAM,QAAU,EAE5B,CACF,CACF,EChpCM,GAAqB,mCAEd,GAAb,cAAoC,EAAa,CAC/C,cACA,YACA,YAEA,YAAY,EAA8B,EAA0B,EAA0B,CAC5F,MAAM,kBAAkB,EACxB,KAAK,cAAgB,EACrB,KAAK,YAAc,EACnB,KAAK,YAAc,CACrB,CAEA,MAAM,SAAyB,CAC7B,MAAM,MAAM,QAAQ,EACpB,IAAM,EAAK,SAAS,eAAe,KAAK,EAAE,EAC1C,GAAI,CAAC,EAAI,OAET,IAAM,EAAO,KAAK,YAAY,QAAQ,EAChC,EAAc,KAAK,cAAc,eAAe,EAChD,EAAgB,KAAK,cAAc,iBAAiB,EACpD,EAAa,EAAY,OAAS,EAExC,EAAG,UAAY;;;;oBAIC,EAAE,gBAAgB,EAAE;wCACA,EAAa,WAAa,QAAQ;;;cAG5D,EAAa,iBAAmB,EAAE,iBAAkB,CAAE,KAAM,GAAM,MAAQ,aAAc,CAAC,EAAA;;;cAGzF,EACE,iFACA,EAAE,kBAAkB,EAAA;;;YAGxB,EAAa;;gBAET,EAAY,IAAI,GAAK,CACrB,IAAM,EAAY,GAAiB,OAAO,EAAc,EAAE,IAAM,OAAO,EAAE,EAAE,EAC3E,MAAO;yFACkE,EAAE,GAAG;;;2BAGnE,EAAE,MAAQ,IAAA,CAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAA;;;+DAGD,EAAE,KAAK;;;;;wBAK9C,EAAY,2DAA6D,GAAA;;;;;;iBAOnF,CAAC,CAAC,CAAC,KAAK,EAAE,EAAA;;;;;;;;;;;;YAYV,GAAA;;uDAEyC,EAAa,SAAW,GAAG;;;;oBAI9D,EAAE,qBAAqB,EAAA;;;;;;2BAMhB,GAAM,MAAQ,GAAG;iCACX,GAAM,MAAQ,oBAAoB;;;;;;;oBAO/C,EAAE,0BAA0B,EAAA;;;;;;;;;;;;mCAYb,EAAE,gCAAgC,EAAE;;;;;oBAKnD,EAAE,oBAAoB,EAAA;;;;;;;;oBAQtB,EAAE,qBAAqB,EAAA;;kBAEzB,EAAa;;;;kBAIX,GAAA;;;;;YAKR,EAAgB;;;;;;YAMd,GAAA;;;MAMV,EAAG,iBAAoC,yBAAyB,CAAC,CAAC,QAAQ,GAAO,CAC/E,EAAI,iBAAiB,EAAa,SAAY,CAC5C,IAAM,EAAW,EAAI,aAAa,gBAAgB,EAC9C,IACF,eAAe,QAAQ,oBAAqB,CAAQ,EACpD,aAAa,QAAQ,EAAwB,CAAQ,EACrD,MAAM,KAAK,cAAc,aAAa,CAAQ,EAC9C,MAAM,KAAK,YAAY,UAAU,iBAAiB,EAEtD,CAAC,CACH,CAAC,EAED,IAAM,EAAO,EAAG,cAAc,kBAAkB,EAC1C,EAAkB,EAAG,cAAc,kBAAkB,EACrD,EAAe,EAAG,cAAc,uBAAuB,EACvD,EAAY,EAAG,cAAc,oBAAoB,EACjD,EAAY,EAAG,cAAc,wBAAwB,EACrD,EAAqB,EAAG,cAAc,uBAAuB,EAC7D,EAAW,EAAG,cAAc,mBAAmB,EAC/C,EAAkB,EAAG,cAAc,wBAAwB,EAC3D,EAAkB,EAAG,cAA2B,4BAA4B,EAElF,GAAiB,iBAAiB,MAAmB,CACnD,GAAI,EAAiB,CACnB,IAAM,EAAW,EAAgB,MAAM,UAAY,EACnD,EAAgB,MAAM,QAAU,EAAW,EAAgB,EAC3D,EAAgB,YAAc,EAAW,qBAAuB,4BAC5D,IACE,GAAmB,CAAC,EAAgB,OAAS,GAAM,OACrD,EAAgB,MAAQ,EAAK,MAE/B,GAAiB,MAAM,EAE3B,CACF,CAAC,EAED,GAAoB,iBAAiB,MAAmB,CACtD,KAAK,YAAY,UAAU,iBAAiB,CAC9C,CAAC,EAED,GAAW,iBAAiB,MAAmB,CACzC,GAAc,GAAmB,GACnC,EAAgB,MAAM,QAAU,EAChC,EAAgB,YAAc,6BAE9B,KAAK,YAAY,UAAU,iBAAiB,CAEhD,CAAC,EAED,GAAc,iBAAiB,MAAmB,CAChD,EAAa,MAAQ,EAAa,MAC/B,QAAQ,MAAO,EAAE,CAAC,CAClB,QAAQ,kBAAmB,EAAE,CAAC,CAC9B,QAAQ,OAAQ,GAAG,CAAC,CACpB,QAAQ,KAAM,EAAE,CACrB,CAAC,EAED,GAAM,iBAAiB,EAAc,KAAO,IAAM,CAEhD,GADA,EAAE,eAAe,EACb,CAAC,GAAmB,CAAC,GAAgB,CAAC,GAAa,CAAC,EAAU,OAElE,IAAM,EAAa,EAAgB,MAAM,KAAK,EACxC,EAAc,EAAa,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAO,EAAE,EAE3D,GAAC,GAAe,EAEpB,IAAI,EAAY,OAAS,GAAK,EAAY,OAAS,IAAM,CAAC,GAAmB,KAAK,CAAW,EAAG,CAC9F,EAAS,YAAc,EAAE,4BAA4B,EACrD,EAAS,MAAM,QAAU,EACzB,MACF,CAEA,EAAU,SAAW,GACrB,EAAU,YAAc,EAAE,oBAAoB,EAC9C,EAAS,MAAM,QAAU,EAEzB,GAAI,CACF,IAAM,EAAO,MAAM,EAAc,eAAgB,CAC/C,OAAQ,EACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,KAAM,EAAY,qBAAsB,CAAY,CAAC,CAC9E,CAAC,EAED,GAAI,CAAC,EAAK,GAAI,CACZ,IAAM,EAAU,MAAM,EAAK,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAClD,MAAU,MAAM,EAAQ,OAAS,uCAAuC,EAAK,OAAO,EAAE,CACxF,CAGA,MAAM,KAAK,YAAY,iBAAiB,EAGxC,IAAM,EAAiB,MAAM,KAAK,cAAc,gBAAgB,EAChE,GAAI,EAAe,OAAS,EAAG,CAC7B,IAAM,EAAe,EAAe,EAAe,OAAS,GAC5D,eAAe,QAAQ,oBAAqB,OAAO,EAAa,EAAE,CAAC,EACnE,aAAa,QAAQ,EAAwB,OAAO,EAAa,EAAE,CAAC,EACpE,MAAM,KAAK,cAAc,aAAa,OAAO,EAAa,EAAE,CAAC,CAC/D,CACA,EAAa,CAAC,CAAC,sBAAsB,SAAS,EAE9C,MAAM,KAAK,YAAY,UAAU,iBAAiB,CACpD,OAAS,EAAU,CACjB,EAAS,YAAc,EAAI,SAAW,iDACtC,EAAS,MAAM,QAAU,CAC3B,QAAU,CACR,EAAU,SAAW,GACrB,EAAU,YAAc,EAAE,qBAAqB,CACjD,CAtCA,CAuCF,CAAC,CACH,CACF,EC3QI,GAA6C,KAOjD,eAAsB,GAA+B,EAA4C,CAS/F,OARI,KAIJ,GAAoB,GAAiC,CAAW,CAAC,CAAC,YAAc,CAC9E,GAAoB,IACtB,CAAC,EAEM,GACT,CAEA,eAAe,GAAiC,EAA4C,CAC1F,IAAM,EAAO,EAAY,QAAQ,EACjC,GAAI,CAAC,EAAM,MAAO,GAElB,IAAM,EAAQ,MAAM,EAAY,oBAAoB,EAIpD,GAHI,CAAC,GAAS,CAAC,EAAM,SAGjB,EAAK,oBAAsB,EAAM,QACnC,MAAO,GAGT,IAAM,EAAQ,SAAS,eAAe,sBAAsB,EACtD,EAAU,SAAS,eAAe,yBAAyB,EAC3D,EAAW,SAAS,eAAe,yBAAyB,EAC5D,EAAQ,SAAS,eAAe,+BAA+B,EAC/D,EAAY,SAAS,eAAe,2BAA2B,EAE/D,EAAc,SAAS,eAAe,6BAA6B,EACnE,EAAY,SAAS,eAAe,wBAAwB,EAC5D,EAAiB,SAAS,eAAe,iCAAiC,EAC1E,EAAmB,SAAS,eAAe,mCAAmC,EAC9E,EAAW,SAAS,eAAe,0BAA0B,EAEnE,GAAI,CAAC,GAAS,CAAC,GAAY,CAAC,EAE1B,OADA,QAAQ,KAAK,4CAA4C,EAClD,GAIL,IACF,EAAU,UAAY,GAAe,EAAM,OAAO,GAGpD,IAAI,EAAe,GAEb,MAAoB,CACpB,GACF,EAAS,SAAW,GACpB,EAAS,gBAAgB,UAAU,EACnC,EAAS,MAAM,OAAS,UACpB,IACF,EAAM,MAAM,OAAS,UACrB,EAAM,MAAM,QAAU,KAEpB,GAAU,EAAS,UAAU,IAAI,QAAQ,EAC7C,EAAU,SAAW,CAAC,EAAS,QAC/B,EAAU,MAAQ,EAAS,QAAU,GAAK,4DAE1C,EAAS,SAAW,GACpB,EAAS,aAAa,WAAY,UAAU,EAC5C,EAAS,QAAU,GACnB,EAAS,MAAM,OAAS,cACpB,IACF,EAAM,MAAM,OAAS,cACrB,EAAM,MAAM,QAAU,OAEpB,GAAU,EAAS,UAAU,OAAO,QAAQ,EAChD,EAAU,SAAW,GACrB,EAAU,MAAQ,sDAEtB,EAEM,MAAoB,CACpB,GAAa,EAAY,UAAU,IAAI,QAAQ,EACnD,EAAY,CACd,EAEI,GAAW,IACb,EAAQ,QAAW,GAAM,CACvB,EAAE,eAAe,EACjB,EAAe,GACf,EAAQ,UAAU,IAAI,gBAAgB,EACtC,EAAY,EACZ,EAAY,UAAU,OAAO,QAAQ,CACvC,GAGE,IAAgB,EAAe,QAAU,GACzC,IAAkB,EAAiB,QAAU,GAE7C,IACF,EAAY,QAAW,GAAkB,CACnC,EAAE,SAAW,GACf,EAAY,CAEhB,GAGF,IAAM,EAAa,GAAqB,CAClC,EAAE,MAAQ,UAAY,GAAe,CAAC,EAAY,UAAU,SAAS,QAAQ,GAC/E,EAAY,CAEhB,EASA,OARI,OAAO,OAAW,KACpB,OAAO,iBAAiB,UAAW,CAAS,EAG9C,EAAS,QAAU,GACnB,EAAY,EACZ,EAAM,UAAU,OAAO,QAAQ,EAExB,IAAI,QAAkB,GAAY,CAIvC,EAAS,aAHsB,CAC7B,EAAY,CACd,EAGA,EAAU,QAAU,SAAY,CAC9B,GAAI,CAAC,GAAgB,CAAC,EAAS,QAC7B,OAEF,EAAU,SAAW,GACrB,IAAM,EAAe,EAAU,YAC/B,EAAU,YAAc,YACxB,GAAI,CAEF,GAAI,MADkB,EAAY,YAAY,EAAM,OAAO,EAC9C,CACX,EAAM,UAAU,IAAI,QAAQ,EAC5B,EAAY,EACR,OAAO,OAAW,KACpB,OAAO,oBAAoB,UAAW,CAAS,EAEjD,EAAS,SAAW,KACpB,EAAU,QAAU,KACpB,EAAU,YAAc,EACxB,EAAQ,EAAI,EACZ,MACF,CACF,OAAS,EAAK,CACZ,QAAQ,MAAM,0BAA2B,CAAG,CAC9C,CACA,EAAU,SAAW,GACrB,EAAU,YAAc,EACxB,MAAM,mEAAmE,CAC3E,CACF,CAAC,CACH,CCjIA,IAAM,GAAN,KAAU,CACR,YACA,cACA,YACA,cACA,YAEA,aAAc,CACZ,KAAK,YAAc,IAAI,GACvB,KAAK,cAAgB,IAAI,OACjB,KAAK,YAAY,QAAQ,MACzB,KAAK,YAAY,gBAAgB,MACjC,KAAK,YAAY,cAAc,CACvC,EACA,KAAK,YAAc,IAAI,GAAY,cAAc,EACjD,KAAK,cAAgB,IAAI,GAAc,KAAK,YAAa,KAAK,cAAe,KAAK,WAAW,EAC7F,KAAK,YAAc,IAAI,EACzB,CAEA,MAAM,MAAsB,CAC1B,EAAU,EACV,EAA4B,EAE5B,IAAM,EAAe,MAAM,GAAkB,EAC7C,GAAI,GAAc,aAAa,QAAS,CACtC,EAAsB,SAAS,KAAM,EAAa,WAAW,EAC7D,MACF,CAGA,GAAI,CAAC,MADwB,GAAsB,EAC9B,CACnB,GAA6B,SAAS,SAAY,OAAO,SAAS,OAAO,CAAC,EAC1E,MACF,CAEA,GAAgC,EAAc,WAAW,EAGzD,IAAM,EAAkB,KAAK,YAAY,iBAAiB,EACtD,GACF,eAAe,WAAW,mBAAmB,EAE/C,IAAM,EAAc,KAAK,YAAY,2BAA2B,EAChE,GAAI,EACF,GAAI,CACF,MAAM,KAAK,YAAY,aAAa,CAAW,CACjD,OAAS,EAAU,CACjB,QAAQ,MAAM,4BAA6B,CAAG,EAC9C,MAAM,EAAI,SAAW,yBAAyB,CAChD,CAIF,IAAM,EAAc,IAAI,GAAmB,KAAK,WAAW,EACrD,EAAiB,IAAI,GAAe,KAAK,cAAe,KAAK,YAAa,KAAK,WAAW,EAC1F,EAAiB,IAAI,GAAe,KAAK,cAAe,KAAK,WAAW,EACxE,EAAgB,IAAI,GAAc,KAAK,cAAe,KAAK,WAAW,EACtE,EAAc,IAAI,GAAY,KAAK,cAAe,KAAK,YAAa,KAAK,WAAW,EAqD1F,GAnDA,KAAK,YAAY,SAAS,CAAW,EACrC,KAAK,YAAY,SAAS,CAAc,EACxC,KAAK,YAAY,SAAS,CAAc,EACxC,KAAK,YAAY,SAAS,CAAa,EACvC,KAAK,YAAY,SAAS,CAAW,EAGrC,KAAK,YAAY,KAAK,EACtB,MAAM,KAAK,cAAc,KAAK,EAC9B,MAAM,KAAK,YAAY,KAAK,EAE5B,KAAK,YAAY,2BAA2B,KAAO,IAAmB,CACpE,MAAM,KAAK,SAAS,CAAM,CAC5B,CAAC,EAED,KAAK,YAAY,aAAgB,GAAoB,CAC/C,IAAY,iBAAoB,CAAC,KAAK,cAAc,iBAAiB,GAAK,IAAY,mBACxF,KAAK,YAAY,aAAa,EAAI,EAElC,KAAK,YAAY,aAAa,EAAK,EAErC,KAAK,YAAY,gBAAgB,CAAO,EACxC,KAAK,YAAY,CAAO,CAC1B,EAGA,KAAK,cAAc,gBAAgB,KAAO,IAAW,CAC/C,GACF,KAAK,YAAY,aAAa,EAAK,EAErC,MAAM,KAAK,cAAc,QAAQ,CACnC,CAAC,EAGD,KAAK,YAAY,cAAc,KAAO,IAAoC,CACxE,MAAM,KAAK,cAAc,QAAQ,EAC7B,CAAC,GAAQ,CAAC,KAAK,YAAY,gBAAgB,GAC7C,KAAK,YAAY,aAAa,EAAI,EAClC,MAAM,KAAK,YAAY,UAAU,eAAe,GACvC,GACT,MAAM,GAA+B,KAAK,WAAW,CAEzD,CAAC,EAED,GAAkB,SAAY,CAC5B,KAAK,YAAY,qBAAqB,EACtC,KAAK,YAAY,aAAa,EAAI,EAClC,MAAM,KAAK,YAAY,UAAU,eAAe,CAClD,CAAC,EAGG,KAAK,YAAY,gBAAgB,EAAG,CACtC,IAAM,EAAc,MAAM,KAAK,cAAc,gBAAgB,EAC7D,GAAI,EAAY,OAAS,EAAG,CAC1B,KAAK,YAAY,aAAa,EAAK,EACnC,IAAM,EAAiB,SAAS,cAAc,qDAAqD,CAAC,EAAE,cAClG,IAAgB,EAAe,MAAM,QAAU,QAEnD,MAAM,KAAK,cAAc,aAAa,EAAY,EAAE,CAAC,EAAE,EACvD,IAAM,EAAU,KAAK,YAAY,cAAc,EAC3C,GAAS,aACX,KAAK,cAAc,cAAc,EAAQ,WAAW,EAEtD,MAAM,KAAK,cAAc,QAAQ,EACjC,MAAM,KAAK,iBAAiB,EAC5B,OAAO,iBAAiB,EAAkB,SAAY,CACpD,MAAM,KAAK,iBAAiB,CAC9B,CAAC,EACD,MACF,CACF,CAGA,IAAM,EAAO,MAAM,KAAK,YAAY,iBAAiB,EAGrD,GAFA,MAAM,KAAK,cAAc,QAAQ,EAE7B,CAAC,EAAM,CACT,KAAK,YAAY,aAAa,EAAI,EAClC,MAAM,KAAK,YAAY,UAAU,eAAe,EAChD,MACF,CAGA,MAAM,GAA+B,KAAK,WAAW,EAGrD,IAAM,EAAc,MAAM,KAAK,cAAc,gBAAgB,EAC7D,GAAI,EAAY,SAAW,EAAG,CAC5B,KAAK,YAAY,aAAa,EAAI,EAClC,MAAM,KAAK,YAAY,UAAU,kBAAkB,EACnD,MACF,CAEA,IAAM,EAAkB,eAAe,QAAQ,mBAAmB,EAGlE,GAAI,EAAY,OAAS,IAAM,GAAmB,CAAC,GAAmB,CAAC,EAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,CAAe,GAAI,CAC/H,KAAK,YAAY,aAAa,EAAI,EAClC,MAAM,KAAK,YAAY,UAAU,kBAAkB,EACnD,MACF,CAGA,KAAK,YAAY,aAAa,EAAK,EACnC,IAAM,EAAkB,GAAmB,EAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,CAAe,CAAC,EAAE,IACjG,aAAa,QAAA,yBAA8B,GAAK,EAAY,KAAK,GAAK,OAAO,EAAE,EAAE,IAAM,aAAa,QAAA,yBAA8B,CAAC,CAAC,EAAE,IACvI,EAAY,EAAE,CAAC,GAEjB,eAAe,QAAQ,oBAAqB,OAAO,CAAc,CAAC,EAClE,aAAa,QAAQ,EAAwB,OAAO,CAAc,CAAC,EACnE,MAAM,KAAK,cAAc,aAAa,CAAc,EACpD,MAAM,KAAK,cAAc,QAAQ,EAIjC,IAAM,EAAe,IADC,gBAAgB,OAAO,SAAS,MACjC,CAAA,CAAU,IAAA,SAAiB,IAAM,OAAO,SAAS,OAAO,WAAW,IAAI,EAAI,OAAO,SAAS,OAAO,MAAM,CAAC,EAAI,MAC9H,GACF,KAAK,cAAc,cAAc,CAAY,EAI/C,MAAM,KAAK,iBAAiB,EAG5B,OAAO,iBAAiB,EAAkB,SAAY,CACpD,MAAM,KAAK,iBAAiB,CAC9B,CAAC,CACH,CAEA,MAAc,SAAS,EAA+B,CACpD,MAAM,KAAK,YAAY,UAAU,CAAM,EACvC,KAAK,YAAY,CAAM,CACzB,CAEA,YAAoB,EAAuB,CACzC,IAAI,EAAO,GACX,OAAQ,EAAR,CACE,IAAK,kBACH,EAAO,aACP,MACF,IAAK,iBACH,EAAO,YACP,MACF,IAAK,eACH,EAAO,UACP,MACF,IAAK,mBACH,EAAO,aAIX,CACI,GAAQ,OAAO,SAAS,OAAS,GACnC,OAAO,QAAQ,aAAa,KAAM,GAAI,CAAI,CAE9C,CAEA,MAAc,kBAAkC,CAC9C,IAAM,EAAW,KAAK,YAAY,gBAAgB,EAC9C,IAAC,KAAK,YAAY,QAAQ,GAAM,IAChC,KAAK,cAAc,eAAe,CAAC,CAAC,SAAW,EAGnD,OADgB,OAAO,SAAS,KAAK,QAAQ,KAAM,EAC3C,EAAR,CACE,IAAK,WACH,MAAM,KAAK,YAAY,UAAU,gBAAgB,EACjD,MACF,IAAK,SACC,EACF,MAAM,KAAK,YAAY,UAAU,iBAAiB,EAElD,MAAM,KAAK,YAAY,UAAU,cAAc,EAEjD,MACF,IAAK,aACC,EACF,MAAM,KAAK,YAAY,UAAU,iBAAiB,EAElD,MAAM,KAAK,YAAY,UAAU,kBAAkB,EAErD,MAEF,QACE,MAAM,KAAK,YAAY,UAAU,iBAAiB,CAEtD,CACF,CACF,EAEI,GAAiB,GAErB,SAAS,IAAkB,CACrB,KACJ,GAAiB,GAEjB,IADgB,GAChB,CAAA,CAAI,KAAK,CAAC,CAAC,MAAM,GAAO,CACtB,QAAQ,MAAM,oCAAqC,CAAG,CACxD,CAAC,EACH,CAEI,SAAS,aAAe,UAC1B,SAAS,iBAAiB,EAA0B,EAAS,EAE7D,GAAU"}