const API = '/api';

const LOCALES = {
    en: {
        'nav.packs': 'Packs', 'nav.news': 'News', 'nav.settings': 'Settings',
        'login.title': 'Sign In', 'login.username': 'Username', 'login.password': 'Password',
        'login.subtitle': 'Welcome back to ZernMC',
        'login.hint': 'Need an account?',
        'login.register': 'Register',
        'login.registerTitle': 'Create Account',
        'login.registerSubtitle': 'Join ZernMC today',
        'login.hasAccount': 'Already have an account?',
        'login.confirm': 'Confirm Password',
        'login.passMismatch': 'Passwords do not match',
        'login.passTooShort': 'Password must be at least 6 characters',
        'login.signingIn': 'Signing in...',
        'loading.text': 'Loading...',
        'sidebar.serverPacks': 'Server Packs', 'sidebar.localPacks': 'Local Packs',
        'pack.empty.title': 'Select a pack',
        'pack.empty.desc': 'Choose a pack from the sidebar to get started',
        'pack.description.loading': 'Loading description...',
        'pack.noDescription': 'No description available',
        'stat.loaderVer': 'Loader Ver', 'stat.files': 'Files', 'stat.size': 'Size',
        'playBar.play': 'Play', 'playBar.passRequired': 'Pass Required',
        'playBar.update': 'Update', 'playBar.updating': 'Updating...',
        'settings.title': 'Settings',
        'settings.activatePass.title': 'Activate Pass',
        'settings.activatePass.desc': 'Enter your pass code to access server packs',
        'settings.activatePass.placeholder': 'Pass code',
        'settings.activatePass.button': 'Activate',
        'settings.ram.title': 'Allocated RAM',
        'settings.resolution.title': 'Game Resolution',
        'settings.resolution.desc': 'Width x Height',
        'settings.jvmArgs.title': 'Extra JVM Arguments',
        'settings.jvmArgs.desc': 'Additional Java VM options',
        'settings.javaPath.title': 'Java Path',
        'settings.server.title': 'Server',
        'settings.server.checking': 'Checking...',
        'settings.server.connected': 'Connected',
        'settings.server.disconnected': 'Disconnected',
        'settings.network.title': 'Network',
        'settings.network.desc': 'Per-service connection mode (direct / via proxy)',
        'settings.network.direct': 'Direct',
        'settings.network.proxy': 'Proxy',
        'settings.network.down': 'Unavailable',
        'settings.network.loading': 'Checking...',
        'settings.network.unavailable': 'Not available',
        'settings.language.title': 'Language',
        'settings.language.desc': 'Interface language',
        'settings.systemJvm.title': 'System-based JVM Optimization',
        'settings.systemJvm.desc': 'Auto-tune JVM flags based on your system (experimental)',
        'settings.systemJvm.info': 'CPU: {cores} cores | RAM: {ram} GB | {flags}',
        'settings.systemJvm.enabled': 'Enabled', 'settings.systemJvm.disabled': 'Disabled',
        'settings.logViewer.title': 'Game Log',
        'settings.logViewer.desc': 'View real-time game logs',
        'settings.logViewer.open': 'Open Log',
        'logViewer.title': 'Game Log',
        'logViewer.noLogs': 'No logs yet. Launch the game to see logs here.',
        'logViewer.copy': 'Copy', 'logViewer.copied': 'Copied!',
        'logViewer.openFile': 'Open File', 'logViewer.close': 'Close',
        'logViewer.opened': 'File opened',
        'logViewer.openFailed': 'Could not open the log file',
        'logViewer.waiting': 'Waiting for game output...',
        'install.title': 'Install Pack',
        'install.tab.serverPack': 'Server Pack', 'install.tab.custom': 'Custom',
        'install.serverPack.label': 'Server Pack',
        'install.localName.label': 'Local Name',
        'install.downloadBtn': 'Download & Install',
        'install.custom.name': 'Pack Name',
        'install.custom.mcVersion': 'Minecraft Version',
        'install.custom.loader': 'Loader',
        'install.custom.loaderVersion': 'Loader Version',
        'install.custom.unavailable': 'Not available yet',
        'install.custom.desc': 'Custom pack installation is disabled in this version. Use Server Pack tab to install packs from the server.',
        'install.progress.installing': 'Installing...',
        'install.enterName': 'Enter a name',
        'install.step': 'Step {current} of {total}: {name}',
        'select.loading': 'Loading...',
        'select.selectPack': 'Select a pack',
        'select.passRequired': 'Pass required',
        'select.noPacks': 'No packs available',
        'select.selectVersion': 'Select version',
        'select.failedLoad': 'Failed to load',
        'select.selectMCFirst': 'Select MC version first',
        'select.placeholder': 'Select...',
        'select.search': 'Search...',
        'packList.empty': 'No packs',
        'news.title': 'News', 'news.empty': 'No news yet', 'news.loading': 'Loading news...',
        'pack.update': 'Update', 'pack.delete': 'Delete',
        'badge.free': 'FREE', 'badge.pro': 'PRO',
        'toast.welcomeBack': 'Welcome back, {username}!',
        'toast.welcome': 'Welcome, {username}!',
        'toast.accountCreated': 'Account created! Welcome, {username}!',
        'toast.loggedOut': 'Logged out',
        'toast.enterCredentials': 'Enter username and password',
        'toast.loginFailed': 'Login failed',
        'login.errNetwork': 'Could not reach the server. Check your connection.',
        'login.errInvalidInput': 'Invalid data. The password must be at least 6 characters.',
        'login.errTaken': 'This username is already taken.',
        'login.errBadCredentials': 'Wrong username or password.',
        'login.errServer': 'The server returned an error. Try again later.',
        'offline.title': 'Offline Mode',
        'offline.subtitle': 'The Zern server is unreachable. You can still launch locally installed packs without an account.',
        'offline.nickname': 'Nickname',
        'offline.instance': 'Pack',
        'offline.noInstances': 'No locally installed packs found',
        'offline.launch': 'Launch Offline',
        'offline.launching': 'Launching...',
        'offline.needNickname': 'Enter a nickname',
        'offline.nickTooLong': 'Nickname must be at most 16 characters',
        'offline.needInstance': 'Select a pack',
        'offline.retry': 'Try again online',
        'offline.exit': 'Exit',
        'offline.stillOffline': 'Server is still unreachable',
        'toast.launching': 'Launching {name}...',
        'toast.launchFailed': 'Launch failed',
        'toast.updated': '{name} updated to v{version}!',
        'toast.updateFailed': 'Update failed',
        'toast.noPackSelected': 'Select a pack first',
        'toast.launched': 'Launched! PID: {pid}',
        'toast.installing': 'Installing...',
        'toast.installComplete': 'Installation complete!',
        'toast.installFailed': 'Installation failed',
        'toast.enterPassCode': 'Enter a pass code',
        'toast.passActivated': 'Pass activated!',
        'toast.activationFailed': 'Activation failed',
        'error.network': 'Network error',
        'pack.modsCount': '{count} mods',
        'pack.worldsCount': '{count} worlds',
        'pack.logsCount': '{count} logs',
        'locale.en': 'English', 'locale.ru': 'Russian',
        'ram.gb': '{value} GB',
        'ram.mbAllocated': '{value} MB allocated',
        'nav.friends': 'Friends',
        'friends.title': 'Friends',
        'friends.add': 'Add',
        'friends.addTitle': 'Add Friend',
        'friends.addLabel': 'Username',
        'friends.addPlaceholder': 'Enter username...',
        'friends.search': 'Search friends...',
        'friends.empty': 'No friends yet',
        'friends.online': 'Online',
        'friends.offline': 'Offline',
        'friends.playing': 'Playing {pack}',
        'friends.noPack': 'Idle',
        'friends.requests': 'Friend Requests',
        'friends.requestFrom': '{user} wants to add you as friend',
        'friends.accept': 'Accept',
        'friends.decline': 'Decline',
        'friends.remove': 'Remove',
        'friends.addSuccess': 'Request sent!',
        'friends.addError': 'Failed to add friend',
        'friends.removeSuccess': 'Friend removed',
        'friends.acceptSuccess': 'Request accepted',
        'friends.passActive': 'Pass Active',
        'friends.passInactive': 'No Pass',
        'playtime.label': 'Playtime',
        'playtime.hours': '{h}h {m}m',
        'playtime.total': 'Total: {h}h',
        'playtime.syncing': 'Syncing...',
        'whitelist.title': 'Additional Mods',
        'whitelist.loading': 'Loading...',
        'whitelist.empty': 'No additional mods available',
        'whitelist.error': 'Failed to load mods',
        'whitelist.install': 'Install Selected',
        'whitelist.installing': 'Installing...',
        'whitelist.installed': 'Installed',
        'whitelist.done': 'Mods installed!',
        'whitelist.selectNone': 'Select mods first',
        'whitelist.installError': 'Failed to install mod',
        'mods.title': 'Mods',
        'mods.loading': 'Loading mods...',
        'mods.empty': 'No mods installed',
        'mods.search': 'Search mods...',
        'mods.enabled': 'Enabled',
        'mods.disabled': 'Disabled',
        'mods.delete': 'Delete',
        'mods.enable': 'Enable',
        'mods.disable': 'Disable',
        'mods.noResults': 'No mods found',
        'mods.deleteConfirm': 'Delete {name}?',
        'mods.count': '{count} mods',
        'nav.admin': 'Admin',
        'admin.title': 'Admin Panel',
        'admin.clients': 'Clients',
        'admin.mods': 'Mods',
        'admin.users': 'Users',
        'admin.passes': 'Passes',
        'admin.news': 'News',
        'admin.clientsOnline': 'Online Clients',
        'admin.noClients': 'No connected clients',
        'admin.modsUpload': 'Upload Mod',
        'admin.modsUploadBtn': 'Upload',
        'admin.modsList': 'Whitelist Mods',
        'admin.modsEmpty': 'No whitelist mods',
        'admin.modsUploaded': 'Mod uploaded successfully!',
        'admin.modsDeleted': 'Mod deleted',
        'admin.usersSearch': 'User Management',
        'admin.usersSearchPlaceholder': 'Username...',
        'admin.search': 'Search',
        'admin.role': 'Role:',
        'admin.save': 'Save',
        'admin.roleUpdated': 'Role updated!',
        'admin.resetPass': 'Reset Password',
        'admin.passReset': 'Password reset! New password: {pass}',
        'admin.noUser': 'No user selected',
        'admin.passesList': 'Pass Management',
        'admin.passesEmpty': 'No passes',
        'admin.newsCreate': 'Create News',
        'admin.newsTitlePlaceholder': 'Title',
        'admin.newsBodyPlaceholder': 'Body (Markdown)',
        'admin.newsCreateBtn': 'Publish',
        'admin.newsManage': 'Manage News',
        'admin.newsCreated': 'News published!',
        'admin.newsDeleted': 'News deleted',
        'admin.newsEmpty': 'No news',
        'admin.packs': 'Packs',
        'admin.packsList': 'Pack Management',
        'admin.packsEmpty': 'No packs',
        'admin.packsToggle': 'Toggle',
        'admin.packsDisabled': 'Disabled',
        'admin.packsEnabled': 'Enabled',
        'admin.packsStatus': 'Status',
        'admin.delete': 'Delete',
        'admin.loading': 'Loading...',
        'admin.error': 'Error',
        'install.preset.label': 'Preset (optional)',
        'install.preset.default': 'Default',
        'pack.disabled': 'Disabled',
        'ctx.openFolder': 'Open in files',
        'ctx.settings': 'Settings',
        'ctx.delete': 'Delete',
        'ctx.deleteConfirm': 'Delete pack {name}? This will delete all files.',
        'ctx.openLogs': 'Open logs',
        'ctx.name': 'Name',
        'ctx.loader': 'Loader',
        'ctx.loaderVersion': 'Loader version',
        'ctx.save': 'Save',
        'ctx.cancel': 'Cancel',
        'ctx.filemanager': 'File manager',
        'onboard.title.install': 'Where to install packs',
        'onboard.desc.install': 'Click + in sidebar or Install pack — choose server or custom.',
        'onboard.title.pass': 'Where to activate pass',
        'onboard.desc.pass': 'Pass unlocks server packs. Activate in Settings → Activate pass.',
        'onboard.title.settings': 'Where settings are',
        'onboard.desc.settings': 'Gear at bottom — RAM, resolution, JVM, Java, language, network.',
        'onboard.title.manage': 'How to manage a pack',
        'onboard.desc.manage': 'Click to open card. Right-click for Open in files / Settings / Delete. Card has Play, Update, delete.',
        'onboard.title.mods': 'Mods in pack',
        'onboard.desc.mods': 'Server packs: toggle only additional mods. Custom: toggle/delete any mods, search on top.',
        'onboard.title.logs': 'Where game logs are',
        'onboard.desc.logs': 'Settings → Game Log — live output. Or right-click → Open in files → logs folder.',
        'onboard.next': 'Next',
        'onboard.back': 'Back',
        'onboard.skip': 'Skip',
        'onboard.done': 'Got it',
        'onboard.progress': 'Step {current} of {total}',
        'onboard.showAgain': 'Show tutorial again',
        'onboard.neverShow': "Don't show again",
        'onboard.confirmTitle': 'Skip tutorial?',
        'onboard.confirmDesc': 'Are you sure you want to skip the tutorial?',
        'onboard.yes': 'Yes',
        'onboard.no': 'No',
        'onboard.infoTitle': 'Tutorial',
        'onboard.infoDesc': 'You can replay the tutorial in Settings.',
    },
    ru: {
        'nav.packs': 'Сборки', 'nav.news': 'Новости', 'nav.settings': 'Настройки',
        'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
        'login.subtitle': 'С возвращением в ZernMC',
        'login.hint': 'Нет аккаунта?',
        'login.register': 'Регистрация',
        'login.registerTitle': 'Создать аккаунт',
        'login.registerSubtitle': 'Присоединяйтесь к ZernMC',
        'login.hasAccount': 'Уже есть аккаунт?',
        'login.confirm': 'Подтвердите пароль',
        'login.passMismatch': 'Пароли не совпадают',
        'login.passTooShort': 'Пароль должен быть минимум 6 символов',
        'login.signingIn': 'Вход...',
        'loading.text': 'Загрузка...',
        'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
        'pack.empty.title': 'Выберите сборку',
        'pack.empty.desc': 'Выберите сборку в боковой панели',
        'pack.description.loading': 'Загрузка описания...',
        'pack.noDescription': 'Описание отсутствует',
        'stat.loaderVer': 'Версия загрузчика', 'stat.files': 'Файлы', 'stat.size': 'Размер',
        'playBar.play': 'Играть', 'playBar.passRequired': 'Требуется проходка',
        'playBar.update': 'Обновить', 'playBar.updating': 'Обновление...',
        'settings.title': 'Настройки',
        'settings.activatePass.title': 'Активировать проходку',
        'settings.activatePass.desc': 'Введите код проходки для доступа к серверным сборкам',
        'settings.activatePass.placeholder': 'Код проходки',
        'settings.activatePass.button': 'Активировать',
        'settings.ram.title': 'Выделенная память',
        'settings.resolution.title': 'Разрешение экрана',
        'settings.resolution.desc': 'Ширина x Высота',
        'settings.jvmArgs.title': 'Дополнительные аргументы JVM',
        'settings.jvmArgs.desc': 'Дополнительные параметры Java VM',
        'settings.javaPath.title': 'Путь к Java',
        'settings.server.title': 'Сервер',
        'settings.server.checking': 'Проверка...',
        'settings.server.connected': 'Подключено',
        'settings.server.disconnected': 'Отключено',
        'settings.network.title': 'Сеть',
        'settings.network.desc': 'Режим подключения по сервисам (напрямую / через прокси)',
        'settings.network.direct': 'Напрямую',
        'settings.network.proxy': 'Прокси',
        'settings.network.down': 'Недоступен',
        'settings.network.loading': 'Проверка...',
        'settings.network.unavailable': 'Недоступно',
        'settings.language.title': 'Язык',
        'settings.language.desc': 'Язык интерфейса',
        'settings.systemJvm.title': 'Системная оптимизация JVM',
        'settings.systemJvm.desc': 'Автонастройка флагов JVM под вашу систему (экспериментально)',
        'settings.systemJvm.info': 'ЦП: {cores} ядер | ОЗУ: {ram} ГБ | {flags}',
        'settings.systemJvm.enabled': 'Включено', 'settings.systemJvm.disabled': 'Выключено',
        'settings.logViewer.title': 'Лог игры',
        'settings.logViewer.desc': 'Просмотр логов игры в реальном времени',
        'settings.logViewer.open': 'Открыть лог',
        'logViewer.title': 'Лог игры',
        'logViewer.noLogs': 'Логов пока нет. Запустите игру, чтобы увидеть логи.',
        'logViewer.copy': 'Копировать', 'logViewer.copied': 'Скопировано!',
        'logViewer.openFile': 'Открыть файл', 'logViewer.close': 'Закрыть',
        'logViewer.opened': 'Файл открыт',
        'logViewer.openFailed': 'Не удалось открыть файл логов',
        'logViewer.waiting': 'Ожидание вывода игры...',
        'install.title': 'Установить сборку',
        'install.tab.serverPack': 'Серверная сборка', 'install.tab.custom': 'Своя',
        'install.serverPack.label': 'Серверная сборка',
        'install.localName.label': 'Локальное имя',
        'install.downloadBtn': 'Скачать и установить',
        'install.custom.name': 'Название сборки',
        'install.custom.mcVersion': 'Версия Minecraft',
        'install.custom.loader': 'Загрузчик',
        'install.custom.loaderVersion': 'Версия загрузчика',
        'install.custom.unavailable': 'Пока недоступно',
        'install.custom.desc': 'Установка своей сборки отключена в этой версии. Используйте вкладку "Серверная сборка".',
        'install.progress.installing': 'Установка...',
        'install.enterName': 'Введите имя',
        'install.step': 'Шаг {current} из {total}: {name}',
        'select.loading': 'Загрузка...',
        'select.selectPack': 'Выберите сборку',
        'select.passRequired': 'Требуется проходка',
        'select.noPacks': 'Нет доступных сборок',
        'select.selectVersion': 'Выберите версию',
        'select.failedLoad': 'Ошибка загрузки',
        'select.selectMCFirst': 'Сначала выберите версию',
        'select.placeholder': 'Выберите...',
        'select.search': 'Поиск...',
        'packList.empty': 'Нет сборок',
        'news.title': 'Новости', 'news.empty': 'Новостей пока нет', 'news.loading': 'Загрузка новостей...',
        'pack.update': 'Обновить', 'pack.delete': 'Удалить',
        'badge.free': 'БЕСПЛАТНО', 'badge.pro': 'ПРО',
        'toast.welcomeBack': 'С возвращением, {username}!',
        'toast.welcome': 'Добро пожаловать, {username}!',
        'toast.accountCreated': 'Аккаунт создан! Добро пожаловать, {username}!',
        'toast.loggedOut': 'Вы вышли',
        'toast.enterCredentials': 'Введите логин и пароль',
        'toast.loginFailed': 'Ошибка входа',
        'login.errNetwork': 'Не удалось связаться с сервером. Проверьте подключение.',
        'login.errInvalidInput': 'Некорректные данные. Пароль должен быть минимум 6 символов.',
        'login.errTaken': 'Это имя пользователя уже занято.',
        'login.errBadCredentials': 'Неверный логин или пароль.',
        'login.errServer': 'Сервер вернул ошибку. Попробуйте позже.',
        'offline.title': 'Оффлайн-режим',
        'offline.subtitle': 'Сервер Zern недоступен. Вы можете запустить локально установленные сборки без аккаунта.',
        'offline.nickname': 'Никнейм',
        'offline.instance': 'Сборка',
        'offline.noInstances': 'Локально установленные сборки не найдены',
        'offline.launch': 'Запустить оффлайн',
        'offline.launching': 'Запуск...',
        'offline.needNickname': 'Введите никнейм',
        'offline.nickTooLong': 'Никнейм должен быть не длиннее 16 символов',
        'offline.needInstance': 'Выберите сборку',
        'offline.retry': 'Попробовать снова онлайн',
        'offline.exit': 'Выйти',
        'offline.stillOffline': 'Сервер всё ещё недоступен',
        'toast.launching': 'Запуск {name}...',
        'toast.launchFailed': 'Ошибка запуска',
        'toast.updated': '{name} обновлён до v{version}!',
        'toast.updateFailed': 'Ошибка обновления',
        'toast.noPackSelected': 'Сначала выберите сборку',
        'toast.launched': 'Запущено! PID: {pid}',
        'toast.installing': 'Установка...',
        'toast.installComplete': 'Установка завершена!',
        'toast.installFailed': 'Ошибка установки',
        'toast.enterPassCode': 'Введите код проходки',
        'toast.passActivated': 'Проходка активирована!',
        'toast.activationFailed': 'Ошибка активации',
        'error.network': 'Ошибка сети',
        'pack.modsCount': '{count} модов',
        'pack.worldsCount': '{count} миров',
        'pack.logsCount': '{count} логов',
        'locale.en': 'Английский', 'locale.ru': 'Русский',
        'ram.gb': '{value} ГБ',
        'ram.mbAllocated': '{value} МБ выделено',
        'nav.friends': 'Друзья',
        'friends.title': 'Друзья',
        'friends.add': 'Добавить',
        'friends.addTitle': 'Добавить друга',
        'friends.addLabel': 'Имя пользователя',
        'friends.addPlaceholder': 'Введите имя...',
        'friends.search': 'Поиск друзей...',
        'friends.empty': 'Пока нет друзей',
        'friends.online': 'В сети',
        'friends.offline': 'Не в сети',
        'friends.playing': 'Играет в {pack}',
        'friends.noPack': 'В простое',
        'friends.requests': 'Заявки в друзья',
        'friends.requestFrom': '{user} хочет добавить вас в друзья',
        'friends.accept': 'Принять',
        'friends.decline': 'Отклонить',
        'friends.remove': 'Удалить',
        'friends.addSuccess': 'Заявка отправлена!',
        'friends.addError': 'Не удалось добавить друга',
        'friends.removeSuccess': 'Друг удалён',
        'friends.acceptSuccess': 'Заявка принята',
        'friends.passActive': 'Проходка активна',
        'friends.passInactive': 'Нет проходки',
        'playtime.label': 'Время игры',
        'playtime.hours': '{h}ч {m}м',
        'playtime.total': 'Всего: {h}ч',
        'playtime.syncing': 'Синхронизация...',
        'whitelist.title': 'Дополнительные моды',
        'whitelist.loading': 'Загрузка...',
        'whitelist.empty': 'Нет дополнительных модов',
        'whitelist.error': 'Ошибка загрузки модов',
        'whitelist.install': 'Установить выбранные',
        'whitelist.installing': 'Установка...',
        'whitelist.installed': 'Установлено',
        'whitelist.done': 'Моды установлены!',
        'whitelist.selectNone': 'Выберите моды',
        'whitelist.installError': 'Ошибка установки мода',
        'mods.title': 'Моды',
        'mods.loading': 'Загрузка модов...',
        'mods.empty': 'Модов не установлено',
        'mods.search': 'Поиск модов...',
        'mods.enabled': 'Включен',
        'mods.disabled': 'Отключен',
        'mods.delete': 'Удалить',
        'mods.enable': 'Включить',
        'mods.disable': 'Выключить',
        'mods.noResults': 'Ничего не найдено',
        'mods.deleteConfirm': 'Удалить {name}?',
        'mods.count': '{count} модов',
        'nav.admin': 'Админка',
        'admin.title': 'Панель администратора',
        'admin.clients': 'Клиенты',
        'admin.mods': 'Моды',
        'admin.users': 'Пользователи',
        'admin.passes': 'Проходки',
        'admin.news': 'Новости',
        'admin.clientsOnline': 'Онлайн клиенты',
        'admin.noClients': 'Нет подключенных клиентов',
        'admin.modsUpload': 'Загрузить мод',
        'admin.modsUploadBtn': 'Загрузить',
        'admin.modsList': 'Вайтлист моды',
        'admin.modsEmpty': 'Нет модов в вайтлисте',
        'admin.modsUploaded': 'Мод успешно загружен!',
        'admin.modsDeleted': 'Мод удалён',
        'admin.usersSearch': 'Управление пользователями',
        'admin.usersSearchPlaceholder': 'Имя пользователя...',
        'admin.search': 'Поиск',
        'admin.role': 'Роль:',
        'admin.save': 'Сохранить',
        'admin.roleUpdated': 'Роль обновлена!',
        'admin.resetPass': 'Сбросить пароль',
        'admin.passReset': 'Пароль сброшен! Новый пароль: {pass}',
        'admin.noUser': 'Пользователь не выбран',
        'admin.passesList': 'Управление проходками',
        'admin.passesEmpty': 'Нет проходок',
        'admin.newsCreate': 'Создать новость',
        'admin.newsTitlePlaceholder': 'Заголовок',
        'admin.newsBodyPlaceholder': 'Текст (Markdown)',
        'admin.newsCreateBtn': 'Опубликовать',
        'admin.newsManage': 'Управление новостями',
        'admin.newsCreated': 'Новость опубликована!',
        'admin.newsDeleted': 'Новость удалена',
        'admin.newsEmpty': 'Нет новостей',
        'admin.packs': 'Сборки',
        'admin.packsList': 'Управление сборками',
        'admin.packsEmpty': 'Нет сборок',
        'admin.packsToggle': 'Переключить',
        'admin.packsDisabled': 'Отключена',
        'admin.packsEnabled': 'Включена',
        'admin.packsStatus': 'Статус',
        'admin.delete': 'Удалить',
        'admin.loading': 'Загрузка...',
        'admin.error': 'Ошибка',
        'install.preset.label': 'Пресет (опционально)',
        'install.preset.default': 'Стандартный',
        'pack.disabled': 'Отключена',
        'ctx.openFolder': 'Открыть в файлах',
        'ctx.settings': 'Настройки',
        'ctx.delete': 'Удалить',
        'ctx.deleteConfirm': 'Удалить сборку {name}? Это удалит все файлы.',
        'ctx.openLogs': 'Открыть логи',
        'ctx.name': 'Название',
        'ctx.loader': 'Загрузчик',
        'ctx.loaderVersion': 'Версия загрузчика',
        'ctx.save': 'Сохранить',
        'ctx.cancel': 'Отмена',
        'ctx.filemanager': 'Файловый менеджер',
        'onboard.title.install': 'Где устанавливать сборки',
        'onboard.desc.install': 'Нажми + в боковой панели или кнопку «Установить сборку» — выбери серверную или свою кастомную.',
        'onboard.title.pass': 'Где активировать проходку',
        'onboard.desc.pass': 'Проходка даёт доступ к серверным сборкам. Активируй её в Настройках → Активировать проходку или через серверные сборки.',
        'onboard.title.settings': 'Где настройки',
        'onboard.desc.settings': 'Шестерёнка внизу — память, разрешение, JVM, Java, язык, сеть. Настрой под своё железо.',
        'onboard.title.manage': 'Как управлять сборкой',
        'onboard.desc.manage': 'Клик — открыть карточку. ПКМ — открыть в файлах / настройки / удалить. В карточке — Играть, Обновить, удалить, описание.',
        'onboard.title.mods': 'Моды в сборке',
        'onboard.desc.mods': 'В серверных — управляй только дополнительными модами. В своих — включай/выключай и удаляй любые моды, поиск сверху.',
        'onboard.title.logs': 'Где логи игры',
        'onboard.desc.logs': 'Настройки → Лог игры — смотри вывод в реальном времени. Или ПКМ → Открыть в файлах → папка logs.',
        'onboard.next': 'Далее',
        'onboard.back': 'Назад',
        'onboard.skip': 'Пропустить',
        'onboard.done': 'Понятно',
        'onboard.progress': 'Шаг {current} из {total}',
        'onboard.showAgain': 'Показать обучение снова',
        'onboard.neverShow': 'Не показывать снова',
        'onboard.confirmTitle': 'Точно пропустить туториал?',
        'onboard.confirmDesc': 'Вы уверены, что хотите пропустить обучение?',
        'onboard.yes': 'Да',
        'onboard.no': 'Нет',
        'onboard.infoTitle': 'Туториал',
        'onboard.infoDesc': 'Туториал можно будет пройти в настройках.',
    }
};

let _localeData = LOCALES.en;
let _currentLocale = 'en';

function t(key, fallback) {
    return _localeData[key] || fallback || key;
}

function tr(key, fallback, replacements) {
    let s = t(key, fallback);
    if (replacements) {
        for (const [k, v] of Object.entries(replacements)) {
            s = s.replace('{' + k + '}', v);
        }
    }
    return s;
}

function applyLocale() {
    document.querySelectorAll('[data-i18n]').forEach(function(el) {
        var key = el.getAttribute('data-i18n');
        var fallback = el.textContent;
        el.textContent = t(key, fallback);
    });
    document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
        var key = el.getAttribute('data-i18n-placeholder');
        el.setAttribute('placeholder', t(key, el.getAttribute('placeholder')));
    });
}

class ZernMCLauncher {
    constructor() {
        this.state = { account: null, instances: [], selectedPack: null, installing: false, serverPacks: [], friends: [], friendRequests: [], playtimeStats: null };
        this.toastTimer = null;
        this.progressPoller = null;
        this._friendsPollInterval = null;
        this._playtimeInterval = null;
        this._playtimeAccumulated = 0;
        this._playtimeStartTime = null;
        this._currentPlaytimePack = null;
        this._openSection = undefined;
        this.init();
    }

    async init() {
        this.bindEvents();
        this.initBg();
        await this.checkAuth();
    }

    setLocale(lang) {
        _currentLocale = lang;
        _localeData = LOCALES[lang] || LOCALES.en;
        applyLocale();
        var sel = document.getElementById('locale-select');
        if (sel) {
            sel.value = lang;
            if (sel.dataset.enhanced) this.refreshCustomSelect(sel);
        }
    }

    // ==================== BACKGROUND — hollow diamonds ====================
    initBg() {
        const c = document.getElementById('bg-canvas');
        const ctx = c.getContext('2d');
        const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
        let mode = 'login'; // login=blue, register=orange
        let modeLerp = 0; // 0=login, 1=register
        let exitProgress = 0; // 0..1 fly-out
        let exitActive = false;
        let rafId = 0;
        const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

        const PALETTES = {
            login: {
                far:  { r: 59,  g: 130, b: 246, a: 0.18 },
                mid:  { r: 59,  g: 130, b: 246, a: 0.32 },
                near: { r: 96,  g: 165, b: 255, a: 0.42 }
            },
            register: {
                far:  { r: 249, g: 115, b: 22,  a: 0.18 },
                mid:  { r: 249, g: 115, b: 22,  a: 0.34 },
                near: { r: 255, g: 162, b: 77,  a: 0.44 }
            }
        };

        const diamonds = [];
        const COUNT = 44;
        const seed = () => Math.random();
        const lerp = (a,b,t) => a + (b-a)*t;
        const lerpColor = (ca, cb, t) => ({
            r: lerp(ca.r, cb.r, t),
            g: lerp(ca.g, cb.g, t),
            b: lerp(ca.b, cb.b, t),
            a: lerp(ca.a, cb.a, t)
        });

        const rebuild = () => {
            diamonds.length = 0;
            const w = c.width / dpr, h = c.height / dpr;
            // distribute with jitter, keep margins so diamonds not clipped
            for (let i = 0; i < COUNT; i++) {
                const layerRoll = seed();
                let layer, size;
                if (layerRoll < 0.35) { layer = 'far'; size = 12 + seed()*6; }
                else if (layerRoll < 0.70) { layer = 'mid'; size = 22 + seed()*10; }
                else { layer = 'near'; size = 36 + seed()*16; }
                // avoid center where login form sits (roughly 360x420 centered)
                let x, y, attempts = 0;
                do {
                    x = seed() * (w - size*1.4) + size*0.7;
                    y = seed() * (h - size*1.4) + size*0.7;
                    attempts++;
                } while (attempts < 5 && Math.abs(x - w/2) < 190 && Math.abs(y - h/2) < 220);
                diamonds.push({
                    x, y, ox: x, oy: y,
                    size,
                    layer,
                    rot: 45 * Math.PI/180,
                    driftX: (seed()-0.5)*0.6,
                    driftY: 0.15 + seed()*0.45,
                    phase: seed()*Math.PI*2,
                    depthMul: layer==='far'?0.15 : layer==='mid'?0.45 : 1.0
                });
            }
        };

        const resize = () => {
            const w = window.innerWidth, h = window.innerHeight;
            c.width = w * dpr; c.height = h * dpr;
            c.style.width = w + 'px'; c.style.height = h + 'px';
            ctx.setTransform(dpr,0,0,dpr,0,0);
            rebuild();
        };
        window.addEventListener('resize', resize);

        // public API
        this.setDiamondMode = (m) => { mode = m==='register'?'register':'login'; };
        // keep compat
        this.setWaveMode = this.setDiamondMode;

        this.animateDiamondExit = () => {
            if (reduceMotion || exitActive) return Promise.resolve();
            exitActive = true;
            exitProgress = 0;
            return new Promise(res => {
                const start = performance.now();
                const dur = 900;
                const ease = t => t<0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2; // easeInOutCubic
                const step = (now) => {
                    const p = Math.min(1, (now - start)/dur);
                    exitProgress = ease(p);
                    if (p < 1) requestAnimationFrame(step);
                    else { exitActive = false; res(); }
                };
                requestAnimationFrame(step);
            });
        };

        let tick = 0;
        const draw = () => {
            const w = c.width / dpr, h = c.height / dpr;
            ctx.clearRect(0, 0, w, h);
            // lerp palette between modes
            const target = mode === 'register' ? 1 : 0;
            modeLerp += (target - modeLerp) * 0.06;
            const palFar  = lerpColor(PALETTES.login.far,  PALETTES.register.far,  modeLerp);
            const palMid  = lerpColor(PALETTES.login.mid,  PALETTES.register.mid,  modeLerp);
            const palNear = lerpColor(PALETTES.login.near, PALETTES.register.near, modeLerp);

            // exit parallax offset (vh in px)
            const farExit  = reduceMotion ? 0 : exitProgress * (h*0.40);
            const midExit  = reduceMotion ? 0 : exitProgress * (h*0.70);
            const nearExit = reduceMotion ? 0 : exitProgress * (h*1.10);

            for (const d of diamonds) {
                const pal = d.layer==='far'? palFar : d.layer==='mid'? palMid : palNear;
                // idle drift
                const t = tick * 0.001 + d.phase;
                const dx = Math.sin(t * 0.6 + d.ox*0.01) * d.driftX;
                const dyBase = reduceMotion ? 0 : (tick * 0.015 * d.depthMul * d.driftY);
                // exit offset + breathing scale
                const exitOff = d.layer==='far'? farExit : d.layer==='mid'? midExit : nearExit;
                const breath = reduceMotion ? 1 : 1 + Math.sin(t*0.8)*0.03;
                const x = d.ox + dx;
                let y = (d.oy + dyBase - exitOff) % (h + d.size*2);
                if (y < -d.size) y += h + d.size*2;
                const s = d.size * breath * (exitActive && d.layer==='near' ? (1 + exitProgress*0.06) : 1);
                const alpha = pal.a * (exitActive ? (1 - exitProgress*0.7) : 1);
                if (alpha < 0.02) continue;
                ctx.save();
                ctx.translate(x, y);
                ctx.rotate(d.rot);
                ctx.strokeStyle = `rgba(${Math.round(pal.r)},${Math.round(pal.g)},${Math.round(pal.b)},${alpha})`;
                ctx.lineWidth = d.layer==='far'? 1 : d.layer==='mid'? 1.1 : 1.4;
                // hollow diamond: centered square
                ctx.strokeRect(-s/2, -s/2, s, s);
                ctx.restore();
            }

            tick += 16;
            rafId = requestAnimationFrame(draw);
        };

        // pause when hidden
        document.addEventListener('visibilitychange', () => {
            if (document.hidden) cancelAnimationFrame(rafId);
            else draw();
        });

        resize();
        draw();
    }

    // ==================== API ====================
    async req(endpoint, opts = {}) {
        try {
            var headers = { 'Content-Type': 'application/json' };
            if (opts.headers) {
                for (var k in opts.headers) headers[k] = opts.headers[k];
            }
            if (typeof fetch !== 'undefined') {
                var controller = new AbortController();
                var timeoutMs = opts.timeout || 15000;
                var timeoutId = setTimeout(function() { controller.abort(); }, timeoutMs);
                try {
                    var r = await fetch(API + endpoint, {
                        method: opts.method || 'GET',
                        headers: headers,
                        body: opts.body || null,
                        signal: controller.signal
                    });
                    clearTimeout(timeoutId);
                    return await r.json();
                } catch (fetchErr) {
                    clearTimeout(timeoutId);
                    return { success: false, error: fetchErr.message || 'Request failed' };
                }
            }
            return await new Promise(function(resolve, reject) {
                var xhr = new XMLHttpRequest();
                xhr.open(opts.method || 'GET', API + endpoint, true);
                for (var k in headers) xhr.setRequestHeader(k, headers[k]);
                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4) {
                        try { resolve(JSON.parse(xhr.responseText)); }
                        catch (e) { resolve({ success: false, error: 'Invalid JSON response' }); }
                    }
                };
                xhr.onerror = function() { resolve({ success: false, error: 'Network error' }); };
                xhr.ontimeout = function() { resolve({ success: false, error: 'Request timeout' }); };
                xhr.timeout = opts.timeout || 30000;
                xhr.send(opts.body || null);
            });
        } catch (e) {
            return { success: false, error: e.message };
        }
    }

    // ==================== AUTH ====================
    async checkAuth() {
        this.showLoading(true);
        const status = await this.req('/offline/status');
        if (status.success && status.offline) {
            this.showOffline();
            this.showLoading(false);
            return;
        }
        const auto = await this.req('/auto-login');
        if (auto.success && auto.autoLogin) {
            this.state.account = auto.data;
            this.enterMain();
            this.toast(tr('toast.welcomeBack', null, {username: auto.data.username}), 'success');
        } else {
            const acct = await this.req('/account');
            if (acct.success) {
                this.state.account = acct.data;
                this.enterMain();
            } else {
                this.showLogin();
            }
        }
        this.showLoading(false);
    }

    async handleLogin(e) {
        e.preventDefault();
        const username = document.getElementById('username').value.trim();
        const password = document.getElementById('password').value;
        const errEl = document.getElementById('login-error');
        const btn = document.getElementById('login-btn');
        const label = document.getElementById('login-btn-label');
        const spinner = btn.querySelector('.spinner');

        if (!username || !password) { this.showLoginError(t('toast.enterCredentials')); return; }

        if (this._registerMode) {
            const confirm = document.getElementById('confirm-password').value;
            if (password !== confirm) { this.showLoginError(t('login.passMismatch')); return; }
            if (password.length < 6) { this.showLoginError(t('login.passTooShort')); return; }
        }

        btn.disabled = true;
        label.textContent = t('login.signingIn');
        spinner.classList.remove('hidden');

        const endpoint = this._registerMode ? '/register' : '/login';
        const r = await this.req(endpoint, { method: 'POST', body: JSON.stringify({ username, password }) });

        btn.disabled = false;
        label.textContent = this._registerMode ? t('login.register') : t('login.title');
        spinner.classList.add('hidden');

        if (r.success) {
            this.state.account = r.data;
            // hollow diamond exit — parallax fly-out
            if (this.animateDiamondExit) await this.animateDiamondExit();
            const loginScreen = document.getElementById('login-screen');
            const loginContainer = document.getElementById('login-container');
            if (loginContainer && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
                loginContainer.style.transition = 'transform 0.9s cubic-bezier(0.16,1,0.3,1), opacity 0.9s ease, filter 0.9s ease';
                loginContainer.style.transform = 'translateY(-110vh) scale(0.92)';
                loginContainer.style.opacity = '0';
                loginContainer.style.filter = 'blur(6px)';
                await new Promise(res => setTimeout(res, 900));
                loginContainer.style.transition = '';
                loginContainer.style.transform = '';
                loginContainer.style.opacity = '';
                loginContainer.style.filter = '';
            }
            if (loginScreen) loginScreen.classList.add('hidden');
            this.enterMain();
            const key = this._registerMode ? 'toast.accountCreated' : 'toast.welcome';
            this.toast(tr(key, null, {username: r.data.username}), 'success');
        } else {
            if (!this._registerMode && r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) {
                var reg = await this.req('/register', { method: 'POST', body: JSON.stringify({ username, password }) });
                if (reg.success) {
                    this.state.account = reg.data;
                    if (this.animateDiamondExit) await this.animateDiamondExit();
                    const loginScreen2 = document.getElementById('login-screen');
                    const loginContainer2 = document.getElementById('login-container');
                    if (loginContainer2 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
                        loginContainer2.style.transition = 'transform 0.9s cubic-bezier(0.16,1,0.3,1), opacity 0.9s ease, filter 0.9s ease';
                        loginContainer2.style.transform = 'translateY(-110vh) scale(0.92)';
                        loginContainer2.style.opacity = '0';
                        loginContainer2.style.filter = 'blur(6px)';
                        await new Promise(res => setTimeout(res, 900));
                        loginContainer2.style.transition = '';
                        loginContainer2.style.transform = '';
                        loginContainer2.style.opacity = '';
                        loginContainer2.style.filter = '';
                    }
                    if (loginScreen2) loginScreen2.classList.add('hidden');
                    this.enterMain();
                    this.toast(tr('toast.accountCreated', null, {username: reg.data.username}), 'success');
                    return;
                }
            }
            this.showLoginError(this.friendlyError(r.error) || r.error || t('toast.loginFailed'));
        }
    }

    friendlyError(err) {
        if (!err) return null;
        const s = String(err);
        if (/failed to fetch|fetch failed|networkerror|ENOTFOUND|ECONNREFUSED|ECONNRESET|timeout|aborted/i.test(s)) {
            return t('login.errNetwork');
        }
        if (s.includes('HTTP 422') || /at least 6|string should have/i.test(s)) {
            return t('login.errInvalidInput');
        }
        if (s.includes('HTTP 409') || /already taken/i.test(s)) {
            return t('login.errTaken');
        }
        if (s.includes('HTTP 401') || /invalid.*(login|password)|wrong.*(login|password)/i.test(s)) {
            return t('login.errBadCredentials');
        }
        if (/HTTP \d{3}/.test(s)) {
            return t('login.errServer');
        }
        return null;
    }

    validatePassword() {
        if (!this._registerMode) return;
        const password = document.getElementById('password');
        const hint = document.getElementById('password-hint');
        const btn = document.getElementById('login-btn');
        const tooShort = password.value.length < 6;
        if (tooShort) {
            password.classList.add('invalid');
            hint.classList.remove('hidden');
        } else {
            password.classList.remove('invalid');
            hint.classList.add('hidden');
        }
        if (btn) btn.disabled = tooShort;
        const errEl = document.getElementById('login-error');
        if (errEl && !errEl.classList.contains('hidden')) {
            errEl.classList.add('hidden');
        }
    }

    toggleMode() {
        this._registerMode = !this._registerMode;
        const isReg = this._registerMode;
        const container = document.getElementById('login-container');
        const title = document.getElementById('login-title');
        const subtitle = document.getElementById('login-subtitle');
        const btnLabel = document.getElementById('login-btn-label');
        const footerHint = document.getElementById('footer-hint');
        const footerAction = document.getElementById('footer-action');
        const confirmField = document.getElementById('confirm-field');

        container.style.transform = 'translateY(-4px)';
        container.style.opacity = '0.6';

        setTimeout(() => {
            title.textContent = isReg ? t('login.registerTitle') : t('login.title');
            subtitle.textContent = isReg ? t('login.registerSubtitle') : t('login.subtitle');
            btnLabel.textContent = isReg ? t('login.register') : t('login.title');
            footerHint.textContent = isReg ? t('login.hasAccount') : t('login.hint');
            footerAction.textContent = isReg ? t('login.title') : t('login.register');

            if (isReg) {
                confirmField.classList.remove('hidden');
                document.getElementById('confirm-password').required = true;
                this.validatePassword();
            } else {
                confirmField.classList.add('hidden');
                document.getElementById('confirm-password').required = false;
                document.getElementById('confirm-password').value = '';
                document.getElementById('login-btn').disabled = false;
                document.getElementById('password').classList.remove('invalid');
                document.getElementById('password-hint').classList.add('hidden');
            }

            document.getElementById('login-error').classList.add('hidden');

            container.style.transform = '';
            container.style.opacity = '';

            if (this.setDiamondMode) this.setDiamondMode(isReg ? 'register' : 'login');
            else if (this.setWaveMode) this.setWaveMode(isReg ? 'register' : 'login');
        }, 150);
    }

    showLoginError(msg) {
        const el = document.getElementById('login-error');
        el.textContent = msg;
        el.classList.remove('hidden');
    }

    showLogin() {
        const c = document.getElementById('login-container');
        if (c) { c.style.transition=''; c.style.transform=''; c.style.opacity=''; c.style.filter=''; }
        document.getElementById('login-screen').classList.remove('hidden');
        document.getElementById('main-screen').classList.add('hidden');
    }

    async logout() {
        this.state.selectedPack = null;
        this.state.instances = [];
        this.state.account = null;
        this.toast(t('toast.loggedOut'));
        document.getElementById('login-screen').classList.remove('hidden');
        document.getElementById('main-screen').classList.add('hidden');
    }

    enterMain() {
        document.getElementById('login-screen').classList.add('hidden');
        document.getElementById('main-screen').classList.remove('hidden');
        const a = this.state.account;
        const avatar = document.getElementById('user-avatar');
        avatar.textContent = (a.username || 'Z')[0].toUpperCase();
        document.getElementById('username-display').textContent = a.username;
        this.updateBadge();
        document.getElementById('header-version').textContent = document.getElementById('version').textContent;
        this.switchView('packs');
        this.loadInstances();
        this.loadSettings();
        this.loadServerPacksList();
        this.loadNews();
        this.loadFriends();
        this.loadPlaytimeStats();
        this.startFriendStatusHeartbeat();
        this.enhanceSelects();
        this.updateAdminNav();
        setTimeout(()=> this.checkOnboarding(), 600);
    }

    async loadServerPacksList() {
        const r = await this.req('/packs');
        if (r.success && r.data) {
            this.state.serverPacks = r.data;
            this.state.serverPacksMap = {};
            r.data.forEach(function(p) {
                app.state.serverPacksMap[p.name] = p;
            });
        }
    }

    // ==================== OFFLINE MODE ====================
    showOffline() {
        document.getElementById('login-screen').classList.add('hidden');
        document.getElementById('main-screen').classList.add('hidden');
        document.getElementById('offline-screen').classList.remove('hidden');
        this.loadOfflineInstances();
    }

    async loadOfflineInstances() {
        const select = document.getElementById('offline-instance');
        if (!select) return;
        select.innerHTML = '';
        const r = await this.req('/instances');
        let instances = (r.success && r.data) ? r.data : [];
        instances = instances.filter(function(inst) { return inst.name; });
        if (instances.length === 0) {
            const opt = document.createElement('option');
            opt.textContent = t('offline.noInstances');
            opt.value = '';
            select.appendChild(opt);
            return;
        }
        instances.forEach(function(inst) {
            const opt = document.createElement('option');
            opt.value = inst.name;
            let label = inst.name;
            if (inst.minecraftVersion) label += ' (' + inst.minecraftVersion + ')';
            opt.textContent = label;
            select.appendChild(opt);
        });
        this.state.offlineInstances = instances;
    }

    async handleOfflineLaunch(e) {
        e.preventDefault();
        const nickname = document.getElementById('offline-nickname').value.trim();
        const name = document.getElementById('offline-instance').value;
        const errEl = document.getElementById('offline-error');
        const btn = document.getElementById('offline-launch-btn');
        const label = btn.querySelector('.btn-label');
        const spinner = btn.querySelector('.spinner');

        errEl.classList.add('hidden');
        if (!nickname) {
            errEl.textContent = t('offline.needNickname');
            errEl.classList.remove('hidden');
            return;
        }
        if (nickname.length > 16) {
            errEl.textContent = t('offline.nickTooLong');
            errEl.classList.remove('hidden');
            return;
        }
        if (!name) {
            errEl.textContent = t('offline.needInstance');
            errEl.classList.remove('hidden');
            return;
        }

        btn.disabled = true;
        label.textContent = t('offline.launching');
        spinner.classList.remove('hidden');

        const r = await this.req('/offline-launch', {
            method: 'POST',
            body: JSON.stringify({ name: name, nickname: nickname }),
            timeout: 120000
        });

        btn.disabled = false;
        label.textContent = t('offline.launch');
        spinner.classList.add('hidden');

        if (r.success) {
            this.toast(tr('toast.launched', null, {pid: String(r.data?.pid || '')}), 'success');
        } else {
            errEl.textContent = r.error || t('toast.launchFailed');
            errEl.classList.remove('hidden');
        }
    }

    async retryOnline() {
        this.showLoading(true);
        document.getElementById('offline-screen').classList.add('hidden');
        const status = await this.req('/offline/status');
        if (status.success && status.offline) {
            this.showOffline();
            this.showLoading(false);
            this.toast(t('offline.stillOffline'), 'warning');
            return;
        }
        await this.checkAuth();
    }

    // ==================== NAV ====================
    bindEvents() {
        document.getElementById('login-form').addEventListener('submit', e => this.handleLogin(e));
        const offlineForm = document.getElementById('offline-form');
        if (offlineForm) offlineForm.addEventListener('submit', e => this.handleOfflineLaunch(e));
        const retryBtn = document.getElementById('offline-retry-btn');
        if (retryBtn) retryBtn.addEventListener('click', () => this.retryOnline());
        const exitBtn = document.getElementById('offline-exit-btn');
        if (exitBtn) exitBtn.addEventListener('click', () => this.req('/shutdown', { method: 'POST' }));
        const pwInput = document.getElementById('password');
        if (pwInput) pwInput.addEventListener('input', () => this.validatePassword());
        const confirmInput = document.getElementById('confirm-password');
        if (confirmInput) confirmInput.addEventListener('input', () => this.validatePassword());
        document.getElementById('login-footer').addEventListener('click', () => this.toggleMode());
        document.getElementById('logout-btn').addEventListener('click', () => this.logout());
        document.getElementById('settings-btn').addEventListener('click', () => this.switchView('settings'));

        document.querySelectorAll('.nav-btn').forEach(btn => {
            btn.addEventListener('click', () => this.switchView(btn.dataset.view));
        });

        document.getElementById('add-pack-btn').addEventListener('click', () => this.showInstallModal());
        document.getElementById('add-server-pack-btn').addEventListener('click', () => this.showInstallModal());

        // Accordion sidebar sections: max one pack list open at a time
        const serverHeader = document.getElementById('server-packs-header');
        const localHeader = document.getElementById('local-packs-header');
        [serverHeader, localHeader].forEach(header => {
            if (!header) return;
            header.addEventListener('click', (e) => {
                if (e.target.closest('.btn-icon')) return;
                const name = header.id === 'server-packs-header' ? 'server' : 'local';
                this._openSection = this._openSection === name ? null : name;
                this.syncOpenSections();
            });
        });

        document.getElementById('close-modal-btn').addEventListener('click', () => this.hideInstallModal());
        document.querySelectorAll('.modal-tab').forEach(t => {
            t.addEventListener('click', () => this.switchInstallTab(t.dataset.tab));
        });
        var loaderSel = document.getElementById('loader-select');
        if (loaderSel) loaderSel.addEventListener('change', e => this.onLoaderChange(e.target.value));
        document.getElementById('install-zernmc-btn').addEventListener('click', () => this.installZernMCPack());
        var customBtn = document.getElementById('install-custom-btn');
        if (customBtn) customBtn.addEventListener('click', () => this.installCustom());
        document.getElementById('play-btn').addEventListener('click', () => this.launchPack());
        document.getElementById('ram-slider').addEventListener('input', e => {
            var val = e.target.value;
            document.getElementById('ram-value').textContent = tr('ram.gb', null, {value: (val / 1024).toFixed(1)});
            document.getElementById('ram-info').textContent = tr('ram.mbAllocated', null, {value: val});
        });
        document.getElementById('ram-slider').addEventListener('change', e => this.saveSettings());
        document.getElementById('activate-pass-btn').addEventListener('click', () => this.activatePass());

        let saveTimer;
        const debouncedSave = () => { clearTimeout(saveTimer); saveTimer = setTimeout(() => this.saveSettings(), 500); };
        document.getElementById('win-width').addEventListener('input', debouncedSave);
        document.getElementById('win-height').addEventListener('input', debouncedSave);
        document.getElementById('jvm-args').addEventListener('input', debouncedSave);
        document.getElementById('java-path-input').addEventListener('input', debouncedSave);

        var logViewerBtn = document.getElementById('show-log-viewer-btn');
        if (logViewerBtn) logViewerBtn.addEventListener('click', () => this.openLogViewer());

        document.querySelectorAll('.modal-backdrop').forEach(m => {
            m.addEventListener('click', e => { if (e.target === m) this.hideInstallModal(); });
        });

        var afModal = document.getElementById('add-friend-modal');
        if (afModal) {
            afModal.addEventListener('click', function(e) { if (e.target === afModal) app.closeAddFriend(); });
        }
        var afInput = document.getElementById('add-friend-input');
        if (afInput) {
            afInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') app.submitAddFriend(); });
        }

        document.querySelectorAll('.admin-tab').forEach(function(t) {
            t.addEventListener('click', function() { app.switchAdminTab(t.dataset.atab); });
        });

        var localeSelect = document.getElementById('locale-select');
        if (localeSelect) {
            localeSelect.addEventListener('change', async () => {
                var lang = localeSelect.value;
                this.setLocale(lang);
                await this.saveSettings();
            });
        }

        var sysJvmToggle = document.getElementById('system-jvm-toggle');
        if (sysJvmToggle) {
            sysJvmToggle.addEventListener('change', () => {
                this.saveSettings();
                var data = { systemBasedJvm: sysJvmToggle.checked, cpuCores: null, totalRamMB: null };
                var infoEl = document.getElementById('system-jvm-info');
                if (infoEl && this.state._sysInfo) {
                    data.cpuCores = this.state._sysInfo.cpuCores;
                    data.totalRamMB = this.state._sysInfo.totalRamMB;
                }
                this.updateSystemJvmInfo(data);
            });
        }
    }

    switchView(view) {
        document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
        document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
        if (view === 'settings') {
            this.loadNetworkStatus();
            if (!this.networkPoller) {
                this.networkPoller = setInterval(() => this.loadNetworkStatus(), 15000);
            }
        } else if (this.networkPoller) {
            clearInterval(this.networkPoller);
            this.networkPoller = null;
        }
    }

    async loadNetworkStatus() {
        const list = document.getElementById('network-list');
        if (!list) return;
        try {
            const r = await this.req('/network/status');
            if (!r.success || !r.data) return;
            const services = r.data.services || [];
            if (!services.length) {
                list.innerHTML = '<span class="setting-badge">' + t('settings.network.unavailable') + '</span>';
                return;
            }
            let html = '';
            services.forEach(function(s) {
                const state = s.proxy ? 'proxy' : (s.healthy ? 'direct' : 'down');
                const labels = { direct: t('settings.network.direct'), proxy: t('settings.network.proxy'), down: t('settings.network.down') };
                const colors = { direct: 'var(--success)', proxy: 'var(--warning)', down: 'var(--error)' };
                html += '<div class="network-row">'
                    + '<span class="network-name">' + s.name.toLowerCase() + '</span>'
                    + '<span class="network-state" style="color:' + colors[state] + '">' + labels[state] + '</span>'
                    + '</div>';
            });
            list.innerHTML = html;
        } catch (e) {}
    }

    // ==================== INSTANCES ====================
    async loadInstances() {
        const r = await this.req('/instances');
        if (r.success && r.data) {
            this.state.instances = r.data;
            this.state.updateMap = {};
            await this.checkUpdatesForServerPacks();
            this.renderSidebar();
        }
    }

    async checkUpdatesForServerPacks() {
        if (!this.state.instances) return;
        for (const inst of this.state.instances) {
            if (!inst.isServerPack) continue;
            // Skip disabled packs
            if (this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled) {
                continue;
            }
            const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
            if (r.success && r.data) {
                this.state.updateMap[inst.name] = r.data.hasUpdate;
            }
        }
    }

    async checkUpdateForPack(inst) {
        if (!inst || !inst.isServerPack) return;
        // Skip disabled packs
        if (this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled) {
            return;
        }
        const r = await this.req('/check-updates?name=' + encodeURIComponent(inst.name));
        if (r.success && r.data) {
            if (!this.state.updateMap) this.state.updateMap = {};
            this.state.updateMap[inst.name] = r.data.hasUpdate;
        }
    }

    hasUpdate(inst) {
        return inst && inst.isServerPack && this.state.updateMap && this.state.updateMap[inst.name] === true;
    }

    syncOpenSections() {
        const serverSection = document.getElementById('server-packs-section');
        const localSection = document.getElementById('local-packs-section');
        if (serverSection) serverSection.classList.toggle('collapsed', this._openSection !== 'server');
        if (localSection) localSection.classList.toggle('collapsed', this._openSection !== 'local');
    }

    renderSidebar() {
        const serverList = document.getElementById('server-packs-list');
        const localList = document.getElementById('local-packs-list');
        const localSection = document.getElementById('local-packs-section');
        serverList.innerHTML = '';
        localList.innerHTML = '';
        if (localSection) localSection.classList.add('hidden');

        if (!this.state.instances || this.state.instances.length === 0) return;

        this.state.instances.forEach(inst => {
            const isZern = inst.isServerPack || inst.category === 'zernmc';
            const targetList = isZern ? serverList : localList;
            const isDisabled = this.state.serverPacksMap && this.state.serverPacksMap[inst.name] && this.state.serverPacksMap[inst.name].disabled;

            const el = document.createElement('div');
            el.className = 'pack-entry' + (this.state.selectedPack && this.state.selectedPack.name === inst.name ? ' selected' : '') + (isDisabled ? ' disabled' : '');
            if (isDisabled) el.setAttribute('data-disabled-text', t('pack.disabled'));
            const hasUpd = this.hasUpdate(inst);
            el.innerHTML = `
                <div class="pack-entry-icon ${isZern ? 'server' : 'local'}">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${isZern
                        ? '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>'
                        : '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'
                    }</svg>
                    ${hasUpd && !isDisabled ? '<span class="update-badge"></span>' : ''}
                </div>
                <div class="pack-entry-info">
                    <div class="pack-entry-name">${this.esc(inst.name)}${hasUpd && !isDisabled ? ' <span class="update-label">' + t('pack.update') + '</span>' : ''}</div>
                    <div class="pack-entry-meta">${inst.version || '?'}${inst.loaderType && inst.loaderType !== 'vanilla' ? ' \u00b7 ' + inst.loaderType : ''}</div>
                </div>
            `;
            if (!isDisabled) {
                el.addEventListener('click', () => this.selectPack(inst));
                el.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showCtxMenu(inst, e.clientX, e.clientY); });
            }
            targetList.appendChild(el);
        });

        if (localSection && localList.children.length > 0) {
            localSection.classList.remove('hidden');
        }

        // Default: open the section that has packs (server list first)
        if (this._openSection === undefined) {
            this._openSection = serverList.children.length > 0 ? 'server'
                : (localList.children.length > 0 ? 'local' : null);
        }
        this.syncOpenSections();
    }

    selectPack(inst) {
        if (this.state.selectedPack && this.state.selectedPack.name === inst.name) {
            this.state.selectedPack = null;
            this.renderSidebar();
            this.showEmptyState();
            return;
        }
        this.state.selectedPack = inst;
        this.renderSidebar();
        // Enable play button immediately so user can click it right away
        this.setPlayBtnText('playBar.play', false);
        this.showPackDetail(inst);
        // Check for updates asynchronously
        this.checkUpdateForPack(inst).then(() => {
            if (this.state.selectedPack === inst) this.showPackDetail(inst);
        });
        // Refresh account in background to check pass status
        this.refreshAccount().then(() => {
            if (this.state.selectedPack === inst) {
                this.showPackDetail(inst);
            }
        });
        const activeView = document.querySelector('.view.active');
        if (activeView && activeView.id !== 'view-packs') {
            this.switchView('packs');
        }
    }

    async refreshAccount() {
        const r = await this.req('/account');
        if (r.success && r.data) {
            if (this.state.account) {
                this.state.account.passActive = r.data.passActive;
                this.state.account.role = r.data.role;
                this.state.account.roleName = r.data.roleName;
            }
        }
    }

    showEmptyState() {
        document.getElementById('pack-empty-state').classList.remove('hidden');
        document.getElementById('pack-detail-content').classList.add('hidden');
        document.getElementById('selected-pack-title').textContent = t('pack.empty.title');
        document.getElementById('selected-pack-meta').textContent = t('pack.empty.desc');
        document.getElementById('play-bar-name').textContent = t('pack.empty.title');
        document.getElementById('play-btn').disabled = true;
        this.setPlayBtnText('playBar.play', false);
    }

    setPlayBtnText(key, disabled) {
        var btn = document.getElementById('play-btn');
        if (!btn) return;
        btn.disabled = disabled;
        btn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> <span data-i18n="' + key + '">' + t(key) + '</span>';
    }

    showPackDetail(inst) {
        document.getElementById('pack-empty-state').classList.add('hidden');
        const detail = document.getElementById('pack-detail-content');
        detail.classList.remove('hidden');

        document.getElementById('detail-name').textContent = inst.name;
        document.getElementById('detail-mc').textContent = inst.version || '?';
        const loader = document.getElementById('detail-loader');
        if (inst.loaderType && inst.loaderType !== 'vanilla') {
            loader.textContent = inst.loaderType;
            loader.classList.remove('hidden');
        } else {
            loader.classList.add('hidden');
        }
        const serverTag = document.getElementById('detail-server');
        if (inst.isServerPack && inst.serverVersion) {
            serverTag.textContent = 'v' + inst.serverVersion;
            serverTag.classList.remove('hidden');
        } else {
            serverTag.classList.add('hidden');
        }
        document.getElementById('detail-loader-ver').textContent = inst.loaderVersion || '-';
        document.getElementById('detail-files').textContent = '...';

        var playtimeEl = document.getElementById('detail-playtime');
        var stats = this.state.playtimeStats;
        if (stats && stats.packs) {
            var packStat = stats.packs.find(function(p) { return p.pack_name === inst.name; });
            if (packStat) {
                var h = Math.floor(packStat.minutes / 60);
                var m = packStat.minutes % 60;
                playtimeEl.textContent = tr('playtime.hours', null, {h: h, m: m});
            } else {
                playtimeEl.textContent = '0m';
            }
        } else {
            playtimeEl.textContent = '-';
        }
        playtimeEl.style.display = 'inline';

        document.getElementById('selected-pack-title').textContent = inst.name;
        document.getElementById('selected-pack-meta').textContent =
            (inst.version || '?') + (inst.loaderType && inst.loaderType !== 'vanilla' ? ' \u00b7 ' + inst.loaderType : '');

        const playBar = document.getElementById('play-bar-name');
        playBar.textContent = inst.name;

        var passActive = this.state.account ? this.state.account.passActive : false;
        if (inst.isServerPack && !passActive) {
            this.setPlayBtnText('playBar.passRequired', true);
        } else if (this.hasUpdate(inst)) {
            this.setPlayBtnText('playBar.update', false);
        } else {
            this.setPlayBtnText('playBar.play', false);
        }

        // Load pack description from cached server packs
        const descEl = document.getElementById('pack-description-text');
        const galleryEl = document.getElementById('pack-gallery');
        galleryEl.innerHTML = '';
        descEl.innerHTML = '';
        if (inst.isServerPack && inst.serverPackName) {
            const pack = this.state.serverPacks.find(p => p.name === inst.serverPackName);
            if (pack && pack.description) {
                descEl.innerHTML = this.mdToHtml(pack.description);
            } else {
                descEl.textContent = t('pack.noDescription');
            }
        } else {
            descEl.textContent = '';
        }

        // Load pack info from local files
        this.loadPackInfo(inst.name);

        // Load whitelist mods for server packs
        const whitelistSection = document.getElementById('whitelist-mods-section');
        if (inst.isServerPack) {
            this.loadWhitelistMods(inst.name);
            whitelistSection.classList.remove('hidden');
        } else {
            whitelistSection.classList.add('hidden');
        }

        // Local mods for any pack (server = readOnly except whitelist)
        this.loadLocalMods(inst.name, inst.isServerPack);
    }

    async loadPackInfo(name) {
        const r = await this.req('/pack-info?name=' + encodeURIComponent(name));
        if (r.success && r.data) {
            document.getElementById('detail-files').textContent = tr('pack.modsCount', null, {count: String(r.data.modsCount || 0)});
            if (r.data.worlds && r.data.worlds.length > 0) {
                document.getElementById('detail-size').textContent = tr('pack.worldsCount', null, {count: r.data.worlds.length});
            }
            if (r.data.recentLogs && r.data.recentLogs.length > 0) {
                var curSize = document.getElementById('detail-size').textContent || '-';
                document.getElementById('detail-size').textContent = curSize + ' \u00b7 ' + tr('pack.logsCount', null, {count: r.data.recentLogs.length});
            }
        }
    }

    async loadWhitelistMods(instanceName) {
        const listEl = document.getElementById('whitelist-mods-list');
        listEl.innerHTML = '<div class="whitelist-loading">' + t('whitelist.loading') + '</div>';
        const r = await this.req('/whitelist/mods');
        if (!r.success || !r.data) {
            listEl.innerHTML = '<div class="whitelist-loading">' + (r.error || t('whitelist.error')) + '</div>';
            return;
        }
        if (r.data.length === 0) {
            listEl.innerHTML = '<div class="whitelist-loading">' + t('whitelist.empty') + '</div>';
            return;
        }
        var html = '';
        r.data.forEach(function(mod) {
            var sizeStr = mod.size > 1048576 ? (mod.size / 1048576).toFixed(1) + ' MB' : Math.ceil(mod.size / 1024) + ' KB';
            html += '<label class="whitelist-mod-item">'
                + '<input type="checkbox" class="whitelist-mod-checkbox" data-hash="' + mod.hash + '" data-name="' + mod.name + '">'
                + '<div class="whitelist-mod-info">'
                + '<div class="whitelist-mod-name">' + this.esc(mod.name) + '</div>'
                + '<div class="whitelist-mod-size">' + sizeStr + '</div>'
                + '</div>'
                + '</label>';
        }, this);
        html += '<button class="whitelist-install-btn" onclick="app.installSelectedWhitelistMods(\'' + instanceName + '\')">'
            + '<span data-i18n="whitelist.install">Install Selected</span></button>';
        listEl.innerHTML = html;
    }

    async installSelectedWhitelistMods(instanceName) {
        var checkboxes = document.querySelectorAll('.whitelist-mod-checkbox:checked');
        if (checkboxes.length === 0) { this.toast(t('whitelist.selectNone'), 'warning'); return; }
        var btn = document.querySelector('.whitelist-install-btn');
        btn.disabled = true;
        btn.textContent = t('whitelist.installing');
        for (var i = 0; i < checkboxes.length; i++) {
            var cb = checkboxes[i];
            var modName = cb.getAttribute('data-name');
            var modHash = cb.getAttribute('data-hash');
            var r = await this.req('/whitelist/mods/install', {
                method: 'POST',
                body: JSON.stringify({ instance: instanceName, modName: modName, modHash: modHash })
            });
            if (r.success) {
                cb.checked = false;
                cb.parentElement.querySelector('.whitelist-mod-info').innerHTML += '<div class="whitelist-mod-status">' + t('whitelist.installed') + '</div>';
            } else {
                this.toast(r.error || t('whitelist.installError'), 'error');
            }
        }
        btn.disabled = false;
        btn.innerHTML = '<span data-i18n="whitelist.install">Install Selected</span>';
        this.toast(t('whitelist.done'), 'success');
        // refresh local mods
        this.loadLocalMods(instanceName, true);
    }

    async loadLocalMods(instanceName, isServerPack) {
        const section = document.getElementById('local-mods-section');
        const listEl = document.getElementById('local-mods-list');
        const countEl = document.getElementById('mods-count');
        if (!section || !listEl) return;
        section.classList.remove('hidden');
        listEl.innerHTML = '<div class="mods-loading">' + t('mods.loading') + '</div>';
        const r = await this.req('/pack-info?name=' + encodeURIComponent(instanceName));
        if (!r.success || !r.data) {
            listEl.innerHTML = '<div class="mods-loading">' + t('mods.empty') + '</div>';
            return;
        }
        const mods = r.data.mods || [];
        // cache for filtering
        this._localModsCache = mods;
        this._localModsIsServer = !!isServerPack;
        this._localModsInstance = instanceName;
        if (countEl) countEl.textContent = tr('mods.count', null, {count: String(mods.length)});
        this.renderLocalMods(mods, isServerPack);
    }

    renderLocalMods(mods, isServerPack) {
        const listEl = document.getElementById('local-mods-list');
        if (!listEl) return;
        if (!mods || mods.length === 0) {
            listEl.innerHTML = '<div class="mods-loading">' + t('mods.empty') + '</div>';
            return;
        }
        const q = (document.getElementById('mods-search')?.value || '').toLowerCase();
        const filtered = q ? mods.filter(m => (m.displayName||m.fileName||'').toLowerCase().includes(q) || (m.modId||'').toLowerCase().includes(q)) : mods;
        if (filtered.length === 0) {
            listEl.innerHTML = '<div class="mods-loading">' + t('mods.noResults') + '</div>';
            return;
        }
        let html = '';
        filtered.forEach(m => {
            const sizeStr = m.size > 1048576 ? (m.size/1048576).toFixed(1)+' MB' : Math.ceil(m.size/1024)+' KB';
            const enabled = m.enabled !== false;
            const badge = enabled ? t('mods.enabled') : t('mods.disabled');
            const badgeCls = enabled ? 'mod-badge-enabled' : 'mod-badge-disabled';
            const canToggle = !isServerPack; // server: read-only for local mods
            const canDelete = !isServerPack; // server: only whitelist mods can be managed
            html += '<div class="mod-item' + (enabled?'':' mod-disabled') + '">'
                + '<div class="mod-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M8 12h8M12 8v8"/></svg></div>'
                + '<div class="mod-info"><div class="mod-name">' + this.esc(m.displayName || m.fileName) + '</div><div class="mod-meta">' + this.esc(m.modId || m.fileName) + ' · ' + this.esc(m.version || '?') + ' · ' + sizeStr + '</div></div>'
                + '<span class="mod-badge ' + badgeCls + '">' + badge + '</span>'
                + (canToggle ? '<button class="btn-secondary btn-sm" onclick="app.toggleLocalMod(\'' + this.esc(m.fileName).replace(/'/g,"\\'") + '\',' + !enabled + ')">' + (enabled? t('mods.disable'):t('mods.enable')) + '</button>' : '')
                + (canDelete ? '<button class="btn-secondary btn-sm btn-danger" onclick="app.deleteLocalMod(\'' + this.esc(m.fileName).replace(/'/g,"\\'") + '\')">' + t('mods.delete') + '</button>' : '')
                + '</div>';
        });
        listEl.innerHTML = html;
    }

    filterLocalMods() {
        if (!this._localModsCache) return;
        this.renderLocalMods(this._localModsCache, this._localModsIsServer);
    }

    async toggleLocalMod(fileName, enable) {
        const inst = this._localModsInstance;
        if (!inst) return;
        const r = await this.req('/mods/toggle', { method:'POST', body: JSON.stringify({instance: inst, file: fileName, enable: enable}) });
        if (r.success) { this.toast(enable? t('mods.enabled'):t('mods.disabled'), 'success'); this.loadLocalMods(inst, this._localModsIsServer); }
        else this.toast(r.error||'Error', 'error');
    }

    async deleteLocalMod(fileName) {
        const inst = this._localModsInstance;
        if (!inst) return;
        if (!confirm(tr('mods.deleteConfirm', null, {name: fileName}))) return;
        const r = await this.req('/mods/delete', { method:'POST', body: JSON.stringify({instance: inst, file: fileName}) });
        if (r.success) { this.toast(t('mods.delete')+': '+fileName, 'success'); this.loadLocalMods(inst, this._localModsIsServer); }
        else this.toast(r.error||'Error', 'error');
    }

    // ==================== ADMIN ====================
    updateAdminNav() {
        var a = this.state.account;
        var btn = document.getElementById('admin-nav-btn');
        if (!btn) return;
        if (a && a.role >= 2) {
            btn.classList.remove('hidden');
        } else {
            btn.classList.add('hidden');
        }
    }

    switchAdminTab(tab) {
        document.querySelectorAll('.admin-tab').forEach(function(t) {
            t.classList.toggle('active', t.dataset.atab === tab);
        });
        document.querySelectorAll('.admin-tab-content').forEach(function(c) {
            c.classList.toggle('active', c.id === 'atab-' + tab);
        });
        if (tab === 'clients') this.adminLoadClients();
        if (tab === 'mods') this.adminLoadMods();
        if (tab === 'users') this.adminClearUser();
        if (tab === 'passes') this.adminLoadPasses();
        if (tab === 'news') this.adminLoadNewsList();
        if (tab === 'packs') this.adminLoadPacks();
    }

    // --- Clients ---
    async adminLoadClients() {
        var el = document.getElementById('admin-clients-list');
        if (!el) return;
        el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/admin/clients');
        if (!r.success) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.error') + ': ' + (r.error || '') + '</div>';
            return;
        }
        var clients = r.clients || r.data || [];
        if (!clients.length) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.noClients') + '</div>';
            return;
        }
        var html = '';
        clients.forEach(function(c) {
            html += '<div class="admin-item">'
                + '<span class="admin-item-name">' + app.esc(c.username || c.name || '?') + '</span>'
                + '<span class="admin-item-meta">IP: ' + app.esc(c.ip || '-') + '</span>'
                + '</div>';
        });
        el.innerHTML = html;
        setTimeout(function() { app.adminLoadClients(); }, 5000);
    }

    // --- Mods ---
    async adminLoadMods() {
        var el = document.getElementById('admin-mods-list');
        if (!el) return;
        el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/admin/whitelist/mods');
        if (!r.success) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
            return;
        }
        var mods = r.mods || r.data || [];
        if (!mods.length) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.modsEmpty') + '</div>';
            return;
        }
        var html = '';
        mods.forEach(function(m) {
            var sizeStr = m.size > 1048576 ? (m.size / 1048576).toFixed(1) + ' MB' : Math.ceil(m.size / 1024) + ' KB';
            html += '<div class="admin-item">'
                + '<span class="admin-item-name">' + app.esc(m.name) + '</span>'
                + '<span class="admin-item-meta">' + sizeStr + '</span>'
                + '<div class="admin-item-actions">'
                + '<button class="btn-secondary btn-sm btn-danger" onclick="app.adminDeleteMod(\'' + m.hash + '\')">' + t('admin.delete') + '</button>'
                + '</div></div>';
        });
        el.innerHTML = html;
    }

    async adminUploadMod() {
        var input = document.getElementById('admin-mod-file');
        var status = document.getElementById('admin-mod-status');
        if (!input || !input.files || !input.files[0]) { this.toast('Select a file', 'error'); return; }
        var file = input.files[0];
        if (!file.name.endsWith('.jar')) { this.toast('Only .jar files', 'error'); return; }
        var reader = new FileReader();
        reader.onload = async function(e) {
            var base64 = e.target.result.split(',')[1];
            var r = await app.req('/admin/whitelist/mods', {
                method: 'POST',
                body: JSON.stringify({ filename: file.name, data: base64 })
            });
            status.classList.remove('hidden');
            if (r.success) {
                status.textContent = t('admin.modsUploaded');
                status.className = 'admin-status success';
                input.value = '';
                app.adminLoadMods();
            } else {
                status.textContent = r.error || t('admin.error');
                status.className = 'admin-status error';
            }
        };
        reader.readAsDataURL(file);
    }

    async adminDeleteMod(hash) {
        var r = await this.req('/admin/whitelist/mods/' + hash, { method: 'DELETE' });
        if (r.success) {
            this.toast(t('admin.modsDeleted'), 'success');
            this.adminLoadMods();
        } else {
            this.toast(r.error || t('admin.error'), 'error');
        }
    }

    // --- Users ---
    async adminSearchUser() {
        var input = document.getElementById('admin-user-search');
        var results = document.getElementById('admin-user-results');
        var query = input.value.trim();
        if (!query) return;
        results.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/admin/users/search', {
            method: 'POST',
            body: JSON.stringify({ search: query })
        });
        if (!r.success) {
            results.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
            return;
        }
        var users = r.users || r.data || [];
        if (!users.length) {
            results.innerHTML = '<div class="admin-empty">' + t('admin.noUser') + '</div>';
            return;
        }
        var html = '';
        users.forEach(function(u) {
            html += '<div class="admin-item" style="cursor:pointer" onclick="app.adminSelectUser(' + u.id + ',\'' + app.esc(u.username) + '\',' + (u.role || 0) + ')">'
                + '<span class="admin-item-name">' + app.esc(u.username) + '</span>'
                + '<span class="admin-item-meta">' + (u.role_name || 'Role ' + (u.role || 0)) + '</span>'
                + '</div>';
        });
        results.innerHTML = html;
    }

    adminClearUser() {
        document.getElementById('admin-user-results').innerHTML = '';
        document.getElementById('admin-user-detail').classList.add('hidden');
        document.getElementById('admin-user-search').value = '';
    }

    adminSelectUser(id, username, role) {
        var detail = document.getElementById('admin-user-detail');
        document.getElementById('admin-user-detail-name').textContent = username;
        document.getElementById('admin-user-role-select').value = String(role);
        detail.dataset.userId = id;
        detail.dataset.username = username;
        detail.classList.remove('hidden');
    }

    async adminSetRole() {
        var detail = document.getElementById('admin-user-detail');
        var userId = detail.dataset.userId;
        var username = detail.dataset.username;
        var role = parseInt(document.getElementById('admin-user-role-select').value);
        if (!userId) { this.toast(t('admin.noUser'), 'error'); return; }
        var r = await this.req('/admin/users/role', {
            method: 'POST',
            body: JSON.stringify({ user_id: parseInt(userId), role: role, username: username })
        });
        if (r.success) {
            this.toast(t('admin.roleUpdated'), 'success');
        } else {
            this.toast(r.error || t('admin.error'), 'error');
        }
    }

    async adminResetPassword() {
        var detail = document.getElementById('admin-user-detail');
        var userId = detail.dataset.userId;
        var username = detail.dataset.username;
        if (!userId) { this.toast(t('admin.noUser'), 'error'); return; }
        var r = await this.req('/admin/users/reset-password', {
            method: 'POST',
            body: JSON.stringify({ user_id: parseInt(userId), username: username })
        });
        if (r.success) {
            this.toast(tr('admin.passReset', null, {pass: r.new_password || r.password || ''}), 'success');
        } else {
            this.toast(r.error || t('admin.error'), 'error');
        }
    }

    // --- Passes ---
    async adminLoadPasses() {
        var el = document.getElementById('admin-passes-list');
        if (!el) return;
        el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/admin/passes');
        if (!r.success) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
            return;
        }
        var passes = r.passes || r.data || [];
        if (!passes.length) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.passesEmpty') + '</div>';
            return;
        }
        var html = '';
        passes.forEach(function(p) {
            html += '<div class="admin-item">'
                + '<span class="admin-item-name">' + app.esc(p.code || p.name || '?') + '</span>'
                + '<span class="admin-item-meta">' + (p.username ? app.esc(p.username) : '') + '</span>'
                + '</div>';
        });
        el.innerHTML = html;
    }

    // --- News ---
    async adminLoadNewsList() {
        var el = document.getElementById('admin-news-list');
        if (!el) return;
        el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/news');
        if (!r.success) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
            return;
        }
        var newsList = [];
        try { newsList = (typeof r.data === 'string' ? JSON.parse(r.data) : r.data).news || []; } catch(e) {}
        if (!newsList.length) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.newsEmpty') + '</div>';
            return;
        }
        var html = '';
        newsList.forEach(function(item, idx) {
            html += '<div class="admin-news-item">'
                + '<div class="admin-news-info">'
                + '<div class="admin-news-title">' + app.esc(item.title) + '</div>'
                + '<div class="admin-news-meta">' + app.esc(item.type) + ' \u00b7 ' + app.esc(item.version || '') + '</div>'
                + '</div>'
                + '<div class="admin-item-actions">'
                + '<button class="btn-secondary btn-sm btn-danger" onclick="app.adminDeleteNews(\'' + idx + '\')">' + t('admin.delete') + '</button>'
                + '</div></div>';
        });
        el.innerHTML = html;
    }

    async adminCreateNews() {
        var title = document.getElementById('admin-news-title').value.trim();
        var type = document.getElementById('admin-news-type').value;
        var version = document.getElementById('admin-news-version').value.trim();
        var body = document.getElementById('admin-news-body').value.trim();
        var status = document.getElementById('admin-news-status');
        if (!title || !body) { this.toast('Fill in title and body', 'error'); return; }
        var r = await this.req('/admin/news', {
            method: 'POST',
            body: JSON.stringify({ title: title, type: type, version: version, body: body })
        });
        status.classList.remove('hidden');
        if (r.success) {
            status.textContent = t('admin.newsCreated');
            status.className = 'admin-status success';
            document.getElementById('admin-news-title').value = '';
            document.getElementById('admin-news-version').value = '';
            document.getElementById('admin-news-body').value = '';
            this.adminLoadNewsList();
        } else {
            status.textContent = r.error || t('admin.error');
            status.className = 'admin-status error';
        }
    }

    async adminDeleteNews(idx) {
        var r = await this.req('/admin/news/delete', {
            method: 'POST',
            body: JSON.stringify({ index: parseInt(idx) })
        });
        if (r.success) {
            this.toast(t('admin.newsDeleted'), 'success');
            this.adminLoadNewsList();
        } else {
            this.toast(r.error || t('admin.error'), 'error');
        }
    }

    // --- Packs ---
    async adminLoadPacks() {
        var el = document.getElementById('admin-packs-list');
        if (!el) return;
        el.innerHTML = '<div class="admin-loading">' + t('admin.loading') + '</div>';
        var r = await this.req('/admin/packs');
        if (!r.success) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.error') + '</div>';
            return;
        }
        var packs = r.packs || [];
        if (!packs.length) {
            el.innerHTML = '<div class="admin-empty">' + t('admin.packsEmpty') + '</div>';
            return;
        }
        var html = '';
        packs.forEach(function(p) {
            var statusText = p.disabled ? t('admin.packsDisabled') : t('admin.packsEnabled');
            var statusClass = p.disabled ? 'admin-pack-disabled' : 'admin-pack-enabled';
            html += '<div class="admin-item">'
                + '<span class="admin-item-name">' + app.esc(p.name) + '</span>'
                + '<span class="admin-item-meta ' + statusClass + '">' + statusText + '</span>'
                + '<div class="admin-item-actions">'
                + '<button class="btn-secondary btn-sm" onclick="app.adminTogglePack(\'' + app.esc(p.name) + '\')">' + t('admin.packsToggle') + '</button>'
                + '</div></div>';
        });
        el.innerHTML = html;
    }

    async adminTogglePack(packName) {
        var r = await this.req('/admin/packs/' + encodeURIComponent(packName) + '/toggle', {
            method: 'POST',
            body: '{}'
        });
        if (r.success) {
            this.toast((r.disabled ? t('admin.packsDisabled') : t('admin.packsEnabled')) + ': ' + packName, 'success');
            this.adminLoadPacks();
        } else {
            this.toast(r.error || t('admin.error'), 'error');
        }
    }

    // ==================== NEWS ====================
    async loadNews() {
        const r = await this.req('/news');
        if (r.success && r.data) {
            let parsed;
            try {
                parsed = typeof r.data === 'string' ? JSON.parse(r.data) : r.data;
            } catch(e) { return; }
            const newsList = parsed.news || [];
            this.renderNews(newsList);
        }
    }

    renderNews(newsList) {
        const grid = document.getElementById('news-grid');
        grid.innerHTML = '';
        if (!newsList.length) {
            grid.innerHTML = '<div class="news-empty"><p><span data-i18n="news.empty">' + t('news.empty') + '</span></p></div>';
            return;
        }
        newsList.forEach((item, idx) => {
            const card = document.createElement('article');
            card.className = 'news-card';
            var tempDiv = document.createElement('div');
            tempDiv.innerHTML = this.mdToHtml(item.body || '');
            var previewText = tempDiv.textContent.replace(/\s+/g, ' ').trim().substring(0, 200);
            card.innerHTML = `
                <div class="news-card-badge type-${this.esc(item.type)}">${this.esc(item.type)}</div>
                <h3>${this.esc(item.title)}</h3>
                <p class="news-preview">${this.esc(previewText)}</p>
                <time>${this.esc(item.version)}</time>
            `;
            card.addEventListener('click', () => this.showNewsModal(item));
            grid.appendChild(card);
        });
    }

    mdToHtml(md) {
        if (!md) return '';
        try {
            var processed = md;
            processed = processed.replace(/\[photo="([^"]+)"\]/g, '![]($1)');
            processed = processed.replace(/\[([^\[\]]+)\]/g, function(match, inner) {
                try {
                    var arr = JSON.parse(match);
                    if (Array.isArray(arr) && arr.length > 0 && typeof arr[0] === 'object' && 'text' in arr[0]) {
                        return arr.map(function(item) {
                            var t = item.text || '';
                            var url = item.click_action === 'open_url' ? item.url : null;
                            if (url) return '[' + t + '](' + url + ')';
                            var parts = [];
                            if (item.bold) parts.push('**');
                            if (item.italic) parts.push('*');
                            parts.push(t);
                            if (item.italic) parts.push('*');
                            if (item.bold) parts.push('**');
                            return parts.join('');
                        }).join('');
                    }
                } catch(e) {}
                return match;
            });
            return marked.parse(processed);
        } catch (e) {
            return '<p>' + this.esc(md) + '</p>';
        }
    }

    showNewsModal(item) {
        let modal = document.getElementById('news-modal');
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'news-modal';
            modal.className = 'modal-backdrop hidden';
            modal.innerHTML = `
                <div class="modal modal-news">
                    <div class="modal-head">
                        <h3 id="news-modal-title"></h3>
                        <button class="modal-close" id="news-modal-close">&times;</button>
                    </div>
                    <div class="modal-body news-modal-body" id="news-modal-body"></div>
                </div>
            `;
            document.getElementById('app').appendChild(modal);
            document.getElementById('news-modal-close').addEventListener('click', () => this.hideNewsModal());
            modal.addEventListener('click', e => { if (e.target === modal) this.hideNewsModal(); });
        }
        document.getElementById('news-modal-title').textContent = item.title;
        const bodyEl = document.getElementById('news-modal-body');
        bodyEl.innerHTML = this.mdToHtml(item.body || '');
        modal.classList.remove('hidden');
    }

    hideNewsModal() {
        const modal = document.getElementById('news-modal');
        if (modal) modal.classList.add('hidden');
    }

    // ==================== LAUNCH ====================
    async launchPack() {
        const inst = this.state.selectedPack;
        if (!inst) {
            this.toast(t('toast.noPackSelected'), 'warning');
            return;
        }
        if (this.hasUpdate(inst)) {
            await this.updateSelectedPack();
            return;
        }
        this.toast(tr('toast.launching', null, {name: inst.name}), 'info');
        const r = await this.req('/launch', { method: 'POST', body: JSON.stringify({ name: inst.name }), timeout: 120000 });
        if (r.success) {
            this.toast(tr('toast.launched', null, {pid: String(r.data?.pid || '')}), 'success');
            this.startPlaytimeTracking(inst.name);
        } else {
            this.toast(r.error || t('toast.launchFailed'), 'error');
        }
    }

    async updateSelectedPack() {
        const inst = this.state.selectedPack;
        if (!inst) return;
        this.setPlayBtnText('playBar.updating', true);
        const r = await this.req('/update', { method: 'POST', body: JSON.stringify({ name: inst.name }), timeout: 120000 });
        if (r.success) {
            this.toast(tr('toast.updated', null, {name: inst.name, version: String(r.data?.version || '')}), 'success');
            if (this.state.updateMap) {
                this.state.updateMap[inst.name] = false;
            }
            await this.loadInstances();
        } else {
            this.toast(r.error || t('toast.updateFailed'), 'error');
            this.setPlayBtnText('playBar.update', false);
        }
    }

    // ==================== INSTALL ====================
    async showInstallModal() {
        document.getElementById('install-modal').classList.remove('hidden');
        const zernmcSel = document.getElementById('zernmc-pack-select');
        const presetContainer = document.getElementById('preset-select-container');
        const presetSel = document.getElementById('preset-select');
        const mcSel = document.getElementById('mc-version-select');
        if (zernmcSel) {
            zernmcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
        }
        if (presetContainer) presetContainer.classList.add('hidden');
        if (mcSel) {
            mcSel.innerHTML = '<option>' + t('select.loading') + '</option>';
        }

        try {
            const packs = await this.req('/packs');
            if (zernmcSel && packs.success && Array.isArray(packs.data) && packs.data.length > 0) {
                if (this.state.account && this.state.account.passActive) {
                    zernmcSel.innerHTML = '<option value="">' + t('select.selectPack') + '</option>';
                    zernmcSel.disabled = false;
                    packs.data.forEach(p => {
                        if (p.disabled) return; // skip disabled packs
                        const o = document.createElement('option');
                        o.value = p.name;
                        o.dataset.presets = JSON.stringify(p.presets || []);
                        o.textContent = (p.displayName || p.name) + ' (' + (p.version || '') + ')';
                        zernmcSel.appendChild(o);
                    });
                    // Update preset selector when pack changes
                    zernmcSel.onchange = function() {
                        const selected = zernmcSel.options[zernmcSel.selectedIndex];
                        const presets = selected && selected.dataset.presets ? JSON.parse(selected.dataset.presets) : [];
                        if (presetSel) {
                            presetSel.innerHTML = '<option value="">' + t('install.preset.default') + '</option>';
                            presets.forEach(function(pr) {
                                const o = document.createElement('option');
                                o.value = pr;
                                o.textContent = pr;
                                presetSel.appendChild(o);
                            });
                        }
                        if (presetContainer) {
                            if (presets.length > 0) {
                                presetContainer.classList.remove('hidden');
                            } else {
                                presetContainer.classList.add('hidden');
                            }
                        }
                        app.enhanceSelects();
                    };
                } else {
                    zernmcSel.innerHTML = '<option value="">' + t('select.passRequired') + '</option>';
                    zernmcSel.disabled = true;
                }
            } else if (zernmcSel) {
                zernmcSel.innerHTML = '<option value="">' + t('select.noPacks') + '</option>';
                zernmcSel.disabled = true;
            }

            if (this.state.account && !this.state.account.passActive) {
                const zernmcTab = document.querySelector('[data-tab="zernmc"]');
                if (zernmcTab) zernmcTab.style.opacity = '0.5';
            } else {
                const zernmcTab = document.querySelector('[data-tab="zernmc"]');
                if (zernmcTab) zernmcTab.style.opacity = '1';
            }
        } catch (e) {
            if (zernmcSel) {
                zernmcSel.innerHTML = '<option value="">' + t('select.failedLoad') + '</option>';
                zernmcSel.disabled = true;
            }
        }
        if (mcSel) this.enhanceSelects();

        if (mcSel) {
            try {
                const mc = await this.req('/mc-versions');
                if (mc.success && Array.isArray(mc.data)) {
                    mcSel.innerHTML = '<option value="">' + t('select.selectVersion') + '</option>';
                    mc.data.forEach(v => {
                        const o = document.createElement('option');
                        o.value = v; o.textContent = v;
                        mcSel.appendChild(o);
                    });
                } else {
                    mcSel.innerHTML = '<option value="">' + t('select.failedLoad') + '</option>';
                }
            } catch (e) {
                mcSel.innerHTML = '<option value="">' + t('select.failedLoad') + '</option>';
            }
        }
        this.enhanceSelects();
    }

    hideInstallModal() {
        document.getElementById('install-modal').classList.add('hidden');
        document.getElementById('install-progress').classList.add('hidden');
        if (this.progressPoller) {
            document.getElementById('inline-install-progress').classList.remove('hidden');
        }
    }

    switchInstallTab(tab) {
        document.querySelectorAll('.modal-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
        document.querySelectorAll('.modal-tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab));
    }

    // ==================== CUSTOM SELECT ====================
    enhanceSelects() {
        document.querySelectorAll('select').forEach(sel => {
            if (sel.dataset.enhanced) {
                this.refreshCustomSelect(sel);
                return;
            }
            sel.dataset.enhanced = '1';
            sel.style.display = 'none';
            const wrap = document.createElement('div');
            wrap.className = 'custom-select-wrap';

            const trigger = document.createElement('div');
            trigger.className = 'custom-select-trigger';
            const placeholder = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : (sel.placeholder || t('select.placeholder'));
            trigger.innerHTML = `<span class="${sel.value ? '' : 'placeholder'}">${placeholder}</span>
                <svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;

            const dropdown = document.createElement('div');
            dropdown.className = 'custom-select-dropdown';
            dropdown.innerHTML = '<div class="custom-select-search"><input type="text" placeholder="' + t('select.search') + '"></div><div class="custom-select-options"></div>';
            const optionsContainer = dropdown.querySelector('.custom-select-options');
            const searchInput = dropdown.querySelector('input');

            sel.querySelectorAll('option').forEach(o => {
                const opt = document.createElement('div');
                opt.className = 'custom-select-option' + (o.selected ? ' selected' : '');
                opt.textContent = o.text;
                opt.dataset.value = o.value;
                optionsContainer.appendChild(opt);
            });

            const updateTrigger = () => {
                const selected = optionsContainer.querySelector('.selected');
                const txt = selected ? selected.textContent : (sel.options[0] ? sel.options[0].text : t('select.placeholder'));
                trigger.innerHTML = `<span>${txt}</span>
                    <svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
            };

            const updateDisabled = () => {
                if (sel.disabled) {
                    trigger.style.opacity = '0.5';
                    trigger.style.cursor = 'not-allowed';
                } else {
                    trigger.style.opacity = '';
                    trigger.style.cursor = '';
                }
            };
            updateDisabled();

            trigger.addEventListener('click', () => {
                if (sel.disabled) return;
                const open = dropdown.classList.toggle('open');
                trigger.classList.toggle('open', open);
                if (open) {
                    searchInput.value = '';
                    searchInput.focus();
                    optionsContainer.querySelectorAll('.custom-select-option').forEach(o => o.classList.remove('hidden'));
                }
            });

            searchInput.addEventListener('input', () => {
                const q = searchInput.value.toLowerCase();
                optionsContainer.querySelectorAll('.custom-select-option').forEach(o => {
                    o.classList.toggle('hidden', q && !o.textContent.toLowerCase().includes(q));
                });
            });

            optionsContainer.addEventListener('click', e => {
                const opt = e.target.closest('.custom-select-option');
                if (!opt || opt.classList.contains('hidden')) return;
                optionsContainer.querySelectorAll('.custom-select-option').forEach(o => o.classList.remove('selected'));
                opt.classList.add('selected');
                sel.value = opt.dataset.value;
                updateTrigger();
                dropdown.classList.remove('open');
                trigger.classList.remove('open');
                sel.dispatchEvent(new Event('change', { bubbles: true }));
            });

            document.addEventListener('click', e => {
                if (!wrap.contains(e.target)) {
                    dropdown.classList.remove('open');
                    trigger.classList.remove('open');
                }
            });

            wrap.appendChild(trigger);
            wrap.appendChild(dropdown);
            sel.parentNode.insertBefore(wrap, sel.nextSibling);
        });
    }

    refreshCustomSelect(sel) {
        if (!sel || !sel.dataset.enhanced) return;
        let wrap = sel.nextElementSibling;
        while (wrap && !wrap.classList.contains('custom-select-wrap')) {
            wrap = wrap.nextElementSibling;
        }
        if (!wrap) return;
        const optionsContainer = wrap.querySelector('.custom-select-options');
        const trigger = wrap.querySelector('.custom-select-trigger');
        optionsContainer.innerHTML = '';
        sel.querySelectorAll('option').forEach(o => {
            const opt = document.createElement('div');
            opt.className = 'custom-select-option' + (o.selected ? ' selected' : '');
            opt.textContent = o.text;
            opt.dataset.value = o.value;
            optionsContainer.appendChild(opt);
        });
        const selected = optionsContainer.querySelector('.selected');
        trigger.innerHTML = '<span>' + (selected ? selected.textContent : t('select.placeholder')) + '</span>' +
            '<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>';
    }

    async onLoaderChange(loader) {
        const f = document.getElementById('loader-ver-field');
        if (loader === 'vanilla') { f.classList.add('hidden'); return; }
        f.classList.remove('hidden');
        const sel = document.getElementById('loader-ver-select');
        sel.innerHTML = '<option>' + t('select.loading') + '</option>';
        const mc = document.getElementById('mc-version-select').value;
        if (!mc) { sel.innerHTML = '<option>' + t('select.selectMCFirst') + '</option>'; this.enhanceSelects(); return; }
        const r = await this.req(`/loader-versions?mc=${mc}&loader=${loader}`);
        if (r.success && r.data) {
            sel.innerHTML = '<option value="">' + t('select.selectVersion') + '</option>';
            r.data.forEach(v => {
                const o = document.createElement('option');
                o.value = v; o.textContent = v;
                sel.appendChild(o);
            });
        } else {
            sel.innerHTML = '<option>' + t('select.failedLoad') + '</option>';
        }
        this.enhanceSelects();
    }

    async installZernMCPack() {
        const packName = document.getElementById('zernmc-pack-select').value;
        if (!packName) { this.toast(t('select.selectPack'), 'error'); return; }
        const preset = document.getElementById('preset-select') ? document.getElementById('preset-select').value : '';
        // Use server pack name as instance name (no local naming)
        const instanceName = packName;

        const body = { name: instanceName, version: packName, loader: 'zernmc' };
        if (preset) body.preset = preset;

        const r = await this.req('/install', {
            method: 'POST',
            body: JSON.stringify(body)
        });
        if (r.success) {
            this.toast(t('toast.installing'), 'info');
            this.startProgressPoll();
        } else {
            this.toast(r.error || t('toast.launchFailed'), 'error');
        }
    }

    async installCustom() {
        const mc = document.getElementById('mc-version-select').value;
        const loader = document.getElementById('loader-select').value;
        const loaderVer = document.getElementById('loader-ver-select').value;
        const name = document.getElementById('custom-instance-name').value.trim();

        if (!mc) { this.toast(t('select.selectVersion'), 'error'); return; }
        if (!name) { this.toast(t('install.enterName'), 'error'); return; }

        const r = await this.req('/install', {
            method: 'POST',
            body: JSON.stringify({ name, version: mc, loader, loaderVersion: loaderVer })
        });
        if (r.success) {
            this.toast(t('toast.installing'), 'info');
            this.startProgressPoll();
        } else {
            this.toast(r.error || t('toast.launchFailed'), 'error');
        }
    }

    startProgressPoll() {
        this.progressPoller = setInterval(async () => {
            const r = await this.req('/install/progress');
            if (r.success && r.data) {
                const modalP = document.getElementById('install-progress');
                modalP.classList.remove('hidden');
                const pct = (r.data.percent || 0) + '%';
                const label = r.data.label || 'Installing...';
                const indeterminate = r.data.inProgress && (!r.data.total || r.data.total <= 0);

                const fill = document.getElementById('progress-fill');
                if (indeterminate) {
                    fill.classList.add('indeterminate');
                    fill.style.width = '';
                } else {
                    fill.classList.remove('indeterminate');
                    fill.style.width = pct;
                }
                document.getElementById('progress-label').textContent = label;

                const inlineProgress = document.getElementById('inline-install-progress');
                if (!inlineProgress.classList.contains('hidden')) {
                    const inlineFill = document.getElementById('inline-progress-fill');
                    if (indeterminate) {
                        inlineFill.classList.add('indeterminate');
                        inlineFill.style.width = '';
                    } else {
                        inlineFill.classList.remove('indeterminate');
                        inlineFill.style.width = pct;
                    }
                    document.getElementById('inline-progress-label').textContent = label;
                    const inlineStage = document.getElementById('inline-progress-stage');
                    if (r.data.stageName && r.data.stageCount > 1) {
                        inlineStage.textContent = tr('install.step', null, {current: String(r.data.stageIndex || 0), total: String(r.data.stageCount), name: r.data.stageName});
                        inlineStage.classList.remove('hidden');
                    } else {
                        inlineStage.classList.add('hidden');
                    }
                }

                const modalStage = document.getElementById('progress-stage');
                if (r.data.stageName && r.data.stageCount > 1) {
                    modalStage.textContent = tr('install.step', null, {current: String(r.data.stageIndex || 0), total: String(r.data.stageCount), name: r.data.stageName});
                    modalStage.classList.remove('hidden');
                } else {
                    modalStage.classList.add('hidden');
                }
                if (!r.data.inProgress) {
                    this.stopProgressPoll();
                    this.hideInstallModal();
                    inlineProgress.classList.add('hidden');
                    var failed = r.data.stageName === 'Failed' || r.data.stageName === 'Error' || (r.data.label && r.data.label.startsWith('Error'));
                    this.toast(failed ? (r.data.label || t('toast.installFailed')) : t('toast.installComplete'), failed ? 'error' : 'success');
                    if (!failed) {
                        await this.loadInstances();
                    }
                }
            }
        }, 500);
    }

    stopProgressPoll() {
        if (this.progressPoller) {
            clearInterval(this.progressPoller);
        this.progressPoller = null;
        this.networkPoller = null;
        }
    }

    // ==================== SETTINGS ====================
    async loadSettings() {
        const r = await this.req('/settings');
        if (r.success && r.data) {
            var ram = r.data.maxMemory || 4096;
            document.getElementById('ram-slider').value = ram;
            document.getElementById('ram-value').textContent = tr('ram.gb', null, {value: (ram / 1024).toFixed(1)});
            document.getElementById('ram-info').textContent = tr('ram.mbAllocated', null, {value: ram});
            document.getElementById('server-url').textContent = r.data.serverUrl || 'http://87.120.187.36:1582';

            if (r.data.windowWidth) {
                document.getElementById('win-width').value = r.data.windowWidth;
            }
            if (r.data.windowHeight) {
                document.getElementById('win-height').value = r.data.windowHeight;
            }
            if (r.data.extraJvmArgs !== undefined) {
                document.getElementById('jvm-args').value = r.data.extraJvmArgs || '';
            }
            if (r.data.javaPath) {
                document.getElementById('java-path-input').value = r.data.javaPath;
            }
            if (r.data.locale) {
                this.setLocale(r.data.locale);
            }
            var sysJvm = !!r.data.systemBasedJvm;
            document.getElementById('system-jvm-toggle').checked = sysJvm;
            this.updateSystemJvmInfo(r.data);
            this.state._sysInfo = { cpuCores: r.data.cpuCores, totalRamMB: r.data.totalRamMB };
        } else {
            document.getElementById('ram-value').textContent = tr('ram.gb', null, {value: '4'});
            document.getElementById('ram-slider').value = '4096';
        }
        var sr = await this.req('/instances');
        if (sr.success) {
            document.getElementById('server-status').textContent = t('settings.server.connected');
            document.getElementById('server-status').style.color = 'var(--success)';
        } else {
            document.getElementById('server-status').textContent = t('settings.server.disconnected');
            document.getElementById('server-status').style.color = 'var(--error)';
        }
    }

    updateSystemJvmInfo(data) {
        var el = document.getElementById('system-jvm-info');
        if (!el) return;
        var cores = data.cpuCores || '-';
        var ramGB = data.totalRamMB ? (data.totalRamMB / 1024).toFixed(1) : '-';
        var flags = data.systemBasedJvm ? t('settings.systemJvm.enabled') : t('settings.systemJvm.disabled');
        el.textContent = tr('settings.systemJvm.info', null, {cores: cores, ram: ramGB, flags: flags});
    }

    async saveSettings() {
        try {
            const ram = document.getElementById('ram-slider').value;
            const w = parseInt(document.getElementById('win-width').value) || 1280;
            const h = parseInt(document.getElementById('win-height').value) || 720;
            const jvm = document.getElementById('jvm-args').value.trim();
            const jp = document.getElementById('java-path-input').value.trim();
            var localeSel = document.getElementById('locale-select');
            var locale = localeSel ? localeSel.value : _currentLocale;
            var sysToggle = document.getElementById('system-jvm-toggle');
            var sysJvm = sysToggle ? sysToggle.checked : false;
            var r = await this.req('/settings', { method: 'POST', body: JSON.stringify({ maxMemory: ram, windowWidth: w, windowHeight: h, extraJvmArgs: jvm, javaPath: jp, locale: locale, systemBasedJvm: sysJvm }) });
            if (!r.success && r.error) this.toast(r.error, 'error');
        } catch (e) {
            this.toast(t('error.network'), 'error');
        }
    }

    // ==================== FRIENDS ====================
    async loadFriends() {
        try {
            var r = await this.req('/friends/list');
            if (r.success !== false) {
                this.state.friends = r.friends || [];
            } else {
                this.state.friends = [];
            }
            r = await this.req('/friends/requests');
            if (r.success !== false) {
                this.state.friendRequests = r.requests || [];
            } else {
                this.state.friendRequests = [];
            }
            this.renderFriends();
            this.renderFriendRequests();
        } catch (e) {
            this.state.friends = [];
            this.state.friendRequests = [];
        }
    }

    startFriendStatusHeartbeat() {
        if (this._friendsPollInterval) clearInterval(this._friendsPollInterval);
        this._friendsPollInterval = setInterval(this.loadFriends.bind(this), 10000);
        // Send online status
        this.req('/friends/status', { method: 'POST', body: JSON.stringify({ online: true, current_pack: this.state.selectedPack ? this.state.selectedPack.name : null }) });
        setInterval(function() {
            app.req('/friends/status', { method: 'POST', body: JSON.stringify({ online: true, current_pack: app.state.selectedPack ? app.state.selectedPack.name : null }) });
        }, 60000);
    }

    renderFriends() {
        var list = document.getElementById('friends-list');
        if (!list) return;
        var query = (document.getElementById('friends-search-input') || {}).value || '';
        var filtered = this.state.friends;
        if (query) {
            var q = query.toLowerCase();
            filtered = filtered.filter(function(f) { return f.username.toLowerCase().includes(q); });
        }
        if (!filtered || filtered.length === 0) {
            list.innerHTML = '<div class="friends-empty">' + t('friends.empty') + '</div>';
            return;
        }
        var online = filtered.filter(function(f) { return f.online; });
        var offline = filtered.filter(function(f) { return !f.online; });
        var html = '';
        if (online.length) {
            html += '<div class="friends-group-label">' + t('friends.online') + ' (' + online.length + ')</div>';
            online.forEach(function(f) { html += renderFriendItem(f); });
        }
        if (offline.length) {
            html += '<div class="friends-group-label">' + t('friends.offline') + ' (' + offline.length + ')</div>';
            offline.forEach(function(f) { html += renderFriendItem(f); });
        }
        list.innerHTML = html;
    }

    renderFriendRequests() {
        var section = document.getElementById('friend-requests-section');
        var list = document.getElementById('friend-requests-list');
        if (!section || !list) return;
        var reqs = this.state.friendRequests;
        if (!reqs || reqs.length === 0) {
            section.classList.add('hidden');
            return;
        }
        section.classList.remove('hidden');
        list.innerHTML = reqs.map(function(r) {
            return '<div class="friend-request-item">' +
                '<div class="friend-request-info">' +
                '<div class="friend-request-avatar">' + r.username[0].toUpperCase() + '</div>' +
                '<div>' +
                '<div class="friend-request-name">' + app.esc(r.username) + '</div>' +
                '<div class="friend-request-text">' + tr('friends.requestFrom', null, {user: app.esc(r.username)}) + '</div>' +
                '</div></div>' +
                '<div class="friend-request-actions">' +
                '<button class="btn-primary btn-sm" onclick="app.acceptFriendRequest(' + r.id + ')">' + t('friends.accept') + '</button>' +
                '<button class="btn-secondary btn-sm btn-danger" onclick="app.declineFriendRequest(' + r.id + ')">' + t('friends.decline') + '</button>' +
                '</div></div>';
        }).join('');
    }

    showAddFriend() {
        document.getElementById('add-friend-modal').classList.remove('hidden');
        document.getElementById('add-friend-input').value = '';
        document.getElementById('add-friend-error').classList.add('hidden');
        document.getElementById('add-friend-input').focus();
    }

    closeAddFriend() {
        document.getElementById('add-friend-modal').classList.add('hidden');
        document.getElementById('add-friend-input').value = '';
        document.getElementById('add-friend-error').classList.add('hidden');
    }

    async submitAddFriend() {
        var input = document.getElementById('add-friend-input');
        var username = input.value.trim();
        if (!username) return;
        var errEl = document.getElementById('add-friend-error');
        var btn = document.getElementById('add-friend-submit');
        btn.disabled = true;
        try {
            var r = await this.req('/friends/add', { method: 'POST', body: JSON.stringify({ username: username }) });
            if (r.detail) {
                errEl.textContent = r.detail;
                errEl.classList.remove('hidden');
            } else {
                this.toast(r.message || t('friends.addSuccess'), 'success');
                this.closeAddFriend();
                this.loadFriends();
            }
        } catch (e) {
            errEl.textContent = t('friends.addError');
            errEl.classList.remove('hidden');
        }
        btn.disabled = false;
    }

    filterFriends() {
        this.renderFriends();
    }

    async acceptFriendRequest(userId) {
        var r = await this.req('/friends/accept', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
        if (r.detail) {
            this.toast(r.detail, 'error');
        } else {
            this.toast(r.message || t('friends.acceptSuccess'), 'success');
            this.loadFriends();
        }
    }

    async declineFriendRequest(userId) {
        var r = await this.req('/friends/remove', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
        if (r.detail) {
            this.toast(r.detail, 'error');
        } else {
            this.toast(r.message || 'Declined', 'info');
            this.loadFriends();
        }
    }

    async removeFriend(userId) {
        var r = await this.req('/friends/remove', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
        if (r.detail) {
            this.toast(r.detail, 'error');
        } else {
            this.toast(r.message || t('friends.removeSuccess'), 'success');
            this.loadFriends();
        }
    }

    // ==================== PLAYTIME ====================
    async loadPlaytimeStats() {
        try {
            var r = await this.req('/playtime/stats');
            if (r.success !== false) {
                this.state.playtimeStats = r;
            }
            if (this.state.selectedPack) this.showPackDetail(this.state.selectedPack);
        } catch (e) {}
    }

    startPlaytimeTracking(packName) {
        this._playtimeAccumulated = 0;
        this._playtimeStartTime = Date.now();
        this._currentPlaytimePack = packName;
        if (this._playtimeInterval) clearInterval(this._playtimeInterval);
        this._playtimeInterval = setInterval(this.syncPlaytime.bind(this), 60000);
    }

    stopPlaytimeTracking() {
        if (this._playtimeInterval) {
            clearInterval(this._playtimeInterval);
            this._playtimeInterval = null;
        }
        if (this._playtimeAccumulated > 0) {
            this.syncPlaytime();
        }
        this._playtimeAccumulated = 0;
        this._playtimeStartTime = null;
        this._currentPlaytimePack = null;
    }

    async syncPlaytime() {
        if (!this._currentPlaytimePack || this._playtimeAccumulated <= 0) return;
        var minutes = Math.floor(this._playtimeAccumulated);
        if (minutes <= 0) return;
        try {
            var r = await this.req('/playtime/sync', { method: 'POST', body: JSON.stringify({ minutes: minutes, pack_name: this._currentPlaytimePack }) });
            if (r.success !== false) {
                this._playtimeAccumulated = 0;
                this.loadPlaytimeStats();
            }
        } catch (e) {}
    }

    async activatePass() {
        const code = document.getElementById('pass-code').value.trim();
        if (!code) { this.toast(t('toast.enterPassCode'), 'error'); return; }
        const r = await this.req('/activate-pass', { method: 'POST', body: JSON.stringify({ pass_code: code }) });
        if (r.success) {
            this.toast(t('toast.passActivated'), 'success');
            document.getElementById('pass-code').value = '';
            this.state.account.passActive = true;
            this.state.account.role = 1;
            this.state.account.roleName = 'PASS_HOLDER';
            this.updateBadge();
            await this.loadInstances();
            await this.loadServerPacksList();
        } else {
            this.toast(r.error || t('toast.activationFailed'), 'error');
        }
    }

    // ==================== TOAST ====================
    toast(msg, type = 'info') {
        const el = document.getElementById('toast');
        el.textContent = msg;
        el.className = 'toast ' + type;
        el.classList.remove('hidden');
        clearTimeout(this.toastTimer);
        this.toastTimer = setTimeout(() => el.classList.add('hidden'), 3000);
    }

    // ==================== LOADING ====================
    showLoading(show) {
        document.getElementById('loading-overlay').classList.toggle('hidden', !show);
    }

    updateBadge() {
        const a = this.state.account;
        if (!a) return;
        const status = document.getElementById('account-status');
        if (a.passActive) {
            status.textContent = t('badge.pro');
            status.className = 'badge badge-pro';
        } else {
            status.textContent = t('badge.free');
            status.className = 'badge badge-free';
        }
        const role = document.getElementById('account-role');
        if (a.roleName) {
            role.textContent = a.roleName;
            role.classList.remove('hidden');
        } else {
            role.classList.add('hidden');
        }
    }

    // ==================== LOG VIEWER ====================
    openLogViewer() {
        var overlay = document.getElementById('log-viewer-overlay');
        if (!overlay) return;
        this._logRenderedLines = null;
        overlay.classList.remove('hidden');
        this._pollLogViewer();
        this._logPollInterval = setInterval(this._pollLogViewer.bind(this), 1500);
    }

    closeLogViewer() {
        var overlay = document.getElementById('log-viewer-overlay');
        if (overlay) overlay.classList.add('hidden');
        if (this._logPollInterval) {
            clearInterval(this._logPollInterval);
            this._logPollInterval = null;
        }
    }

    async _pollLogViewer() {
        try {
            var r = await this.req('/game-logs');
            if (r.success && r.data) {
                this._renderLogViewer(r.data);
            }
        } catch(e) {}
    }

    _renderLogViewer(raw) {
        var content = document.getElementById('log-viewer-content');
        if (!content) return;
        if (!raw || !raw.trim()) {
            this._logRenderedLines = null;
            content.innerHTML = '<div class="log-line log-empty">' + t('logViewer.noLogs') + '</div>';
            return;
        }
        var lines = raw.split('\n').filter(function(l) { return l.trim(); });
        var cleaned = lines.map(function(l) { return l.replace(/§[0-9a-fklmnor]/g, ''); });

        var stick = content.scrollTop + content.clientHeight >= content.scrollHeight - 40;

        var prev = this._logRenderedLines;
        var appendFrom = 0;
        if (prev && prev.length > 0 && lines.length >= prev.length) {
            var same = true;
            for (var i = 0; i < prev.length; i++) {
                if (cleaned[i] !== prev[i]) { same = false; break; }
            }
            if (same) appendFrom = prev.length;
        }

        this._logRenderedLines = cleaned;
        if (appendFrom > 0) {
            content.insertAdjacentHTML('beforeend', lines.slice(appendFrom).map(this._renderLogLine.bind(this)).join(''));
        } else {
            content.innerHTML = lines.map(this._renderLogLine.bind(this)).join('');
        }
        if (stick) content.scrollTop = content.scrollHeight;
    }

    _renderLogLine(line) {
        var cls = 'log-line';
        var upper = line.toUpperCase();
        if (upper.includes('ERROR') || upper.includes('FATAL')) cls += ' log-error';
        else if (upper.includes('WARN')) cls += ' log-warn';
        else if (upper.includes('INFO')) cls += ' log-info';
        else if (upper.includes('DEBUG') || upper.includes('TRACE')) cls += ' log-debug';
        var text = line.replace(/§[0-9a-fklmnor]/g, '');
        return '<div class="' + cls + '">' + this.esc(text) + '</div>';
    }

    async copyLogs() {
        var content = document.getElementById('log-viewer-content');
        if (!content) return;
        var text = content.textContent;
        try {
            var r = await this.req('/clipboard', { method: 'POST', body: JSON.stringify({ text: text }) });
            if (r && r.success) {
                this.toast(t('logViewer.copied'), 'success');
                return;
            }
        } catch(e) {}
        // Fallback for plain-browser use (outside the launcher)
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(text).then(function() {
                app.toast(t('logViewer.copied'), 'success');
            }).catch(function() {
                app.toast(t('logViewer.copied'), 'success');
            });
        } else {
            var ta = document.createElement('textarea');
            ta.value = text;
            ta.style.position = 'fixed';
            ta.style.opacity = '0';
            document.body.appendChild(ta);
            ta.select();
            try { document.execCommand('copy'); } catch(e) {}
            document.body.removeChild(ta);
            this.toast(t('logViewer.copied'), 'success');
        }
    }

    async openLogFile() {
        try {
            var r = await this.req('/open-log-file', { method: 'POST' });
            if (r && r.success) {
                this.toast(t('logViewer.opened'), 'success');
            } else {
                this.toast((r && r.error) || t('logViewer.openFailed'), 'error');
            }
        } catch(e) {
            this.toast(t('logViewer.openFailed'), 'error');
        }
    }

    replayOnboarding() {
        localStorage.removeItem('zern_onboardBlocks');
        localStorage.removeItem('zern_onboardVersion');
        // force show all blocks
        const curVer = (document.getElementById('version')?.textContent || document.getElementById('header-version')?.textContent || '1.1.1').trim() || '1.1.1';
        const BLOCKS = [
            {id:'install',  titleKey:'onboard.title.install',  descKey:'onboard.desc.install',  sel:['#add-server-pack-btn','#add-pack-btn','#server-packs-header .btn-icon','#local-packs-header .btn-icon'], addedIn:'1.1.1'},
            {id:'settings', titleKey:'onboard.title.settings', descKey:'onboard.desc.settings', sel:'#settings-btn', addedIn:'1.1.1'},
            {id:'pass',     titleKey:'onboard.title.pass',     descKey:'onboard.desc.pass',     sel:'#pass-code', addedIn:'1.1.1', requiresView:'settings'},
            {id:'manage',   titleKey:'onboard.title.manage',   descKey:'onboard.desc.manage',   sel:['.pack-entry','#selected-pack-title','#pack-empty-state'], addedIn:'1.1.1'},
            {id:'mods',     titleKey:'onboard.title.mods',     descKey:'onboard.desc.mods',     sel:'#local-mods-section', addedIn:'1.1.1'},
            {id:'logs',     titleKey:'onboard.title.logs',     descKey:'onboard.desc.logs',     sel:'#show-log-viewer-btn', addedIn:'1.1.1'},
        ];
        this.showOnboarding(BLOCKS, curVer, []);
    }

    esc(s) {
        if (!s) return '';
        const d = document.createElement('div');
        d.textContent = s;
        return d.innerHTML;
    }

    // ==================== CTX MENU (PKM) ====================
    async showCtxMenu(inst, x, y) {
        this.hideCtxMenu();
        const isServer = !!inst.isServerPack;
        // fetch fm name once
        let fmName = this._fmName;
        if (!fmName) {
            try { const r = await this.req('/filemanager'); if (r.success) fmName = r.data?.name || 'File Manager'; } catch(e) {}
            this._fmName = fmName || 'File Manager';
            fmName = this._fmName;
        }
        const menu = document.createElement('div');
        menu.id = 'ctx-menu';
        menu.className = 'ctx-menu';
        menu.innerHTML = `
            <button class="ctx-item" data-act="open"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> ${t('ctx.openFolder')} <span style="margin-left:auto;opacity:0.5;font-size:11px">${this.esc(fmName)}</span></button>
            <button class="ctx-item" data-act="settings"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg> ${t('ctx.settings')}</button>
            <div class="ctx-sep"></div>
            <button class="ctx-item danger" data-act="delete"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> ${t('ctx.delete')}</button>
        `;
        const backdrop = document.createElement('div');
        backdrop.id = 'ctx-backdrop';
        backdrop.className = 'ctx-backdrop';
        backdrop.addEventListener('click', () => this.hideCtxMenu());
        backdrop.addEventListener('contextmenu', (e)=>{ e.preventDefault(); this.hideCtxMenu(); });
        document.body.appendChild(backdrop);
        document.body.appendChild(menu);
        // position with flip
        const pad = 8;
        let mx = x, my = y;
        const rect = menu.getBoundingClientRect();
        if (mx + rect.width + pad > window.innerWidth) mx = window.innerWidth - rect.width - pad;
        if (my + rect.height + pad > window.innerHeight) my = window.innerHeight - rect.height - pad;
        menu.style.left = mx + 'px';
        menu.style.top = my + 'px';
        menu.querySelector('[data-act="open"]').addEventListener('click', () => { this.hideCtxMenu(); this.openPackFolder(inst.name); });
        menu.querySelector('[data-act="settings"]').addEventListener('click', () => { this.hideCtxMenu(); this.openPackSettings(inst); });
        menu.querySelector('[data-act="delete"]').addEventListener('click', () => { this.hideCtxMenu(); this.deletePack(inst.name); });
        const onKey = (e)=>{ if(e.key==='Escape') this.hideCtxMenu(); };
        document.addEventListener('keydown', onKey, {once:true});
        this._ctxKeyHandler = onKey;
    }
    hideCtxMenu() {
        document.getElementById('ctx-menu')?.remove();
        document.getElementById('ctx-backdrop')?.remove();
        if (this._ctxKeyHandler) { document.removeEventListener('keydown', this._ctxKeyHandler); this._ctxKeyHandler=null; }
    }
    async openPackFolder(name) {
        const r = await this.req('/pack/open-folder', { method:'POST', body: JSON.stringify({name}) });
        if (!r.success) this.toast(r.error||'Failed to open folder','error');
    }
    openPackSettings(inst) {
        const isServer = !!inst.isServerPack;
        let modal = document.getElementById('pack-settings-modal');
        if (modal) modal.remove();
        modal = document.createElement('div');
        modal.id = 'pack-settings-modal';
        modal.className = 'modal-backdrop';
        modal.innerHTML = `
            <div class="modal">
                <div class="modal-head"><h3>${t('ctx.settings')} — ${this.esc(inst.name)}</h3><button class="modal-close" id="ps-close">&times;</button></div>
                <div class="modal-body pack-settings-form">
                    <div class="field"><label>${t('ctx.name')}</label><input type="text" id="ps-name" value="${this.esc(inst.name)}" ${isServer?'disabled':''}></div>
                    <div class="field"><label>${t('ctx.loader')}</label>
                        <select id="ps-loader" ${isServer?'disabled':''}>
                            <option value="vanilla" ${inst.loaderType==='vanilla'?'selected':''}>Vanilla</option>
                            <option value="fabric" ${inst.loaderType==='fabric'?'selected':''}>Fabric</option>
                            <option value="forge" ${inst.loaderType==='forge'?'selected':''}>Forge</option>
                            <option value="neoforge" ${inst.loaderType==='neoforge'?'selected':''}>NeoForge</option>
                        </select>
                    </div>
                    <div class="field ${inst.loaderType==='vanilla'?'hidden':''}" id="ps-loader-ver-field"><label>${t('ctx.loaderVersion')}</label><input type="text" id="ps-loader-ver" value="${this.esc(inst.loaderVersion||'')}" placeholder="0.16.0" ${isServer?'disabled':''}></div>
                    ${isServer?'<p style="font-size:12px;color:var(--text-muted)">'+t('mods.disabled')+' — server pack is read-only</p>':''}
                    <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:8px">
                        <button class="btn-secondary" id="ps-cancel">${t('ctx.cancel')}</button>
                        <button class="btn-primary" id="ps-save" ${isServer?'disabled':''}>${t('ctx.save')}</button>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(modal);
        modal.addEventListener('click', e=>{ if(e.target===modal) modal.remove(); });
        document.getElementById('ps-close').addEventListener('click', ()=>modal.remove());
        document.getElementById('ps-cancel').addEventListener('click', ()=>modal.remove());
        const loaderSel = document.getElementById('ps-loader');
        loaderSel?.addEventListener('change', e=>{
            const f=document.getElementById('ps-loader-ver-field');
            if(e.target.value==='vanilla') f.classList.add('hidden'); else f.classList.remove('hidden');
        });
        document.getElementById('ps-save').addEventListener('click', async ()=>{
            const newName = document.getElementById('ps-name').value.trim();
            const loader = document.getElementById('ps-loader').value;
            const loaderVer = document.getElementById('ps-loader-ver').value.trim();
            const body = {name: inst.name};
            if (newName && newName!==inst.name) body.newName=newName;
            if (loader) body.loader=loader;
            if (loaderVer) body.loaderVersion=loaderVer;
            const r = await this.req('/pack/settings', {method:'POST', body: JSON.stringify(body)});
            if (r.success) { modal.remove(); this.toast(t('ctx.save')+' ✓','success'); await this.loadInstances(); if(newName) this.state.selectedPack=null; }
            else this.toast(r.error||'Error','error');
        });
    }
    async deletePack(name) {
        if (!confirm(tr('ctx.deleteConfirm', null, {name}))) return;
        const r = await this.req('/pack/delete', {method:'POST', body: JSON.stringify({name})});
        if (r.success) { this.toast(t('ctx.delete')+': '+name,'success'); if(this.state.selectedPack?.name===name) this.showEmptyState(); await this.loadInstances(); }
        else this.toast(r.error||'Error','error');
    }

    // ==================== ONBOARDING ====================
    async checkOnboarding() {
        // show only on first login or version diff, highlight-only — persist via Config + localStorage
        const curVer = (document.getElementById('version')?.textContent || document.getElementById('header-version')?.textContent || '1.1.1').trim() || '1.1.1';
        let storedVer = localStorage.getItem('zern_onboardVersion') || '';
        let completed = (localStorage.getItem('zern_onboardBlocks') || '').split(',').filter(Boolean);
        try {
            const r = await this.req('/settings');
            if (r.success && r.data) {
                const srvVer = r.data.onboardedVersion || '';
                const srvBlocks = (r.data.completedOnboardBlocks || '').split(',').filter(Boolean);
                if (!storedVer && srvVer) { storedVer = srvVer; localStorage.setItem('zern_onboardVersion', srvVer); }
                if (completed.length===0 && srvBlocks.length) { completed = srvBlocks; localStorage.setItem('zern_onboardBlocks', srvBlocks.join(',')); }
                // if local newer, sync to server
                if (storedVer && !srvVer) { this.req('/settings', {method:'POST', body: JSON.stringify({onboardedVersion: storedVer, completedOnboardBlocks: completed.join(',')})}); }
            }
        } catch(e){}
        const firstLogin = !storedVer; // no onboard yet
        const BLOCKS = [
            {id:'install',  titleKey:'onboard.title.install',  descKey:'onboard.desc.install',  sel:['#add-server-pack-btn','#add-pack-btn','#server-packs-header .btn-icon','#local-packs-header .btn-icon'], addedIn:'1.1.1'},
            {id:'settings', titleKey:'onboard.title.settings', descKey:'onboard.desc.settings', sel:'#settings-btn', addedIn:'1.1.1'},
            {id:'pass',     titleKey:'onboard.title.pass',     descKey:'onboard.desc.pass',     sel:'#pass-code', addedIn:'1.1.1', requiresView:'settings'},
            {id:'manage',   titleKey:'onboard.title.manage',   descKey:'onboard.desc.manage',   sel:['.pack-entry','#selected-pack-title','#pack-empty-state'], addedIn:'1.1.1'},
            {id:'mods',     titleKey:'onboard.title.mods',     descKey:'onboard.desc.mods',     sel:'#local-mods-section', addedIn:'1.1.1'},
            {id:'logs',     titleKey:'onboard.title.logs',     descKey:'onboard.desc.logs',     sel:'#show-log-viewer-btn', addedIn:'1.1.1'},
        ];
        let toShow;
        if (firstLogin) toShow = BLOCKS;
        else {
            // version diff: show only blocks added after storedVer
            const isNewer = (a,b)=> a.localeCompare(b, undefined, {numeric:true})>0;
            toShow = BLOCKS.filter(b=> !completed.includes(b.id) && isNewer(b.addedIn, storedVer));
            if (toShow.length===0 && storedVer !== curVer) {
                // version bump but all blocks already completed -> no show
                localStorage.setItem('zern_onboardVersion', curVer);
                return;
            }
        }
        if (toShow.length===0) return;
        this.showOnboarding(toShow, curVer, completed);
    }

    showOnboarding(blocks, curVer, completed) {
        let idx = 0;
        const overlay = document.createElement('div');
        overlay.className = 'onboard-overlay';
        overlay.id = 'onboard-overlay';
        document.body.appendChild(overlay);
        const card = document.createElement('div');
        card.className = 'onboard-card';
        card.id = 'onboard-card';
        document.body.appendChild(card);
        const spot = document.createElement('div');
        spot.className = 'onboard-spot';
        spot.id = 'onboard-spot';
        const label = document.createElement('div');
        label.className = 'onboard-spot-label';
        label.id = 'onboard-spot-label';
        document.body.appendChild(spot);
        document.body.appendChild(label);

        const resolveTarget = (b) => {
            const sels = Array.isArray(b.sel) ? b.sel : [b.sel];
            for (const s of sels) {
                for (const el of document.querySelectorAll(s)) {
                    const r = el.getBoundingClientRect();
                    const style = getComputedStyle(el);
                    const visible = r.width>0 && r.height>0 && style.display!=='none' && style.visibility!=='hidden' && el.offsetParent!==null;
                    if (!visible) continue;
                    if (el.closest('.hidden')) continue;
                    // check collapsed accordion
                    if (el.closest('.pack-list-collapse') && el.closest('.section-collapsible.collapsed')) continue;
                    return el;
                }
            }
            return null;
        };
        const ensureView = async (b) => {
            if (b.requiresView==='settings' || b.id==='pass' || b.id==='logs') {
                this.switchView('settings');
                await new Promise(r=> requestAnimationFrame(()=> requestAnimationFrame(r)));
            } else if (b.id==='install' || b.id==='manage' || b.id==='mods') {
                this.switchView('packs');
                await new Promise(r=> requestAnimationFrame(()=> requestAnimationFrame(r)));
            }
        };
        const persist = (vers, blocksArr) => {
            const v = vers || curVer;
            const b = blocksArr.join(',');
            localStorage.setItem('zern_onboardBlocks', b);
            localStorage.setItem('zern_onboardVersion', v);
            // persist to backend Config so WebView localStorage volatility doesn't resurrect tutorial
            this.req('/settings', {method:'POST', body: JSON.stringify({onboardedVersion: v, completedOnboardBlocks: b})}).catch(()=>{});
        };
        const goNext = async () => {
            if (document.getElementById('onboard-never')?.checked) {
                const all = [...new Set(blocks.map(x=>x.id).concat(completed))];
                persist(curVer, all);
                closeAll(); return;
            }
            if (idx===blocks.length-1) {
                const newCompleted = [...new Set([...completed, ...blocks.map(x=>x.id)])];
                persist(curVer, newCompleted);
                closeAll();
            } else { idx++; await render(); }
        };
        const goBack = async () => { if(idx>0){ idx--; await render(); } };
        const showConfirm = (onYes) => {
            let m = document.createElement('div');
            m.className='modal-backdrop'; m.style.zIndex='410';
            m.innerHTML=`<div class="modal modal-sm"><div class="modal-head"><h3>${t('onboard.confirmTitle')||'Точно пропустить туториал?'}</h3><button class="modal-close" id="c-close">&times;</button></div><div class="modal-body"><p style="font-size:13px;color:var(--text-secondary)">${t('onboard.confirmDesc')||'Вы уверены?'}</p><div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px"><button class="btn-secondary btn-sm" id="c-no">${t('onboard.no')||'Нет'}</button><button class="btn-primary btn-sm" id="c-yes">${t('onboard.yes')||'Да'}</button></div></div></div>`;
            document.body.appendChild(m);
            const close=()=>m.remove();
            m.querySelector('#c-close').onclick=close;
            m.querySelector('#c-no').onclick=close;
            m.addEventListener('click', e=>{ if(e.target===m) close(); });
            m.querySelector('#c-yes').onclick=()=>{ close(); onYes(); };
        };
        const showInfo = () => {
            let m=document.createElement('div'); m.className='modal-backdrop'; m.style.zIndex='411';
            m.innerHTML=`<div class="modal modal-sm"><div class="modal-head"><h3>${t('onboard.infoTitle')||'Туториал'}</h3><button class="modal-close" id="i-close">&times;</button></div><div class="modal-body"><p style="font-size:13px;color:var(--text-secondary)">${t('onboard.infoDesc')||'Туториал можно будет пройти в настройках.'}</p><div style="display:flex;justify-content:flex-end;margin-top:16px"><button class="btn-primary btn-sm" id="i-ok">OK</button></div></div></div>`;
            document.body.appendChild(m);
            const close=()=>m.remove();
            m.querySelector('#i-close').onclick=close;
            m.querySelector('#i-ok').onclick=close;
            m.addEventListener('click', e=>{ if(e.target===m) close(); });
        };
        const requestClose = () => {
            showConfirm(()=>{ closeAll(); showInfo(); });
        };
        const render = async () => {
            const b = blocks[idx];
            await ensureView(b);
            card.innerHTML = `
                <div class="onboard-progress">${tr('onboard.progress', null, {current:String(idx+1), total:String(blocks.length)})}</div>
                <h3>${t(b.titleKey)}</h3>
                <p>${t(b.descKey)}</p>
                <div class="onboard-actions">
                    <button class="btn-secondary btn-sm" id="onboard-skip">${t('onboard.skip')}</button>
                    ${idx>0?'<button class="btn-secondary btn-sm" id="onboard-back">'+t('onboard.back')+'</button>':''}
                    <button class="btn-primary btn-sm" id="onboard-next">${idx===blocks.length-1? t('onboard.done'): t('onboard.next')}</button>
                </div>
                <label style="display:flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--text-muted)"><input type="checkbox" id="onboard-never"> ${t('onboard.neverShow')}</label>
            `;
            const el = resolveTarget(b);
            if (el) {
                const r = el.getBoundingClientRect();
                spot.style.left = (r.left-4)+'px';
                spot.style.top = (r.top-4)+'px';
                spot.style.width = (r.width+8)+'px';
                spot.style.height = (r.height+8)+'px';
                spot.style.display='block';
                label.textContent = t(b.titleKey);
                label.style.left = (r.left + r.width/2)+'px';
                label.style.top = (r.top)+'px';
                label.style.display='block';
                el.scrollIntoView({block:'center', behavior:'smooth'});
            } else {
                spot.style.display='none'; label.style.display='none';
            }
            document.getElementById('onboard-skip').addEventListener('click', requestClose);
            document.getElementById('onboard-next').addEventListener('click', goNext);
            const backBtn=document.getElementById('onboard-back');
            if(backBtn) backBtn.addEventListener('click', goBack);
        };
        const closeAll = ()=>{
            overlay.remove(); card.remove(); spot.remove(); label.remove();
            document.removeEventListener('keydown', escHandler);
        };
        spot.addEventListener('click', (e)=>{ e.stopPropagation(); goNext(); });
        label.addEventListener('click', (e)=>{ e.stopPropagation(); goNext(); });
        const escHandler = (e)=>{ if(e.key==='Escape') requestClose(); };
        document.addEventListener('keydown', escHandler);
        overlay.addEventListener('click', (e)=>{ if(e.target===overlay) requestClose(); });
        render();
    }
}

function renderFriendItem(f) {
    var initial = (f.username || '?')[0].toUpperCase();
    var statusClass = f.online ? 'online' : 'offline';
    var statusText = f.online ? t('friends.online') : t('friends.offline');
    var packText = f.online && f.current_pack ? tr('friends.playing', null, {pack: f.current_pack}) : (f.online ? t('friends.noPack') : '');
    var roleBadge = f.role && f.role > 0 ? '<span class="badge badge-role badge-sm">' + app.esc('Role ' + f.role) + '</span>' : '';
    return '<div class="friend-item" data-user-id="' + f.id + '">' +
        '<div class="friend-avatar ' + statusClass + '">' + initial + '</div>' +
        '<div class="friend-info">' +
        '<div class="friend-name-row">' +
        '<span class="friend-name">' + app.esc(f.username) + '</span>' +
        roleBadge +
        '</div>' +
        '<div class="friend-status">' +
        '<span class="friend-status-dot ' + statusClass + '"></span>' +
        '<span>' + statusText + '</span>' +
        (packText ? '<span class="friend-pack"> \u00b7 ' + packText + '</span>' : '') +
        '</div>' +
        '</div>' +
        '<button class="btn-icon friend-remove-btn" title="Remove" onclick="app.removeFriend(' + f.id + ')">' +
        '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
        '</button></div>';
}

var renderer = new marked.Renderer();
renderer.image = function(token) {
    var src = token.href;
    var alt = (token.text || '').replace(/"/g, '&quot;');
    var titleAttr = token.title ? ' title="' + token.title.replace(/"/g, '&quot;') + '"' : '';
    return '<img class="news-photo" src="' + src.replace(/"/g, '&quot;') + '" alt="' + alt + '"' + titleAttr + ' style="cursor:pointer" onclick="window.open(\'' + src.replace(/'/g, "\\'") + '\', \'_blank\')">';
};
renderer.link = function(token) {
    var href = token.href;
    var text = marked.parseInline(token.text);
    var titleAttr = token.title ? ' title="' + token.title.replace(/"/g, '&quot;') + '"' : '';
    return '<a href="#" class="news-link"' + titleAttr + ' onclick="window.open(\'' + href.replace(/'/g, "\\'") + '\',\'_blank\');return false">' + text + '</a>';
};
marked.setOptions({ renderer: renderer });

var app = new ZernMCLauncher();
