Files
Haven/public/app.html
T

5218 lines
424 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-haven-theme-api="1">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="/manifest.webmanifest">
<title>Haven</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cG9seWdvbiBwb2ludHM9IjUwLDMgOTMsMjggOTMsNzIgNTAsOTcgNyw3MiA3LDI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM2YjRmZGIiIHN0cm9rZS13aWR0aD0iOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjx0ZXh0IHg9IjUwIiB5PSI2MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLEhlbHZldGljYSxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iYm9sZCIgZm9udC1zaXplPSIzOCIgZmlsbD0iIzZiNGZkYiIgb3BhY2l0eT0iMC45NSI+SDwvdGV4dD48L3N2Zz4=">
<link rel="stylesheet" href="/css/style.css?v=4.22.25">
<script src="/js/theme-compat.js?v=4.18.3"></script>
<script src="/js/theme-init.js?v=4.22.9"></script>
</head>
<body data-haven-page="app">
<div id="app" data-haven-region="app-shell">
<div id="app-body" data-haven-region="workspace">
<!-- ─── Server Bar (far left, like Discord) ─────── -->
<nav class="server-bar" id="server-bar" data-haven-region="server-rail">
<div class="server-icon home active" id="home-server" data-i18n-title="app.sidebar.this_server" title="This Server">
<span class="server-icon-text">⬡</span>
<span class="server-status-dot online"></span>
</div>
<div class="server-separator"></div>
<div id="server-list" data-haven-region="server-list"></div>
<button class="server-icon add-server" id="add-server-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">
<span class="server-icon-text">+</span>
</button>
<button class="server-icon manage-servers" id="manage-servers-btn" data-i18n-title="app.sidebar.manage_servers" title="Manage Servers">
<span class="server-icon-text">⚙</span>
</button>
<button class="server-icon sync-servers" id="sync-servers-btn" data-i18n-title="app.sidebar.sync_servers" title="Sync Server List">
<span class="server-icon-text">⟳</span>
</button>
</nav>
<!-- ─── Left Sidebar ────────────────────────────── -->
<aside class="sidebar" data-haven-region="navigation-sidebar">
<div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
<div class="sidebar-header">
<div class="brand">
<span class="logo-sm">⬡</span>
<span class="brand-text">HAVEN</span>
</div>
<button id="mobile-sidebar-close" class="mobile-panel-close" data-i18n-title="modals.common.close" data-i18n-aria-label="app.actions.close_sidebar" title="Close" aria-label="Close sidebar">&times;</button>
<div class="user-bar" data-haven-region="account">
<span class="led on" id="connection-led" data-i18n-title="app.status.server_connection" title="Server connection"></span>
<div class="user-names">
<span class="current-user" id="current-user"></span>
<span class="login-name" id="login-name" data-i18n-title="app.profile.login_username" title="Your login username"></span>
</div>
<button id="rename-btn" class="icon-btn small" data-i18n-title="app.actions.change_display_name" title="Change display name">✏️</button>
<button id="logout-btn" class="icon-btn" data-i18n-title="app.actions.logout" title="Logout">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
</div>
</div>
<div class="sidebar-mod-container" id="sidebar-mod-container" data-haven-region="sidebar-content">
<!-- Mobile server bubbles (visible only on mobile) -->
<div class="sidebar-section mobile-servers-section" id="mobile-servers-section">
<h5 class="section-label mobile-servers-toggle" id="mobile-servers-toggle" style="cursor:pointer;user-select:none">
<span class="collapse-toggle-arrow" id="mobile-servers-arrow">▾</span>
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.servers">Servers</span>
</h5>
<div class="mobile-servers-row" id="mobile-servers-row">
<div class="mobile-servers-scroll" id="mobile-servers-scroll"></div>
<button class="mobile-srv-add-btn" id="mobile-srv-add-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">+</button>
</div>
</div>
<div class="sidebar-section" data-mod-id="join" data-haven-region="join-channel">
<h5 class="section-label collapsible-section-toggle" id="join-section-toggle" style="cursor:pointer;user-select:none">
<span class="collapse-toggle-arrow" id="join-section-arrow">▾</span>
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.join_channel">Join a Channel</span>
</h5>
<div class="collapsible-section-body" id="join-section-body">
<div class="input-row">
<input type="text" id="channel-code-input"
data-i18n-placeholder="app.sidebar.join_placeholder" placeholder="Enter code..." maxlength="8" spellcheck="false">
<button id="join-channel-btn" class="btn-sm btn-accent" data-i18n="app.sidebar.join_btn">Join</button>
</div>
</div>
</div>
<div class="sidebar-section" id="admin-controls" data-mod-id="create" data-haven-region="create-channel" style="display:none">
<h5 class="section-label collapsible-section-toggle" id="create-section-toggle" style="cursor:pointer;user-select:none">
<span class="collapse-toggle-arrow" id="create-section-arrow">▾</span>
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.create_channel">Create Channel</span>
</h5>
<div class="collapsible-section-body" id="create-section-body">
<div class="input-row select-row" id="new-channel-template-row" style="gap:6px;margin-bottom:4px">
<select id="new-channel-template" data-i18n-title="app.sidebar.templates.hint" title="Start from a template: it ticks the boxes and sets topic, read-only, announcement and slow mode for you" style="flex:1;font-size:0.8rem"></select>
<button id="new-channel-template-del" class="btn-sm" type="button" data-i18n-title="app.sidebar.templates.delete_title" title="Delete this saved template" style="display:none">🗑</button>
</div>
<div class="input-row">
<input type="text" id="new-channel-name"
data-i18n-placeholder="app.sidebar.channel_name_placeholder" placeholder="Channel name..." maxlength="50">
<button id="create-channel-btn" class="btn-sm btn-accent">+</button>
</div>
<label class="checkbox-row" data-i18n-title="app.sidebar.private_hint" title="Invite-only" style="margin:6px 0 2px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
<input type="checkbox" id="new-channel-private">
<span>🔒 <span data-i18n="app.sidebar.private_channel">Private</span></span>
</label>
<label class="checkbox-row" data-i18n-title="app.sidebar.temporary_hint" title="Auto-deletes when empty" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
<input type="checkbox" id="new-channel-temporary">
<span>⏱️ <span data-i18n="app.sidebar.temporary_channel">Temporary</span></span>
</label>
<label class="checkbox-row" data-i18n-title="app.sidebar.forum_hint" title="Each message starts a topic; replies go in its thread" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
<input type="checkbox" id="new-channel-forum">
<span>🗂️ <span data-i18n="app.sidebar.forum_channel">Forum</span></span>
</label>
<label class="checkbox-row" id="new-channel-add-all-row" data-i18n-title="app.sidebar.add_all_hint" title="Everyone joins automatically" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
<input type="checkbox" id="new-channel-add-all">
<span>👥 <span data-i18n="app.sidebar.add_all_members">Add all members</span></span>
</label>
<div id="temp-channel-duration-row" style="display:none;margin:4px 0 2px;padding-left:22px">
<div class="input-row" style="gap:6px;align-items:center">
<input type="number" id="new-channel-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" data-i18n-placeholder="app.sidebar.hours" placeholder="Hours">
<span style="font-size:0.75rem;opacity:0.6" data-i18n="app.sidebar.hours">hours</span>
</div>
</div>
</div>
</div>
<div class="sidebar-split" id="sidebar-split" data-mod-id="channels">
<div class="sidebar-section channel-section" id="channels-pane" data-haven-region="channels">
<h5 class="section-label channels-toggle" id="channels-toggle" style="cursor:pointer;user-select:none"><span class="channels-toggle-arrow" id="channels-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.channels">Channels</span><button class="icon-btn small" id="sub-channel-panel-btn" data-i18n-title="app.actions.sub_channel_subscriptions" title="Sub-channel subscriptions" style="font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2;display:none">📡</button><button class="icon-btn small arrange-channels-btn" id="arrange-channels-btn" data-i18n-title="channels.arrange.button" title="Arrange my channels" style="font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2">⇅</button><button class="icon-btn small" id="organize-channels-btn" data-i18n-title="app.actions.organize_channels" title="Organize channels" style="display:none;font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2">📋</button></h5>
<div class="channel-list" id="channel-list"></div>
</div>
<div class="sidebar-split-handle" id="sidebar-split-handle" data-i18n-title="app.actions.drag_to_resize" title="Drag to resize"></div>
<div class="sidebar-section dm-section-pane" id="dm-pane" data-haven-region="direct-messages">
<h5 class="section-label dm-section-label dm-toggle" id="dm-toggle-header" style="cursor:pointer;user-select:none"><span class="dm-toggle-arrow" id="dm-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.direct_messages">Direct Messages</span><span style="display:flex;align-items:center;gap:4px;position:relative;z-index:2"><span class="dm-unread-count" id="dm-unread-badge" style="display:none"></span><button class="icon-btn small" id="new-group-dm-btn" data-i18n-title="groups.new_group" title="New group DM" style="font-size:0.85rem;padding:2px 6px;opacity:0.6">👥</button><button class="icon-btn small" id="organize-dms-btn" data-i18n-title="app.actions.organize_dms" title="Organize DMs" style="font-size:0.85rem;padding:2px 6px;opacity:0.6">📋</button></span></h5>
<div class="dm-list-scroll" id="dm-list"></div>
</div>
</div>
</div>
<!-- Pinned bottom area (never pushed off screen) -->
<div class="sidebar-bottom" data-haven-region="sidebar-footer">
<!-- Theme popup (floats above bottom bar) -->
<div id="theme-popup" class="theme-popup" style="display:none">
<div class="theme-popup-header">
<span class="theme-popup-title" data-i18n="app.theme.title">Theme</span>
<button id="theme-popup-close" class="icon-btn small" data-i18n-title="app.theme.close" title="Close">&times;</button>
</div>
<div class="theme-selector" id="theme-selector" data-haven-region="theme-picker">
<button class="theme-btn active" data-theme="haven" title="Haven"><span class="theme-icon">⬡</span></button>
<button class="theme-btn" data-theme="discord" title="Discord"><span class="theme-icon">🎮</span></button>
<button class="theme-btn" data-theme="matrix" title="Matrix"><span class="theme-icon">Ⅿ</span></button>
<button class="theme-btn" data-theme="tron" title="Tron"><span class="theme-icon">◈</span></button>
<button class="theme-btn" data-theme="halo" title="HALO"><span class="theme-icon">⌁</span></button>
<button class="theme-btn" data-theme="lotr" title="LoTR"><span class="theme-icon">⚜</span></button>
<button class="theme-btn" data-theme="cyberpunk" title="Cyberpunk"><span class="theme-icon">⚡</span></button>
<button class="theme-btn" data-theme="nord" title="Nord"><span class="theme-icon">❄</span></button>
<button class="theme-btn" data-theme="dracula" title="Dracula"><span class="theme-icon">🧛</span></button>
<button class="theme-btn" data-theme="bloodborne" title="Bloodborne"><span class="theme-icon">🩸</span></button>
<button class="theme-btn" data-theme="darksouls" title="Dark Souls"><span class="theme-icon">🔥</span></button>
<button class="theme-btn" data-theme="eldenring" title="Elden Ring"><span class="theme-icon">💍</span></button>
<button class="theme-btn" data-theme="ice" title="Ice"><span class="theme-icon">🧊</span></button>
<button class="theme-btn" data-theme="abyss" title="Abyss"><span class="theme-icon">🌊</span></button>
<button class="theme-btn" data-theme="minecraft" title="Minecraft"><span class="theme-icon">⛏️</span></button>
<button class="theme-btn" data-theme="ffx" title="Final Fantasy X"><span class="theme-icon">⚔️</span></button>
<button class="theme-btn" data-theme="zelda" title="Zelda"><span class="theme-icon">🗡️</span></button>
<button class="theme-btn" data-theme="fallout" title="Fallout (PIP Boy)"><span class="theme-icon">☢️</span></button>
<button class="theme-btn" data-theme="scripture" title="Scripture"><span class="theme-icon">✝️</span></button>
<button class="theme-btn" data-theme="chapel" title="Chapel"><span class="theme-icon">⛪</span></button>
<button class="theme-btn" data-theme="gospel" title="Gospel"><span class="theme-icon">🕊️</span></button>
<button class="theme-btn" data-theme="midnightpurple" title="Midnight Purple"><span class="theme-icon">🔮</span></button>
<button class="theme-btn" data-theme="crt" title="CRT"><span class="theme-icon">📺</span></button>
<button class="theme-btn" data-theme="win95" title="Windows 95"><span class="theme-icon">🪟</span></button>
<button class="theme-btn" data-theme="custom" title="Custom"><span class="theme-icon" id="custom-theme-swatch">🎨</span></button>
<button class="theme-btn" data-theme="rgb" title="RGB"><span class="theme-icon">🌈</span></button>
<button class="theme-btn" data-theme="daylight" title="Daylight (Light)"><span class="theme-icon">☀️</span></button>
<button class="theme-btn" data-theme="cloudy" title="Cloudy (Light)"><span class="theme-icon">☁️</span></button>
</div>
<!-- Effect overlay picker (mashup) -->
<div class="effect-overlay-section">
<div class="theme-popup-title" style="margin-top: 2px;" data-i18n="app.theme.effect_overlay">Effect Overlay</div>
<div class="effect-selector" id="effect-selector">
<button class="effect-btn active" data-effect="auto" data-i18n-title="app.theme.match_theme" title="Match theme">⟳</button>
<button class="effect-btn" data-effect="none" data-i18n-title="app.theme.no_effects" title="No effects">🚫</button>
<button class="effect-btn" data-effect="crt" data-i18n-title="app.theme.crt_scanlines" title="CRT Scanlines">📺</button>
<button class="effect-btn" data-effect="matrix" data-i18n-title="app.theme.matrix_rain" title="Matrix Rain">Ⅿ</button>
<button class="effect-btn" data-effect="matrixbars" data-i18n-title="app.theme.matrix_scan_lines" title="Matrix Scan Lines">▤</button>
<button class="effect-btn" data-effect="nord" data-i18n-title="app.theme.snowfall" title="Snowfall">❄</button>
<button class="effect-btn" data-effect="darksouls" data-i18n-title="app.theme.campfire" title="Campfire">🔥</button>
<button class="effect-btn" data-effect="eldenring" data-i18n-title="app.theme.golden_grace" title="Golden Grace">💍</button>
<button class="effect-btn" data-effect="bloodborne" data-i18n-title="app.theme.blood_vignette" title="Blood Vignette">🩸</button>
<button class="effect-btn" data-effect="fallout" data-i18n-title="app.theme.phosphor_glow" title="Phosphor Glow">☢️</button>
<button class="effect-btn" data-effect="ffx" data-i18n-title="app.theme.water_flow" title="Water Flow">⚔️</button>
<button class="effect-btn" data-effect="ice" data-i18n-title="app.theme.frost_shimmer" title="Frost Shimmer">🧊</button>
<button class="effect-btn" data-effect="cyberpunk" data-i18n-title="app.theme.glitch" title="Glitch">⚡</button>
<button class="effect-btn" data-effect="lotr" data-i18n-title="app.theme.candlelight" title="Candlelight">⚜</button>
<button class="effect-btn" data-effect="abyss" data-i18n-title="app.theme.ocean_depth" title="Ocean Depth">🌊</button>
<button class="effect-btn" data-effect="scripture" data-i18n-title="app.theme.cross_light" title="Cross Light">✝️</button>
<button class="effect-btn" data-effect="chapel" data-i18n-title="app.theme.stained_glass" title="Stained Glass">⛪</button>
<button class="effect-btn" data-effect="gospel" data-i18n-title="app.theme.divine_radiance" title="Divine Radiance">🕊️</button>
</div>
</div>
<!-- Custom theme triangle picker -->
<div id="custom-theme-editor" style="display:none">
<canvas id="custom-hue-bar" width="206" height="18"></canvas>
<canvas id="custom-triangle" width="206" height="180"></canvas>
<label class="toggle-row theme-base-toggle">
<span data-i18n="app.theme.light_mode">Light Mode</span>
<input type="checkbox" data-color-base-toggle>
</label>
</div>
<!-- RGB theme controls -->
<div id="rgb-theme-editor" class="rgb-theme-editor" style="display:none">
<label class="toggle-row theme-base-toggle">
<span data-i18n="app.theme.light_mode">Light Mode</span>
<input type="checkbox" data-color-base-toggle>
</label>
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.speed">Speed</span>
<input type="range" id="rgb-speed-slider" min="1" max="100" value="30" class="slider-sm rgb-slider">
</label>
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.vibrancy">Vibrancy</span>
<input type="range" id="rgb-vibrancy-slider" min="10" max="100" value="75" class="slider-sm rgb-slider">
</label>
</div>
<!-- Effect speed slider (dynamic-effect themes) -->
<div id="effect-speed-editor" class="rgb-theme-editor" style="display:none">
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.effect_speed">Effect Speed</span>
<input type="range" id="effect-speed-slider" min="15" max="200" value="100" class="slider-sm rgb-slider">
</label>
</div>
<!-- Sacred intensity slider (religious themes) -->
<div id="sacred-intensity-editor" class="rgb-theme-editor" style="display:none">
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.sacred_intensity">Sacred Intensity</span>
<input type="range" id="sacred-intensity-slider" min="20" max="250" value="100" class="slider-sm rgb-slider">
</label>
</div>
<!-- Glitch frequency slider (cyberpunk) -->
<div id="glitch-freq-editor" class="rgb-theme-editor" style="display:none">
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.glitch_frequency">Glitch Frequency</span>
<input type="range" id="glitch-freq-slider" min="5" max="100" value="50" class="slider-sm rgb-slider">
</label>
</div>
<!-- CRT effect sliders (vignette darkness + scanline intensity) -->
<div id="crt-editor" class="rgb-theme-editor" style="display:none">
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.crt_vignette">CRT Vignette</span>
<input type="range" id="crt-vignette-slider" min="0" max="100" value="50" class="slider-sm rgb-slider">
</label>
<label class="rgb-slider-row">
<span class="rgb-slider-label" data-i18n="app.theme.crt_scanlines">CRT Scanlines</span>
<input type="range" id="crt-scanline-slider" min="0" max="80" value="45" class="slider-sm rgb-slider">
</label>
</div>
</div>
<!-- Bottom bar -->
<div id="voice-bar" class="voice-bar" style="display:none"></div>
<div class="sidebar-bottom-bar" data-haven-region="sidebar-actions">
<button id="theme-popup-toggle" class="sidebar-bottom-btn" data-i18n-title="app.theme.title" title="Themes">🎨</button>
<button class="sidebar-bottom-btn" id="activities-btn" data-i18n-title="modals.activities.title" title="Activities">🎮</button>
<button class="sidebar-bottom-btn" id="sidebar-members-btn" data-i18n-title="modals.all_members.title" title="All Members" style="display:none">👥</button>
<button id="mobile-settings-btn" class="sidebar-bottom-btn mobile-settings-btn" data-i18n-title="settings.title" title="Settings">⚙️</button>
<span style="flex:1"></span>
<button id="donors-btn" class="sidebar-bottom-btn donate-btn">❤️</button>
<button id="voice-mute-btn" class="sidebar-bottom-btn voice-header-btn" data-i18n-title="voice.mute" title="Mute" style="display:none">🎙️</button>
<button id="voice-deafen-btn" class="sidebar-bottom-btn voice-header-btn" data-i18n-title="voice.deafen" title="Deafen" style="display:none">🔊</button>
</div>
</div>
</aside>
<!-- ─── Main Content ────────────────────────────── -->
<main class="main" data-haven-region="main">
<!-- Server Banner (always behind all content) -->
<div id="server-banner-display" class="server-banner-display" style="display:none">
<img id="server-banner-img" class="server-banner-img" src="" data-i18n-alt="media.server_banner_alt" alt="Server banner">
</div>
<header class="channel-header" data-haven-region="channel-header">
<button id="mobile-menu-btn" class="mobile-menu-btn" data-i18n-title="header.mobile_menu" data-i18n-aria-label="header.mobile_menu" title="Menu" aria-label="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<div class="mobile-server-dropdown" id="mobile-server-dropdown">
<button class="mobile-server-btn" id="mobile-server-btn" data-i18n-title="header.mobile_servers" data-i18n-aria-label="header.mobile_servers" title="Servers" aria-label="Servers">⬡</button>
<div class="mobile-server-menu" id="mobile-server-menu">
<div class="mobile-server-menu-header" data-i18n="header.mobile_servers">Servers</div>
<div id="mobile-server-list"></div>
<button class="mobile-server-add" id="mobile-server-add-btn" data-i18n="header.mobile_add_server">+ Add Server</button>
</div>
</div>
<div class="channel-info">
<h3 id="channel-header-name" data-i18n="header.select_channel">Select a channel</h3>
</div>
<div class="header-actions-box" id="header-actions-box" style="display:none">
<button id="thread-mentions-pill" class="icon-btn small thread-mentions-pill" data-i18n-title="header.thread_mentions" title="Mentions in threads" style="display:none">🔔 <span id="thread-mentions-pill-count">0</span></button>
<span class="channel-code-tag" id="channel-code-display" data-i18n-title="header.channel_code" title="Channel code"></span>
<button id="copy-code-btn" class="icon-btn small" data-i18n-title="header.copy_code" title="Copy code" data-i18n-aria-label="header.copy_code" aria-label="Copy code">📋</button>
<button id="channel-code-settings-btn" class="icon-btn small" data-i18n-title="header.channel_code_settings" title="Channel code settings" data-i18n-aria-label="header.channel_code_settings" aria-label="Channel code settings">⚙️</button>
<span class="header-actions-divider"></span>
<button id="search-toggle-btn" class="icon-btn small" data-i18n-title="header.search_messages" title="Search messages (Ctrl+F)" data-i18n-aria-label="header.search_messages" aria-label="Search messages (Ctrl+F)">🔍</button>
<button id="pinned-toggle-btn" class="icon-btn small" data-i18n-title="header.pinned_messages" title="Pinned messages" data-i18n-aria-label="header.pinned_messages" aria-label="Pinned messages">📌</button>
<button id="gallery-toggle-btn" class="icon-btn small" data-i18n-title="header.media_gallery" title="View files & media" data-i18n-aria-label="header.media_gallery" aria-label="View files & media">🖼</button>
<button id="threads-toggle-btn" class="icon-btn small" data-i18n-title="header.thread_list" title="View threads" data-i18n-aria-label="header.thread_list" aria-label="View threads">🧵</button>
<button id="move-select-btn" class="icon-btn small" data-i18n-title="header.select_messages_to_move" title="Select messages to move" data-i18n-aria-label="header.select_messages_to_move" aria-label="Select messages to move" style="display:none">☰</button>
<div class="e2e-menu-wrapper" style="display:none" id="e2e-menu-wrapper">
<button id="e2e-menu-btn" class="icon-btn small" data-i18n-title="header.encryption_options" title="Encryption options" data-i18n-aria-label="header.encryption_options" aria-label="Encryption options">🔐</button>
<div class="e2e-dropdown" id="e2e-dropdown" style="display:none">
<button class="e2e-dropdown-item" id="e2e-verify-btn">🔑 <span data-i18n="header.verify_encryption">Verify Encryption</span></button>
<button class="e2e-dropdown-item" id="e2e-recover-btn">🔄 <span data-i18n="header.recover_encryption">Recover Keys from Backup</span></button>
<button class="e2e-dropdown-item e2e-danger" id="e2e-reset-btn">⚠️ <span data-i18n="header.reset_encryption">Reset Encryption Keys</span></button>
</div>
</div>
</div>
<div class="search-container" id="search-container" style="display:none">
<input type="text" id="search-input" class="search-input" data-i18n-placeholder="header.search_placeholder" placeholder="Search... from:user in:#channel has:image" maxlength="200">
<button id="search-filter-btn" class="icon-btn small" data-i18n-title="header.filter_toggle" title="Filters" data-i18n-aria-label="header.filter_toggle" aria-label="Filters">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
</button>
<button id="search-close-btn" class="icon-btn small" data-i18n-title="header.search_close" title="Close search" data-i18n-aria-label="header.search_close" aria-label="Close search">&times;</button>
<!-- Filter picker popover: appears with the search box, appends
filter tokens client-side. Public search only. (phase 3) -->
<div id="search-filter-popover" class="search-filter-popover" style="display:none">
<div class="sfp-root" id="sfp-root">
<div class="sfp-recent" id="sfp-recent" style="display:none"></div>
<div class="sfp-chips">
<span class="sfp-label" data-i18n="header.filter_by">Filter by</span>
<button type="button" class="sfp-chip" data-sfp-filter="from">from:</button>
<button type="button" class="sfp-chip" data-sfp-filter="in">in:</button>
<button type="button" class="sfp-chip" data-sfp-filter="has">has:</button>
<button type="button" class="sfp-chip" data-sfp-filter="tag">tag:</button>
<button type="button" class="sfp-chip" data-sfp-filter="before">before:</button>
<button type="button" class="sfp-chip" data-sfp-filter="after">after:</button>
<button type="button" class="sfp-chip" data-sfp-filter="during">during:</button>
<button type="button" class="sfp-chip" data-sfp-filter="pinned">pinned</button>
</div>
<div class="sfp-sort" id="sfp-sort">
<span class="sfp-label" data-i18n="header.sort_by">Sort</span>
<button type="button" class="sfp-sort-btn" data-sort="newest" data-i18n="header.sort_newest">Newest</button>
<button type="button" class="sfp-sort-btn" data-sort="oldest" data-i18n="header.sort_oldest">Oldest</button>
<button type="button" class="sfp-sort-btn" data-sort="relevant" data-i18n="header.sort_relevant">Relevant</button>
</div>
</div>
<div class="sfp-picker" id="sfp-picker" style="display:none">
<div class="sfp-picker-head">
<button type="button" class="sfp-back" id="sfp-back" data-i18n-title="modals.common.back" title="Back" data-i18n-aria-label="modals.common.back" aria-label="Back">‹</button>
<input type="text" class="sfp-filter-input" id="sfp-filter-input" autocomplete="off" spellcheck="false" data-i18n-placeholder="header.filter_placeholder" placeholder="Filter...">
<div class="sfp-date-wrap" id="sfp-date-wrap" style="display:none">
<input type="date" class="sfp-date-input" id="sfp-date-input">
<button type="button" class="sfp-chip" id="sfp-date-apply" data-i18n="header.filter_apply">Apply</button>
</div>
</div>
<div class="sfp-list" id="sfp-list"></div>
</div>
</div>
</div>
<!-- Update available banner -->
<a id="update-banner" class="update-banner" href="#" style="display:none" target="_blank" rel="noopener">
<span class="update-pulse"></span>
<span class="update-text" data-i18n="header.update_available">Update Available</span>
</a>
<!-- Low disk warning (admins only, #5505) -->
<span id="disk-low-banner" class="disk-low-banner" style="display:none" role="status">
<span class="disk-low-dot"></span>
<span class="disk-low-text" data-i18n="banners.disk_low">Server disk almost full</span>
</span>
<!-- Desktop app banner -->
<!-- The dismiss button is a SIBLING of the link, not inside it. A
<button> nested in an <a> is invalid HTML: the parser hoists it
out, so clicking the X activated the anchor and opened the
download page instead of dismissing. Same trap as the Android
banner below, which hit it with nested <button>s. -->
<span id="desktop-app-banner" class="desktop-app-banner" style="display:none">
<a class="desktop-app-link" href="https://haven-app.com/#download" target="_blank" rel="noopener">
<span class="desktop-app-icon">🖥️</span>
<span class="desktop-app-text" data-i18n="header.get_desktop_app">Get the Desktop App</span>
</a>
<button type="button" class="desktop-app-dismiss" id="desktop-app-dismiss" data-i18n-title="modals.common.dismiss" data-i18n-aria-label="modals.common.dismiss" title="Dismiss" aria-label="Dismiss">&times;</button>
</span>
<!-- Android beta banner (must be <div>, not <button>; nesting
<button> inside <button> is invalid HTML and causes the
dismiss × to be hoisted out of its parent by the parser,
creating a phantom X in the channel header on mobile) -->
<div id="android-beta-banner" class="android-beta-banner" role="button" tabindex="0" style="display:none">
<span class="android-beta-icon">📱</span>
<span class="android-beta-text" data-i18n="header.android_beta">Android App</span>
<button class="android-beta-dismiss" id="android-beta-dismiss" data-i18n-title="modals.common.dismiss" data-i18n-aria-label="modals.common.dismiss" title="Dismiss" aria-label="Dismiss">&times;</button>
</div>
<!-- Spacer pushes voice controls to the right -->
<div style="flex:1"></div>
<!-- Right-side: dynamic indicators, then voice (always furthest right) -->
<div class="voice-controls">
<button id="mobile-users-btn" class="mobile-users-btn" data-i18n-title="modals.all_members.title" data-i18n-aria-label="modals.all_members.title" title="Members" aria-label="Members">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</button>
<!-- Dynamic indicators (music, streams) inject here via JS -->
<button id="voice-join-btn" class="btn-voice" style="display:none"><span data-i18n="voice.join">🎤 Join Voice</span></button>
<div id="voice-active-indicator" class="voice-active-indicator" style="display:none">
<span class="voice-indicator-icon">🎤</span>
<span class="voice-indicator-text" data-i18n="header.voice_active">Voice Active</span>
</div>
</div>
</header>
<!-- Welcome Screen -->
<div id="no-channel-msg" class="welcome-screen" data-haven-region="welcome">
<div class="welcome-content">
<div class="welcome-icon">⬡</div>
<h2 data-i18n="welcome.title">Welcome to Haven</h2>
<p data-i18n="welcome.subtitle">Join a channel with a code from your friends,<br>
or create one if you're the admin.</p>
</div>
</div>
<!-- Chat Area -->
<div id="message-area" class="message-area" data-haven-region="message-area" style="display:none">
<!-- Webcam Grid -->
<div id="webcam-container" class="webcam-container" data-haven-region="webcams" style="display:none">
<div class="webcam-header">
<span id="webcam-label">📷 <span data-i18n="media.cameras">Cameras</span></span>
<div class="webcam-header-controls">
<div class="webcam-layout-picker" id="webcam-layout-picker" data-i18n-title="media.camera_layout" title="Camera layout">
<button class="stream-layout-btn" id="webcam-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
<div class="stream-layout-menu" id="webcam-layout-menu">
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ <span data-i18n="media.layout_auto">Auto</span></button>
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ <span data-i18n="media.layout_stack">Stack</span></button>
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ <span data-i18n="media.layout_side">Side</span></button>
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ <span data-i18n="media.layout_2x2">2×2</span></button>
</div>
</div>
<label class="stream-size-label" data-i18n-title="media.camera_size" title="Camera display size">
📷
<input type="range" id="webcam-size-slider" class="stream-size-slider" min="15" max="70" value="25">
</label>
<button id="webcam-collapse-btn" class="icon-btn small" data-i18n-title="media.minimize_cameras" title="Minimize cameras">─</button>
<button id="webcam-close-btn" class="icon-btn small" data-i18n-title="media.close_cameras" title="Close cameras">✕</button>
</div>
</div>
<div id="webcam-grid" class="webcam-grid"></div>
</div>
<!-- Screen Share Viewer (multi-stream) -->
<div id="screen-share-container" class="screen-share-container" data-haven-region="screen-shares" style="display:none">
<div class="screen-share-header">
<span id="screen-share-label">🖥️ <span data-i18n="media.streams">Streams</span></span>
<div class="screen-share-header-controls">
<div class="stream-layout-picker" id="stream-layout-picker" data-i18n-title="media.stream_layout" title="Stream layout">
<button class="stream-layout-btn" id="stream-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
<div class="stream-layout-menu" id="stream-layout-menu">
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ <span data-i18n="media.layout_auto">Auto</span></button>
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ <span data-i18n="media.layout_stack">Stack</span></button>
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ <span data-i18n="media.layout_side">Side</span></button>
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ <span data-i18n="media.layout_2x2">2×2</span></button>
</div>
</div>
<label class="stream-size-label" data-i18n-title="media.stream_size" title="Stream display size">
🖥️
<input type="range" id="stream-size-slider" class="stream-size-slider" min="20" max="90" value="50">
</label>
<button id="screen-share-minimize" class="icon-btn small" data-i18n-title="media.minimize_streams" title="Minimize streams">─</button>
<button id="screen-share-close" class="icon-btn small" data-i18n-title="media.close_streams" title="Close streams">✕</button>
</div>
</div>
<div id="screen-share-grid" class="screen-share-grid"></div>
</div>
<!-- Music Player Panel -->
<div id="music-panel" class="music-panel" data-haven-region="music-player" style="display:none">
<div class="music-panel-header">
<div class="music-panel-header-left">
<button id="music-popout-btn" class="icon-btn small" data-i18n-title="media.music_popout" title="Pop out player">⧉</button>
<div class="music-panel-copy">
<span id="music-panel-label">🎶 <span data-i18n="media.listening_together">Listening Together</span></span>
<span id="music-up-next" class="music-up-next" data-i18n="media.music_up_next_empty">Up next: Nothing queued</span>
</div>
</div>
<span id="music-activity-hint" class="music-activity-hint"></span>
<div class="music-panel-controls">
<button id="music-queue-btn" class="icon-btn small" data-i18n-title="media.music_queue" title="Queue">☰</button>
<button id="music-play-pause-btn" class="icon-btn small" data-i18n-title="media.music_play_pause" title="Play/Pause">⏸</button>
<button id="music-next-btn" class="icon-btn small" data-i18n-title="media.music_next" title="Next track" style="display:none">⏭</button>
<span id="music-time-current" class="music-time">0:00</span>
<input type="range" id="music-seek-slider" class="music-seek-slider" min="0" max="100" value="0" step="0.1" data-i18n-title="media.music_seek" title="Seek">
<span id="music-time-duration" class="music-time">0:00</span>
<button id="music-mute-btn" class="icon-btn small" data-i18n-title="media.music_mute" title="Mute music">🔊</button>
<input type="range" id="music-volume-slider" class="stream-vol-slider" min="0" max="100" value="80" data-i18n-title="media.music_volume" title="Music volume">
<button id="music-close-btn" class="icon-btn small" data-i18n-title="media.minimize" title="Minimize">─</button>
<button id="music-stop-btn" class="icon-btn small" data-i18n-title="media.music_stop" title="Close / stop music">✕</button>
</div>
</div>
<div id="music-embed-container" class="music-embed-container"></div>
</div>
<div id="pinned-panel" class="pinned-panel" data-haven-region="pinned-messages" style="display:none">
<div class="pinned-panel-header">
<span id="pinned-count">📌 <span data-i18n="header.pinned_count">Pinned Messages</span></span>
<div class="pinned-panel-actions">
<button id="pinned-fullscreen-btn" class="icon-btn small" data-i18n-title="media.open_fullscreen" title="Open fullscreen">⤢</button>
<button id="pinned-popup-btn" class="icon-btn small" data-i18n-title="media.popout_panel" title="Pop out to floating panel">⧉</button>
<button id="pinned-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
</div>
<div id="pinned-list" class="pinned-list"></div>
</div>
<div class="messages" id="messages" data-haven-region="message-list">
<!-- Floating "⋯" button for touch-device message actions fallback -->
<button id="msg-more-btn" data-i18n-aria-label="app.actions.message_actions" aria-label="Message actions">⋯</button>
</div>
<button id="jump-to-bottom" class="jump-to-bottom" data-i18n-title="app.actions.jump_to_bottom" title="Jump to bottom">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="typing-indicator" id="typing-indicator"></div>
<div id="reply-bar" class="reply-bar" style="display:none">
<span class="reply-bar-text" id="reply-preview-text"></span>
<button class="reply-bar-close" id="reply-close-btn" data-i18n-title="header.cancel_reply" title="Cancel reply">&times;</button>
</div>
<div id="image-queue-bar" class="image-queue-bar" style="display:none"></div>
<div id="tag-queue-bar" class="tag-queue-bar" style="display:none">
<span class="tag-queue-label" id="tag-queue-label" data-i18n="tags.attachment_tags">Tags</span>
<div id="tag-queue-chips" class="tag-queue-chips"></div>
<button id="tag-add-btn" class="tag-add-btn" type="button" data-i18n-title="tags.add_tag" title="Add tag">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<span data-i18n="tags.add_tag">Add tag</span>
</button>
<div id="tag-popup" class="tag-popup" style="display:none">
<input id="tag-popup-input" class="tag-popup-input" type="text" maxlength="20" autocomplete="off" spellcheck="false" data-i18n-placeholder="tags.search_placeholder" placeholder="Search tags…">
<div id="tag-popup-list" class="tag-popup-list"></div>
</div>
</div>
<div id="tag-frequent-bar" class="tag-frequent-bar" style="display:none"></div>
<div id="upload-progress-bar" class="upload-progress-bar" style="display:none">
<div class="upload-progress-track"><div class="upload-progress-fill" id="upload-progress-fill"></div></div>
<span class="upload-progress-text" id="upload-progress-text" data-i18n="header.uploading">Uploading...</span>
<button class="upload-progress-cancel" id="upload-cancel-btn" data-i18n-title="common.cancel_upload" data-i18n-aria-label="common.cancel_upload" title="Cancel upload" aria-label="Cancel upload">&times;</button>
</div>
<div id="read-only-note" class="read-only-note" style="display:none">🔒 <span data-i18n="app.messages.read_only_note">Read-only channel. Others can't post here, but you can.</span></div>
<div id="message-input-area" class="message-input-area" data-haven-region="composer">
<div class="pip-input-resizer" aria-hidden="true"></div>
<button id="composer-plus-btn" class="btn-composer-plus" type="button" data-i18n-title="app.input_bar.plus_btn" title="More" data-i18n-aria-label="app.input_bar.plus_btn" aria-label="More" aria-expanded="false">+</button>
<div class="input-actions-box">
<button id="upload-btn" class="btn-upload" data-i18n-title="media.upload_file_title" title="Upload File" data-i18n-aria-label="media.upload_file_title" aria-label="Upload File">
<svg class="upload-icon-default" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<span class="upload-icon-clippy">📎</span>
</button>
<span class="input-actions-divider"></span>
<button id="emoji-btn" class="btn-emoji" data-i18n-title="app.input_bar.emoji_btn" title="Emoji" data-i18n-aria-label="app.input_bar.emoji_btn" aria-label="Emoji">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M8 14s1.5 2 4 2 4-2 4-2"/>
<circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/>
<circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/>
</svg>
</button>
<span class="input-actions-divider"></span>
<button id="gif-btn" class="btn-gif" data-i18n-title="app.input_bar.gif_btn" title="Search GIFs" data-i18n-aria-label="app.input_bar.gif_btn" aria-label="Search GIFs"><span data-i18n="app.input_bar.gif_label">GIF</span></button>
<span class="input-actions-divider"></span>
<button id="poll-btn" class="btn-poll" data-i18n-title="app.input_bar.poll_btn" title="Create Poll" data-i18n-aria-label="app.input_bar.poll_btn" aria-label="Create Poll">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="6" y1="20" x2="6" y2="14"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="2" y1="20" x2="22" y2="20"/>
</svg>
</button>
<span class="input-actions-divider" id="time-divider"></span>
<button id="time-btn" class="btn-poll btn-time" data-i18n-title="app.input_bar.time_btn" title="Insert timestamp" data-i18n-aria-label="app.input_bar.time_btn" aria-label="Insert timestamp">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="12" y1="12" x2="12" y2="7"/>
<line x1="12" y1="12" x2="15.5" y2="13.5"/>
</svg>
</button>
<span class="input-actions-divider"></span>
<button id="format-btn" class="btn-poll btn-format" data-i18n-title="app.input_bar.format_btn" title="Formatting and commands" data-i18n-aria-label="app.input_bar.format_btn" aria-label="Formatting and commands">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>
</svg>
</button>
<span class="input-actions-divider"></span>
<button id="voice-btn" class="btn-poll btn-voice-msg" data-i18n-title="app.input_bar.voice_btn" title="Record a voice message" data-i18n-aria-label="app.input_bar.voice_btn" aria-label="Record a voice message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="3" width="6" height="11" rx="3"/>
<path d="M5 11a7 7 0 0 0 14 0"/>
<line x1="12" y1="18" x2="12" y2="21"/>
<line x1="9" y1="21" x2="15" y2="21"/>
</svg>
</button>
<span class="input-actions-divider" id="self-destruct-divider"></span>
<button id="self-destruct-btn" class="btn-poll btn-self-destruct" aria-pressed="false" data-i18n-title="app.input_bar.self_destruct_btn" title="Self-destructing message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>
</svg>
</button>
<span class="input-actions-divider" id="highlight-divider" style="display:none"></span>
<button id="highlight-btn" class="btn-poll btn-highlight" aria-pressed="false" data-i18n-title="app.input_bar.highlight_btn" title="Official message" style="display:none">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1z"/><path d="M15 9a4 4 0 0 1 0 6"/><path d="M18 6a8 8 0 0 1 0 12"/></svg>
</button>
<span class="input-actions-divider" id="burn-divider" style="display:none"></span>
<button id="burn-btn" class="btn-burn" data-i18n-title="app.input_bar.burn_btn" title="Burn after read (DM only)" style="display:none">🔥</button>
</div>
<div id="emoji-picker" class="emoji-picker" style="display:none"></div>
<div id="gif-picker" class="gif-picker" style="display:none">
<div class="gif-picker-tabs">
<button type="button" class="gif-tab active" data-gif-tab="search" data-i18n="gifs.tab_search">Search</button>
<button type="button" class="gif-tab" data-gif-tab="favorites" data-i18n="gifs.tab_favorites">★ Favorites</button>
</div>
<div class="gif-picker-header">
<input type="text" id="gif-search-input" data-i18n-placeholder="header.gif_search_placeholder" placeholder="Search GIFs..." maxlength="100" autocomplete="off">
</div>
<div class="gif-picker-grid" id="gif-grid"></div>
<div class="gif-picker-footer" data-i18n="header.powered_by_giphy">Powered by GIPHY</div>
</div>
<div id="format-picker" class="gif-picker format-picker" style="display:none">
<div class="gif-picker-tabs">
<button type="button" class="gif-tab active" data-format-tab="markdown" data-i18n="format_picker.tab_formatting">Formatting</button>
<button type="button" class="gif-tab" data-format-tab="commands" data-i18n="format_picker.tab_commands">Commands</button>
</div>
<div class="format-picker-list" id="format-picker-list"></div>
</div>
<div id="voice-record-bar" class="voice-record-bar" style="display:none">
<span class="voice-rec-dot" aria-hidden="true"></span>
<span id="voice-rec-time" class="voice-rec-time">0:00</span>
<span class="voice-rec-hint" data-i18n="voice_message.recording_hint">Recording. Click the mic or Send to post it, Escape to throw it away.</span>
<button type="button" class="btn-sm" id="voice-rec-cancel" data-i18n="modals.common.cancel">Cancel</button>
<button type="button" class="btn-sm btn-accent" id="voice-rec-send" data-i18n="voice_message.send">Send</button>
</div>
<div id="mention-dropdown" class="mention-dropdown" style="display:none"></div>
<div id="channel-dropdown" class="mention-dropdown" style="display:none"></div>
<div id="emoji-dropdown" class="emoji-dropdown" style="display:none"></div>
<div id="slash-dropdown" class="slash-dropdown" style="display:none"></div>
<div id="persona-dropdown" class="mention-dropdown" style="display:none"></div>
<div id="ferry-dropdown" class="mention-dropdown" style="display:none"></div>
<textarea dir="auto" id="message-input" data-i18n-placeholder="header.message_placeholder_commands" placeholder="Type a message... (/ for commands)"
rows="1" maxlength="2000"></textarea>
<button id="send-btn" class="btn-send" data-i18n-title="media.send_title" title="Send" data-i18n-aria-label="media.send_title" aria-label="Send">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
<input type="file" id="file-input" multiple style="display:none">
</div>
</main>
<!-- ─── Soundboard Sidebar (twin to voice/users, sits directly LEFT of right-sidebar) ─── -->
<!-- Soundboard toggle arrow, fixed, sits at left edge of soundboard panel.
The note marks it apart from the members handle right above it, which
is otherwise the same little arrow in the same place. (#5534) -->
<button id="sb-sidebar-toggle-btn" class="sb-sidebar-toggle-btn" style="display:none" data-i18n-title="media.toggle_soundboard" title="Toggle soundboard panel"><span class="sb-toggle-mark" aria-hidden="true">♪</span><span class="sb-toggle-arrow">&#x276E;</span></button>
<aside id="sb-sidebar-panel" class="sb-sidebar-panel sb-hidden" data-haven-region="soundboard">
<div id="sb-sidebar-resize-handle" class="sb-sidebar-resize-handle"></div>
<div class="sb-sidebar-header">&#x1F3B5; <span data-i18n="voice.panel.soundboard">Soundboard</span></div>
<div class="sb-sidebar-body">
<input type="text" id="sb-sidebar-search" data-i18n-placeholder="modals.sound_manager.search_placeholder" placeholder="Search sounds..." class="settings-text-input" style="width:100%;box-sizing:border-box;font-size:0.75rem">
<div id="sb-sidebar-grid" class="soundboard-grid sidebar-mode"></div>
</div>
</aside>
<!-- Voice/users sidebar collapse arrow: fixed, sits at left edge of voice/users panel -->
<button id="sidebar-toggle-btn" class="sidebar-collapse-btn" data-i18n-title="app.actions.toggle_members_panel" data-i18n-aria-label="app.actions.toggle_members_panel" title="Toggle members panel" aria-label="Toggle members panel">&#x276F;</button>
<!-- ─── Right Sidebar ───────────────────────────── -->
<aside class="right-sidebar" id="right-sidebar" data-haven-region="context-sidebar">
<div class="right-sidebar-resize-handle" id="right-sidebar-resize-handle"></div>
<!-- Search results overlay: sits over voice + member list, persists across
channel switches, only closes on the X. (search-overhaul phase 1) -->
<div id="search-panel" class="search-panel" data-haven-region="search-results" style="display:none">
<div class="search-panel-header">
<span id="search-panel-count" data-i18n="header.results">Results</span>
<button id="search-panel-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div id="search-panel-banner" class="search-panel-banner" style="display:none">
<span data-i18n="header.search_stale">A channel or role you have has changed, please refresh the search to continue.</span>
<button id="search-rerun-btn" class="btn-secondary small" data-i18n="header.search_refresh">Refresh search</button>
</div>
<div id="search-panel-loading" class="search-panel-loading" style="display:none">
<span class="search-spinner"></span><span data-i18n="header.searching">Searching...</span>
</div>
<div id="search-panel-list" class="search-panel-list"></div>
<div id="search-panel-pager" class="search-panel-pager" style="display:none">
<button id="search-page-prev" class="icon-btn small" data-i18n-title="header.search_previous" title="Previous page">‹</button>
<span class="search-page-label">
<input id="search-page-input" class="search-page-input" type="text" inputmode="numeric" value="1" data-i18n-aria-label="header.search_page_number" aria-label="Page number">
<span id="search-page-total" class="search-page-total"></span>
</span>
<button id="search-page-next" class="icon-btn small" data-i18n-title="header.search_next" title="Next page">›</button>
</div>
</div>
<!-- Voice users section -->
<div class="right-sidebar-voice" id="right-sidebar-voice" data-haven-region="voice-roster">
<div class="panel">
<div class="voice-panel-header">
<h5 class="panel-title" style="margin-bottom:0"><span class="title-emoji">🔊 </span><span data-i18n="right_sidebar.voice_title">Voice</span></h5>
<button id="mobile-right-close" class="mobile-panel-close mobile-panel-close-right" data-i18n-title="modals.common.close" data-i18n-aria-label="right_sidebar.close_panel" title="Close" aria-label="Close panel">&times;</button>
<div class="voice-header-actions">
<button id="voice-mute-btn-header" class="voice-header-btn" data-i18n-title="voice.mute" title="Mute" style="display:none">🎙️</button>
<button id="voice-deafen-btn-header" class="voice-header-btn" data-i18n-title="voice.deafen" title="Deafen" style="display:none">🔊</button>
</div>
</div>
<button id="voice-join-mobile" class="btn-voice mobile-voice-join" style="display:none"><span data-i18n="voice.join">🎤 Join Voice</span></button>
<div id="voice-users" class="user-list">
<p class="muted-text" data-i18n="right_sidebar.no_one_in_voice">No one in voice</p>
</div>
</div>
</div>
<!-- Scrollable users section -->
<div class="right-sidebar-users" id="right-sidebar-users" data-haven-region="member-list">
<div class="panel">
<h5 class="panel-title"><span class="title-emoji">👥 </span><span data-i18n="right_sidebar.online_title">Online</span></h5>
<div class="user-search-wrap" id="user-search-wrap" style="display:none">
<input type="search" id="user-search" class="user-search-input" autocomplete="off" spellcheck="false"
data-i18n-placeholder="right_sidebar.search_members" placeholder="Search members...">
<button type="button" class="user-search-clear" id="user-search-clear" style="display:none"
data-i18n-title="right_sidebar.search_clear" title="Clear">&times;</button>
</div>
<div id="online-users" class="user-list">
<p class="muted-text" data-i18n="right_sidebar.select_channel">Select a channel</p>
</div>
</div>
</div>
<!-- Invite users button pinned under user list -->
<div class="panel sidebar-invite-panel" id="sidebar-invite-panel" style="display:none">
<button class="panel btn-invite-popout" id="right-btn-invite-popout" data-i18n-title="app.actions.invite" title="Invite Users">
<svg width="16" height="16" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="14" cy="11" r="6"/>
<path d="M5,26c0-4.971,4.029-9,9-9 c1.864,0,3.596,0.567,5.032,1.537"/>
<circle cx="24" cy="24" r="7"/>
<line x1="24" y1="28" x2="24" y2="20"/>
<line x1="20" y1="24" x2="28" y2="24"/>
</svg>
<span data-i18n="app.actions.invite">Invite Users</span>
</button>
</div>
<!-- Voice settings sub-panel (slides up above controls) -->
<div id="voice-settings-panel" class="voice-settings-panel" data-haven-region="voice-settings" style="display:none">
<div class="voice-settings-section-label">🎧 <span data-i18n="voice_settings.audio_devices">Audio Devices</span></div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.input">Input</label>
<select id="voice-input-device" class="voice-settings-select">
<option value="" data-i18n="voice_settings.default_mic">Default Microphone</option>
</select>
</div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.output">Output</label>
<select id="voice-output-device" class="voice-settings-select">
<option value="" data-i18n="voice_settings.default_speaker">Default Speaker</option>
</select>
</div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.camera">Camera</label>
<select id="voice-cam-device" class="voice-settings-select">
<option value="" data-i18n="voice_settings.default_camera">Default Camera</option>
</select>
</div>
<div class="voice-settings-divider"></div>
<div class="voice-settings-row">
<label class="voice-settings-label">🤫 <span data-i18n="voice_settings.noise_reduction">Noise Reduction</span></label>
<select id="voice-noise-mode" class="voice-settings-select" data-i18n-title="voice_settings.noise_mode_hint" title="Off = no processing, Gate = silence below threshold, Suppress = AI noise removal (RNNoise)">
<option value="off" data-i18n="voice_settings.off">Off</option>
<option value="gate" data-i18n="voice_settings.noise_gate">Noise Gate</option>
<option value="suppress" data-i18n="voice_settings.suppression_ai">Suppression (AI)</option>
</select>
</div>
<div class="voice-settings-row" id="noise-gate-row">
<label class="voice-settings-label" style="font-size:0.72rem;opacity:0.8" data-i18n="voice_settings.gate_sensitivity">Gate Sensitivity</label>
<input type="range" id="voice-ns-slider" class="ns-slider" min="0" max="100" value="10" data-i18n-title="voice_settings.gate_slider_hint" title="Noise gate: 0 = off, 100 = aggressive">
</div>
<div class="voice-settings-row mic-meter-row">
<label class="voice-settings-label">🎙️ <span data-i18n="voice_settings.mic_level">Mic Level</span></label>
<div class="mic-meter" id="mic-level-meter">
<div class="mic-meter-fill" id="mic-meter-fill"></div>
<div class="mic-meter-threshold" id="mic-meter-threshold" data-i18n-title="voice_settings.gate_threshold" title="Gate threshold"></div>
</div>
</div>
<div class="voice-settings-divider"></div>
<div class="voice-settings-section-label">🖥️ <span data-i18n="voice_settings.screen_share_quality">Screen Share Quality</span></div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.resolution">Resolution</label>
<select id="screen-res-select" class="voice-settings-select">
<option value="source" data-i18n="voice_settings.resolution_source">Source</option>
<option value="720">720p</option>
<option value="1080" selected>1080p</option>
<option value="1440">1440p</option>
</select>
</div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.bitrate">Bitrate</label>
<div class="bitrate-stepper">
<button id="screen-bitrate-minus" class="bitrate-step-btn" aria-label="−">−</button>
<input id="screen-bitrate-value" class="bitrate-value" inputmode="numeric"
autocomplete="off" spellcheck="false" value="8000 Kbps">
<button id="screen-bitrate-plus" class="bitrate-step-btn" aria-label="+">+</button>
</div>
</div>
<div class="voice-settings-row">
<label class="voice-settings-label" data-i18n="voice_settings.frame_rate">Frame Rate</label>
<select id="screen-fps-select" class="voice-settings-select">
<option value="15">15 fps</option>
<option value="30" selected>30 fps</option>
<option value="60">60 fps</option>
</select>
</div>
</div>
<!-- Voice controls bar (pinned above settings) -->
<div id="voice-panel" class="voice-panel" data-haven-region="voice-controls" style="display:none">
<button id="voice-cam-btn" class="voice-panel-btn" data-i18n-title="voice.panel.camera" title="Camera">📷</button>
<button id="screen-share-btn" class="voice-panel-btn" data-i18n-title="voice.screen_share" title="Share Screen">🖥️</button>
<button id="voice-soundboard-btn" class="voice-panel-btn" data-i18n-title="voice.panel.soundboard" title="Soundboard">🎵</button>
<button id="voice-listen-together-btn" class="voice-panel-btn" data-i18n-title="voice.panel.listen_together" title="Listen Together">🎶</button>
<span class="voice-panel-divider"></span>
<button id="voice-settings-toggle" class="voice-panel-btn" data-i18n-title="voice.panel.settings" title="Voice Settings">⚙️</button>
<button id="voice-leave-sidebar-btn" class="voice-panel-btn voice-panel-leave" data-i18n-title="voice.leave" title="Leave Voice">✕</button>
</div>
<!-- Settings button pinned at bottom -->
<div class="panel sidebar-settings-panel">
<button class="btn-settings-popout" id="open-settings-btn" data-i18n-title="app.actions.settings" title="Settings">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span data-i18n="app.actions.settings">Settings</span>
</button>
</div>
</aside>
<!-- Mobile overlay (click to close sidebars) -->
<div id="mobile-overlay" class="mobile-overlay"></div>
</div>
<!-- ─── Status Bar (bottom) ───────────────────────── -->
<div class="status-bar" id="status-bar" data-haven-region="status-bar" style="display:none">
<div class="status-item">
<span class="led on" id="status-server-led"></span>
<span class="label" data-i18n="status_bar.server">Server</span>
<span class="value" id="status-server-text" data-i18n="app.status.connected">Connected</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="label" data-i18n="status_bar.ping">Ping</span>
<span class="value" id="status-ping">--</span>
<span class="label" data-i18n="status_bar.ping_unit">ms</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="label" data-i18n="status_bar.channel">Channel</span>
<span class="value" id="status-channel" data-i18n="status_bar.channel_none">None</span>
</div>
<div class="divider"></div>
<div class="status-item status-online-trigger" id="status-online-trigger" data-i18n-title="status_bar.online_title" title="Click to see who's online">
<span class="label" data-i18n="status_bar.online">Online</span>
<span class="value" id="status-online-count">0</span>
</div>
<span class="spacer"></span>
<div class="status-item status-url-item" id="status-url-item">
<span class="value status-url-text" id="status-url-text" data-i18n-title="status_bar.copy_server_address" title="Click to copy server address"></span>
<button class="status-url-toggle" id="status-url-toggle" data-i18n-title="status_bar.toggle_server_address" title="Show/hide server address">👁</button>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="value" id="status-clock"></span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="value" id="status-version"></span>
</div>
</div>
<!-- Status bar toggle tab (visible when bar is hidden) -->
<button class="status-bar-toggle-tab" id="status-bar-toggle" data-i18n-title="status_bar.toggle_bar" title="Toggle status bar (debug footer)">📊</button>
<!-- Online users overlay (floats above status bar) -->
<div id="online-overlay" class="online-overlay" style="display:none">
<div class="online-overlay-header">
<span class="online-overlay-title" data-i18n="status_bar.users_overlay_title">Users</span>
<button id="online-overlay-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div id="online-overlay-list" class="online-overlay-list"></div>
</div>
</div>
<!-- Hidden audio container for voice streams -->
<div id="audio-container" style="display:none"></div>
<!-- Toast notifications -->
<div id="toast-container"></div>
<!-- Move Messages Toolbar (floating above messages) -->
<div id="move-msg-toolbar" class="move-msg-toolbar" style="display:none">
<span id="move-msg-count">0 selected</span>
<button class="btn-sm btn-accent" id="move-msg-move-btn" data-i18n="modals.move_messages.move_to">Move to...</button>
<button class="btn-sm btn-danger-fill" id="move-msg-delete-btn" data-i18n="modals.move_messages.delete_btn">Delete</button>
<button class="btn-sm" id="move-msg-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
</div>
<!-- Move Messages Channel Picker Modal -->
<div class="modal-overlay" id="move-msg-modal" style="display:none">
<div class="modal">
<h3 data-i18n="modals.move_messages.title">Move Messages</h3>
<p class="modal-desc" id="move-msg-desc" data-i18n="modals.move_messages.desc">Select a destination channel</p>
<div class="move-msg-channel-list" id="move-msg-channel-list"></div>
<div class="modal-actions">
<button class="btn-sm" id="move-msg-modal-cancel" data-i18n="modals.common.cancel">Cancel</button>
</div>
</div>
</div>
<!-- Thread Panel (slides in from right) -->
<!-- Pinned Messages PiP floating panel (draggable overlay) -->
<div id="pins-pip-panel" class="pins-pip-panel" style="display:none">
<div class="pins-pip-header">
<span class="pins-pip-icon" aria-hidden="true">📌</span>
<span id="pins-pip-title" class="pins-pip-title" data-i18n="header.pinned_count">Pinned Messages</span>
<div class="pins-pip-spacer"></div>
<button id="pins-pip-fullscreen" class="icon-btn small" data-i18n-title="media.toggle_fullscreen" title="Toggle fullscreen">⤢</button>
<button id="pins-pip-popin" class="icon-btn small" data-i18n-title="media.pop_back_sidebar" title="Pop back in to sidebar">⧈</button>
<button id="pins-pip-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div id="pins-pip-list" class="pins-pip-list pinned-list"></div>
</div>
<!-- DM PiP floating panel (overlay; doesn't switch the active channel) -->
<div id="dm-pip-panel" class="dm-pip-panel" style="display:none">
<div id="dm-pip-banner" class="dm-pip-banner" aria-hidden="true"></div>
<div class="dm-pip-header">
<div id="dm-pip-avatar-wrap" class="dm-pip-avatar-wrap" aria-hidden="true"></div>
<span id="dm-pip-title" class="dm-pip-title" data-i18n="dm_runtime.title">DM</span>
<div class="dm-pip-spacer"></div>
<button id="dm-pip-fullscreen" class="icon-btn small" data-i18n-title="media.open_full_pane" title="Open in full pane">▣</button>
<button id="dm-pip-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div id="dm-pip-messages" class="dm-pip-messages"></div>
<div id="dm-pip-reply-bar" class="dm-pip-reply-bar" style="display:none">
<span id="dm-pip-reply-preview-text"></span>
<button id="dm-pip-reply-close-btn" class="icon-btn small" data-i18n-aria-label="header.cancel_reply" aria-label="Cancel reply">&times;</button>
</div>
<div id="dm-pip-image-queue-bar" class="image-queue-bar" style="display:none"></div>
<div class="dm-pip-input-area">
<div class="pip-input-resizer" aria-hidden="true"></div>
<button id="dm-pip-upload-btn" class="pip-input-emoji-btn" data-i18n-title="media.upload_file_title" title="Upload File"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📎</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg></span></button>
<input type="file" id="dm-pip-file-input" multiple style="display:none">
<button id="dm-pip-emoji-btn" class="pip-input-emoji-btn" data-i18n-title="dm_runtime.emoji" title="Emoji"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/></svg></span></button>
<button id="dm-pip-gif-btn" class="pip-input-emoji-btn pip-input-gif-btn" data-i18n-title="app.input_bar.gif_btn" title="Search GIFs"><span data-i18n="app.input_bar.gif_label">GIF</span></button>
<textarea dir="auto" id="dm-pip-input" class="dm-pip-input" data-i18n-placeholder="dm_runtime.reply_placeholder" placeholder="Reply…" rows="1"></textarea>
<button id="dm-pip-send" class="dm-pip-send" data-i18n-title="dm_runtime.send" title="Send">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
</svg>
</button>
</div>
</div>
<div id="thread-panel" class="thread-panel" data-haven-region="thread-panel" style="display:none">
<div class="thread-panel-resizer" id="thread-panel-resizer" aria-hidden="true"></div>
<div class="thread-panel-header">
<div class="thread-panel-header-top">
<span class="thread-panel-icon">🧵</span>
<span id="thread-panel-title" class="thread-panel-title" data-i18n="thread_runtime.title">Thread</span>
<button id="thread-panel-pip" class="icon-btn small" data-i18n-title="thread_runtime.pop_out" title="Pop out thread (PiP)" aria-pressed="false">⧉</button>
<button id="thread-panel-close" class="icon-btn small" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div class="thread-forum-bar" id="thread-forum-bar" style="display:none"></div>
<div class="thread-parent-meta" id="thread-parent-meta">
<div class="thread-parent-avatar-wrap" id="thread-parent-avatar-wrap"></div>
<span class="thread-parent-name" id="thread-parent-name" data-i18n="thread_runtime.starter">Thread starter</span>
</div>
<div class="thread-parent-preview" id="thread-parent-preview"></div>
</div>
<div class="thread-messages" id="thread-messages"></div>
<div class="thread-reply-bar" id="thread-reply-bar" style="display:none">
<span id="thread-reply-preview-text"></span>
<button id="thread-reply-close-btn" class="icon-btn small" data-i18n-aria-label="thread_runtime.cancel_reply" aria-label="Cancel thread reply">&times;</button>
</div>
<div id="thread-image-queue-bar" class="image-queue-bar" style="display:none"></div>
<div class="thread-closed-note" id="thread-closed-note" style="display:none">🔒 <span id="thread-closed-text"></span></div>
<div class="thread-input-area">
<div class="pip-input-resizer" aria-hidden="true"></div>
<button id="thread-emoji-btn" class="pip-input-emoji-btn" data-i18n-title="dm_runtime.emoji" title="Emoji"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"/></svg></span></button>
<button id="thread-gif-btn" class="pip-input-emoji-btn pip-input-gif-btn" data-i18n-title="app.input_bar.gif_btn" title="Search GIFs"><span data-i18n="app.input_bar.gif_label">GIF</span></button>
<textarea dir="auto" id="thread-input" class="thread-input" data-i18n-placeholder="thread_runtime.reply_placeholder" placeholder="Reply in thread..." rows="1"></textarea>
<button id="thread-send-btn" class="thread-send-btn" data-i18n-title="dm_runtime.send" title="Send">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
</svg>
</button>
</div>
</div>
<!-- First-Time Setup Wizard (Admin only) -->
<div class="modal-overlay" id="setup-wizard-modal" style="display:none">
<div class="modal modal-wizard">
<div class="wizard-header">
<span class="wizard-hex">⬡</span>
<h2 data-i18n="modals.wizard.title">Welcome to Haven</h2>
<p class="wizard-subtitle" data-i18n="modals.wizard.subtitle">Let's get your server ready. This only takes a minute.</p>
</div>
<div class="wizard-steps">
<!-- Step indicators -->
<div class="wizard-step-indicators">
<div class="wizard-indicator active" data-step="1">1</div>
<div class="wizard-indicator-line"></div>
<div class="wizard-indicator" data-step="2">2</div>
<div class="wizard-indicator-line"></div>
<div class="wizard-indicator" data-step="3">3</div>
<div class="wizard-indicator-line"></div>
<div class="wizard-indicator" data-step="4">4</div>
</div>
<!-- Step 1: Server basics -->
<div class="wizard-step" id="wizard-step-1" style="display:block">
<h3>🏠 <span data-i18n="modals.wizard.step1_title">Your Server</span></h3>
<p data-i18n="modals.wizard.step1_desc">Everything is already running. Here's what was set up automatically:</p>
<div class="wizard-checklist">
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_ssl">SSL certificates generated (encrypted connections)</span></div>
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_db">Database created</span></div>
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_keys">Security keys generated</span></div>
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_admin">Admin account created (that's you!)</span></div>
</div>
<div class="wizard-info-box">
<strong data-i18n="modals.wizard.server_name_label">Your server name:</strong>
<input type="text" id="wizard-server-name" class="wizard-input" value="Haven" placeholder="e.g. My Server, The Hideout" data-i18n-placeholder="modals.wizard.server_name_placeholder">
<span class="wizard-hint" data-i18n="modals.wizard.server_name_hint">This is what your friends see in their server list.</span>
</div>
</div>
<!-- Step 2: Create first channel -->
<div class="wizard-step" id="wizard-step-2" style="display:none">
<h3>📢 <span data-i18n="modals.wizard.step2_title">Create Your First Channel</span></h3>
<p data-i18n="modals.wizard.step2_desc">Channels are where conversations happen. Create one to get started.</p>
<div class="wizard-info-box">
<strong data-i18n="modals.wizard.channel_name_label">Channel name:</strong>
<input type="text" id="wizard-channel-name" class="wizard-input" value="General" placeholder="e.g. General, Gaming, Hangout" data-i18n-placeholder="modals.wizard.channel_name_placeholder">
<span class="wizard-hint" data-i18n="modals.wizard.channel_name_hint">You can create more channels later from the sidebar.</span>
</div>
<div id="wizard-channel-result" style="display:none">
<div class="wizard-check" id="wizard-channel-created">✅ <span data-i18n="modals.wizard.channel_created">Channel created!</span></div>
<div class="wizard-code-display">
<span data-i18n="modals.wizard.invite_code">Invite code:</span>
<code id="wizard-channel-code"></code>
<button id="wizard-copy-code" class="wizard-btn-small" data-i18n="modals.wizard.copy_btn">Copy</button>
</div>
<span class="wizard-hint" data-i18n="modals.wizard.share_hint">Share this code with friends so they can join this channel.</span>
</div>
</div>
<!-- Step 3: Connectivity check -->
<div class="wizard-step" id="wizard-step-3" style="display:none">
<h3>🌐 <span data-i18n="modals.wizard.step3_title">Internet Access</span></h3>
<p data-i18n="modals.wizard.step3_desc">Want friends outside your WiFi to connect? Let's check if your server is reachable from the internet.</p>
<div id="wizard-port-checking" style="display:none">
<div class="wizard-spinner"></div>
<span data-i18n="modals.wizard.checking_port">Checking port reachability...</span>
</div>
<div id="wizard-port-result" style="display:none"></div>
<button id="wizard-check-port-btn" class="wizard-btn-secondary">🔍 <span data-i18n="modals.wizard.check_connection_btn">Check My Connection</span></button>
<div class="wizard-info-box" style="margin-top:16px">
<span class="wizard-hint" data-i18n-html="modals.wizard.lan_only_hint"><strong>LAN only?</strong> If your friends are on the same WiFi, you can skip this. It will work automatically.</span>
</div>
</div>
<!-- Step 4: You're ready -->
<div class="wizard-step" id="wizard-step-4" style="display:none">
<h3>🎉 <span data-i18n="modals.wizard.step4_title">You're All Set!</span></h3>
<p data-i18n="modals.wizard.step4_desc">Haven is running and ready for your friends.</p>
<div class="wizard-checklist">
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_configured">Server configured</span></div>
<div class="wizard-check" id="wizard-summary-channel">✅ <span data-i18n="modals.wizard.check_channel_created">First channel created</span></div>
<div class="wizard-check" id="wizard-summary-port">⏭️ <span data-i18n="modals.wizard.check_port_skipped">Port check skipped</span></div>
</div>
<div class="wizard-info-box">
<strong data-i18n="modals.wizard.how_friends_join">How friends join:</strong>
<ol class="wizard-steps-list">
<li><span data-i18n="modals.wizard.step_browser_prefix">They open</span> <code id="wizard-final-url">https://YOUR_IP:3000</code> <span data-i18n="modals.wizard.step_browser_suffix">in their browser</span></li>
<li data-i18n="modals.wizard.step_cert">Click "Advanced" → "Proceed" on the certificate warning</li>
<li data-i18n="modals.wizard.step_register">Register a username and password</li>
<li data-i18n="modals.wizard.step_code">Enter the channel invite code you share with them</li>
</ol>
</div>
<div class="wizard-info-box">
<span class="wizard-hint">💡 <span data-i18n-html="modals.wizard.tip"><strong>Tip:</strong> You can always access settings with the ⚙️ gear icon in the sidebar. This wizard won't show again.</span></span>
</div>
</div>
</div>
<div class="wizard-footer">
<button id="wizard-back-btn" class="wizard-btn-secondary" style="display:none" data-i18n="modals.wizard.back_btn">← Back</button>
<div style="flex:1"></div>
<button id="wizard-skip-btn" class="wizard-btn-text" data-i18n="modals.wizard.skip_btn">Skip Setup</button>
<button id="wizard-next-btn" class="wizard-btn-primary" data-i18n="modals.wizard.next_btn">Next →</button>
</div>
</div>
</div>
<!-- Channel Code Settings Modal (Admin) -->
<div class="modal-overlay" id="code-settings-modal" style="display:none">
<div class="modal">
<h3 data-i18n="modals.code_settings.title">Channel Code Settings</h3>
<p class="modal-desc" id="code-settings-channel-name"></p>
<div class="form-group">
<label data-i18n="modals.code_settings.visibility_label">Code Visibility</label>
<select id="code-visibility-select" class="form-select">
<option value="public" data-i18n="modals.code_settings.visibility_public">Public: everyone can see the code</option>
<option value="private" data-i18n="modals.code_settings.visibility_private">Private: only admins see the code</option>
</select>
</div>
<div class="form-group">
<label data-i18n="modals.code_settings.mode_label">Code Mode</label>
<select id="code-mode-select" class="form-select">
<option value="static" data-i18n="modals.code_settings.mode_static">Static: code never changes</option>
<option value="dynamic" data-i18n="modals.code_settings.mode_dynamic">Dynamic: code rotates automatically</option>
</select>
</div>
<div class="form-group" id="rotation-type-group" style="display:none">
<label data-i18n="modals.code_settings.rotation_label">Rotation Trigger</label>
<select id="code-rotation-type-select" class="form-select">
<option value="time" data-i18n="modals.code_settings.rotation_time">Time-based: rotate every X minutes</option>
<option value="joins" data-i18n="modals.code_settings.rotation_joins">Join-based: rotate after X new joins</option>
</select>
</div>
<div class="form-group" id="rotation-interval-group" style="display:none">
<label id="rotation-interval-label" data-i18n="modals.code_settings.interval_label">Rotation Interval (minutes)</label>
<input type="number" id="code-rotation-interval" min="1" max="10000" value="60">
</div>
<div class="modal-actions">
<button class="btn-sm" id="code-settings-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="code-rotate-now-btn">🔄 <span data-i18n="modals.code_settings.rotate_now_btn">Rotate Now</span></button>
<button class="btn-sm btn-accent" id="code-settings-save-btn" data-i18n="modals.common.save">Save</button>
</div>
</div>
</div>
<!-- Add Server Modal -->
<div class="modal-overlay" id="add-server-modal" style="display:none">
<div class="modal">
<h3 id="add-server-modal-title" data-i18n="modals.add_server.title">Add a Server</h3>
<p class="modal-desc" data-i18n="modals.add_server.desc">Connect to a friend's Haven server</p>
<div class="form-group">
<label data-i18n="modals.add_server.name_label">Server Name</label>
<input type="text" id="add-server-name-input" placeholder='e.g. Jake&apos;s Haven' maxlength="30">
</div>
<div class="form-group">
<label data-i18n="modals.add_server.address_label">Server Address</label>
<input type="text" id="server-url-input" data-i18n-placeholder="modals.add_server.address_placeholder" placeholder="e.g. https://192.168.1.5:3000" list="known-servers-datalist">
<datalist id="known-servers-datalist"></datalist>
</div>
<div class="form-group">
<label data-i18n="modals.add_server.icon_label">Icon URL</label> <span class="muted-text" data-i18n="modals.add_server.icon_optional">(optional)</span>
<input type="text" id="add-server-icon-input" data-i18n-placeholder="modals.add_server.icon_placeholder" placeholder="https://... or leave blank">
</div>
<label class="eula-check-row" style="margin-bottom:12px;gap:8px">
<input type="checkbox" id="server-auto-icon" checked>
<span data-i18n="modals.add_server.auto_icon">Auto-pull icon from server</span>
</label>
<div class="modal-actions">
<button class="btn-sm" id="cancel-server-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="save-server-btn" data-i18n="modals.add_server.add_btn">Add Server</button>
</div>
</div>
</div>
<!-- Timezone / Time Format Modal -->
<div class="modal-overlay" id="timezone-modal" style="display:none">
<div class="modal">
<h3>🌎 <span data-i18n="modals.timezone.title">Time &amp; Timezone</span></h3>
<p class="modal-desc" data-i18n="modals.timezone.desc">Set your preferred timezone and formatting here. These settings will be persisted to your account and will be used across all your devices. Your timezone is never revealed to any user.</p>
<div class="form-group">
<label data-i18n="modals.timezone.zone_label">Timezone</label>
<select id="timezone-select" class="form-select" style="width:100%"></select>
</div>
<div class="form-group">
<label data-i18n="modals.timezone.format_label">Time format</label>
<select id="timeformat-select" class="form-select" style="width:100%">
<option value="12" data-i18n="modals.timezone.format_12">12-hour</option>
<option value="24" data-i18n="modals.timezone.format_24">24-hour (military)</option>
</select>
</div>
<div class="tz-preview" style="margin:4px 0 12px;padding:12px;border:1px solid var(--border, rgba(255,255,255,0.08));border-radius:8px;text-align:center">
<small class="muted-text" style="display:block;opacity:0.6;margin-bottom:4px" data-i18n="modals.timezone.preview_label">This is how times will look</small>
<div id="timezone-preview" style="font-size:1.05rem;font-weight:600">-</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-sm danger" id="timezone-erase-btn" style="display:none" data-i18n="modals.timezone.erase">Erase</button>
<button type="button" class="btn-sm" id="timezone-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button type="button" class="btn-sm btn-accent" id="timezone-confirm-btn" data-i18n="modals.timezone.confirm">Confirm</button>
</div>
</div>
</div>
<!-- Rename / Profile Modal -->
<div class="modal-overlay" id="rename-modal" style="display:none">
<div class="modal">
<h3 data-i18n="modals.edit_profile.title">Edit Profile</h3>
<!-- Display Name -->
<p class="modal-desc" data-i18n="modals.edit_profile.display_name_desc">Letters, numbers, underscores, and spaces (2-20 chars)</p>
<div class="form-group">
<label data-i18n="modals.edit_profile.display_name_label">Display Name</label>
<input type="text" id="rename-input" data-i18n-placeholder="modals.edit_profile.display_name_placeholder" placeholder="New display name..." maxlength="20">
</div>
<!-- Bio -->
<div class="form-group" style="margin-top:8px">
<label data-i18n="modals.edit_profile.bio_label">Bio</label> <span class="muted-text" data-i18n="modals.edit_profile.bio_max">(max 190 chars)</span>
<textarea id="edit-profile-bio" class="edit-profile-textarea" maxlength="190" data-i18n-placeholder="modals.edit_profile.bio_placeholder" placeholder="Tell people about yourself…" rows="2"></textarea>
</div>
<!-- Avatar Customizer -->
<div class="avatar-settings" style="margin-bottom: 16px;">
<h5 class="settings-section-subtitle" style="margin-top:0" data-i18n="modals.edit_profile.avatar_title">Avatar</h5>
<div class="avatar-upload-row" style="margin-bottom: 10px;">
<div class="avatar-upload-preview" id="avatar-upload-preview"></div>
<button type="button" class="btn-sm" id="avatar-upload-btn" data-i18n="modals.edit_profile.upload_btn">Upload</button>
<button type="button" class="btn-sm" id="avatar-crop-btn" data-i18n="modals.edit_profile.edit_btn" disabled>Edit</button>
<button type="button" class="btn-sm" id="avatar-remove-btn" data-i18n="modals.edit_profile.clear_btn">Clear</button>
<input type="file" id="avatar-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
</div>
<small class="muted-text" style="display:block;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.avatar_hint">JPG, PNG, GIF (animated!), WebP · Max 2 MB</small>
<small class="muted-text" id="avatar-edit-note" style="display:none;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.avatar_edit_animated">Animated images can't be edited.</small>
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.border_title">Border</h5>
<div class="avatar-upload-row" style="margin-bottom: 10px;">
<div class="avatar-upload-preview" id="border-upload-preview"></div>
<button type="button" class="btn-sm" id="border-upload-btn" data-i18n="modals.edit_profile.upload_btn">Upload</button>
<button type="button" class="btn-sm" id="border-crop-btn" data-i18n="modals.edit_profile.edit_btn">Edit</button>
<button type="button" class="btn-sm" id="border-remove-btn" data-i18n="modals.edit_profile.clear_btn">Clear</button>
<input type="file" id="border-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
</div>
<small class="muted-text" style="display:block;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.border_hint">JPG, PNG, GIF (animated!), WebP · Max 2 MB</small>
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.animate_title">Animated profile</h5>
<select id="animate-profile-select" class="form-select" style="width:100%;margin-bottom:8px" data-i18n-title="modals.edit_profile.animate_title" title="Animated profile">
<option value="trigger" data-i18n="modals.edit_profile.animate_trigger">Trigger</option>
<option value="disabled" data-i18n="modals.edit_profile.animate_disabled">Disabled</option>
</select>
<button type="button" class="learn-more-toggle" id="animate-learn-more-toggle" aria-expanded="false" aria-controls="animate-learn-more-body">
<span class="collapse-toggle-arrow collapsed" id="animate-learn-more-arrow" aria-hidden="true">▾</span>
<span data-i18n="modals.edit_profile.animate_learn_more">Learn more</span>
</button>
<div class="collapsible-section-body collapsed" id="animate-learn-more-body">
<small class="muted-text" style="display:block;margin-bottom:6px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_intro">Controls how animations on your profile will appear for every user on Haven. This applies to both your avatar and border, assuming you upload an animated image such as a GIF or APNG.</small>
<small class="muted-text" style="display:block;margin-bottom:4px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_trigger">Trigger: Animations on your profile will loop when a user puts their cursor over your message, or opens your profile card.</small>
<small class="muted-text" style="display:block;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.animate_note_disabled">Disabled: Animations will be completely disabled for your profile. Existing animated images uploaded to your profile will remain untouched, however, only the first frame of the image will be displayed on your profile.</small>
</div>
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.shape_title">Shape</h5>
<div class="avatar-shape-picker" id="avatar-shape-picker">
<button type="button" class="avatar-shape-btn active" data-shape="circle" data-i18n-title="modals.edit_profile.shape_circle" title="Circle"><div class="shape-preview shape-circle"></div></button>
<button type="button" class="avatar-shape-btn" data-shape="rounded" data-i18n-title="modals.edit_profile.shape_rounded" title="Rounded"><div class="shape-preview shape-rounded"></div></button>
<button type="button" class="avatar-shape-btn" data-shape="squircle" data-i18n-title="modals.edit_profile.shape_squircle" title="Squircle"><div class="shape-preview shape-squircle"></div></button>
<button type="button" class="avatar-shape-btn" data-shape="hex" data-i18n-title="modals.edit_profile.shape_hexagon" title="Hexagon"><div class="shape-preview shape-hex"></div></button>
<button type="button" class="avatar-shape-btn" data-shape="diamond" data-i18n-title="modals.edit_profile.shape_diamond" title="Diamond"><div class="shape-preview shape-diamond"></div></button>
</div>
</div>
<!-- Groups -->
<div class="groups-section" id="rename-modal-groups-section" style="margin-top:14px;border-top:1px solid var(--border);padding-top:12px">
<div class="groups-section-header">
<h5 class="settings-section-subtitle" style="margin:0" data-i18n="modals.edit_profile.groups_title">Groups</h5>
<button type="button" class="btn-sm" id="manage-groups-btn" data-i18n="modals.edit_profile.manage_groups_btn">Manage Groups</button>
<button type="button" class="btn-sm btn-accent" id="save-groups-btn" data-i18n="modals.edit_profile.save_groups_btn" style="display:none">Manage Groups</button>
</div>
<p class="muted-text" style="margin:6px 0 8px;font-size:.78rem;line-height:1.35" data-i18n="modals.edit_profile.groups_hint">
Groups are displayed as roles on your profile card. They don't grant permissions, but they can grant channel access!
</p>
<div id="user-profile-groups-list" class="user-profile-groups-list"></div>
<div id="user-profile-groups-manager" class="user-profile-groups-manager" style="display:none"></div>
</div>
<!-- Personas (#86, #5349) -->
<div class="personas-section" style="margin-top:14px;border-top:1px solid var(--border);padding-top:12px">
<div class="personas-section-header">
<h5 class="settings-section-subtitle" style="margin:0" data-i18n="modals.edit_profile.personas_title">👥 Personas</h5>
<button type="button" class="btn-sm" id="add-persona-btn" data-i18n="modals.edit_profile.add_persona">+ Add Persona</button>
</div>
<p class="muted-text" style="margin:6px 0 8px;font-size:.78rem;line-height:1.35" data-i18n-html="modals.edit_profile.personas_hint">
Type <code>::Name your message</code> in chat to send as that persona (or pick from the autocomplete that appears when you start a message with <code>::</code>). Your real account stays linked for moderation.
</p>
<div id="personas-list" class="personas-list"></div>
</div>
<div class="modal-actions">
<span id="avatar-save-status" class="settings-hint" style="flex:1"></span>
<button class="btn-sm" id="cancel-rename-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="save-rename-btn" data-i18n="modals.common.save">Save</button>
</div>
</div>
</div>
<!-- Border Edit Modal. An append-only op log is the source of truth: each tool
appends one fraction-based op (crop / move / resize / rotate / distort),
folded in order as nested CSS wrappers. The right-side list shows the log
with per-entry undo. Nothing is baked; the same fold drives real rendering.
The avatar reuses this editor with only Crop; its result is drawn
to a canvas and uploaded as the new avatar image. -->
<div class="modal-overlay" id="border-crop-modal" style="display:none">
<div class="modal">
<h3 data-i18n="modals.border_crop.title">Edit</h3>
<div class="border-crop-body">
<div class="border-crop-main">
<div class="border-crop-modes" id="border-crop-modes">
<button type="button" class="border-crop-mode-btn active" data-mode="crop" data-i18n="modals.border_crop.mode_crop">Crop</button>
<button type="button" class="border-crop-mode-btn" data-mode="move" data-i18n="modals.border_crop.mode_move">Move</button>
<button type="button" class="border-crop-mode-btn" data-mode="resize" data-i18n="modals.border_crop.mode_resize">Resize</button>
<button type="button" class="border-crop-mode-btn" data-mode="rotate" data-i18n="modals.border_crop.mode_rotate">Rotate</button>
<button type="button" class="border-crop-mode-btn" data-mode="distort" data-i18n="modals.border_crop.mode_distort">Distort</button>
<button type="button" class="border-crop-mode-btn" data-mode="opacity" data-i18n="modals.border_crop.mode_opacity">Opacity</button>
</div>
<button type="button" class="border-crop-autocrop-btn" id="border-crop-autocrop-btn" data-i18n="modals.border_crop.autocrop">Crop out invisible pixels</button>
<button type="button" class="border-crop-autocrop-btn border-crop-lock-btn" id="border-crop-lock-btn" aria-pressed="false" data-i18n="modals.border_crop.lock_sides" data-i18n-title="modals.border_crop.lock_sides_hint" title="Crop all sides evenly">Lock sides</button>
<div class="border-crop-rotate-actions" id="border-crop-rotate-actions">
<button type="button" class="btn-sm" id="border-rotate-ccw">-90°</button>
<button type="button" class="btn-sm" id="border-rotate-cw">+90°</button>
</div>
<div class="border-crop-resize-actions" id="border-crop-resize-actions">
<label class="border-resize-anchor-label" for="border-resize-anchor" data-i18n="modals.border_crop.anchor_label">Resize anchor</label>
<select id="border-resize-anchor" class="form-select">
<option value="center" data-i18n="modals.border_crop.anchor_center">Center</option>
<option value="corner" data-i18n="modals.border_crop.anchor_corner">Corner</option>
</select>
</div>
<div class="border-crop-opacity-actions" id="border-crop-opacity-actions">
<input type="range" id="border-opacity-slider" min="1" max="100" value="100">
<span class="border-opacity-value" id="border-opacity-value">100%</span>
</div>
<div class="border-crop-stage">
<div class="border-crop-preview mode-crop" id="border-crop-preview">
<div class="border-crop-layers" id="border-crop-layers"></div>
<div class="border-crop-box" id="border-crop-box">
<div class="border-crop-handle crop-edge crop-top" data-edge="top" data-i18n-title="modals.border_crop.crop_top" title="Crop top"></div>
<div class="border-crop-handle crop-edge crop-right" data-edge="right" data-i18n-title="modals.border_crop.crop_right" title="Crop right"></div>
<div class="border-crop-handle crop-edge crop-bottom" data-edge="bottom" data-i18n-title="modals.border_crop.crop_bottom" title="Crop bottom"></div>
<div class="border-crop-handle crop-edge crop-left" data-edge="left" data-i18n-title="modals.border_crop.crop_left" title="Crop left"></div>
<div class="border-crop-handle corner-handle corner-tl" data-corner="tl" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
<div class="border-crop-handle corner-handle corner-tr" data-corner="tr" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
<div class="border-crop-handle corner-handle corner-bl" data-corner="bl" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
<div class="border-crop-handle corner-handle corner-br" data-corner="br" data-i18n-title="modals.border_crop.distort_corner" title="Distort corner"></div>
<div class="border-crop-handle scale-handle" data-i18n-title="modals.border_crop.mode_resize" title="Resize">⤢</div>
<div class="border-crop-handle rotate-handle" data-i18n-title="modals.border_crop.mode_rotate" title="Rotate">⟳</div>
</div>
</div>
</div>
</div>
<div class="border-crop-side">
<h5 class="border-crop-side-title" data-i18n="modals.border_crop.history_title">Edits</h5>
<div class="border-crop-history" id="border-crop-history"></div>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm" id="border-crop-reset-btn" data-i18n="modals.border_crop.discard">Discard</button>
<button class="btn-sm btn-accent" id="border-crop-done-btn" data-i18n="modals.common.done">Done</button>
</div>
</div>
</div>
<!-- Admin Action Modal (Kick/Ban/Mute/Delete) -->
<div class="modal-overlay" id="admin-action-modal" style="display:none">
<div class="modal">
<h3 id="admin-action-title" data-i18n="modals.admin_action.title">Moderate User</h3>
<p class="modal-desc" id="admin-action-desc"></p>
<div class="form-group" id="admin-reason-group">
<label data-i18n="modals.admin_action.reason_label">Reason (optional)</label>
<input type="text" id="admin-action-reason" data-i18n-placeholder="modals.admin_action.reason_placeholder" placeholder="Reason..." maxlength="200">
</div>
<div class="form-group" id="admin-duration-group" style="display:none">
<label data-i18n="modals.admin_action.duration_label">Duration (minutes)</label>
<input type="number" id="admin-action-duration" value="10" min="1" max="43200" step="1">
</div>
<div class="form-group" id="admin-scrub-group" style="display:none">
<label class="toggle-row">
<span data-i18n="modals.admin_action.scrub_label">Scrub messages</span>
<input type="checkbox" id="admin-scrub-checkbox">
</label>
<div id="admin-scrub-scope-row" style="display:none;margin-top:6px;">
<label data-i18n="modals.admin_action.scope_label">Scope</label>
<select id="admin-scrub-scope" class="settings-select" style="width:100%;margin-top:4px;">
<option value="channel" data-i18n="modals.admin_action.scope_channel">This channel only</option>
<option value="server" data-i18n="modals.admin_action.scope_server">Server-wide</option>
</select>
</div>
<small class="settings-hint" data-i18n="modals.admin_action.scrub_hint">Removes the user's messages (archived/protected messages are preserved)</small>
</div>
<div class="form-group" id="admin-purge-group" style="display:none">
<label class="toggle-row">
<span data-i18n="modals.admin_action.purge_label">Replace messages with placeholder</span>
<input type="checkbox" id="admin-purge-checkbox">
</label>
<div id="admin-purge-message-row" style="display:none;margin-top:6px;">
<label data-i18n="modals.admin_action.purge_message_label">Replacement text (optional)</label>
<input type="text" id="admin-purge-message" maxlength="200" data-i18n-placeholder="modals.admin_action.purge_message_placeholder" placeholder="User banned.">
</div>
<small class="settings-hint" data-i18n="modals.admin_action.purge_hint">Replaces the user's messages with placeholder text instead of deleting them. Default: "User banned."</small>
</div>
<div class="form-group" id="admin-ban-ip-group" style="display:none">
<label class="toggle-row">
<span data-i18n="modals.admin_action.ban_ip_label">Also ban recent IP address(es)</span>
<input type="checkbox" id="admin-ban-ip-checkbox">
</label>
<small class="settings-hint" data-i18n="modals.admin_action.ban_ip_hint">Bans up to the 5 most recently seen IPs for this user. Warning: an IP ban may affect other users on the same network (CGNAT, VPN, school/work). Requires the "ban_ip" permission.</small>
</div>
<div class="modal-actions">
<button class="btn-sm" id="cancel-admin-action-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent btn-danger-fill" id="confirm-admin-action-btn" data-i18n="modals.common.confirm">Confirm</button>
</div>
</div>
</div>
<!-- Settings Popout Modal -->
<div class="modal-overlay" id="settings-modal" style="display:none">
<div class="modal modal-settings" data-haven-region="settings">
<div class="settings-header">
<h3>⚙️ <span data-i18n="settings.title">Settings</span></h3>
<div class="settings-tab-bar" id="settings-tab-bar">
<button class="settings-tab active" data-tab="user">👤 <span data-i18n="settings.tab.user">User</span></button>
<button class="settings-tab settings-tab-admin" data-tab="admin" style="display:none">🛡️ <span data-i18n="settings.tab.admin">Admin</span></button>
</div>
<button class="settings-close-btn" id="close-settings-btn" data-i18n-title="modals.common.close" data-i18n-aria-label="modals.common.close" title="Close" aria-label="Close">&times;</button>
</div>
<div class="settings-layout">
<nav class="settings-nav" id="settings-nav">
<!-- Grouped nav. The settings body is one long scrolling column and
these are scroll-to anchors, so every section that exists needs
an entry here; anything missing is findable only by scrolling
past it, which is how Activity ended up effectively hidden. -->
<div class="settings-nav-user">
<div class="settings-nav-group-label" data-i18n="settings.nav.group_appearance">Appearance</div>
<div class="settings-nav-item active" data-target="section-language">🗣️ <span data-i18n="settings.nav.localization">Localization</span></div>
<div class="settings-nav-item" data-target="section-layout">📐 <span data-i18n="settings.nav.layout">Layout &amp; Density</span></div>
<div class="settings-nav-item" data-target="section-zoom">♿ <span data-i18n="settings.nav.accessibility">Accessibility</span></div>
<div class="settings-nav-item" data-target="section-reaction-size">😀 <span data-i18n="settings.nav.reaction_size">Reaction Size</span></div>
<div class="settings-nav-item" data-target="section-role-display">🎨 <span data-i18n="settings.nav.role_display">Role Display</span></div>
<div class="settings-nav-item" data-target="section-toolbar-icons">🧰 <span data-i18n="settings.nav.toolbar_icons">Interface Icons</span></div>
<div class="settings-nav-item" data-target="section-image-display">🖼️ <span data-i18n="settings.nav.image_display">Image Display</span></div>
<div class="settings-nav-item" data-target="section-embed-size">📺 <span data-i18n="settings.nav.embeds_media">Embeds &amp; Media</span></div>
<div class="settings-nav-item" data-target="section-banner-display">🏞️ <span data-i18n="settings.nav.banner_display">Banner Display</span></div>
<div class="settings-nav-item" data-target="section-toggle-style">🎚️ <span data-i18n="settings.nav.toggle_style">Toggle Style</span></div>
<div class="settings-nav-item" data-target="section-statusbar">📊 <span data-i18n="settings.nav.status_bar">Status Bar</span></div>
<div class="settings-nav-group-label" data-i18n="settings.nav.group_chat">Chat</div>
<div class="settings-nav-item" data-target="section-chat-behavior">💬 <span data-i18n="settings.nav.chat_behavior">Chat Behavior</span></div>
<div class="settings-nav-item" data-target="section-soundboard-mode">🎛️ <span data-i18n="settings.nav.soundboard">Soundboard</span></div>
<div class="settings-nav-item" data-target="section-sounds">🔔 <span data-i18n="settings.nav.sounds">Sounds</span></div>
<div class="settings-nav-item" data-target="section-push">📲 <span data-i18n="settings.nav.push">Push</span></div>
<div class="settings-nav-group-label" data-i18n="settings.nav.group_privacy_presence">Privacy &amp; Presence</div>
<div class="settings-nav-item" data-target="section-activity">🎮 <span data-i18n="settings.nav.activity">Activity</span></div>
<div class="settings-nav-item" data-target="section-score-badges">🚢 <span data-i18n="settings.nav.score_badges">Score Badges</span></div>
<div class="settings-nav-group-label" data-i18n="settings.nav.group_account_security">Account &amp; Security</div>
<div class="settings-nav-item" data-target="section-password">🔒 <span data-i18n="settings.nav.password">Password</span></div>
<div class="settings-nav-item" data-target="section-e2e-passphrase">🗝️ <span data-i18n="settings.nav.e2e_passphrase">Encryption</span></div>
<div class="settings-nav-item" data-target="section-sessions">💻 <span data-i18n="settings.nav.sessions">Sessions</span></div>
<div class="settings-nav-item" data-target="section-2fa">🔐 <span data-i18n="settings.nav.two_factor">Two-Factor</span></div>
<div class="settings-nav-item" data-target="section-recovery">🔑 <span data-i18n="settings.nav.recovery">Recovery</span></div>
<div class="settings-nav-item" data-target="section-delete-account">⚠️ <span data-i18n="settings.nav.account">Account</span></div>
<div class="settings-nav-group-label" data-i18n="settings.nav.group_advanced">Advanced</div>
<div class="settings-nav-item" data-target="section-plugins">🧩 <span data-i18n="settings.nav.plugins">Plugins &amp; Themes</span></div>
<div class="settings-nav-item" data-target="section-desktop-shortcuts" id="desktop-shortcuts-nav" style="display:none">⌨️ <span data-i18n="settings.nav.shortcuts">Shortcuts</span></div>
<div class="settings-nav-item" data-target="section-desktop-app" id="desktop-app-nav" style="display:none">🖥️ <span data-i18n="settings.nav.desktop_app">Desktop App</span></div>
<div class="settings-nav-item" data-target="section-debug">🐞 <span data-i18n="settings.nav.debug">Debug</span></div>
</div>
<div class="settings-nav-admin-group" style="display:none">
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_server">Server</div>
<div class="settings-nav-item settings-nav-admin" data-target="section-update" style="display:none">⬆️ <span data-i18n="settings.nav.server_updates">Server Updates</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-extension-updates" style="display:none">🧩 <span data-i18n="settings.nav.extension_updates">Extension Updates</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-branding" style="display:none">🏠 <span data-i18n="settings.nav.branding">Branding</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-presence" style="display:none">🟢 <span data-i18n="settings.nav.presence">Presence &amp; Activity</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-custom-tos" style="display:none">⚖️ <span data-i18n="settings.nav.tos">Terms of Service</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-connectivity" style="display:none">📡 <span data-i18n="settings.nav.connectivity">Voice &amp; Connectivity</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-large-server" style="display:none">🏟️ <span data-i18n="settings.nav.large_server">Large Server Setup</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-tunnel" style="display:none">🧭 <span data-i18n="settings.nav.tunnel">Tunnel</span></div>
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_people">People &amp; Access</div>
<div class="settings-nav-item settings-nav-admin" data-target="section-members" style="display:none">👥 <span data-i18n="settings.nav.members">Members</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-roles" style="display:none">👑 <span data-i18n="settings.nav.roles">Roles</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-moderation" style="display:none">🛡️ <span data-i18n="settings.nav.moderation">Moderation</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-security" style="display:none">🔒 <span data-i18n="settings.nav.security">Security</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-automod" style="display:none">🛡️ <span data-i18n="settings.nav.automod">Moderation</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-whitelist" style="display:none">🚪 <span data-i18n="settings.nav.whitelist">Sign-ups</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-invite" style="display:none">🌐 <span data-i18n="settings.nav.invite">Invite Code</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-guests" style="display:none">👤 <span data-i18n="settings.nav.guests">Guest Access</span></div>
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_content">Content</div>
<div class="settings-nav-item settings-nav-admin" data-target="section-emojis" style="display:none">😎 <span data-i18n="settings.nav.emojis">Emojis</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-stickers" style="display:none">🖼️ <span data-i18n="settings.nav.stickers">Stickers</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-sounds-admin" style="display:none">🔊 <span data-i18n="settings.nav.admin_sounds">Sounds</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-uploads" style="display:none">📁 <span data-i18n="settings.nav.limits">Limits</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-tags-admin" style="display:none">🏷️ <span data-i18n="settings.nav.tags">Tags</span></div>
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_integrations">Integrations</div>
<div class="settings-nav-item settings-nav-admin" data-target="section-bots" style="display:none">🤖 <span data-i18n="settings.nav.bots">Bots</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-ferry" style="display:none">🛶 <span data-i18n="settings.nav.ferry">Ferry</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-import" style="display:none">📦 <span data-i18n="settings.nav.import">Import</span></div>
<div class="settings-nav-group-label settings-nav-admin" style="display:none" data-i18n="settings.nav.group_maintenance">Maintenance</div>
<div class="settings-nav-item settings-nav-admin" data-target="section-backup" style="display:none">💾 <span data-i18n="settings.nav.backup">Backup</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-template" style="display:none">🧩 <span data-i18n="settings.nav.template">Server Template</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-cleanup" style="display:none">🗑️ <span data-i18n="settings.nav.cleanup">Cleanup</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-modmode" style="display:none">🔧 <span data-i18n="settings.nav.mod_mode">Mod Mode</span></div>
<div class="settings-nav-item settings-nav-admin" data-target="section-audit-log" style="display:none">📜 <span data-i18n="settings.nav.audit_log">Audit Log</span></div>
</div>
</nav>
<div class="settings-body" id="settings-body-user">
<!-- Localization Section -->
<div class="settings-section" id="section-language">
<h5 class="settings-section-title">🗣️ <span data-i18n="settings.localization_section.title">Localization</span></h5>
<label class="select-row">
<span data-i18n="settings.language_section.label">Language</span>
<select id="language-select" class="settings-select">
<option value="auto" data-i18n="settings.language_section.auto">Automatic</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="pl">Polski</option>
<option value="ru">Русский</option>
<option value="zh">中文</option>
<option value="pt">Português (Brasil)</option>
<option value="fa">فارسی</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.language_section.hint">Changes apply immediately</small>
<div class="settings-subsection" id="timezone-subsection">
<small class="settings-group-label">🌎 <span data-i18n="settings.timezone_section.title">Timezone</span></small>
<div class="select-row">
<span id="timezone-current-summary" data-i18n="settings.timezone_section.not_set">Using this device's timezone</span>
<button type="button" class="btn-sm" id="configure-time-btn" data-i18n="settings.timezone_section.configure">Configure Time</button>
</div>
<small class="settings-hint" data-i18n="settings.timezone_section.hint">Saved to your account and used on every device</small>
</div>
</div>
<div class="settings-section" id="section-layout">
<h5 class="settings-section-title">📐 <span data-i18n="settings.layout.title">Layout</span></h5>
<div class="settings-subsection" id="layout-style-subsection">
<small class="settings-group-label">🧩 <span data-i18n="settings.layout.style.title">Sidebar Layout</span></small>
<!-- Original, then every layout plugin on this server (theme.js) -->
<div class="layout-picker" id="layout-picker" role="radiogroup" data-i18n-aria-label="app.theme.layout" aria-label="Layout">
<button type="button" class="layout-opt active" data-layout="" role="radio" aria-checked="true" data-i18n="app.theme.layout_original">Original</button>
</div>
<small class="settings-hint" data-i18n="settings.layout.style.hint">Original is Haven's own layout. The others come from layout plugins on this server. Saved per browser.</small>
</div>
<div class="settings-subsection" id="layout-categories-subsection">
<small class="settings-group-label">🗂️ <span data-i18n="settings.layout.categories.title">Channel Categories</span></small>
<select id="category-layout-select" class="settings-select">
<option value="" data-i18n="settings.layout.categories.server">Server default</option>
<option value="list" data-i18n="settings.layout.categories.list">List</option>
<option value="tabs" data-i18n="settings.layout.categories.tabs">Tabs</option>
</select>
<small class="settings-hint" data-i18n="settings.layout.categories.hint">Show category headers as a list or as a grid of tabs. Server default follows the admin's choice. Saved per browser.</small>
</div>
<div class="settings-subsection" id="layout-density-subsection">
<small class="settings-group-label">📏 <span data-i18n="settings.layout.density.title">Layout Density</span></small>
<div class="density-picker" id="density-picker">
<button type="button" class="density-btn" data-density="compact" data-i18n-title="settings.layout.density.compact_hint" title="Compact: tighter spacing, smaller avatars">
<span class="density-icon">▪️</span>
<span class="density-label" data-i18n="settings.layout.density.compact">Compact</span>
</button>
<button type="button" class="density-btn active" data-density="cozy" data-i18n-title="settings.layout.density.cozy_hint" title="Cozy: default balanced layout">
<span class="density-icon">◾</span>
<span class="density-label" data-i18n="settings.layout.density.cozy">Cozy</span>
</button>
<button type="button" class="density-btn" data-density="spacious" data-i18n-title="settings.layout.density.spacious_hint" title="Spacious: more breathing room">
<span class="density-icon">⬛</span>
<span class="density-label" data-i18n="settings.layout.density.spacious">Spacious</span>
</button>
</div>
</div>
<div class="settings-subsection" id="channel-scrolling-subsection">
<small class="settings-group-label">📜 <span data-i18n="settings.layout.channel_scrolling.title">Channel Scrolling</span></small>
<div class="channel-scroll-picker" id="channel-scroll-picker">
<button type="button" class="density-btn active" data-channel-scroll="separate" data-i18n-title="settings.layout.channel_scrolling.separate_hint" title="Separate: separate scrollbars for channels and DMs">
<span class="density-icon">📜📜</span>
<span class="density-label" data-i18n="settings.layout.channel_scrolling.separate">Separate</span>
</button>
<button type="button" class="density-btn" data-channel-scroll="combined" data-i18n-title="settings.layout.channel_scrolling.combined_hint" title="Combined: one scrollbar for both channels and DMs">
<span class="density-icon">📜</span>
<span class="density-label" data-i18n="settings.layout.channel_scrolling.combined">Combined</span>
</button>
</div>
</div>
<div class="settings-subsection" id="composer-buttons-subsection">
<small class="settings-group-label">✉️ <span data-i18n="settings.layout.composer.title">Message Box</span></small>
<label class="toggle-row">
<span data-i18n="settings.layout.composer.hide_send">Hide the Send button</span>
<input type="checkbox" id="hide-send-btn">
</label>
<small class="settings-hint" style="display:block" data-i18n="settings.layout.composer.hide_send_hint">Enter still sends. Frees the end of the message box if you never click the button.</small>
<label class="toggle-row">
<span data-i18n="settings.layout.composer.compact">Fold the buttons into one + button</span>
<input type="checkbox" id="compact-composer">
</label>
<small class="settings-hint" style="display:block" data-i18n="settings.layout.composer.compact_hint">Upload, emoji, GIF and the rest sit behind a single + at the start of the message box, so the box gets the room.</small>
</div>
</div>
<!-- Accessibility. The id stays section-zoom: layout plugins anchor
their own cards to it, and Zoom is the first thing in here. Every
option is saved per device and applied by theme-init.js before
first paint. -->
<div class="settings-section" id="section-zoom" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">♿ <span data-i18n="settings.accessibility.title">Accessibility</span></h5>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.hint">Saved on this device. Each change applies right away.</small>
<div class="settings-subsection" id="a11y-zoom-subsection">
<small class="settings-group-label" id="ui-zoom-label">🔍 <span data-i18n="settings.zoom.title">Zoom</span></small>
<div class="zoom-control" id="zoom-control">
<button type="button" class="zoom-step" id="zoom-out-btn" title="Zoom out" aria-label="Zoom out" data-i18n-title="settings.zoom.out" data-i18n-aria-label="settings.zoom.out">−</button>
<input type="range" class="zoom-slider" id="ui-zoom-slider" min="70" max="150" step="5" value="100" aria-label="Interface zoom" data-i18n-aria-label="settings.zoom.slider_label">
<button type="button" class="zoom-step" id="zoom-in-btn" title="Zoom in" aria-label="Zoom in" data-i18n-title="settings.zoom.in" data-i18n-aria-label="settings.zoom.in">+</button>
<output class="zoom-value" id="ui-zoom-value" for="ui-zoom-slider">100%</output>
</div>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.zoom.hint">Scales the whole interface. Drag for the exact size you want.</small>
</div>
<div class="settings-subsection" id="a11y-text-size-subsection">
<small class="settings-group-label" id="text-size-label">🔠 <span data-i18n="settings.accessibility.text_size.title">Message Text Size</span></small>
<div class="zoom-control" id="text-size-control">
<button type="button" class="zoom-step" id="text-size-out-btn" title="Smaller text" aria-label="Smaller text" data-i18n-title="settings.accessibility.text_size.smaller" data-i18n-aria-label="settings.accessibility.text_size.smaller">−</button>
<input type="range" class="zoom-slider" id="text-size-slider" min="80" max="200" step="5" value="100" aria-label="Message text size" data-i18n-aria-label="settings.accessibility.text_size.slider_label">
<button type="button" class="zoom-step" id="text-size-in-btn" title="Larger text" aria-label="Larger text" data-i18n-title="settings.accessibility.text_size.larger" data-i18n-aria-label="settings.accessibility.text_size.larger">+</button>
<output class="zoom-value" id="text-size-value" for="text-size-slider">100%</output>
<button type="button" class="a11y-reset-btn" id="text-size-reset-btn" data-i18n="settings.accessibility.reset">Reset</button>
</div>
<div class="a11y-text-preview message-content" data-i18n="settings.accessibility.text_size.preview">The quick brown fox jumps over the lazy dog.</div>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.text_size.hint">Messages, threads, forum posts and the message box, on top of Zoom. The rest of the interface stays as it is.</small>
</div>
<div class="settings-subsection" id="a11y-spacing-subsection">
<small class="settings-group-label" id="line-spacing-label">↕️ <span data-i18n="settings.accessibility.line_spacing.title">Line Spacing</span></small>
<div class="density-picker" id="line-spacing-picker" role="group" aria-labelledby="line-spacing-label">
<button type="button" class="density-btn active" data-line-spacing="normal">
<span class="density-label" data-i18n="settings.accessibility.line_spacing.normal">Normal</span>
</button>
<button type="button" class="density-btn" data-line-spacing="relaxed">
<span class="density-label" data-i18n="settings.accessibility.line_spacing.relaxed">Relaxed</span>
</button>
<button type="button" class="density-btn" data-line-spacing="loose">
<span class="density-label" data-i18n="settings.accessibility.line_spacing.loose">Loose</span>
</button>
</div>
<small class="settings-group-label" id="letter-spacing-label" style="margin-top:10px">↔️ <span data-i18n="settings.accessibility.letter_spacing.title">Letter Spacing</span></small>
<div class="density-picker" id="letter-spacing-picker" role="group" aria-labelledby="letter-spacing-label">
<button type="button" class="density-btn active" data-letter-spacing="normal">
<span class="density-icon" aria-hidden="true">abc</span>
<span class="density-label" data-i18n="settings.accessibility.letter_spacing.normal">Normal</span>
</button>
<button type="button" class="density-btn" data-letter-spacing="wide">
<span class="density-icon" aria-hidden="true" style="letter-spacing:0.2em">abc</span>
<span class="density-label" data-i18n="settings.accessibility.letter_spacing.wide">Wide</span>
</button>
</div>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.spacing_hint">Applies where you read and write messages.</small>
</div>
<div class="settings-subsection" id="a11y-font-subsection">
<small class="settings-group-label" id="readable-font-label">🔤 <span data-i18n="settings.accessibility.font.title">Font</span></small>
<select id="readable-font-select" class="settings-select" aria-labelledby="readable-font-label">
<option value="theme" data-i18n="settings.accessibility.font.theme">Theme font</option>
<option value="system" data-i18n="settings.accessibility.font.system">System font</option>
<option value="hyperlegible" data-i18n="settings.accessibility.font.hyperlegible">Atkinson Hyperlegible</option>
<option value="dyslexic" data-i18n="settings.accessibility.font.dyslexic">OpenDyslexic</option>
</select>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.font.hint">Replaces the theme's font across the app. Atkinson Hyperlegible keeps similar letters apart; OpenDyslexic weights the bottom of each letter. Code keeps its monospace font.</small>
</div>
<div class="settings-subsection" id="a11y-motion-subsection">
<small class="settings-group-label" id="reduce-motion-label">🌙 <span data-i18n="settings.accessibility.motion.title">Reduce Motion</span></small>
<div class="density-picker" id="reduce-motion-picker" role="group" aria-labelledby="reduce-motion-label">
<button type="button" class="density-btn active" data-reduce-motion="system">
<span class="density-label" data-i18n="settings.accessibility.motion.system">Follow system</span>
</button>
<button type="button" class="density-btn" data-reduce-motion="on">
<span class="density-label" data-i18n="settings.accessibility.motion.on">On</span>
</button>
<button type="button" class="density-btn" data-reduce-motion="off">
<span class="density-label" data-i18n="settings.accessibility.motion.off">Off</span>
</button>
</div>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.motion.hint">Stops animations, theme effects, shimmering role colors and smooth scrolling. GIFs, animated emoji and profile pictures play only while you hover or tap them.</small>
</div>
<div class="settings-subsection" id="a11y-display-subsection">
<small class="settings-group-label">👁️ <span data-i18n="settings.accessibility.display.title">Display</span></small>
<label class="toggle-row">
<span data-i18n="settings.accessibility.display.high_contrast">High contrast</span>
<input type="checkbox" id="a11y-high-contrast">
</label>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.display.high_contrast_hint">Brighter secondary text, stronger borders and a thick focus ring, on any theme.</small>
<label class="toggle-row">
<span data-i18n="settings.accessibility.display.underline_links">Underline links</span>
<input type="checkbox" id="a11y-underline-links">
</label>
<label class="toggle-row">
<span data-i18n="settings.accessibility.display.large_targets">Larger click targets</span>
<input type="checkbox" id="a11y-large-targets">
</label>
<small class="settings-hint" style="display:block" data-i18n="settings.accessibility.display.large_targets_hint">Bigger buttons and taller rows in the sidebars, the message box and the message actions.</small>
</div>
</div>
<!-- Emoji Reaction Size -->
<div class="settings-section" id="section-reaction-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">😀 <span data-i18n="settings.reaction_size.title">Reaction Size</span></h5>
<div class="font-size-picker" id="emoji-size-picker">
<button type="button" class="density-btn" data-emojisize="small" title="Small reactions" data-i18n-title="settings.reaction_size.small_hint">
<span class="density-icon" style="font-size:0.6875rem">😀</span>
<span class="density-label" data-i18n="settings.reaction_size.small">Small</span>
</button>
<button type="button" class="density-btn active" data-emojisize="normal" title="Normal reactions (default)" data-i18n-title="settings.reaction_size.normal_hint">
<span class="density-icon" style="font-size:0.875rem">😀</span>
<span class="density-label" data-i18n="settings.reaction_size.normal">Normal</span>
</button>
<button type="button" class="density-btn" data-emojisize="large" title="Large reactions" data-i18n-title="settings.reaction_size.large_hint">
<span class="density-icon" style="font-size:1.125rem">😀</span>
<span class="density-label" data-i18n="settings.reaction_size.large">Large</span>
</button>
<button type="button" class="density-btn" data-emojisize="x-large" title="Extra large reactions" data-i18n-title="settings.reaction_size.xl_hint">
<span class="density-icon" style="font-size:1.375rem">😀</span>
<span class="density-label" data-i18n="settings.reaction_size.xl">XL</span>
</button>
</div>
</div>
<!-- Role Color Display -->
<div class="settings-section" id="section-role-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🎨 <span data-i18n="settings.role_display.title">Role Display</span></h5>
<div class="density-picker" id="role-display-picker">
<button type="button" class="density-btn active" data-roledisplay="colored-name" title="Color the username with the role color" data-i18n-title="settings.role_display.colored_name_hint">
<span class="density-icon" style="color:#5865f2;font-weight:bold">A</span>
<span class="density-label" data-i18n="settings.role_display.colored_name">Colored Name</span>
</button>
<button type="button" class="density-btn" data-roledisplay="dot" title="Show a colored dot next to the name (legacy)" data-i18n-title="settings.role_display.dot_hint">
<span class="density-icon">●</span>
<span class="density-label" data-i18n="settings.role_display.dot">Dot</span>
</button>
</div>
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.role_display.hint">How role colors are shown next to usernames in chat and the member list.</small>
</div>
<!-- Toolbar Icon Style -->
<div class="settings-section" id="section-toolbar-icons" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🧰 <span data-i18n="settings.toolbar_icons.title">Interface Icons</span></h5>
<div class="density-picker" id="toolbar-icon-picker">
<button type="button" class="density-btn active" data-toolbaricons="mono" title="Use sleek monochrome icons in message toolbars" data-i18n-title="settings.toolbar_icons.monochrome_hint">
<span class="density-icon" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="9"></circle>
<path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-linecap="round"></path>
<circle cx="9.2" cy="10.2" r="1" fill="currentColor" stroke="none"></circle>
<circle cx="14.8" cy="10.2" r="1" fill="currentColor" stroke="none"></circle>
</svg>
</span>
<span class="density-label" data-i18n="settings.toolbar_icons.monochrome">Monochrome</span>
</button>
<button type="button" class="density-btn" data-toolbaricons="emoji" title="Use colorful emoji icons in message toolbars" data-i18n-title="settings.toolbar_icons.colorful_hint">
<span class="density-icon">😀</span>
<span class="density-label" data-i18n="settings.toolbar_icons.colorful">Colorful Emoji</span>
</button>
<button type="button" class="density-btn" data-toolbaricons="glyphs" title="Draw the whole interface's icons with the Haven Glyphs font instead of emoji" data-i18n-title="settings.toolbar_icons.glyphs_hint">
<span class="density-icon" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M12 2l2.9 6.2 6.8.8-5 4.7 1.3 6.7L12 17.1 5.9 20.4l1.3-6.7-5-4.7 6.8-.8z"/>
</svg>
</span>
<span class="density-label" data-i18n="settings.toolbar_icons.glyphs">Glyphs</span>
</button>
</div>
<small class="settings-hint" style="display:block" data-i18n="settings.toolbar_icons.glyphs_note">Monochrome and Colorful Emoji change the message toolbars. Glyphs redraws icons across the whole interface with the bundled Haven Glyphs plugin; message text, reactions and pickers are left alone.</small>
<label class="toolbar-slots-row" for="toolbar-visible-slots">
<span data-i18n="settings.toolbar_icons.visible_slots">Visible toolbar slots before overflow</span>
<input type="range" id="toolbar-visible-slots" min="1" max="7" step="1" value="3">
<span id="toolbar-visible-slots-value" class="toolbar-slots-value">3</span>
</label>
<div class="toolbar-order-wrap">
<div class="toolbar-order-head">
<span data-i18n="settings.toolbar_icons.order">Toolbar slot order</span>
<button type="button" class="btn-sm" id="toolbar-order-reset-btn" data-i18n="settings.toolbar_icons.reset">Reset</button>
</div>
<div id="toolbar-order-list" class="toolbar-order-list"></div>
</div>
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.toolbar_icons.hint">Applies to message and thread hover action bars.</small>
</div>
<div class="settings-section" id="section-image-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🖼️ <span data-i18n="settings.image_display.title">Image Display</span></h5>
<!-- Image Display Mode -->
<!-- Renamed from the Layout Density section's id, which it duplicated.
getElementById always resolved to the first match, so this section
had no working nav anchor at all. -->
<div class="settings-subsection" id="image-mode-subsection">
<small class="settings-group-label">🖼️ <span data-i18n="settings.image_display.image_mode.title">Image Display Mode</span></small>
<div class="density-picker" id="image-mode-picker">
<button type="button" class="density-btn active" data-image-mode="thumbnail" data-i18n-title="settings.image_display.image_mode.thumbnail_hint" title="Thumbnail: small preview, click to enlarge">
<span class="density-icon">🔍</span>
<span class="density-label" data-i18n="settings.image_display.image_mode.thumbnail">Thumbnail</span>
</button>
<button type="button" class="density-btn" data-image-mode="full" data-i18n-title="settings.image_display.image_mode.full_hint" title="Full: large inline images like Discord">
<span class="density-icon">🖼️</span>
<span class="density-label" data-i18n="settings.image_display.image_mode.full">Full</span>
</button>
</div>
</div>
<!-- Animated profile pictures (viewer side). The pfp owner can already turn
their own animation off for everyone; this is the other half, so a
viewer who doesn't want moving avatars can stop seeing them. -->
<div class="settings-subsection" id="animate-pfp-subsection">
<small class="settings-group-label">✨ <span data-i18n="settings.image_display.animate_pfp.title">Animated Profile Pictures</span></small>
<div class="density-picker" id="animate-pfp-picker">
<button type="button" class="density-btn" data-animpfp="always" data-i18n-title="settings.image_display.animate_pfp.always_hint" title="Always: animated avatars loop constantly">
<span class="density-icon">▶️</span>
<span class="density-label" data-i18n="settings.image_display.animate_pfp.always">Always</span>
</button>
<button type="button" class="density-btn active" data-animpfp="hover" data-i18n-title="settings.image_display.animate_pfp.hover_hint" title="On hover: play while you hover a message or open a profile">
<span class="density-icon">🖱️</span>
<span class="density-label" data-i18n="settings.image_display.animate_pfp.hover">On hover</span>
</button>
<button type="button" class="density-btn" data-animpfp="never" data-i18n-title="settings.image_display.animate_pfp.never_hint" title="Never: show the first frame only">
<span class="density-icon">⏸️</span>
<span class="density-label" data-i18n="settings.image_display.animate_pfp.never">Never</span>
</button>
</div>
<small class="settings-hint" data-i18n="settings.image_display.animate_pfp.hint">Applies to everyone's avatars and borders as you see them. Anyone who has turned animation off for their own profile stays still regardless.</small>
</div>
<!-- Animated images in chat (#5526). Separate from the avatar setting above:
a GIF someone posted is content, an avatar is ambient, so this one
stays at Always unless the viewer asks otherwise. -->
<div class="settings-subsection" id="animate-chat-subsection">
<small class="settings-group-label">🎞️ <span data-i18n="settings.image_display.animate_chat.title">Animated Images in Chat</span></small>
<div class="density-picker" id="animate-chat-picker">
<button type="button" class="density-btn active" data-animchat="always" data-i18n-title="settings.image_display.animate_chat.always_hint" title="Always: GIFs loop constantly">
<span class="density-icon">▶️</span>
<span class="density-label" data-i18n="settings.image_display.animate_chat.always">Always</span>
</button>
<button type="button" class="density-btn" data-animchat="hover" data-i18n-title="settings.image_display.animate_chat.hover_hint" title="On hover: play only while you point at the image">
<span class="density-icon">🖱️</span>
<span class="density-label" data-i18n="settings.image_display.animate_chat.hover">On hover</span>
</button>
<button type="button" class="density-btn" data-animchat="never" data-i18n-title="settings.image_display.animate_chat.never_hint" title="Never: show the first frame only">
<span class="density-icon">⏸️</span>
<span class="density-label" data-i18n="settings.image_display.animate_chat.never">Never</span>
</button>
</div>
<small class="settings-hint" data-i18n="settings.image_display.animate_chat.hint">Applies to GIFs and other animated images posted in messages, for you only. Nobody else's view changes.</small>
</div>
</div>
<!-- Embed / Link Preview Size -->
<div class="settings-section" id="section-embed-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🔗 <span data-i18n="settings.embed_display.title">Link Previews</span></h5>
<div class="density-picker" id="embed-size-picker">
<button type="button" class="density-btn" data-embed-size="off" title="Off: hide all link previews" data-i18n-title="settings.embed_display.off_hint">
<span class="density-icon">🚫</span>
<span class="density-label" data-i18n="settings.embed_display.off">Off</span>
</button>
<button type="button" class="density-btn" data-embed-size="small" title="Small: compact card with small media" data-i18n-title="settings.embed_display.small_hint">
<span class="density-icon">🔗</span>
<span class="density-label" data-i18n="settings.embed_display.small">Small</span>
</button>
<button type="button" class="density-btn active" data-embed-size="medium" title="Medium: balanced card with medium media" data-i18n-title="settings.embed_display.medium_hint">
<span class="density-icon">🖼️</span>
<span class="density-label" data-i18n="settings.embed_display.medium">Medium</span>
</button>
<button type="button" class="density-btn" data-embed-size="full" title="Full: large media, author, and post stats" data-i18n-title="settings.embed_display.full_hint">
<span class="density-icon">📰</span>
<span class="density-label" data-i18n="settings.embed_display.full">Full</span>
</button>
</div>
</div>
<!-- Banner Display (client-side) -->
<div class="settings-section" id="section-banner-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px; display:none;">
<h5 class="settings-section-title">🏞️ <span data-i18n="settings.banner_display.title">Banner Display</span></h5>
<label style="margin-top:4px;display:flex;align-items:center;gap:8px;font-size:0.75rem">
<span style="white-space:nowrap" data-i18n="settings.banner_display.height">Banner height</span>
<input type="range" id="banner-height-slider" min="80" max="400" step="10" value="180" style="flex:1">
<span id="banner-height-value" style="min-width:36px;text-align:right;font-variant-numeric:tabular-nums">180px</span>
</label>
<label style="margin-top:4px;display:flex;align-items:center;gap:8px;font-size:0.75rem">
<span style="white-space:nowrap" data-i18n="settings.banner_display.offset">Vertical offset</span>
<input type="range" id="banner-offset-slider" min="0" max="100" step="1" value="0" style="flex:1">
<span id="banner-offset-value" style="min-width:30px;text-align:right;font-variant-numeric:tabular-nums">0%</span>
</label>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.banner_display.header_style">Header style</span>
<select id="banner-header-mode" style="max-width:160px">
<option value="full" data-i18n="settings.banner_display.full">Full Header (opaque)</option>
<option value="shaded" data-i18n="settings.banner_display.shaded">Shaded Header</option>
<option value="minimal" data-i18n="settings.banner_display.minimal">Minimal Header</option>
<option value="transparent" data-i18n="settings.banner_display.transparent">Transparent Header</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.banner_display.hint">How the banner interacts with the channel header area. These settings are per-device.</small>
</div>
<!-- Toggle Style (sliders vs checkboxes) -->
<div class="settings-section" id="section-toggle-style" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🎚️ <span data-i18n="settings.toggle_style.title">Toggle Style</span></h5>
<div class="font-size-picker" id="toggle-style-picker">
<button type="button" class="density-btn active" data-togglestyle="switch" data-i18n-title="settings.toggle_style.switch_hint" title="Sliding switches (default)">
<span class="density-icon">🎚️</span>
<span class="density-label" data-i18n="settings.toggle_style.switch">Sliders</span>
</button>
<button type="button" class="density-btn" data-togglestyle="box" data-i18n-title="settings.toggle_style.box_hint" title="Classic square checkboxes">
<span class="density-icon">☑️</span>
<span class="density-label" data-i18n="settings.toggle_style.box">Boxes</span>
</button>
</div>
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.toggle_style.hint">Applies to on/off settings. Lists you tick items in, like permissions and channel pickers, always stay as boxes.</small>
</div>
<!-- Status Bar -->
<div class="settings-section" id="section-statusbar" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">📊 <span data-i18n="settings.status_bar_section.title">Status Bar</span></h5>
<label class="toggle-row">
<span data-i18n="settings.status_bar_section.show">Show Status Bar</span>
<input type="checkbox" id="show-status-bar">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.status_bar_section.hint">Show the status bar at the bottom of the screen with server, ping, channel, and online info.</small>
</div>
<!-- Chat Behavior -->
<div class="settings-section" id="section-chat-behavior" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">💬 <span data-i18n="settings.chat_behavior.title">Chat</span></h5>
<label class="toggle-row">
<span data-i18n="settings.chat_behavior.up_arrow_edit">Up Arrow Edits Last Message</span>
<input type="checkbox" id="up-arrow-edit" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.up_arrow_edit_hint">Press the Up arrow key on an empty input to quickly edit your last message.</small>
<label class="toggle-row">
<span data-i18n="settings.chat_behavior.dm_fullscreen">Open DMs in fullscreen on single click</span>
<input type="checkbox" id="dm-fullscreen-default">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.dm_fullscreen_hint">Single-click a DM to open it in the full pane instead of a floating PiP. Double-click opens the PiP.</small>
<label class="toggle-row">
<span data-i18n="settings.chat_behavior.hover_profile_card">Show profile card on hover</span>
<input type="checkbox" id="hover-profile-card" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.hover_profile_card_hint">Hovering over a name or avatar shows a quick profile preview. Clicking opens the full card either way.</small>
<label class="select-row">
<span data-i18n="settings.chat_behavior.official_style">Official messages</span>
<select id="official-style-select" style="max-width:160px">
<option value="badge" data-i18n="settings.chat_behavior.official_style_badge">Badge</option>
<option value="text" data-i18n="settings.chat_behavior.official_style_text">Colored text</option>
<option value="off" data-i18n="settings.chat_behavior.official_style_off">Off</option>
</select>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.official_style_hint">How messages posted as a role look to you. Badge: the role's name next to the message. Colored text: the text in the role's color, and a run of them reads as one block. Off: they look like any other message. Saved on this device.</small>
<div id="official-mode-setting" style="display:none">
<label class="toggle-row">
<span data-i18n="settings.chat_behavior.official_mode">Keep official mode on</span>
<input type="checkbox" id="official-mode-toggle">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.chat_behavior.official_mode_hint">Every message you send where you can post as your role goes out official, until you turn this off. The megaphone then sends just the next message plain. Saved to your account, so it follows you to every device.</small>
</div>
</div>
<!-- Soundboard Sidebar Setting -->
<div class="settings-section" id="section-soundboard-mode" style="border-top: 1px solid var(--border-light); padding-top: 14px; margin-top: 8px;">
<h5 class="settings-section-title">🎛️ <span data-i18n="settings.soundboard_section.title">Soundboard</span></h5>
<label class="checkbox-row" style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:0.8125rem">
<input type="checkbox" id="soundboard-sidebar-mode-settings" style="cursor:pointer">
<span data-i18n="settings.soundboard_section.sidebar_mode">Sidebar mode</span>
</label>
<small class="settings-hint" style="margin-top:4px;display:block" data-i18n="settings.soundboard_section.sidebar_mode_hint">Open the soundboard as a pinned sidebar panel instead of a popup.</small>
</div>
<!-- Sounds Section -->
<div class="settings-section" id="section-sounds">
<h5 class="settings-section-title">🔔 <span data-i18n="settings.sounds_section.title">Sounds</span></h5>
<div class="notif-settings">
<label class="toggle-row">
<span data-i18n="settings.sounds_section.notifications">Notifications</span>
<input type="checkbox" id="notif-enabled">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.notifications_hint">General message sounds (off by default). @Mentions, replies, and DMs always notify.</small>
<label class="select-row">
<span data-i18n="settings.sounds_section.mute_server">Mute this server</span>
<select id="notif-server-mute-mode" class="settings-select">
<option value="off" data-i18n="mute.mode_off">Off</option>
<option value="all" data-i18n="mute.mode_all">Mute all</option>
<option value="mentions" data-i18n="mute.mode_mentions">Mute except @mentions</option>
</select>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.mute_server_hint">Silences sounds, pop-ups, push notifications and the taskbar badge from this server. DMs still come through. Saved on your account, so it applies on every device. You can also right-click the server's icon.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.mentions">@Mentions</span>
<input type="checkbox" id="notif-mentions-enabled" checked>
</label>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.role_mentions">@Role mentions</span>
<input type="checkbox" id="notif-role-mentions-enabled" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.role_mentions_hint">Pings for roles you hold, like @Moderators. Off means they count as ordinary messages.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.replies">Replies</span>
<input type="checkbox" id="notif-replies-enabled" checked>
</label>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.direct_messages">Direct Messages</span>
<input type="checkbox" id="notif-dm-enabled" checked>
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.popup_limit">Limit pop-up notifications</span>
<select id="notif-popup-cooldown">
<option value="0" data-i18n="settings.sounds_section.popup_off">Off (show every one)</option>
<option value="30000" data-i18n="settings.sounds_section.popup_30s">At most 1 every 30 sec</option>
<option value="60000" data-i18n="settings.sounds_section.popup_1m">At most 1 per minute</option>
<option value="300000" data-i18n="settings.sounds_section.popup_5m">At most 1 every 5 min</option>
<option value="900000" data-i18n="settings.sounds_section.popup_15m">At most 1 every 15 min</option>
<option value="-1" data-i18n="settings.sounds_section.popup_never">Never (sounds and badges still work)</option>
<option value="custom" data-i18n="settings.sounds_section.popup_custom">Custom…</option>
</select>
</label>
<label class="select-row" id="notif-popup-custom-row" style="display:none">
<span data-i18n="settings.sounds_section.popup_custom_minutes">Minutes between pop-ups</span>
<input type="number" id="notif-popup-custom-minutes" min="1" max="1440" step="1" class="settings-number-input" style="width:5.5rem">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.popup_limit_hint">Throttle how often desktop/browser notification pop-ups appear, so a burst of messages or a flaky connection can't keep popping the app up. Notification sounds and unread badges are unaffected. Never also turns off push notifications on this device.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.auto_accept_streams">Auto-accept Streams</span>
<input type="checkbox" id="auto-accept-streams" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.auto_accept_streams_hint">Automatically open screen shares when someone starts streaming. Turn off to join manually.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.hide_voice_panel">Hide Voice Panel</span>
<input type="checkbox" id="hide-voice-panel">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.hide_voice_panel_hint">Hide the voice users panel on desktop. Voice users are still shown in channel indicators.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.sidebar_voice_controls">Sidebar Voice Controls</span>
<input type="checkbox" id="sidebar-voice-controls">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.sidebar_voice_controls_hint">Show mute/deafen buttons in the sidebar instead of the voice panel header.</small>
<label class="toggle-row">
<span data-i18n="settings.sounds_section.voice_action_cues">Mute / PTT Cues</span>
<input type="checkbox" id="notif-voice-action-cues-enabled" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.voice_action_cues_hint">Play short cue tones when muting, unmuting, deafening, or using push-to-talk in Haven Desktop.</small>
<label class="volume-row">
<span data-i18n="settings.sounds_section.volume">Volume</span>
<input type="range" id="notif-volume" min="0" max="100" value="50" class="slider-sm">
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.messages">Messages</span>
<select id="notif-msg-sound">
<option value="ping">Ping</option>
<option value="chime">Chime</option>
<option value="blip">Blip</option>
<option value="bell">Bell</option>
<option value="drop">Drop</option>
<option value="alert">Alert</option>
<option value="chord">Chord</option>
<option value="none">None</option>
</select>
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.msg_sent">Msg Sent</span>
<select id="notif-sent-sound">
<option value="swoosh">Swoosh</option>
<option value="blip">Blip</option>
<option value="ping">Ping</option>
<option value="chime">Chime</option>
<option value="drop">Drop</option>
<option value="none">None</option>
</select>
</label>
<div class="notif-divider"></div>
<label class="volume-row">
<span data-i18n="settings.sounds_section.mention_vol">@Mention Vol</span>
<input type="range" id="notif-mention-volume" min="0" max="100" value="80" class="slider-sm">
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.mentions">@Mentions</span>
<select id="notif-mention-sound">
<option value="bell">Bell</option>
<option value="alert">Alert</option>
<option value="chord">Chord</option>
<option value="ping">Ping</option>
<option value="chime">Chime</option>
<option value="blip">Blip</option>
<option value="drop">Drop</option>
<option value="none">None</option>
</select>
</label>
<div class="notif-divider"></div>
<label class="volume-row">
<span data-i18n="settings.sounds_section.reply_vol">Reply Vol</span>
<input type="range" id="notif-reply-volume" min="0" max="100" value="80" class="slider-sm">
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.replies">Replies</span>
<select id="notif-reply-sound">
<option value="chime">Chime</option>
<option value="bell">Bell</option>
<option value="alert">Alert</option>
<option value="chord">Chord</option>
<option value="ping">Ping</option>
<option value="blip">Blip</option>
<option value="drop">Drop</option>
<option value="none">None</option>
</select>
</label>
<div class="notif-divider"></div>
<label class="volume-row">
<span data-i18n="settings.sounds_section.join_vol">Join Vol</span>
<input type="range" id="notif-join-volume" min="0" max="100" value="80" class="slider-sm">
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.user_joined">User Joined</span>
<select id="notif-join-sound">
<option value="chime">Chime</option>
<option value="bell">Bell</option>
<option value="ping">Ping</option>
<option value="blip">Blip</option>
<option value="chord">Chord</option>
<option value="none">None</option>
</select>
</label>
<div class="notif-divider"></div>
<label class="volume-row">
<span data-i18n="settings.sounds_section.leave_vol">Leave Vol</span>
<input type="range" id="notif-leave-volume" min="0" max="100" value="80" class="slider-sm">
</label>
<label class="select-row">
<span data-i18n="settings.sounds_section.user_left">User Left</span>
<select id="notif-leave-sound">
<option value="drop">Drop</option>
<option value="chime">Chime</option>
<option value="ping">Ping</option>
<option value="blip">Blip</option>
<option value="none">None</option>
</select>
</label>
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<button class="btn-sm btn-full btn-accent" id="open-sound-manager-user-btn">🎵 <span data-i18n="settings.sounds_section.sound_manager_btn">Sound Manager</span></button>
<small class="settings-hint" style="display:block;margin-top:4px" data-i18n="settings.sounds_section.sound_manager_hint">Browse sounds, assign to events, set hotkeys</small>
</div>
</div>
</div>
<!-- Push Notifications Section -->
<div class="settings-section" id="section-push">
<h5 class="settings-section-title">📲 <span data-i18n="settings.push_section.title">Push Notifications</span></h5>
<div class="notif-settings">
<label class="toggle-row">
<span data-i18n="settings.push_section.enable">Enable Push</span>
<input type="checkbox" id="push-notif-enabled">
</label>
<small class="settings-hint" id="push-notif-status" data-i18n="settings.push_section.checking">Checking...</small>
<small class="settings-hint" style="opacity:0.5" data-i18n="settings.push_section.hint">Receive notifications even when Haven is closed</small>
</div>
</div>
<!-- Activity / Rich Presence -->
<div class="settings-section" id="section-activity" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🎮 <span data-i18n="settings.activity_section.title">Activity</span></h5>
<label class="toggle-row">
<span data-i18n="settings.activity_section.share">Share my activity</span>
<input type="checkbox" id="share-activity">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.activity_section.share_hint">Others can see what you're playing or listening to. Games and music only appear once you link an account below. Nothing is ever shared while your status is Invisible.</small>
<div id="activity-suboptions" style="display:none;padding-left:10px;border-left:2px solid var(--border-light);margin-bottom:10px">
<label class="toggle-row">
<span data-i18n="settings.activity_section.show_games">Show games</span>
<input type="checkbox" id="share-game-activity">
</label>
<label class="toggle-row">
<span data-i18n="settings.activity_section.show_music">Show music</span>
<input type="checkbox" id="share-music-activity">
</label>
</div>
<h5 class="settings-section-title" style="margin-top:12px">🔗 <span data-i18n="settings.activity_section.connections">Connections</span></h5>
<div id="connections-list">
<small class="settings-hint" data-i18n="modals.common.loading">Loading…</small>
</div>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.activity_section.connections_hint">Haven's own music player needs no connection. It shares automatically while you're listening in a voice channel.</small>
<!-- Listening presence (any music player) -->
<div id="listening-connection" style="margin-top:14px">
<h5 class="settings-section-title">🎶 <span data-i18n="settings.listening.title">Listening Webhook</span></h5>
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n-html="settings.listening.note">Please refer to the <a href="https://github.com/ancsemi/Haven/blob/main/docs/listening-api.md" target="_blank" rel="noopener noreferrer">Listening API</a>.</small>
<label class="toggle-row">
<span data-i18n="settings.listening.toggle">Show my music on Haven</span>
<input type="checkbox" id="listening-enabled">
</label>
<div id="listening-url-row" hidden style="margin-top:8px">
<label class="connection-field">
<span data-i18n="settings.listening.url_label">Your webhook URL</span>
<input type="text" id="listening-url" readonly autocomplete="off" spellcheck="false">
</label>
<small class="settings-hint" style="display:block;margin-top:4px" data-i18n="settings.listening.url_hint">Point your player's plugin or script at this URL. Keep it secret; anyone with it can post to your profile.</small>
</div>
</div>
</div>
<!-- Game Score Badges -->
<div class="settings-section" id="section-score-badges" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
<h5 class="settings-section-title">🚢 <span data-i18n="settings.score_badges.title">Score Badges</span></h5>
<label class="toggle-row">
<span data-i18n="settings.score_badges.hide_others">Hide other players' score badges</span>
<input type="checkbox" id="hide-other-scores">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.score_badges.hide_others_hint">Don't show 🚢 badges next to other users in the member list.</small>
<label class="toggle-row">
<span data-i18n="settings.score_badges.hide_own">Hide my score badge from this server</span>
<input type="checkbox" id="hide-own-score">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.score_badges.hide_own_hint">Other users (and you) won't see your 🚢 badge anywhere on this server, including the leaderboard.</small>
<label class="toggle-row">
<span data-i18n="settings.nsfw.hide">Hide NSFW channels</span>
<input type="checkbox" id="hide-nsfw-channels">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.nsfw.hide_hint">Channels an admin marked 🔞 leave your sidebar until you turn this off. Handy on a phone in public.</small>
<label class="toggle-row">
<span data-i18n="settings.nsfw.blur">Blur NSFW topics until clicked</span>
<input type="checkbox" id="blur-nsfw-topics" checked>
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.nsfw.blur_hint">The picture and preview of a topic marked 🔞 sit behind a blur, like a spoiler. Turn this off to see them plainly.</small>
</div>
<!-- Password Change Section -->
<div class="settings-section" id="section-password">
<h5 class="settings-section-title">🔒 <span data-i18n="settings.password_section.title">Password</span></h5>
<div class="password-change-form">
<div class="form-group compact">
<input type="password" id="current-password" data-i18n-placeholder="settings.password_section.current_placeholder" placeholder="Current password" maxlength="128" autocomplete="current-password">
</div>
<div class="form-group compact">
<input type="password" id="new-password" data-i18n-placeholder="settings.password_section.new_placeholder" placeholder="New password (8+ chars)" maxlength="128" autocomplete="new-password">
</div>
<div class="form-group compact">
<input type="password" id="confirm-password" data-i18n-placeholder="settings.password_section.confirm_placeholder" placeholder="Confirm new password" maxlength="128" autocomplete="new-password">
</div>
<button class="btn-sm btn-full" id="change-password-btn" data-i18n="settings.password_section.change_btn">Change Password</button>
<small class="settings-hint" id="password-status"></small>
</div>
</div>
<!-- Encryption Passphrase Section -->
<div class="settings-section" id="section-e2e-passphrase">
<h5 class="settings-section-title">🗝️ <span data-i18n="settings.e2e_passphrase.title">Encryption Passphrase</span></h5>
<small class="settings-hint" data-i18n="settings.e2e_passphrase.hint">Your encrypted DMs use a key that is backed up on the server, so your other devices can use it too. Normally that backup is locked with your login password, which the server receives every time you sign in. Lock it with a separate passphrase instead and the server can never open it. You type the passphrase once on each new device, and nobody can reset it for you, including the server admin.</small>
<p class="settings-hint" id="e2e-pp-state" style="font-weight:600"></p>
<div class="password-change-form">
<div class="form-group compact">
<input type="password" id="e2e-pp-new" data-i18n-placeholder="settings.e2e_passphrase.new_placeholder" placeholder="New passphrase (8+ characters)" maxlength="256" autocomplete="new-password">
</div>
<div class="form-group compact">
<input type="password" id="e2e-pp-confirm" data-i18n-placeholder="settings.e2e_passphrase.confirm_placeholder" placeholder="Confirm passphrase" maxlength="256" autocomplete="new-password">
</div>
<button class="btn-sm btn-full" id="e2e-pp-save-btn" data-i18n="settings.e2e_passphrase.save_btn">Use This Passphrase</button>
<div id="e2e-pp-revert-area" style="display:none;margin-top:10px">
<div class="form-group compact">
<input type="password" id="e2e-pp-password" data-i18n-placeholder="settings.e2e_passphrase.password_placeholder" placeholder="Your login password" maxlength="128" autocomplete="current-password">
</div>
<button class="btn-sm btn-full" id="e2e-pp-revert-btn" data-i18n="settings.e2e_passphrase.revert_btn">Go Back to My Login Password</button>
</div>
<small class="settings-hint" id="e2e-pp-status"></small>
</div>
</div>
<!-- Active Sessions Section -->
<div class="settings-section" id="section-sessions">
<h5 class="settings-section-title">💻 <span data-i18n="settings.sessions_section.title">Active Sessions</span></h5>
<small class="settings-hint" data-i18n="settings.sessions_section.hint">Devices with Haven open right now. A device that is signed in but closed will not be listed, so sign out everywhere if you think someone else has your account.</small>
<div id="sessions-list" class="sessions-list"></div>
<button class="btn-sm btn-full btn-danger" id="revoke-sessions-btn" data-i18n="settings.sessions_section.revoke_btn">Sign out all other devices</button>
<small class="settings-hint" id="sessions-status"></small>
</div>
<!-- Two-Factor Authentication Section -->
<div class="settings-section" id="section-2fa">
<h5 class="settings-section-title">🔐 <span data-i18n="settings.two_factor_section.title">Two-Factor Authentication</span></h5>
<!-- Status display -->
<div id="totp-status-area">
<p class="settings-hint" id="totp-status-text"></p>
</div>
<!-- Enable flow (shown when TOTP is OFF) -->
<div id="totp-enable-area" style="display:none">
<p class="settings-hint" style="margin-bottom:10px" data-i18n="settings.two_factor_section.enable_hint">Protect your account with a TOTP authenticator app (Google Authenticator, Authy, etc.).</p>
<button class="btn-sm btn-full" id="totp-enable-btn" data-i18n="settings.two_factor_section.enable_btn">Enable Two-Factor Auth</button>
</div>
<!-- Setup flow (shown after clicking Enable) -->
<div id="totp-setup-area" style="display:none">
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.setup_hint">Scan this QR code with your authenticator app:</p>
<div style="text-align:center;margin:12px 0">
<img id="totp-qr-img" data-i18n-alt="settings.two_factor_section.qr_alt" alt="QR Code" style="max-width:200px;border-radius:8px;background:#fff;padding:8px">
</div>
<p class="settings-hint" style="margin-bottom:4px" data-i18n="settings.two_factor_section.manual_hint">Or enter this secret manually:</p>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px">
<code id="totp-secret-text" style="font-size:0.875rem;letter-spacing:2px;user-select:all;flex:1;word-break:break-all"></code>
<button class="btn-sm" id="totp-copy-secret" data-i18n-title="settings.two_factor_section.copy_secret_title" title="Copy secret" style="white-space:nowrap">📋 <span data-i18n="settings.two_factor_section.copy_secret_btn">Copy</span></button>
</div>
<div class="form-group compact">
<input type="text" id="totp-verify-code" data-i18n-placeholder="settings.two_factor_section.verify_placeholder" placeholder="Enter 6-digit code to confirm" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" style="text-align:center;font-size:1.125rem;letter-spacing:6px">
</div>
<button class="btn-sm btn-full" id="totp-verify-setup-btn" data-i18n="settings.two_factor_section.verify_btn">Verify & Activate</button>
<button class="btn-sm btn-full" id="totp-cancel-setup-btn" data-i18n="modals.common.cancel" style="margin-top:6px;opacity:0.7">Cancel</button>
<small class="settings-hint" id="totp-setup-status"></small>
</div>
<!-- Backup codes display (shown after successful setup) -->
<div id="totp-backup-area" style="display:none">
<h5 style="margin-bottom:6px">🗝️ <span data-i18n="settings.two_factor_section.backup_title">Backup Codes</span></h5>
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.backup_hint">Save these codes somewhere safe. Each can be used once if you lose access to your authenticator.</p>
<div id="totp-backup-codes" style="font-family:monospace;font-size:0.9375rem;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
<button class="btn-sm btn-full" id="totp-copy-backup-btn" style="margin-top:8px">📋 <span data-i18n="settings.two_factor_section.copy_backup_btn">Copy Backup Codes</span></button>
<button class="btn-sm btn-full" id="totp-backup-done-btn" data-i18n="settings.two_factor_section.done_btn" style="margin-top:6px">Done</button>
</div>
<!-- Manage area (shown when TOTP is ON) -->
<div id="totp-manage-area" style="display:none">
<p class="settings-hint" style="color:var(--accent);margin-bottom:6px">✅ <span data-i18n="settings.two_factor_section.enabled_text">Two-factor authentication is enabled</span></p>
<p class="settings-hint" id="totp-backup-remaining" style="margin-bottom:12px"></p>
<div class="form-group compact">
<input type="password" id="totp-disable-password" data-i18n-placeholder="settings.two_factor_section.disable_password_placeholder" placeholder="Enter password to modify" maxlength="128" autocomplete="current-password">
</div>
<div style="display:flex;gap:8px">
<button class="btn-sm btn-danger-fill" id="totp-disable-btn" data-i18n="settings.two_factor_section.disable_btn" style="flex:1">Disable 2FA</button>
<button class="btn-sm btn-accent" id="totp-regen-backup-btn" data-i18n="settings.two_factor_section.regen_backup_btn" style="flex:1">Regenerate Backup Codes</button>
</div>
<small class="settings-hint" id="totp-manage-status"></small>
</div>
</div>
<!-- Account Recovery Codes Section -->
<div class="settings-section" id="section-recovery">
<h5 class="settings-section-title">🔑 <span data-i18n="settings.recovery_section.title">Account Recovery</span></h5>
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.desc">Generate one-time recovery codes to regain access if you forget your password. Store them somewhere safe: they work like a last-resort key.</p>
<div class="settings-warning-box" id="recovery-e2e-warning" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.4);border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:0.82rem;color:var(--text-secondary)">
<span data-i18n-html="settings.recovery_section.e2e_warning">⚠️ <strong>Important:</strong> Using a recovery code to reset your password will permanently and irreversibly destroy your end-to-end encryption keys. All encrypted direct message history will become unreadable. There is no way to undo this.</span>
</div>
<!-- Status -->
<p class="settings-hint" id="recovery-code-status" style="margin-bottom:10px"></p>
<!-- Generate flow -->
<div id="recovery-generate-area">
<div class="form-group compact">
<input type="password" id="recovery-gen-password" data-i18n-placeholder="settings.recovery_section.confirm_password_placeholder" placeholder="Confirm your password" maxlength="128" autocomplete="current-password">
</div>
<button class="btn-sm btn-full btn-accent" id="recovery-generate-btn" data-i18n="settings.recovery_section.generate_btn">Generate Recovery Codes</button>
<small class="settings-hint" id="recovery-gen-status"></small>
</div>
<!-- Codes display (shown after generation) -->
<div id="recovery-codes-area" style="display:none">
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.save_hint">Save these codes now. They will not be shown again. Each code can only be used once.</p>
<div id="recovery-codes-list" style="font-family:monospace;font-size:0.9375rem;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
<button class="btn-sm btn-full" id="recovery-copy-btn" style="margin-top:8px">📋 <span data-i18n="settings.recovery_section.copy_codes_btn">Copy Codes</span></button>
<button class="btn-sm btn-full" id="recovery-codes-done-btn" data-i18n="settings.recovery_section.done_btn" style="margin-top:6px">Done</button>
</div>
</div>
<!-- Delete Account Section -->
<div class="settings-section" id="section-delete-account">
<h5 class="settings-section-title">⚠️ <span data-i18n="settings.delete_account_section.title">Delete Account</span></h5>
<p class="settings-hint" style="margin-bottom:8px;" data-i18n="settings.delete_account_section.desc">Permanently delete your account. This cannot be undone.</p>
<button class="btn-sm btn-danger-fill" id="delete-account-btn" data-i18n="settings.delete_account_section.btn">Delete My Account</button>
<div id="self-purge-block" style="display:none;margin-top:14px">
<h5 class="settings-section-title">🧹 <span data-i18n="settings.self_purge.title">Delete All My Messages</span></h5>
<p class="settings-hint" style="margin-bottom:8px;" data-i18n="settings.self_purge.desc">Remove every message and file you ever posted here, in one go, and keep your account. Messages a moderator has protected stay. This cannot be undone.</p>
<button class="btn-sm btn-danger-fill" id="self-purge-btn" data-i18n="settings.self_purge.btn">Delete All My Messages</button>
</div>
</div>
<!-- Plugins & Themes Section -->
<div class="settings-section" id="section-plugins">
<h5 class="settings-section-title">🧩 <span data-i18n="settings.plugins_section.title">Plugins & Themes</span></h5>
<p class="settings-hint" data-i18n-html="settings.plugins_section.hint">Drop <code>.plugin.js</code> files into the <code>plugins/</code> folder and <code>.theme.css</code> files into the <code>themes/</code> folder, then refresh.</p>
<div id="extension-safe-mode-notice" style="display:none;margin:10px 0 12px;padding:10px;border:1px solid var(--warning);border-radius:var(--radius);background:var(--bg-tertiary)">
<strong id="extension-safe-mode-title" data-i18n="settings.plugins_section.safe_mode_title">Extension Safe Mode</strong>
<p id="extension-safe-mode-desc" class="settings-hint" style="margin:4px 0 10px" data-i18n="settings.plugins_section.safe_mode_desc">Custom themes and plugins are suppressed for this session. You can reset them or exit without changing your saved choices.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn-sm btn-danger-fill" id="extension-reset-btn" data-i18n="settings.plugins_section.reset_customizations">Reset themes and plugins</button>
<button class="btn-sm" id="extension-safe-mode-exit-btn" data-i18n="settings.plugins_section.exit_safe_mode">Exit Safe Mode</button>
</div>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<button class="btn-sm btn-accent" id="plugin-refresh-btn">🔄 <span data-i18n="settings.plugins_section.refresh_btn">Refresh</span></button>
</div>
<h6 style="margin:10px 0 6px;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.plugins_label">Plugins</h6>
<div id="plugin-list" class="plugin-list"></div>
<h6 style="margin:14px 0 6px;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.themes_label">Themes</h6>
<div id="theme-list" class="plugin-list"></div>
</div>
<!-- Desktop Shortcuts Section (only shown in Haven Desktop) -->
<div class="settings-section" id="section-desktop-shortcuts" style="display:none">
<h5 class="settings-section-title">⌨️ <span data-i18n="settings.desktop_shortcuts_section.title">Desktop Shortcuts</span></h5>
<p class="settings-desc" data-i18n="settings.desktop_shortcuts_section.desc">Global keyboard shortcuts, active even when Haven is in the background. Click a row to rebind, or “Clear” to disable.</p>
<div class="shortcut-table" id="shortcut-table">
<div class="shortcut-row" id="shortcut-row-mute">
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.mute_unmute">Mute / Unmute</span>
<span class="shortcut-key" id="shortcut-key-mute">-</span>
<div class="shortcut-btns">
<button class="btn-xs shortcut-record-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
<button class="btn-xs shortcut-clear-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
</div>
</div>
<div class="shortcut-row" id="shortcut-row-deafen">
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.deafen_undeafen">Deafen / Undeafen</span>
<span class="shortcut-key" id="shortcut-key-deafen">-</span>
<div class="shortcut-btns">
<button class="btn-xs shortcut-record-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
<button class="btn-xs shortcut-clear-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
</div>
</div>
<div class="shortcut-row" id="shortcut-row-ptt">
<span class="shortcut-label">
<span data-i18n="settings.desktop_shortcuts_section.ptt">Push-to-Talk</span>
<select id="ptt-mode-select" class="settings-select" style="font-weight:400;font-size:0.85em;margin-left:6px;padding:1px 4px" data-i18n-title="settings.desktop_shortcuts_section.ptt_mode_title" title="How the PTT key behaves">
<option value="hold" data-i18n="settings.desktop_shortcuts_section.ptt_hold">hold</option>
<option value="toggle" data-i18n="settings.desktop_shortcuts_section.ptt_toggle_mode">toggle</option>
</select>
</span>
<span class="shortcut-key" id="shortcut-key-ptt" data-i18n="settings.desktop_shortcuts_section.not_set">Not set</span>
<div class="shortcut-btns">
<button class="btn-xs shortcut-record-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
<button class="btn-xs shortcut-clear-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
</div>
</div>
</div>
<p class="settings-hint" id="shortcut-status" style="margin-top:8px" data-i18n="settings.desktop_shortcuts_section.binding_hint">Lone modifiers (Alt / Ctrl / Shift) and extra mouse buttons (Mouse4 / Mouse5) can be bound here. Some bindings require a recent Haven Desktop build to register at the OS level.</p>
</div>
<!-- Desktop App Settings Section (only shown in Haven Desktop) -->
<div class="settings-section" id="section-desktop-app" style="display:none">
<h5 class="settings-section-title">🖥️ <span data-i18n="settings.desktop_app_section.title">Desktop App</span></h5>
<p class="settings-desc" data-i18n="settings.desktop_app_section.desc">Settings specific to Haven Desktop.</p>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-start-on-login">
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_on_login">Start on Login</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_on_login_hint">Launch Haven Desktop automatically when you log in to your computer.</p>
</div>
<div class="desktop-pref-row" id="pref-start-hidden-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-start-hidden">
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_hidden">Start Hidden to Tray</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_hidden_hint">When launching on login, start minimized to the system tray instead of showing the window.</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-minimize-to-tray">
<span class="toggle-label" data-i18n="settings.desktop_app_section.minimize_to_tray">Minimize to Tray on Close</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.minimize_to_tray_hint">Closing the window hides Haven to the system tray instead of quitting.</p>
</div>
<div class="desktop-pref-row" id="pref-flash-taskbar-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-flash-taskbar">
<span class="toggle-label" data-i18n="settings.desktop_app_section.flash_taskbar">Flash the Taskbar for Notifications</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.flash_taskbar_hint">While Haven is in the background, a notification also lights up its taskbar button until you open Haven. Turn this off to keep the taskbar quiet; notifications still show.</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-hide-menu-bar">
<span class="toggle-label" data-i18n="settings.desktop_app_section.hide_menu_bar">Hide Menu Bar</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.hide_menu_bar_hint">Hide the File / Edit / View / Window / Help toolbar. Press Alt to temporarily show it.</p>
</div>
<div class="desktop-pref-row" style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border)">
<span class="settings-hint" id="desktop-version-info" style="opacity:0.6"></span>
</div>
</div>
<!-- Debug Section -->
<div class="settings-section" id="section-debug">
<h5 class="settings-section-title">🐞 <span data-i18n="settings.debug_section.title">Debug</span></h5>
<p class="settings-desc" data-i18n="settings.debug_section.desc">Experimental client-side toggles. These don't change anything on the server.</p>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-local-talk-indicator">
<span class="toggle-label" data-i18n="settings.debug_section.local_talk_indicator">Local mic talk indicator</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.local_talk_indicator_hint">Highlight your own avatar the moment your mic crosses the talk threshold, instead of waiting for the server to echo it back. Default off: the server echo is usually preferred because it confirms your audio actually reached the server.</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-voice-mute-on-join">
<span class="toggle-label" data-i18n="settings.debug_section.mute_on_join">Always join voice muted</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.mute_on_join_hint">Force the mic to start muted every time you join a voice channel. Useful if you tend to lurk first, or if you don't want to risk hot-mic'ing right after joining. You can unmute normally once you're in.</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-voice-listener-only">
<span class="toggle-label" data-i18n="settings.debug_section.listener_only">Join voice without microphone (listener only)</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.listener_only_hint">Join voice channels without requesting microphone access. You'll hear everyone but won't be able to talk. Useful if you don't own a mic, can't grant mic permission right now, or just want to listen in. Haven will also fall back to listener-only automatically if the mic can't be opened for any reason. (#5380)</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-screen-share-voice-proc">
<span class="toggle-label" data-i18n="settings.debug_section.screen_share_voice_proc">Apply voice processing to screen-share audio</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.screen_share_voice_proc_hint">Re-enable echo cancellation, noise suppression, and auto gain control on the audio captured from your screen share. Default off as of 3.17.3 because these filters mangle music and game audio. Turn this on if you're sharing voice content (tutorial narration, a podcast, a meeting) where the cleanup actually helps. Does not affect your microphone, which always gets full voice processing.</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-screen-audio-direct">
<span class="toggle-label" data-i18n="settings.debug_section.screen_audio_webaudio">Web Audio mixing for screen-share audio</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.screen_audio_webaudio_hint">By default Haven plays incoming screen-share audio straight through the audio element, which keeps it in sync with the video over a TURN/relay connection. Turn this on to route it through Haven's Web Audio mixer instead, which unlocks the per-stream volume boost above 100% but can cause audio stutter and audio/video desync over a relay (it's fine on a local network). Leave it off if a screen share's audio stutters or drifts out of sync. (#5426)</p>
</div>
<div class="settings-group">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-screen-relay-profile">
<span class="toggle-label" data-i18n="settings.debug_section.screen_relay_profile">Gentler screen share for relay connections</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.screen_relay_profile_hint">Turn this on if your screen share stutters and drifts out of sync for viewers, but only when the call goes through a TURN server and never on your local network. It lowers the video bitrate back to the pre-3.18.1 values and lets the encoder drop frames when the connection is struggling, instead of holding the framerate and letting the backlog grow. Picture quality will be lower. Only the person sharing needs it on. (#5426)</p>
</div>
<div class="settings-group">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-recover-muted-streams">
<span class="toggle-label" data-i18n="settings.debug_section.recover_muted_streams">Recover streams that freeze</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.recover_muted_streams_hint">Turn this on if someone's stream freezes for you while you can still hear them, and only leaving and rejoining fixes it. Haven then reloads the stream for you, and asks the person sharing to resend it if that doesn't help. It can also reload a stream that is only showing a still screen, so leave it off unless you see the freeze. Only the person watching needs it on.</p>
</div>
<div class="settings-group">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-screen-relay-auto" checked>
<span class="toggle-label" data-i18n="settings.debug_section.screen_relay_auto">Soften screen share automatically for relayed viewers</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.screen_relay_auto_hint">On by default. When a viewer can only be reached through a TURN relay, your share to that one viewer uses the gentler profile above, while viewers on a direct connection keep the full-quality one. Turn this off if you would rather the toggle above be the only thing that decides. (#5426)</p>
</div>
<div class="desktop-pref-row">
<label class="toggle-row">
<input type="checkbox" id="pref-debug-voice-glare-ice-fix">
<span class="toggle-label" data-i18n="settings.debug_section.voice_glare_ice_fix">Repair voice audio after a reconnect (experimental)</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.voice_glare_ice_fix_hint">If you sometimes rejoin a voice call and still can't hear one or more people until you leave and rejoin, turn this on. When two people reconnect at the same moment their connections can collide mid-repair and one direction of audio stays dead; this makes Haven re-run the repair so the audio comes back on its own. Off by default while it's still being tested. (#5444)</p>
</div>
<!-- Desktop-only debug setting: shown when running in Haven Desktop -->
<div class="desktop-pref-row" id="pref-force-sdr-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-force-sdr">
<span class="toggle-label" data-i18n="settings.desktop_app_section.force_sdr">Force SDR (sRGB) Color</span>
</label>
<p class="settings-hint" data-i18n="settings.desktop_app_section.force_sdr_hint">Fix washed-out or over-saturated colors on HDR monitors. Requires restart.</p>
</div>
<!-- Desktop-only: G-Sync / VRR FPS-drop workaround (#35) -->
<div class="desktop-pref-row" id="pref-disable-gpu-vsync-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-disable-gpu-vsync">
<span class="toggle-label" data-i18n="settings.debug_section.disable_gpu_vsync">Disable GPU vsync (Nvidia G-Sync fix)</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.disable_gpu_vsync_hint">Workaround for Nvidia G-Sync / VRR monitors where Haven Desktop drops to ~5 FPS after the window is minimized or hidden behind another window. Tells Chromium not to wait for the monitor's vsync signal before drawing frames. May cause visible tearing on monitors without variable refresh rate. Requires restart to take effect.</p>
</div>
<div class="desktop-pref-row" id="pref-unlimit-frame-rate-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-unlimit-frame-rate">
<span class="toggle-label" data-i18n="settings.debug_section.unlimit_frame_rate">Remove Chromium frame-rate cap (Nvidia G-Sync fix)</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.unlimit_frame_rate_hint">Pairs with the GPU vsync toggle above. Removes Chromium's internal frame-rate limit, which on some Nvidia G-Sync setups gets stuck at a very low value and never recovers. Only useful if the vsync toggle alone didn't help. Can increase CPU/GPU usage. Requires restart to take effect.</p>
</div>
<div class="desktop-pref-row" id="pref-linux-vaapi-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-linux-vaapi">
<span class="toggle-label" data-i18n="settings.debug_section.linux_vaapi">Hardware video encoding on Linux (VA-API)</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.linux_vaapi_hint">Linux only. Lets Haven Desktop use your graphics card's VA-API video encoder for screen sharing even where Chromium's driver blocklist would not, including on Nvidia cards. It can lower CPU use while you share, but some drivers give black or broken video, so it is off by default. Requires restart to take effect.</p>
</div>
<div class="desktop-pref-row" id="pref-linux-x11-row" style="display:none">
<label class="toggle-row">
<input type="checkbox" id="pref-linux-x11">
<span class="toggle-label" data-i18n="settings.debug_section.linux_x11">Run in X11 mode on Linux</span>
</label>
<p class="settings-hint" data-i18n="settings.debug_section.linux_x11_hint">Linux only. Runs Haven Desktop through X11 (XWayland) instead of native Wayland. Try it if the window flickers, which some Nvidia cards do on Wayland. In this mode screen sharing may only list some windows. Requires restart to take effect.</p>
</div>
</div>
</div><!-- /settings-body-user -->
<!-- Admin settings body (separate tab) -->
<div class="settings-body" id="settings-body-admin" style="display:none">
<!-- Admin Section (hidden for non-admins) -->
<div class="settings-section admin-settings-section" id="admin-mod-panel" style="display:none">
<!-- Server Updates -->
<div class="admin-settings" id="section-update">
<h5 class="settings-section-subtitle">⬆️ <span data-i18n="settings.admin.update_title">Server Updates</span></h5>
<small class="settings-hint" data-i18n="settings.admin.update_hint">Check for new Haven releases and apply them in place. The server will create a pre-update backup automatically and restart itself.</small>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
<button class="btn-sm btn-accent" id="update-check-btn" data-i18n="settings.admin.update_check_btn">Check for Updates</button>
<button class="btn-sm" id="update-run-btn" disabled data-i18n="settings.admin.update_run_btn">Update Now</button>
</div>
<div id="update-status" style="margin-top:10px;padding:8px;border:1px solid var(--border);border-radius:4px;display:none"></div>
</div>
<!-- Plugin and Theme Updates -->
<div class="admin-settings" id="section-extension-updates">
<h5 class="settings-section-subtitle">🧩 <span data-i18n="settings.extension_updates.title">Extension Updates</span></h5>
<small class="settings-hint" data-i18n="settings.extension_updates.summary">Check supported plugins and themes for new releases from their declared GitHub repositories.</small>
<div style="margin-top:8px">
<button id="extension-update-check" data-i18n="settings.extension_updates.check" class="btn-sm btn-accent">Check for updates</button>
</div>
<p id="extension-update-status" role="status" class="settings-hint"></p>
<div id="extension-update-results" class="plugin-list"></div>
</div>
<!-- Server Branding -->
<div class="admin-settings" id="section-branding">
<h5 class="settings-section-subtitle">🏠 <span data-i18n="settings.admin.branding_title">Server Branding</span></h5>
<label class="select-row">
<span data-i18n="settings.admin.server_name">Server Name</span>
<input type="text" id="server-name-input" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="HAVEN">
</label>
<label class="select-row">
<span data-i18n="settings.admin.login_title">Login Title</span>
<input type="text" id="server-title-input" maxlength="40" class="settings-text-input" style="max-width:200px" data-i18n-placeholder="settings.admin.login_title_placeholder" placeholder="(shown on login page)">
</label>
<small class="settings-hint" style="margin-top:2px;display:block" data-i18n="settings.admin.login_title_hint">Custom title displayed on the login screen below the HAVEN logo.</small>
<!-- Server Icon -->
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.server_icon_hint">Server icon (square, max 2 MB)</small>
<div class="server-icon-upload-area">
<div class="server-icon-preview" id="server-icon-preview">
<span class="server-icon-text">⬡</span>
</div>
<div class="server-icon-upload-controls">
<input type="file" id="server-icon-file" accept="image/*" style="font-size:0.6875rem;max-width:160px">
<div style="display:flex;gap:4px;margin-top:4px">
<button class="btn-sm btn-accent" id="server-icon-upload-btn" data-i18n="settings.admin.upload_btn">Upload</button>
<button class="btn-sm" id="server-icon-remove-btn" data-i18n="settings.admin.remove_btn">Remove</button>
</div>
</div>
</div>
</div>
<!-- Server Banner -->
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.server_banner_hint">Server banner (wide, max 4 MB)</small>
<div class="server-banner-upload-area">
<div class="server-banner-preview" id="server-banner-preview">
<span class="muted-text" style="font-size:0.6875rem" data-i18n="settings.admin.no_banner">No banner</span>
</div>
<div class="server-banner-upload-controls">
<input type="file" id="server-banner-file" accept="image/jpeg,image/png,image/gif,image/webp" style="font-size:0.6875rem;max-width:160px">
<div style="display:flex;gap:4px;margin-top:4px">
<button class="btn-sm btn-accent" id="server-banner-upload-btn" data-i18n="settings.admin.upload_btn">Upload</button>
<button class="btn-sm" id="server-banner-remove-btn" data-i18n="settings.admin.remove_btn">Remove</button>
</div>
</div>
</div>
<small class="settings-hint" data-i18n="settings.admin.server_banner_display_hint">Banner display settings (height, offset, header style) are in User Settings.</small>
</div>
<!-- Theme & Welcome -->
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.appearance_welcome">Appearance &amp; Welcome</small>
<label class="select-row">
<span data-i18n="settings.admin.default_theme">Default Theme</span>
<select id="default-theme-select" style="max-width:160px">
<option value="" data-i18n="settings.admin.no_theme">None (user's choice)</option>
<option value="haven">Haven</option>
<option value="discord">Discord</option>
<option value="matrix">Matrix</option>
<option value="tron">Tron</option>
<option value="halo">Halo</option>
<option value="lotr">Lord of the Rings</option>
<option value="cyberpunk">Cyberpunk</option>
<option value="nord">Nord</option>
<option value="dracula">Dracula</option>
<option value="bloodborne">Bloodborne</option>
<option value="darksouls">Dark Souls</option>
<option value="eldenring">Elden Ring</option>
<option value="ice">Ice</option>
<option value="abyss">Abyss</option>
<option value="minecraft">Minecraft</option>
<option value="ffx">Final Fantasy</option>
<option value="zelda">Zelda</option>
<option value="fallout">Fallout</option>
<option value="scripture">Scripture</option>
<option value="chapel">Chapel</option>
<option value="gospel">Gospel</option>
<option value="midnightpurple">Midnight Purple</option>
<option value="crt">CRT</option>
<option value="win95">Windows 95</option>
<option value="rgb">RGB</option>
<option value="daylight">Daylight (Light)</option>
<option value="cloudy">Cloudy (Light)</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.default_theme_hint">New users see this theme on first visit. They can still pick their own. A theme shared under Defaults for New Members is this same setting: changing one changes the other.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.default_locale">Default Language</span>
<select id="default-locale-select" style="max-width:160px">
<option value="" data-i18n="settings.admin.no_locale">Auto-detect (browser)</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="pl">Polski</option>
<option value="ru">Русский</option>
<option value="zh">中文</option>
<option value="pt">Português (Brasil)</option>
<option value="fa">فارسی</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.default_locale_hint">New users see Haven in this language on first visit. They can still pick their own.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.welcome_message">Welcome Message</span>
<input type="text" id="welcome-message-input" maxlength="500" class="settings-text-input" style="max-width:280px" data-i18n-placeholder="settings.admin.welcome_message_placeholder" placeholder="e.g. Welcome {user}!">
</label>
<small class="settings-hint" data-i18n-html="settings.admin.welcome_message_hint">Shown when a new user joins a channel. Use <code>{user}</code> for the username. Leave blank to disable.</small>
</div>
<!-- Defaults for new members (#5739): app-member-defaults.js -->
<div class="settings-group member-defaults" id="member-defaults-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.member_defaults.title">Defaults for New Members</small>
<p class="settings-hint" style="margin:0 0 8px" data-i18n="settings.admin.member_defaults.hint">Share your own look and feel. New members start with it once, and anyone can change any of it afterwards. Tick what to share; the right column is your current value. These buttons take effect right away, no Save needed.</p>
<div id="member-defaults-list" class="member-defaults-list"></div>
<div class="member-defaults-actions">
<button type="button" class="btn-sm btn-accent" id="member-defaults-capture" data-i18n="settings.admin.member_defaults.capture">Use my current settings as the defaults</button>
<button type="button" class="btn-sm" id="member-defaults-clear" data-i18n="settings.admin.member_defaults.clear">Clear defaults</button>
</div>
<div class="member-defaults-summary" id="member-defaults-summary"></div>
<div class="member-defaults-actions">
<button type="button" class="btn-sm" id="member-defaults-push" data-i18n="settings.admin.member_defaults.push">Apply once to everyone now</button>
</div>
<small class="settings-hint" data-i18n="settings.admin.member_defaults.push_hint">Everyone gets the saved defaults once more at their next load, but only for settings they never changed: ones still at Haven's default or at the previous shared default.</small>
</div>
<!-- Custom Themes -->
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.custom_themes_title">Custom Themes</small>
<p class="settings-hint" style="margin:0 0 8px" data-i18n-html="settings.admin.custom_themes_hint">Drop <code>.theme.css</code> files into the <code>themes/</code> folder, then check the ones you want to appear in the theme picker for all users.</p>
<div id="admin-theme-list" style="display:flex;flex-direction:column;gap:6px">
<span style="font-size:0.75rem;color:var(--text-muted)" data-i18n-html="settings.admin.no_themes">No themes found. Add <code>.theme.css</code> files to the <code>themes/</code> folder and refresh.</span>
</div>
</div>
</div>
<!-- Presence & Activity -->
<div class="admin-settings" id="section-presence" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🟢 <span data-i18n="settings.admin.presence_title">Presence &amp; Activity</span></h5>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.auto_away_enabled">Enable automatic Away status</span>
<input type="checkbox" id="auto-away-enabled" checked>
</label>
<small class="settings-hint" style="display:block;margin-top:6px;margin-bottom:8px" data-i18n="settings.admin.auto_away_enabled_hint">Turn this off to keep users Online until they change their status themselves.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.auto_away_visible">Visible tab (minutes)</span>
<input type="number" id="auto-away-visible-minutes" min="1" max="60" step="1" value="5" class="settings-number-input">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.admin.auto_away_visible_hint">Time without input before an active tab changes the status to Away.</small>
<label class="select-row">
<span data-i18n="settings.admin.auto_away_hidden">Hidden tab (minutes)</span>
<input type="number" id="auto-away-hidden-minutes" min="1" max="60" step="1" value="2" class="settings-number-input">
</label>
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.admin.auto_away_hidden_hint">Time without input before a hidden tab changes the status to Away.</small>
</div>
<!-- Custom Terms of Service (Admin only) -->
<div class="admin-settings" id="section-custom-tos" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">⚖️ <span data-i18n="settings.admin.custom_tos_title">Custom Terms of Service</span></h5>
<small class="settings-hint" data-i18n="settings.admin.custom_tos_hint">Add custom terms that appear before the default ToS on the login page. Supports plain text. Leave empty to show only the default Haven ToS.</small>
<textarea id="custom-tos-input" class="settings-textarea" rows="6" maxlength="50000" data-i18n-placeholder="settings.admin.custom_tos_placeholder" placeholder="Enter your custom terms of service..." style="margin-top:8px;width:100%;resize:vertical;min-height:80px;font-size:0.75rem;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-family:inherit;"></textarea>
</div>
<!-- Voice and Connectivity -->
<div class="admin-settings" id="section-connectivity" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">📡 <span data-i18n="settings.admin.connectivity_title">Voice &amp; Connectivity</span></h5>
<small class="settings-hint" data-i18n="settings.admin.connectivity_hint">STUN/TURN servers let voice and screen share connect between users on different networks. Leave blank to use Haven's built-in STUN pool. If users outside your local network get stuck on "ICE: Connecting...", set your own servers here.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.ice_disabled">Disable STUN/TURN (direct connections only)</span>
<input type="checkbox" id="voice-ice-disabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.ice_disabled_hint">Use this for LAN-only servers. Voice and screen sharing will use direct local network connections; users on different networks may not connect.</small>
<label class="select-row" style="margin-top:8px; align-items:flex-start">
<span data-i18n="settings.admin.stun_urls">STUN servers</span>
<textarea id="stun-urls-input" rows="3" class="settings-text-input" placeholder="stun:stun.cloudflare.com:3478&#10;stun:global.stun.twilio.com:3478" style="flex:1; resize:vertical; font-family:monospace; font-size:0.75rem"></textarea>
</label>
<small class="settings-hint" data-i18n="settings.admin.stun_urls_hint">One stun: URI per line (or comma separated). Blank = built-in defaults.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.turn_url">TURN server (optional)</span>
<input type="text" id="turn-url-input" class="settings-text-input" placeholder="turn:turn.example.com:3478" style="flex:1">
</label>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.turn_username">TURN username</span>
<input type="text" id="turn-username-input" class="settings-text-input" autocomplete="off" style="flex:1">
</label>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.turn_password">TURN password</span>
<input type="password" id="turn-password-input" class="settings-text-input" autocomplete="new-password" style="flex:1">
</label>
<small class="settings-hint" data-i18n="settings.admin.turn_hint">A TURN server relays traffic for users behind strict NATs that STUN alone can't connect. Only needed if some users still can't connect after setting STUN. Username/password apply to static-credential TURN servers.</small>
<div style="margin-top:12px; padding-top:10px; border-top:1px solid var(--border)">
<button class="btn-sm btn-accent" id="test-connectivity-btn">🩺 <span data-i18n="settings.admin.test_connectivity_btn">Test voice connectivity</span></button>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.test_connectivity_hint">Checks the servers above the same way a call does, and says in plain English what works and what doesn't. Save your changes first.</small>
<div id="connectivity-test-result" style="display:none;margin-top:10px"></div>
</div>
</div>
<!-- Large Server Setup (Admin only) -->
<div class="admin-settings" id="section-large-server" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🏟️ <span data-i18n="settings.admin.large_server.title">Large Server Setup</span></h5>
<small class="settings-hint" data-i18n="settings.admin.large_server.hint">What a big community needs, in one place. A server for friends and family can skip this page.</small>
<div class="large-server-card">
<div class="large-server-card-title">🔊 <span data-i18n="settings.admin.large_server.relay_title">Voice relay</span></div>
<small class="settings-hint" data-i18n="settings.admin.large_server.relay_hint">Normally everyone in a call sends their voice and video straight to everyone else. That works for a handful of people, but past about ten each person's upload runs out. With a relay, each person sends once, to this server, and the server passes it on, so calls can hold many more people. It uses this server's internet connection instead of theirs. One thing changes: direct calls go person to person, but relayed calls pass through this server, so whoever runs it could in principle listen in, as with any voice server.</small>
<div class="large-server-choices" role="radiogroup">
<label class="large-server-choice">
<input type="radio" name="voice-relay-mode" value="off">
<span><strong data-i18n="settings.admin.large_server.mode_off">Off</strong>
<small data-i18n="settings.admin.large_server.mode_off_hint">Direct connections, as always. Best for small groups.</small></span>
</label>
<label class="large-server-choice">
<input type="radio" name="voice-relay-mode" value="builtin">
<span><strong data-i18n="settings.admin.large_server.mode_builtin">Built-in relay</strong>
<small data-i18n="settings.admin.large_server.mode_builtin_hint">Runs inside Haven. Nothing else to install: open one port and switch it on. Recommended.</small></span>
</label>
<label class="large-server-choice large-server-choice-disabled">
<input type="radio" name="voice-relay-mode" value="livekit" disabled>
<span><strong data-i18n="settings.admin.large_server.mode_livekit">LiveKit</strong>
<small data-i18n="settings.admin.large_server.mode_livekit_hint">Use a LiveKit server you run yourself, for more control. Coming in a later update.</small></span>
</label>
</div>
<div id="voice-relay-builtin-options" style="display:none">
<p class="large-server-status" id="voice-relay-status"></p>
<label class="select-row">
<span data-i18n="settings.admin.large_server.port">Port</span>
<input type="number" id="voice-relay-port" class="settings-text-input" min="1024" max="65527" placeholder="40000" style="width:110px">
</label>
<small class="settings-hint" id="voice-relay-port-hint"></small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.large_server.address">Public address</span>
<input type="text" id="voice-relay-address" class="settings-text-input" data-i18n-placeholder="settings.admin.large_server.address_placeholder" placeholder="Found automatically" style="flex:1">
<button type="button" class="btn-sm" id="voice-relay-detect" data-i18n="settings.admin.large_server.detect">Detect</button>
</label>
<small class="settings-hint" data-i18n="settings.admin.large_server.address_hint">Where people connect to reach the relay: this server's public IP address or domain name. Leave it empty and Haven works it out when the relay starts. Everyone in a relayed call can see this address (and, so people on your own network connect directly, the server's local address), as with any voice server; a server hidden behind a proxy is not hidden from them.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.large_server.workers">CPU cores</span>
<select id="voice-relay-workers">
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option>
<option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.large_server.workers_hint">One core carries many calls. Add more when calls get very large or numerous; each extra core uses the next port up.</small>
</div>
<div class="large-server-install" id="voice-relay-install" style="display:none">
<small class="settings-hint" data-i18n="settings.admin.large_server.install_hint">The relay is not installed on this server yet. Installing it downloads about 10 MB, once; Haven updates keep it. It works on Windows, Linux (including Raspberry Pi and Docker) and Apple Silicon Macs.</small>
<div style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap">
<button type="button" class="btn-sm btn-accent" id="voice-relay-install-btn" data-i18n="settings.admin.large_server.install_btn">Install the relay</button>
<small class="muted-text" id="voice-relay-install-log"></small>
</div>
</div>
<div style="margin-top:10px; display:flex; align-items:center; gap:8px">
<button type="button" class="btn-sm btn-accent" id="voice-relay-save" data-i18n="settings.admin.large_server.save">Save relay settings</button>
<small class="muted-text" id="voice-relay-save-note" data-i18n="settings.admin.large_server.save_note">Takes effect straight away. Calls in progress reconnect by themselves.</small>
</div>
</div>
<div class="large-server-card">
<div class="large-server-card-title">✅ <span data-i18n="settings.admin.large_server.more_title">Also worth setting for a big server</span></div>
<div class="large-server-checklist">
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_members">Member list</strong>
<small data-i18n="settings.admin.large_server.item_members_hint">Show online members only, so a server with thousands of accounts does not list every one of them.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-members" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_signups">Sign-ups</strong>
<small data-i18n="settings.admin.large_server.item_signups_hint">A captcha and a limit on new accounts per hour stop floods of spam accounts.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-whitelist" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_automod">Auto-moderation</strong>
<small data-i18n="settings.admin.large_server.item_automod_hint">Block links from brand-new accounts and step up to bans automatically when moderators cannot watch every channel.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-automod" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_guests">Guest access</strong>
<small data-i18n="settings.admin.large_server.item_guests_hint">Decide whether people can drop in without an account, and whether guests may use voice.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-guests" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_limits">Limits</strong>
<small data-i18n="settings.admin.large_server.item_limits_hint">Message length, upload size and attachments per message keep storage and bandwidth in check.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-uploads" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_backup">Backups and cleanup</strong>
<small data-i18n="settings.admin.large_server.item_backup_hint">Automatic backups, and clearing out old files so the disk does not fill up.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-backup" data-i18n="settings.admin.large_server.open">Open</button>
</div>
<div class="large-server-item">
<div><strong data-i18n="settings.admin.large_server.item_turn">TURN server</strong>
<small data-i18n="settings.admin.large_server.item_turn_hint">Lets people on very strict networks (some offices, schools, mobile carriers) connect to voice at all.</small></div>
<button type="button" class="btn-sm" data-large-server-open="section-connectivity" data-i18n="settings.admin.large_server.open">Open</button>
</div>
</div>
</div>
</div>
<!-- Tunnel settings (Admin only) -->
<div class="admin-settings" id="section-tunnel" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🧭 <span data-i18n="settings.admin.tunnel_title">Tunnel</span></h5>
<label class="select-row">
<span data-i18n="settings.admin.tunnel_provider">Provider</span>
<select id="tunnel-provider-select">
<option value="localtunnel">LocalTunnel</option>
<option value="cloudflared">Cloudflared</option>
</select>
</label>
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
<button class="btn-sm btn-accent" id="tunnel-toggle-btn" data-i18n="settings.admin.tunnel_start_btn">Start Tunnel</button>
<span id="tunnel-status-display" class="muted-text" data-i18n="settings.admin.tunnel_inactive" style="font-size:0.6875rem;">Inactive</span>
</div>
</div>
<!-- show members -->
<div class="admin-settings" id="section-members" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">👥 <span data-i18n="settings.admin.members_title">Members</span></h5>
<label class="select-row">
<span data-i18n="settings.admin.show_members">Show Members</span>
<select id="member-visibility-select">
<option value="online" data-i18n="settings.admin.members_online_only">Online Only</option>
<option value="all" data-i18n="settings.admin.members_all">All Members</option>
<option value="none" data-i18n="settings.admin.members_hidden">Hidden</option>
</select>
</label>
<div class="settings-btn-grid">
<button class="btn-sm btn-accent" id="view-bans-btn">📋 <span data-i18n="settings.admin.view_bans_btn">View Bans</span></button>
<button class="btn-sm btn-accent" id="view-ip-bans-btn">🚫 <span data-i18n="settings.admin.view_ip_bans_btn">View IP Bans</span></button>
<button class="btn-sm btn-accent" id="view-deleted-users-btn">🗑️ <span data-i18n="settings.admin.view_deleted_btn">View Deleted Users</span></button>
<button class="btn-sm btn-accent" id="view-all-members-btn">👥 <span data-i18n="settings.admin.view_all_members_btn">View All Members</span></button>
</div>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.update_banner_label">Update banner (admins only)</span>
<input type="checkbox" id="update-banner-admin-only">
</label>
<small class="settings-hint" data-i18n="settings.admin.update_banner_hint">When enabled, the "update available" banner is only shown to admins</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.allow_self_purge_label">Members can delete all their own messages</span>
<input type="checkbox" id="allow-self-purge">
</label>
<small class="settings-hint" data-i18n="settings.admin.allow_self_purge_hint">Puts a Delete All My Messages button in everyone's Settings, next to Delete Account, for people who want to leave nothing behind. It works whether or not their role may delete messages one by one; messages a moderator has protected are kept.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.category_layout_label">Show categories as tabs</span>
<input type="checkbox" id="category-layout-tabs">
</label>
<small class="settings-hint" data-i18n="settings.admin.category_layout_hint">Lays the category headers out as a grid of tabs above their channels, wrapping onto more rows when there are many. Right-click a category to give it a short name or emoji to show while it is collapsed.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.hide_disabled_badges_label">Hide the crossed-out icons in the channel list</span>
<input type="checkbox" id="hide-disabled-badges">
</label>
<small class="settings-hint" data-i18n="settings.admin.hide_disabled_badges_hint">Channels with screen share or music turned off show small crossed-out icons next to their name. Turn this on to hide those icons for everyone.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.admin_pw_reset_label">Allow admin password reset</span>
<input type="checkbox" id="admin-password-reset-enabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.admin_pw_reset_hint">⚠️ When enabled, admins can reset any user's password to a one-time temp value. The user must change it on next login. Resetting a password also wipes that user's E2E DM history (their wrap key derives from the password). The fact that this is enabled is exposed via /api/public-config so users can see whether admins on this server have this power.</small>
</div>
<!-- Role Management (Admin only) -->
<div class="admin-settings" id="section-roles" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">👑 <span data-i18n="settings.admin.roles_title">Role Management</span></h5>
<small class="settings-hint" data-i18n="settings.admin.roles_hint">Roles decide who can do what. Open Permissions to set what each role can do, or to give someone a role.</small>
<div style="margin-top:8px;display:flex;gap:6px">
<button class="btn-sm btn-accent" id="open-role-editor-btn">⚙️ <span data-i18n="settings.admin.manage_roles_btn">Permissions</span></button>
<button class="btn-sm" id="post-role-menu-btn" data-i18n-title="settings.admin.role_menu.hint" title="Post a message people react to, or click, to give themselves a role">🎭 <span data-i18n="settings.admin.role_menu.post">Post a role menu</span></button>
<button class="btn-sm" id="reset-roles-btn" data-i18n-title="settings.admin.reset_roles_title" title="Reset all roles to deployment defaults">🔄 <span data-i18n="settings.admin.reset_roles_btn">Reset to Default</span></button>
</div>
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.role_icon_visibility">Role Icon Visibility</small>
<label class="toggle-row">
<span data-i18n="settings.admin.role_icon_sidebar">Show role icons in sidebar</span>
<input type="checkbox" id="role-icon-sidebar" checked>
</label>
<label class="toggle-row">
<span data-i18n="settings.admin.role_icon_chat">Show role icons in chat</span>
<input type="checkbox" id="role-icon-chat">
</label>
<label class="toggle-row">
<span data-i18n="settings.admin.role_icon_after_name">Show role icons after username</span>
<input type="checkbox" id="role-icon-after-name">
</label>
<small class="settings-hint" data-i18n="settings.admin.role_icon_hint">Controls where role icons appear. "After username" moves icons to the right side of the name instead of the left.</small>
</div>
<div class="settings-group" style="margin-top:12px">
<small class="settings-group-label" data-i18n="settings.admin.channel_creator_role_group">Channel Creator Role</small>
<label class="select-row">
<span data-i18n="settings.admin.channel_creator_role_label">Role given to a channel's creator</span>
<select id="channel-creator-role-select">
<option value="default" data-i18n="settings.admin.channel_creator_role_default">Default (highest channel role)</option>
<option value="none" data-i18n="settings.admin.channel_creator_role_none">None</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.channel_creator_role_hint">When a non-admin who can create channels makes one, they are given this role inside that channel so they can manage it. Admins are never affected.</small>
</div>
<div id="roles-list-preview" style="margin-top:8px;">
</div>
</div>
<!-- Moderation -->
<div class="admin-settings" id="section-moderation" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.moderation_title">Moderation</span></h5>
<!-- Idle-online oversight (v3.46.0) -->
<small class="settings-group-label" style="margin-top:8px;display:block">🕵️ <span data-i18n="settings.admin.idle_online_group">Idle-online accounts</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.idle_online_desc">Lists accounts that have been connected and showing as online for hours without posting, joining voice, or changing their status. A normal client goes idle on its own, so an account that stays green and silent for this long is often a script parked to watch the server rather than a person. This is just something to look at, not an automatic action.</small>
<div class="settings-btn-grid" style="margin-top:8px;align-items:center">
<label class="select-row" style="margin:0">
<span data-i18n="settings.admin.idle_online_threshold">Flag after (hours)</span>
<input type="number" class="settings-number-input" id="idle-online-hours" min="1" max="168" step="1" value="4">
</label>
<button class="btn-sm btn-accent" id="idle-online-refresh">🔄 <span data-i18n="settings.admin.idle_online_check">Check now</span></button>
</div>
<div id="idle-online-list" class="whitelist-list" style="margin-top:8px"></div>
</div>
<!-- Security -->
<div class="admin-settings" id="section-security" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🔒 <span data-i18n="settings.admin.security_title">Security</span></h5>
<!-- (#12) SSO / OIDC -->
<small class="settings-group-label" style="margin-top:8px;display:block">🔑 <span data-i18n="settings.admin.oidc_group">Single sign-on (OIDC)</span></small>
<small class="settings-hint" style="display:block" data-i18n-html="settings.admin.oidc_desc">Let members sign in with an account from your own identity provider (Authentik, Keycloak, Authelia, Auth0 and anything else that speaks OpenID Connect) instead of a Haven password. Fill in the issuer address and client ID here, then put the client secret in your .env file as OIDC_CLIENT_SECRET and restart. The secret is deliberately kept out of the database so your backups never carry it. Register this address with your provider as the redirect URI: <code>https://your-haven-address/api/auth/oidc/callback</code></small>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.oidc_e2e_note">Encrypted DMs are unlocked by a key derived from what you type when you sign in, and an SSO member never types a password here. They are asked to set a separate encryption passphrase on their first sign-in, and for it again on each new device. Nobody, including you, can reset that passphrase for them.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.oidc_enabled_label">Enable SSO sign-in</span>
<input type="checkbox" id="oidc-enabled">
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.oidc_issuer_label">Issuer URL</span>
<input type="text" class="settings-text-input" id="oidc-issuer-url" placeholder="https://auth.example.com" spellcheck="false" autocomplete="off" style="flex:1">
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.oidc_client_id_label">Client ID</span>
<input type="text" class="settings-text-input" id="oidc-client-id" spellcheck="false" autocomplete="off" style="flex:1">
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.oidc_scopes_label">Scopes</span>
<input type="text" class="settings-text-input" id="oidc-scopes" placeholder="openid profile email" spellcheck="false" autocomplete="off" style="flex:1">
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.oidc_button_label_label">Button text</span>
<input type="text" class="settings-text-input" id="oidc-button-label" data-i18n-placeholder="settings.admin.oidc_button_placeholder" placeholder="Sign in with SSO" spellcheck="false" autocomplete="off" style="flex:1">
</label>
<label class="toggle-row" style="margin-top:6px">
<span data-i18n="settings.admin.oidc_create_users_label">Create a Haven account on first sign-in</span>
<input type="checkbox" id="oidc-create-users" checked>
</label>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.oidc_create_users_hint">Off means only people who already have a Haven account linked to your provider can sign in this way. Since linking an existing account is not built yet, leaving this off will turn away everyone new.</small>
<div id="oidc-secret-warning" class="settings-hint" style="display:none;margin-top:6px;color:var(--warning,#e0a800)" data-i18n="settings.admin.oidc_secret_warning">OIDC_CLIENT_SECRET is not set in the server environment, so the sign-in button stays hidden until it is.</div>
<small class="settings-group-label" style="margin-top:8px;display:block">🔗 <span data-i18n="settings.admin.referrer_policy_group">Referrer configuration</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.referrer_policy_desc">Sets the Referrer-Policy header Haven sends to browsers, which controls how much of the current page's address is shared when the browser loads resources or follows links to other sites. The recommended options cover almost every server; the advanced policies are standard browser values left to your discretion. Policies that would send the full page address to other sites are not offered, because Haven invite links live in that address.</small>
<ul class="settings-hint" style="margin:6px 0 0;padding-left:20px">
<li data-i18n-html="settings.admin.referrer_policy_opt_same"><strong>Same-Origin (default):</strong> within this server, shares the full address; to other sites, shares nothing.</li>
<li style="margin-top:4px" data-i18n-html="settings.admin.referrer_policy_opt_none"><strong>No-Referrer:</strong> shares nothing anywhere, not even within this server.</li>
<li style="margin-top:4px" data-i18n-html="settings.admin.referrer_policy_opt_default"><strong>Strict-Origin-When-Cross-Origin:</strong> within this server, shares the full address; to other sites, shares only the origin. This was Haven's default before 3.41.0. Pick it to go back to the old behaviour.</li>
</ul>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.referrer_policy_note">Same-Origin and No-Referrer send nothing to other sites, so either one fixes embeds whose CDN rejects a cross-origin referrer, such as Twitter/X video. If an image or embed somewhere else stops loading after this change, that host wants a referrer: switch to Strict-Origin-When-Cross-Origin to restore the pre-3.41.0 behaviour.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.referrer_policy_label">Referrer policy</span>
<select id="referrer-policy-select">
<optgroup label="Recommended" data-i18n-label="settings.admin.referrer_recommended">
<option value="same-origin" data-i18n="settings.admin.referrer_same_origin">Same-Origin (default)</option>
<option value="no-referrer" data-i18n="settings.admin.referrer_no_referrer">No-Referrer</option>
<option value="strict-origin-when-cross-origin" data-i18n="settings.admin.referrer_strict_cross_origin">Strict-Origin-When-Cross-Origin (Haven's pre-3.41.0 default)</option>
</optgroup>
<optgroup label="Advanced: use at your own discretion" data-i18n-label="settings.admin.referrer_advanced">
<option value="strict-origin">Strict-Origin</option>
<option value="origin">Origin</option>
<option value="origin-when-cross-origin">Origin-When-Cross-Origin</option>
</optgroup>
</select>
</label>
<small class="settings-group-label" style="margin-top:14px;display:block">🖼️ <span data-i18n="settings.admin.media_proxy_group">Image privacy</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.media_proxy_desc">Haven fetches images from other sites itself and serves them from its own cache, so your members' browsers never contact those sites. With this off, anyone who posts an image link collects the IP address and browser details of everyone who scrolls past it, without a single click. It also means embeds keep working after the original link expires or the site goes down. Costs some bandwidth and disk on your server; each image is fetched once and cached for 30 days.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.media_proxy_label">Proxy and cache remote images</span>
<input type="checkbox" id="media-proxy-enabled">
</label>
<div id="media-cache-stats" class="settings-hint" style="margin-top:4px"></div>
<small class="settings-group-label" style="margin-top:14px;display:block">🎙️ <span data-i18n="settings.admin.voice_privacy_group">Voice privacy</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.voice_relay_desc">Haven voice is peer-to-peer, so by default everyone in a call can see everyone else's IP address. It is how the connection is made, it needs no clicking, and nobody is told it happened. Relay-only mode routes all voice through your TURN server instead, so participants only ever see the TURN server's address. This needs a TURN server configured under Voice &amp; Connectivity, and it uses your server's bandwidth for every call.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.voice_relay_label">Relay-only voice (hide member IPs)</span>
<input type="checkbox" id="voice-force-relay">
</label>
<small class="settings-group-label" style="margin-top:14px;display:block">🔔 <span data-i18n="settings.admin.fcm_privacy_group">FCM Privacy</span></small>
<small class="settings-hint" style="display:block" data-i18n-html="settings.admin.fcm_desc">FCM (Firebase Cloud Messaging) is Google's system for delivering push notifications to Android phones. It's what wakes your phone when you receive a new message while Haven is closed. This means that all notifications (including your Haven username, channel name and message content) sent to your Android phone have to pass through Google's servers to reach your phone.<br><br>While this may be fine for most users, some may prefer to keep Google completely out of the equation. <a href="https://unifiedpush.org" target="_blank" rel="noopener">UnifiedPush</a> is a decentralized notification standard that supports several notification apps, and if a UnifiedPush compatible app is installed on your phone, Haven Mobile will prioritize it over FCM.</small>
<small class="settings-hint" style="display:block;margin-top:8px" data-i18n-html="settings.admin.fcm_note"><strong>Dangerous Action Warning: Before disabling this, please ensure every member in your Haven server including yourself has a UnifiedPush compatible app installed on their phone. Otherwise, some members of your server will not receive any mobile notifications.</strong></small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.fcm_enabled_label">Enable Google FCM Push Notifications</span>
<input type="checkbox" id="fcm-enabled">
</label>
</div>
<!-- Auto-mod -->
<div class="admin-settings" id="section-automod" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.automod_title">Auto-Mod</span></h5>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.automod_desc">Checks the links in messages, edits, DMs, profiles and channel topics against a domain policy you control. Blocked content is rejected before it is saved, so it never reaches anyone else. This matters more than it sounds: Haven renders linked images and link previews directly from the other site, so a hostile link exposes the IP address of everyone who scrolls past it, not just whoever clicks.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.automod_enabled">Enabled</span>
<input type="checkbox" id="automod-enabled">
</label>
<div id="automod-body" style="margin-top:8px">
<label class="select-row">
<span data-i18n="settings.admin.automod_mode_label">Link policy</span>
<select id="automod-link-mode">
<option value="off" data-i18n="settings.admin.automod_mode_off">Off: allow every link</option>
<option value="allowlist" data-i18n="settings.admin.automod_mode_allowlist">Allowlist: block everything not listed</option>
<option value="blocklist" data-i18n="settings.admin.automod_mode_blocklist">Blocklist: allow everything except listed</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.automod_mode_hint">Allowlist is the strict option and the one that stops links you have never seen before. Blocklist only stops domains you already know about.</small>
<small class="settings-group-label" style="margin-top:14px;display:block">🌐 <span data-i18n="settings.admin.automod_domains_group">Domains</span></small>
<div class="whitelist-add-row" style="margin-top:6px">
<input type="text" id="automod-domain-input" placeholder="example.com" maxlength="253">
<select id="automod-domain-mode" style="max-width:110px">
<option value="allow" data-i18n="settings.admin.automod_allow">Allow</option>
<option value="deny" data-i18n="settings.admin.automod_block">Block</option>
</select>
<button class="btn-sm btn-accent" id="automod-domain-add" data-i18n="settings.admin.automod_add">Add</button>
</div>
<label class="toggle-row" style="margin-top:6px">
<span data-i18n="settings.admin.automod_include_subs">Include subdomains</span>
<input type="checkbox" id="automod-include-subdomains" checked>
</label>
<div id="automod-domain-list" class="whitelist-list" style="margin-top:8px"></div>
<small class="settings-group-label" style="margin-top:14px;display:block">🔤 <span data-i18n="settings.admin.automod_words_group">Words</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.automod_words_hint">Groups of words that block a message, each worth a number of strikes towards the escalation below. Whole words only, case does not matter, one word or phrase per line. Staff at or above the skip level are not checked.</small>
<div id="automod-word-groups" style="margin-top:6px"></div>
<div class="settings-btn-grid" style="margin-top:6px">
<button class="btn-sm" id="automod-word-add-group" data-i18n="settings.admin.automod_words_add">+ Add group</button>
<button class="btn-sm btn-accent" id="automod-word-save" data-i18n="settings.admin.automod_words_save">Save words</button>
</div>
<small class="settings-group-label" style="margin-top:14px;display:block">⚙️ <span data-i18n="settings.admin.automod_rules_group">Rules</span></small>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.automod_min_age">New accounts can't post links for (hours)</span>
<input type="number" class="settings-number-input" id="automod-min-account-hours" min="0" max="8760" step="1">
</label>
<small class="settings-hint" data-i18n="settings.admin.automod_min_age_hint">0 disables this. Applies even to allowed domains, and is the single most effective rule against register-post-link-get-banned-repeat.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.automod_post_wait">New accounts must wait before posting (minutes)</span>
<input type="number" class="settings-number-input" id="automod-new-account-post-minutes" min="0" max="10080" step="1">
</label>
<small class="settings-hint" data-i18n="settings.admin.automod_post_wait_hint">0 turns this off. A new account cannot post messages, forum topics, thread replies, polls or files, or message anyone, until it is this old. Up to 10080 (one week). Members at or above the role level below are not held back. Guest accounts are always new, so with this on, guests can only read.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.automod_exempt_level">Skip filtering at role level</span>
<input type="number" class="settings-number-input" id="automod-exempt-level" min="0" max="100" step="1">
</label>
<small class="settings-hint" data-i18n="settings.admin.automod_exempt_hint">Members at or above this level are never filtered. Admins are always exempt.</small>
<label class="toggle-row" style="margin-top:8px"><span data-i18n="settings.admin.automod_scan_edits">Check message edits</span><input type="checkbox" id="automod-scan-edits"></label>
<label class="toggle-row"><span data-i18n="settings.admin.automod_scan_dms">Check direct messages</span><input type="checkbox" id="automod-scan-dms"></label>
<small class="settings-hint" data-i18n="settings.admin.automod_scan_dms_hint">Direct messages are end-to-end encrypted, so the server genuinely cannot read them and cannot block a DM as it is sent. Instead the check runs in the recipient's app after it decrypts: a link to a domain you have not allowed is shown as plain text with a warning rather than something clickable, and images from those domains are not loaded. Senders are warned before the message goes out. This relies on the recipient running an app that implements it, so treat it as a strong deterrent rather than a guarantee.</small>
<label class="toggle-row"><span data-i18n="settings.admin.automod_scan_profile">Check profiles and channel topics</span><input type="checkbox" id="automod-scan-profile"></label>
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_ip_urls">Block links to raw IP addresses</span><input type="checkbox" id="automod-block-ip-urls"></label>
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_punycode">Block lookalike (internationalized) domains</span><input type="checkbox" id="automod-block-punycode"></label>
<label class="toggle-row"><span data-i18n="settings.admin.automod_block_obfuscated">Block disguised links (hxxp://, evil[.]com)</span><input type="checkbox" id="automod-block-obfuscated"></label>
<label class="toggle-row"><span data-i18n="settings.admin.automod_preview_allowlist">Only preview allowed domains</span><input type="checkbox" id="automod-preview-allowlist-only"></label>
<small class="settings-hint" data-i18n="settings.admin.automod_preview_hint">Strongly recommended. Link previews load images straight from the other site in every viewer's browser, which hands that site everyone's IP address without a single click.</small>
<small class="settings-group-label" style="margin-top:14px;display:block">⚖️ <span data-i18n="settings.admin.automod_escalation_group">Escalation</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.automod_escalation_hint">Counts blocked attempts within a rolling window. A blocked link only counts for accounts less than a week old; other members are just told the site is not allowed. Set any threshold to 0 to disable that step. Note that a muted member cannot send anything at all, so they stop collecting strikes until the mute runs out. If you want a fast automatic ban, set the ban threshold close to the mute threshold, or turn muting off.</small>
<label class="select-row" style="margin-top:6px"><span data-i18n="settings.admin.automod_window">Window (hours)</span><input type="number" class="settings-number-input" id="automod-window-hours" min="1" max="8760" step="1"></label>
<label class="select-row"><span data-i18n="settings.admin.automod_warn_at">Warn after</span><input type="number" class="settings-number-input" id="automod-warn-at" min="0" max="1000" step="1"></label>
<label class="select-row"><span data-i18n="settings.admin.automod_mute_at">Mute after</span><input type="number" class="settings-number-input" id="automod-mute-at" min="0" max="1000" step="1"></label>
<label class="select-row"><span data-i18n="settings.admin.automod_mute_mins">Mute length (minutes)</span><input type="number" class="settings-number-input" id="automod-mute-minutes" min="1" max="43200" step="1"></label>
<label class="select-row"><span data-i18n="settings.admin.automod_ban_at">Ban after</span><input type="number" class="settings-number-input" id="automod-ban-at" min="0" max="1000" step="1"></label>
<label class="toggle-row" style="margin-top:6px"><span data-i18n="settings.admin.automod_ban_ip">Auto-bans also ban the IP</span><input type="checkbox" id="automod-ban-ip"></label>
<small class="settings-hint" data-i18n="settings.admin.automod_ban_ip_hint">⚠️ Leave off unless you understand the risk. Shared and mobile-carrier addresses mean an IP ban can catch people who did nothing.</small>
<label class="select-row" style="margin-top:8px"><span data-i18n="settings.admin.automod_log_channel">Log to channel</span><input type="text" class="settings-text-input" id="automod-log-channel" data-i18n-placeholder="settings.admin.automod_log_channel_placeholder" placeholder="channel code (optional)" maxlength="8" style="max-width:9rem"></label>
<small class="settings-group-label" style="margin-top:14px;display:block">📊 <span data-i18n="settings.admin.automod_activity_group">Recent activity</span></small>
<div class="settings-btn-grid" style="margin-top:6px">
<button class="btn-sm btn-accent" id="automod-refresh-log">🔄 <span data-i18n="settings.admin.automod_refresh">Refresh</span></button>
</div>
<div id="automod-top-hosts" style="margin-top:8px"></div>
<div id="automod-log-list" class="whitelist-list" style="margin-top:8px"></div>
</div>
</div>
<!-- Whitelist -->
<div class="admin-settings" id="section-whitelist" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🚪 <span data-i18n="settings.admin.whitelist_title">Who can sign up</span></h5>
<label class="toggle-row">
<span data-i18n="settings.admin.whitelist_enabled">📋 Whitelist</span>
<input type="checkbox" id="whitelist-enabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.whitelist_hint">When enabled, only pre-approved usernames can register</small>
<div id="whitelist-panel" style="margin-top:8px;">
<div class="whitelist-add-row">
<input type="text" id="whitelist-username-input" data-i18n-placeholder="settings.admin.whitelist_username_placeholder" placeholder="Username..." maxlength="20" class="settings-text-input">
<button class="btn-sm btn-accent" id="whitelist-add-btn" data-i18n="settings.admin.whitelist_add_btn">Add</button>
</div>
<div id="whitelist-list" class="whitelist-list">
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
</div>
</div>
<!-- (#5344) Registration token: independent of whitelist; can be used together or alone -->
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
<label class="toggle-row">
<span data-i18n="settings.admin.registration_token_required">🔑 Require invite token</span>
<input type="checkbox" id="registration-token-enabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.registration_token_hint">When enabled, anyone registering must enter the token below. Works alongside the whitelist (both checks must pass) or on its own.</small>
<label class="toggle-row">
<span data-i18n="settings.admin.registration_token_invite_bypass">Allow invitation links to bypass token requirement</span>
<input type="checkbox" id="invites-bypass-registration-token">
</label>
<div id="registration-token-display" style="display:flex;align-items:center;gap:6px;margin-top:6px;">
<code id="registration-token-value" style="font-family:monospace;font-size:1rem;letter-spacing:0.05em;opacity:0.7">-</code>
<button class="btn-sm" id="copy-registration-token-btn" data-i18n-title="settings.admin.registration_token_copy" title="Copy token">📋</button>
</div>
<div style="display:flex;gap:4px;margin-top:4px;">
<button class="btn-sm btn-accent" id="generate-registration-token-btn" data-i18n="settings.admin.registration_token_generate">Generate / Reroll</button>
<button class="btn-sm" id="clear-registration-token-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
</div>
</div>
<!-- Opt-in Cloudflare Turnstile CAPTCHA on registration -->
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
<label class="toggle-row">
<span data-i18n="settings.admin.captcha_required">🛡️ Require CAPTCHA on registration</span>
<input type="checkbox" id="registration-captcha-enabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.captcha_hint">Adds a Cloudflare Turnstile challenge to the sign-up form to block automated mass registration. It's free: create a widget at the Cloudflare Turnstile dashboard and paste both keys below. Leave off if you don't need it.</small>
<div class="form-group" style="margin-top:8px;">
<label for="turnstile-site-key" style="font-size:0.85rem;" data-i18n="settings.admin.captcha_site_key">Turnstile site key</label>
<input type="text" id="turnstile-site-key" class="settings-text-input" autocomplete="off" placeholder="0x4AAAAAAA..." maxlength="200">
</div>
<div class="form-group" style="margin-top:6px;">
<label for="turnstile-secret-key" style="font-size:0.85rem;" data-i18n="settings.admin.captcha_secret_key">Turnstile secret key</label>
<input type="password" id="turnstile-secret-key" class="settings-text-input" autocomplete="off" data-i18n-placeholder="settings.admin.captcha_secret_placeholder" placeholder="Kept private on the server" maxlength="200">
</div>
</div>
<!-- Opt-in global registration rate limit -->
<div style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
<label class="toggle-row">
<span data-i18n="settings.admin.registration_rate_limit">⏱️ Limit new sign-ups per hour</span>
<input type="checkbox" id="registration-rate-limit-enabled">
</label>
<small class="settings-hint" data-i18n="settings.admin.registration_rate_limit_hint">Caps how many new accounts can be created across the whole server each hour, to slow a bot flood even when it comes from many different addresses. Sign-ups above the cap are asked to try again later, so set it comfortably above your normal rate.</small>
<div class="form-group" style="margin-top:8px;display:flex;align-items:center;gap:8px;">
<label for="registration-rate-limit-per-hour" style="font-size:0.85rem;" data-i18n="settings.admin.registration_rate_limit_max">Max new accounts per hour</label>
<input type="number" id="registration-rate-limit-per-hour" class="settings-number-input" min="1" max="100000" value="20" style="width:90px">
</div>
</div>
</div>
<!-- invites -->
<div class="admin-settings" id="section-invite" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle invite-server-config">🌐 <span data-i18n="settings.admin.invite_title">Server Invite Code</span></h5>
<small class="settings-hint invite-server-config" data-i18n="settings.admin.invite_hint">A single code that adds people to every channel &amp; sub-channel on the server</small>
<div class="invite-server-config" style="margin-top:8px;">
<div id="server-code-display" style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<code id="server-code-value" style="font-family:monospace;font-size:1rem;letter-spacing:0.05em;opacity:0.7">-</code>
<button class="btn-sm" id="copy-server-code-btn" data-i18n-title="settings.admin.copy_code_title" title="Copy code">📋</button>
</div>
<div style="display:flex;gap:4px;">
<button class="btn-sm btn-accent" id="generate-server-code-btn" data-i18n="settings.admin.invite_generate_btn">Generate</button>
<button class="btn-sm" id="clear-server-code-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
</div>
</div>
<div class="invite-server-config" style="margin-top:12px;">
<label class="select-row">
<span data-i18n="settings.admin.vanity_title">Vanity Invite Link</span>
<input type="text" id="vanity-code-input" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="my-server">
</label>
<small class="settings-hint" data-i18n-html="settings.admin.vanity_hint">Custom invite slug (3-32 chars, letters, numbers, hyphens, underscores). People can join via <code>/invite/your-slug</code></small>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="btn-sm btn-accent" id="vanity-code-save-btn" data-i18n="modals.common.save">Save</button>
<button class="btn-sm" id="vanity-code-clear-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
</div>
</div>
<!-- (#5345) Default channels for invite-code joiners.
Private channels are always excluded; this lets the admin
narrow it further to a curated set of public channels. -->
<div class="invite-server-config" style="margin-top:14px; padding-top:10px; border-top:1px dashed var(--border);">
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.default_join_title">📋 Default channels for invite joiners</h6>
<small class="settings-hint" data-i18n-html="settings.admin.default_join_hint">Pick which public channels people land in when they use the invite code or vanity link. Private channels are <strong>never</strong> auto-joined regardless of selection. Leave everything checked to grant all public channels.</small>
<div id="default-join-channels-list" style="margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels...</p>
</div>
<div style="display:flex;gap:4px;margin-top:6px">
<button class="btn-sm" id="default-join-channels-all-btn" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
<button class="btn-sm" id="default-join-channels-none-btn" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
<button class="btn-sm btn-accent" id="default-join-channels-save-btn" type="button" data-i18n="modals.common.save">Save</button>
</div>
</div>
<!-- Managed invite links live in their own popout (each card is large,
so keep the settings panel uncluttered). -->
<div id="invite-links-block">
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.invite_links_title">🔗 Invite Links</h6>
<small class="settings-hint" data-i18n="settings.admin.invite_links_hint">Shareable links that drop people straight into the channels you pick, no code to type. Each link can be turned off, limited to a number of people, or set to expire.</small>
<button class="btn-sm btn-accent" id="open-invite-links-btn" type="button" style="margin-top:8px">🔗 <span data-i18n="settings.admin.invite_links_manage">Manage Invite Links</span> <span id="invite-links-count" class="muted-text" style="font-weight:normal"></span></button>
</div>
<div id="invite-links-settings-block" class="invite-server-config">
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_invite_uses">Max invite link uses</span>
<input type="number" class="settings-number-input" id="max-invite-uses" min="0" max="100000" step="1">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_invite_uses_hint">The maximum number of uses an invite link can be set to. Admin and manage-server permission ignore this limit. (0 = unlimited)</small>
</div>
</div>
<!-- (#5381) Guest mode: password-less drop-in access scoped to a whitelist of channels -->
<div class="admin-settings" id="section-guests" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle" data-i18n="settings.admin.guests_title">👤 Join as Guest</h5>
<small class="settings-hint" data-i18n="settings.admin.guests_hint">Show a "Join as Guest" button on the login page. Guests pick a username, get a temporary account with no password, and can only see the channels you whitelist below. Guests cannot DM, and their account is deleted when they disconnect (username freed).</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.guests_allow">Allow guests</span>
<input type="checkbox" id="guests-enabled">
</label>
<label class="toggle-row" style="margin-top:6px">
<span data-i18n="settings.admin.guests_allow_voice">Guests can join voice and video</span>
<input type="checkbox" id="guests-allow-voice" checked>
</label>
<small class="settings-hint" data-i18n="settings.admin.guests_allow_voice_hint">Off, guests can read and type in the channels below but cannot join a call, share a screen or turn on a camera.</small>
<div style="margin-top:10px;">
<h6 style="margin:0 0 4px;font-size:0.85rem;font-weight:600" data-i18n="settings.admin.guests_channels_title">📋 Channels guests can access</h6>
<small class="settings-hint" data-i18n="settings.admin.guests_channels_hint">Pick which channels guests can see and post in. Pick none and the Guest button won't be very useful. DMs are never available to guests.</small>
<div id="guest-channels-list" style="margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels...</p>
</div>
<div style="display:flex;gap:4px;margin-top:6px">
<button class="btn-sm" id="guest-channels-all-btn" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
<button class="btn-sm" id="guest-channels-none-btn" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
<button class="btn-sm btn-accent" id="guest-channels-save-btn" type="button" data-i18n="modals.common.save">Save</button>
</div>
</div>
</div>
<!-- emojis -->
<div class="admin-settings" id="section-emojis" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">😎 <span data-i18n="settings.admin.emojis_title">Custom Emojis</span></h5>
<small class="settings-hint" data-i18n="settings.admin.emojis_hint">Upload images for custom server emojis</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-accent" id="open-emoji-manager-btn">⚙️ <span data-i18n="settings.admin.manage_emojis_btn">Manage Emojis</span></button>
</div>
<small class="settings-group-label" style="margin-top:14px;display:block">🔄 <span data-i18n="settings.admin.emoji_auto_update_group">Built-in emoji updates</span></small>
<small class="settings-hint" style="display:block" data-i18n="settings.admin.emoji_auto_update_desc">Allow Haven to check Unicode.org for new emoji standards. This check is only performed at server boot, and only when the current standard file on disk was created over a month ago. If a new standard is available, Haven will automatically download the emoji pack and regenerate all your built-in emojis based on the new standard. This does not affect your custom emojis in any way.</small>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.emoji_auto_update_disk">When this setting is turned off, Haven will use the latest available standard on disk. If a standard was never downloaded before, Haven will default to Unicode standard 16.0 which is bundled with the server.</small>
<label class="toggle-row" style="margin-top:8px">
<span data-i18n="settings.admin.emoji_auto_update_label">Enable automatic Unicode.org upgrade</span>
<input type="checkbox" id="unicode-emoji-auto-update">
</label>
<small class="settings-hint" style="display:block;margin-top:6px" data-i18n="settings.admin.emoji_auto_update_ps">PS: This feature has nothing to do with emoji styling. That is entirely dependent on your OS, and rendering will vary from machine to machine. This feature only guarantees you'll have the latest available emojis built into your Haven server.</small>
</div>
<!-- stickers -->
<div class="admin-settings" id="section-stickers" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🖼️ <span data-i18n="settings.admin.stickers_title">Stickers</span></h5>
<small class="settings-hint" data-i18n="settings.admin.stickers_hint">Upload sticker images grouped into packs</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-accent" id="open-sticker-manager-btn">⚙️ <span data-i18n="settings.admin.manage_stickers_btn">Manage Stickers</span></button>
</div>
</div>
<!-- sounds -->
<div class="admin-settings" id="section-sounds-admin" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🔊 <span data-i18n="settings.admin.sounds_title">Custom Sounds</span></h5>
<small class="settings-hint" data-i18n="settings.admin.sounds_hint">Upload audio files for custom notification sounds</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-accent" id="open-sound-manager-btn">⚙️ <span data-i18n="settings.admin.manage_sounds_btn">Manage Sounds</span></button>
</div>
</div>
<!-- Uploads and limits -->
<div class="admin-settings" id="section-uploads" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">📁 <span data-i18n="settings.admin.uploads_title">Uploads &amp; Limits</span></h5>
<label class="select-row">
<span data-i18n="settings.admin.max_upload">Max Upload Size (MB)</span>
<input type="number" id="max-upload-mb" min="1" max="102400" value="25" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_upload_hint">Maximum file size users can upload in MB (default 25). No hard cap: limited by available disk space.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_attachments">Max Attachments per Message</span>
<input type="number" id="max-attachments" min="1" max="50" value="10" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_attachments_hint">How many files someone can queue on one message by dropping, pasting or picking them (1 to 50, default 10). Each one still has to fit the size limit above.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_tags_per_attachment">Max Tags per Attachment</span>
<input type="number" id="max-tags-per-attachment" min="1" max="10" value="3" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_tags_per_attachment_hint">How many upload tags a user can put on one attachment (1 to 10, default 3).</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_tag_len">Max Tag Length (characters)</span>
<input type="number" id="max-tag-len" min="1" max="50" value="20" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_tag_len_hint">Maximum characters in an upload tag name (1 to 50, default 20).</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_message_chars">Max Message Length (characters)</span>
<input type="number" id="max-message-chars" min="200" max="100000" value="2000" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_message_chars_hint">Maximum characters per message, post, or DM (200-100000, default 2000)</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_sound">Max Sound File Size (KB)</span>
<input type="number" id="max-sound-kb" min="256" max="10240" value="1024" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_sound_hint">Max soundboard upload size (256-10240 KB, default 1024 / 1 MB)</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_emoji">Max Emoji File Size (KB)</span>
<input type="number" id="max-emoji-kb" min="64" max="1024" value="256" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_emoji_hint">Max custom emoji upload size (64-1024 KB, default 256)</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_sticker">Max Sticker File Size (KB)</span>
<input type="number" id="max-sticker-kb" min="256" max="10240" value="1024" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_sticker_hint">Max sticker upload size (256-10240 KB, default 1024 / 1 MB). Bump this up to allow larger GIF-style stickers.</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.max_poll_options">Max Poll Options</span>
<input type="number" id="max-poll-options" min="2" max="25" value="10" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.max_poll_hint">Maximum answer choices per poll (2-25, default 10)</small>
<label class="select-row" style="margin-top:8px">
<span data-i18n="settings.admin.session_duration">Login session duration</span>
<select id="session-duration-days" class="settings-select">
<option value="0" data-i18n="settings.admin.session_duration_never">Never expire (recommended)</option>
<option value="1" data-i18n="settings.admin.session_duration_1_day">1 day</option>
<option value="7" data-i18n="settings.admin.session_duration_7_days">7 days</option>
<option value="30" data-i18n="settings.admin.session_duration_30_days">30 days</option>
<option value="90" data-i18n="settings.admin.session_duration_90_days">90 days</option>
<option value="365" data-i18n="settings.admin.session_duration_365_days">365 days</option>
</select>
</label>
<small class="settings-hint" data-i18n="settings.admin.session_duration_hint">How long a login token stays valid before users must sign in again. "Never expire" keeps users logged in until they hit Logout or change their password. Applies to new logins only; existing tokens keep their original expiry until they expire or rotate.</small>
</div>
<!-- Tags (Admin / manage_tags): manage the upload-tag vocabulary -->
<div class="admin-settings" id="section-tags-admin" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🏷️ <span data-i18n="settings.admin.tags_title">Tags</span></h5>
<small class="settings-hint" data-i18n="settings.admin.tags_hint">Manage the upload-tag vocabulary. Renaming or deleting a tag applies to every attachment that uses it and cannot be undone.</small>
<div class="tag-admin-add">
<input type="text" id="tag-admin-new" class="settings-text-input" maxlength="50" data-i18n-placeholder="settings.admin.tags_add_placeholder" placeholder="New tag name" autocomplete="off">
<button class="btn-sm btn-accent" id="tag-admin-add-btn" data-i18n="settings.admin.tags_add_btn">Add tag</button>
</div>
<div id="tag-admin-list" class="tag-admin-list">
<p class="muted-text" data-i18n="settings.admin.tags_loading">Loading…</p>
</div>
</div>
<!-- Webhooks / Bots (Admin only) -->
<div class="admin-settings" id="section-bots" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🤖 <span data-i18n="settings.admin.bots_title">Webhooks / Bots</span></h5>
<small class="settings-hint" data-i18n="settings.admin.bots_hint">Create and manage webhook bots that can post messages to channels.</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-full btn-accent" id="open-bot-editor-btn">⚙️ <span data-i18n="settings.admin.manage_bots_btn">Manage Bots</span></button>
</div>
<div id="webhooks-list" style="margin-top:8px;">
<p class="muted-text" data-i18n="settings.admin.no_bots">No bots configured</p>
</div>
</div>
<!-- Ferry: Haven <-> Discord bridge (Admin only) -->
<div class="admin-settings" id="section-ferry" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🛶 <span data-i18n="settings.admin.ferry_title">Ferry (Discord bridge)</span></h5>
<small class="settings-hint" data-i18n="settings.admin.ferry_hint">Relay messages between Haven channels and Discord channels. Needs your own Discord bot, which takes about two minutes to create.</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-full btn-accent" id="open-ferry-btn">🛶 <span data-i18n="settings.admin.manage_ferry_btn">Set up Ferry</span></button>
</div>
<p class="muted-text" id="ferry-summary" style="margin-top:8px;font-size:.78rem;" data-i18n="settings.admin.ferry_not_configured">Not configured</p>
</div>
<!-- Discord Import (Admin only) -->
<div class="admin-settings" id="section-import" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">📦 <span data-i18n="settings.admin.import_title">Discord Import</span></h5>
<small class="settings-hint" data-i18n="settings.admin.import_hint">Import channels and messages from a Discord export file</small>
<div style="margin-top:8px;">
<button class="btn-sm btn-full btn-accent" id="open-import-btn">📦 <span data-i18n="settings.admin.import_btn">Import from Discord</span></button>
</div>
</div>
<!-- Backups -->
<div class="admin-settings" id="section-backup" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">💾 <span data-i18n="settings.admin.backup_title">Server Backup</span></h5>
<small class="settings-hint" data-i18n="settings.admin.backup_hint">Download a backup of this server, or restore from a previous backup. Useful when you don't have shell access to the host machine.</small>
<div class="settings-group" style="margin-top:10px">
<small class="settings-group-label" data-i18n="settings.admin.backup_download_label">Download Backup</small>
<div class="backup-checkboxes" style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="channels" checked> <span data-i18n="settings.admin.backup_inc_channels">Channels &amp; roles</span></label>
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="users" checked> <span data-i18n="settings.admin.backup_inc_users">Users (account list)</span></label>
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="settings" checked> <span data-i18n="settings.admin.backup_inc_settings">Server settings</span></label>
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="messages"> <span data-i18n="settings.admin.backup_inc_messages">Messages (chat history)</span></label>
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="dms"> <span data-i18n="settings.admin.backup_inc_dms">Direct messages</span> <small class="settings-hint" style="margin-left:6px;opacity:.75" data-i18n="settings.admin.backup_inc_dms_hint">(E2E ciphertext; only original key holders can read)</small></label>
<label class="checkbox-row"><input type="checkbox" class="backup-include" value="files"> <span data-i18n="settings.admin.backup_inc_files">Uploaded files</span></label>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
<button class="btn-sm btn-accent" id="backup-download-btn">⬇️ <span data-i18n="settings.admin.backup_download_btn">Download Backup</span></button>
<button class="btn-sm" id="backup-select-all-btn" type="button" data-i18n="settings.admin.backup_select_all">Select All</button>
<button class="btn-sm" id="backup-select-none-btn" type="button" data-i18n="settings.admin.backup_select_none">Clear</button>
</div>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.backup_modes_hint">Pick what to include. Messages and uploaded files can make the backup very large; channels, users and settings alone are usually small. To restore or move a server, tick Messages: that carries the whole database, so everyone keeps their account and password. It also means the file holds everyone's login details (passwords stored scrambled, never readable), 2FA secrets and emails, so keep it somewhere safe.</small>
</div>
<div class="settings-group" style="margin-top:14px">
<small class="settings-group-label" data-i18n="settings.admin.backup_restore_label">Restore From Backup</small>
<div style="display:flex;gap:6px;align-items:center;margin-top:4px;flex-wrap:wrap">
<input type="file" id="backup-restore-file" accept=".zip" style="flex:1 1 200px;min-width:0;max-width:100%">
<button class="btn-sm btn-accent" id="backup-restore-btn">⬆️ <span data-i18n="settings.admin.backup_restore_btn">Restore</span></button>
</div>
<div id="restore-progress" class="restore-progress" style="display:none">
<div class="restore-progress-track"><div class="restore-progress-fill" id="restore-progress-fill"></div></div>
<small class="settings-hint restore-progress-label" id="restore-progress-label" data-i18n="header.uploading">Uploading…</small>
</div>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.backup_restore_full_note">Only full backups can be restored here, which means the backup must have had both Messages and Uploaded files checked when it was made. Smaller channels/users/settings backups have to be re-imported by hand.</small>
<small class="settings-hint" style="margin-top:6px;display:block;color:var(--danger,#e84a4a)" data-i18n-html="settings.admin.backup_restore_warning"><strong>Warning:</strong> Restoring overwrites the current server data. The server will restart automatically, so your hosting setup must support auto-restart (Docker, systemd, the Haven installer service, etc.). The previous database is preserved as <code>haven.db.pre-restore</code> for one cycle.</small>
</div>
<div class="settings-group" style="margin-top:14px">
<small class="settings-group-label" data-i18n="settings.admin.auto_backup_label">Automatic Backups</small>
<label class="toggle-row" style="margin-top:4px">
<span data-i18n="settings.admin.auto_backup_enable">Enable scheduled backups</span>
<input type="checkbox" id="auto-backup-enabled">
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.auto_backup_interval">Interval</span>
<select id="auto-backup-interval" class="settings-number-input">
<option value="6" data-i18n="settings.admin.auto_backup_every_6_hours">Every 6 hours</option>
<option value="12" data-i18n="settings.admin.auto_backup_every_12_hours">Every 12 hours</option>
<option value="24" selected data-i18n="settings.admin.auto_backup_daily">Daily</option>
<option value="168" data-i18n="settings.admin.auto_backup_weekly">Weekly</option>
<option value="720" data-i18n="settings.admin.auto_backup_monthly">Monthly (~30d)</option>
</select>
</label>
<label class="select-row" style="margin-top:6px">
<span data-i18n="settings.admin.auto_backup_retention">Keep last N backups</span>
<input type="number" id="auto-backup-retention" min="1" max="50" value="7" class="settings-number-input">
</label>
<div class="backup-checkboxes" style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
<small class="settings-hint" data-i18n="settings.admin.auto_backup_sections_label">Include in scheduled backups:</small>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="channels" checked> <span data-i18n="settings.admin.backup_inc_channels">Channels &amp; roles</span></label>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="users" checked> <span data-i18n="settings.admin.auto_backup_inc_users">Users</span></label>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="settings" checked> <span data-i18n="settings.admin.backup_inc_settings">Server settings</span></label>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="messages" checked> <span data-i18n="settings.admin.auto_backup_inc_messages">Messages</span></label>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="dms"> <span data-i18n="settings.admin.backup_inc_dms">Direct messages</span> <small class="settings-hint" style="margin-left:6px;opacity:.75" data-i18n="settings.admin.auto_backup_inc_dms_hint">(E2E ciphertext)</small></label>
<label class="checkbox-row"><input type="checkbox" class="auto-backup-include" value="files"> <span data-i18n="settings.admin.backup_inc_files">Uploaded files</span></label>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
<button class="btn-sm btn-accent" id="auto-backup-save-btn" data-i18n="settings.admin.auto_backup_save">Save Schedule</button>
<button class="btn-sm btn-accent" id="auto-backup-run-now-btn" data-i18n="settings.admin.auto_backup_run_now">Run Now</button>
<button class="btn-sm btn-accent" id="auto-backup-refresh-btn" data-i18n="settings.admin.auto_backup_refresh">Refresh List</button>
</div>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.auto_backup_hint">Auto-backups are saved to the server's data directory and pruned to your retention count. They use the same format as manual downloads, so you can restore any of them above.</small>
<div id="auto-backup-list" style="margin-top:10px;display:flex;flex-direction:column;gap:4px"></div>
</div>
</div>
<div class="admin-settings" id="section-template" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🧩 <span data-i18n="settings.admin.template.title">Server Template</span></h5>
<small class="settings-hint" data-i18n="settings.admin.template.hint">Save this server's layout as a file others can use to set up theirs the same way, or set this server up from someone else's template.</small>
<div class="settings-group" style="margin-top:10px">
<small class="settings-group-label" data-i18n="settings.admin.template.export_label">Export</small>
<div style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
<label class="toggle-row"><span data-i18n="settings.admin.template.export_posts">Pinned setup posts (welcome, rules, channel intros)</span><input type="checkbox" id="template-export-posts" checked></label>
<label class="toggle-row"><span data-i18n="settings.admin.template.export_assets">Server icon, banner, role icons and themes</span><input type="checkbox" id="template-export-assets" checked></label>
</div>
<button class="btn-sm btn-accent" id="template-export-btn" style="margin-top:8px">⬇️ <span data-i18n="settings.admin.template.export_btn">Download Template</span></button>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.template.export_note">Includes the name and branding, categories, channels and their settings, roles and permissions, self-assign role menus, webhook slots and link rules. Never includes members, messages, DMs, encryption keys, invite codes, webhook tokens, passwords or API keys.</small>
</div>
<div class="settings-group" style="margin-top:14px">
<small class="settings-group-label" data-i18n="settings.admin.template.import_label">Import</small>
<input type="file" id="template-file" accept=".json,application/json" style="margin-top:4px;max-width:100%">
<div style="display:flex;flex-direction:column;gap:4px;margin-top:8px">
<label class="select-row"><span data-i18n="settings.admin.template.mode_label">When something already exists</span><select id="template-mode">
<option value="merge" data-i18n="settings.admin.template.mode_merge">Add missing: leave it alone</option>
<option value="replace" data-i18n="settings.admin.template.mode_replace">Replace layout: update it to match</option>
</select></label>
<label class="toggle-row"><span data-i18n="settings.admin.template.opt_posts">Add setup posts and role menus</span><input type="checkbox" id="template-opt-posts" checked></label>
<label class="toggle-row"><span data-i18n="settings.admin.template.opt_webhooks">Create the template's webhooks (with new tokens)</span><input type="checkbox" id="template-opt-webhooks" checked></label>
<label class="toggle-row"><span data-i18n="settings.admin.template.opt_join">Add current members to new public channels</span><input type="checkbox" id="template-opt-join" checked></label>
</div>
<div id="template-preview" class="template-preview" style="display:none;margin-top:8px"></div>
<button class="btn-sm btn-accent" id="template-apply-btn" style="margin-top:8px" disabled>🧩 <span data-i18n="settings.admin.template.apply_btn">Apply Template</span></button>
<small class="settings-hint" style="margin-top:6px;display:block" data-i18n="settings.admin.template.import_note">Nothing is deleted in either mode: channels and roles that are not in the template stay as they are.</small>
</div>
</div>
<!-- Cleanup -->
<div class="admin-settings" id="section-cleanup" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🗑️ <span data-i18n="settings.admin.cleanup_title">Auto-Cleanup</span></h5>
<label class="toggle-row">
<span data-i18n="settings.admin.cleanup_enabled">Enabled</span>
<input type="checkbox" id="cleanup-enabled">
</label>
<label class="select-row">
<span data-i18n="settings.admin.cleanup_max_age">Max Age (days)</span>
<input type="number" id="cleanup-max-age" min="0" max="3650" value="0" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.cleanup_age_hint">Delete messages older than N days (0 = off)</small>
<label class="select-row">
<span data-i18n="settings.admin.cleanup_max_size">Max DB Size (MB)</span>
<input type="number" id="cleanup-max-size" min="0" max="100000" value="0" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.cleanup_size_hint">Trim oldest messages when DB exceeds N MB (0 = off)</small>
<label class="select-row">
<span data-i18n="settings.admin.cleanup_max_uploads">Max Uploads Size (MB)</span>
<input type="number" id="cleanup-max-uploads" min="0" max="10000000" value="0" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.cleanup_uploads_hint">When the uploads folder passes N MB, remove the oldest messages with files until it is back under (0 = off). Avatars, emoji and stickers count toward it but are never removed.</small>
<label class="select-row">
<span data-i18n="settings.admin.deleted_retention">Keep deleted files for (days)</span>
<input type="number" id="deleted-retention-days" min="1" max="3650" value="7" class="settings-number-input">
</label>
<small class="settings-hint" data-i18n="settings.admin.deleted_retention_hint">Attachments from deleted messages and channels are held this long, then removed for good. This runs whether or not auto-cleanup is enabled (default 7).</small>
<label class="toggle-row">
<span data-i18n="settings.admin.keep_self_destructed">Keep self-destructed attachments</span>
<input type="checkbox" id="keep-self-destructed" checked>
</label>
<small class="settings-hint" data-i18n="settings.admin.keep_self_destructed_hint">On: when a self-destructing message expires, its files are held as long as other deleted files (set above), so a moderator can still recover something abusive. Off: they are deleted for good. Keep it on for community servers; small servers where everyone trusts each other may prefer off.</small>
<button class="btn-sm btn-full btn-accent" id="run-cleanup-now-btn" style="margin-top:4px;">🧹 <span data-i18n="settings.admin.cleanup_run_btn">Run Cleanup Now</span></button>
</div>
<!-- Mod Mode (Admin only) -->
<div class="admin-settings" id="section-modmode" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">🔧 <span data-i18n="settings.admin.modmode_title">Mod Mode</span></h5>
<small class="settings-hint" data-i18n="settings.admin.modmode_hint">Rearrange sidebar sections and snap server/sidebar/status panels</small>
<button class="btn-sm btn-full" id="mod-mode-settings-toggle" style="margin-top:6px;">🔧 <span data-i18n="settings.admin.modmode_toggle_btn">Toggle Mod Mode</span></button>
<button class="btn-sm btn-full" id="mod-mode-reset" data-i18n="settings.admin.modmode_reset_btn" style="margin-top:6px;">↺ Reset Layout</button>
</div>
<!-- Audit Log (Admin only) -->
<div class="admin-settings" id="section-audit-log" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">📜 <span data-i18n="settings.admin.audit_log_title">Audit Log</span></h5>
<small class="settings-hint" data-i18n="settings.admin.audit_log_hint">Track admin and moderator actions: channel changes, role updates, bans, kicks, server settings, and more.</small>
<button class="btn-sm btn-full btn-accent" id="open-audit-log-btn" style="margin-top:6px;">📜 <span data-i18n="settings.admin.audit_log_btn">View Audit Log</span></button>
</div>
</div>
</div><!-- /settings-body-admin -->
</div><!-- /settings-layout -->
<!-- Admin save bar: changes only apply when Save is clicked -->
<div class="admin-save-bar" style="display:none">
<button class="btn-accent btn-admin-save" id="admin-save-btn">💾 <span data-i18n="settings.admin.save_btn">Save Settings</span></button>
<small class="settings-hint" style="text-align:center;margin-top:4px" data-i18n="settings.admin.save_hint">Changes only apply when you click Save. Close (✕) to cancel.</small>
</div>
</div>
</div>
<!-- Discord Import Modal -->
<div class="modal-overlay" id="import-modal" style="display:none">
<div class="modal" style="width:560px">
<h3>📦 <span data-i18n="modals.discord_import.title">Import from Discord</span></h3>
<!-- Step 1: Upload or Connect -->
<div id="import-step-upload">
<div class="import-tabs">
<button class="import-tab active" data-tab="file">📁 <span data-i18n="modals.discord_import.file_tab">Upload File</span></button>
<button class="import-tab" data-tab="connect">🔗 <span data-i18n="modals.discord_import.connect_tab">Connect to Discord</span></button>
</div>
<!-- Tab: Upload File -->
<div id="import-tab-file">
<p class="settings-hint" style="margin-bottom:10px" data-i18n="modals.discord_import.upload_hint">
Upload a Discord export file to import channels and messages into Haven.
</p>
<div class="import-format-info">
<div class="import-format-row">
<strong>DiscordChatExporter</strong>
<span class="muted-text" data-i18n="modals.discord_import.format_dce">(JSON or ZIP; recommended, includes all users)</span>
</div>
<div class="import-format-row">
<strong>Discord Data Package</strong>
<span class="muted-text" data-i18n="modals.discord_import.format_ddp">(ZIP from Settings → Privacy; your messages only)</span>
</div>
</div>
<div class="import-dropzone" id="import-dropzone">
<input type="file" id="import-file-input" accept=".json,.zip" style="display:none">
<div class="import-dropzone-content">
<span class="import-dropzone-icon">📁</span>
<span><span data-i18n="modals.discord_import.drop_text">Drop file here or </span><a href="#" id="import-browse-link" data-i18n="modals.discord_import.browse_link">browse</a></span>
<span class="muted-text" style="font-size:0.6875rem" data-i18n="modals.discord_import.dropzone_hint">.json or .zip, up to 500 MB</span>
</div>
</div>
<div id="import-upload-progress" style="display:none">
<div class="import-progress-bar"><div class="import-progress-fill" id="import-progress-fill"></div></div>
<p class="muted-text" id="import-upload-status" style="margin-top:6px;text-align:center" data-i18n="modals.discord_import.uploading">Uploading...</p>
</div>
</div>
<!-- Tab: Connect to Discord -->
<div id="import-tab-connect" style="display:none">
<div id="import-connect-step-token">
<p class="settings-hint" style="margin-bottom:8px" data-i18n="modals.discord_import.connect_hint">
Pull a Discord server's history straight into Haven.
</p>
<div class="import-ferry-box">
<strong data-i18n="modals.discord_import.ferry_title">🛶 Use your Ferry bot (recommended)</strong>
<p data-i18n="modals.discord_import.ferry_desc">Your Ferry bot reads the history for you. Nothing to paste, and it's within Discord's rules. It can import every channel it can see on Discord.</p>
<button class="btn-sm btn-accent" id="import-ferry-connect-btn" data-i18n="modals.discord_import.ferry_btn">Use my Ferry bot</button>
<p class="muted-text" id="import-ferry-missing" style="display:none"><span data-i18n="modals.discord_import.ferry_missing">Ferry isn't set up on this server yet. It takes about two minutes, and you only do it once.</span> <a href="#" id="import-open-ferry" data-i18n="modals.discord_import.ferry_setup_link">Set up Ferry</a></p>
</div>
<details class="import-personal-login">
<summary data-i18n="modals.discord_import.personal_summary">Or use your personal Discord login (not recommended)</summary>
<div class="import-token-risk">
<strong data-i18n="modals.discord_import.risk_title">⚠️ Read this first</strong>
<p data-i18n="modals.discord_import.risk_rules">This uses your personal Discord login token. Discord's rules don't allow apps to use a personal account this way, so Discord could lock or ban the account you use.</p>
<p data-i18n="modals.discord_import.risk_takeover">Anyone who gets your token can take over your Discord account. Only paste it into a Haven server you run yourself, and never because someone asked you to.</p>
<p data-i18n="modals.discord_import.risk_safer">Your Ferry bot, above, does the same import within Discord's rules.</p>
<label class="import-token-risk-ack">
<input type="checkbox" id="import-token-risk-ack">
<span data-i18n="modals.discord_import.risk_ack">I understand the risks, and this is my own Haven server</span>
</label>
</div>
<div id="import-connect-gated" style="display:none">
<details class="import-token-help">
<summary data-i18n="modals.discord_import.token_help_summary">How do I get my Discord token?</summary>
<div class="import-token-steps">
<ol>
<li data-i18n-html="modals.discord_import.token_step_1">Open <a href="https://discord.com/app" target="_blank" rel="noopener">discord.com/app</a> in Chrome/Edge (or use the desktop app)</li>
<li data-i18n-html="modals.discord_import.token_step_2">Press <kbd>F12</kbd> to open DevTools</li>
<li data-i18n-html="modals.discord_import.token_step_3">Click the <strong>Application</strong> tab (click <code>&gt;&gt;</code> in the toolbar if you don't see it)</li>
<li data-i18n-html="modals.discord_import.token_step_4">In the left sidebar, expand <strong>Local Storage</strong> → click <strong>https://discord.com</strong></li>
<li data-i18n-html="modals.discord_import.token_step_5">Find the <code>token</code> key and copy the value (without the quotes)</li>
</ol>
<p style="margin-top:8px" data-i18n-html="modals.discord_import.desktop_app_hint"><strong>Desktop app:</strong> If F12 doesn't open DevTools, close Discord fully, then edit:</p>
<code class="import-token-snippet">%appdata%\discord\settings.json</code>
<p style="margin-top:4px" data-i18n="modals.discord_import.add_line_hint">Add this line anywhere inside the { }:</p>
<code class="import-token-snippet">"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true</code>
<p style="margin-top:4px" data-i18n="modals.discord_import.restart_hint">Restart Discord, then follow the steps above.</p>
<p class="muted-text" style="margin-top:8px">⚠️ <span data-i18n="modals.discord_import.token_warning">Your token is like a password. Haven uses it only for this import and never stores it.</span></p>
</div>
</details>
<div style="display:flex;gap:8px;margin-top:10px">
<input type="password" id="import-discord-token" class="import-token-input" data-i18n-placeholder="modals.discord_import.token_placeholder" placeholder="Paste your Discord token here">
<button class="btn-sm btn-accent" id="import-connect-btn" data-i18n="modals.discord_import.connect_btn">Connect</button>
</div>
</div>
</details>
<p class="muted-text" id="import-connect-status" style="margin-top:6px;display:none"></p>
</div>
<div id="import-connect-step-servers" style="display:none">
<div class="import-info-bar" style="margin-bottom:8px">
<span><span data-i18n="modals.discord_import.connected_as">Connected as</span> <strong id="import-discord-username"></strong></span>
<a href="#" id="import-connect-disconnect" data-i18n="modals.discord_import.disconnect" style="font-size:0.6875rem;margin-left:auto">Disconnect</a>
</div>
<p class="settings-hint" style="margin-bottom:6px" data-i18n="modals.discord_import.pick_server">Pick a server to import from:</p>
<div id="import-server-list" class="import-server-grid"></div>
</div>
<div id="import-connect-step-channels" style="display:none">
<div class="import-info-bar" style="margin-bottom:8px">
<span id="import-connect-guild-name" style="font-weight:600"></span>
<a href="#" id="import-connect-back-servers" data-i18n="modals.discord_import.back_to_servers" style="font-size:0.6875rem;margin-left:auto">← Back to servers</a>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.select_channels">Select channels to fetch:</p>
<a href="#" id="import-connect-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:0.6875rem;white-space:nowrap">Deselect All</a>
</div>
<div class="import-channel-list" id="import-connect-channel-list"></div>
<div style="display:flex;gap:8px;margin-top:10px;justify-content:flex-end">
<button class="btn-sm btn-accent" id="import-fetch-btn">📥 <span data-i18n="modals.discord_import.fetch_btn">Fetch Messages</span></button>
</div>
<p class="muted-text" id="import-fetch-status" style="margin-top:6px;display:none"></p>
</div>
</div>
</div>
<!-- Step 2: Preview & Select -->
<div id="import-step-preview" style="display:none">
<div class="import-info-bar">
<span class="import-format-badge" id="import-format-badge"></span>
<span id="import-server-name" style="font-weight:600"></span>
<span class="muted-text" id="import-total-msgs"></span>
</div>
<div style="display:flex;align-items:center;gap:8px;margin:8px 0 6px">
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.preview_select_hint">Select channels to import. You can rename them before importing.</p>
<a href="#" id="import-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:0.6875rem;white-space:nowrap">Deselect All</a>
</div>
<div class="import-channel-list" id="import-channel-list"></div>
<div style="display:flex;gap:8px;margin-top:12px;justify-content:flex-end">
<button class="btn-sm" id="import-back-btn" data-i18n="modals.common.back">← Back</button>
<button class="btn-sm btn-accent" id="import-execute-btn">📦 <span data-i18n="modals.discord_import.import_selected_btn">Import Selected</span></button>
</div>
</div>
<!-- Step 3: Done -->
<div id="import-step-done" style="display:none;text-align:center;padding:20px 0">
<div style="font-size:3rem;margin-bottom:12px">✅</div>
<p id="import-done-msg" style="font-size:0.875rem" data-i18n="modals.discord_import.import_complete">Import complete!</p>
</div>
<div class="modal-actions" style="margin-top:12px">
<button class="btn-sm" id="close-import-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Permissions matrix -->
<div class="modal-overlay" id="perm-matrix-overlay" style="display:none">
<div class="modal modal-wide perm-matrix-modal">
<div class="perm-matrix-header">
<h3>👑 <span data-i18n="settings.admin.perm_matrix.title">Permissions</span></h3>
<div class="perm-view-toggle" role="tablist">
<button type="button" class="perm-view-btn is-active" id="perm-view-roles" data-i18n="settings.admin.perm_matrix.view_roles">Roles</button>
<button type="button" class="perm-view-btn" id="perm-view-users" data-i18n="settings.admin.perm_matrix.view_users">Users</button>
</div>
</div>
<p class="muted-text perm-matrix-lead" id="perm-matrix-lead" data-i18n="settings.admin.perm_matrix.lead_roles">Set what each role can do. Everyone who holds a role gets what is ticked in its column.</p>
<div id="perm-roles-pane">
<div class="perm-matrix-toolbar">
<button type="button" class="btn-sm btn-accent" id="perm-add-role-btn">+ <span data-i18n="settings.admin.perm_matrix.add_role">Add role</span></button>
</div>
<div class="perm-matrix-scroll">
<table class="perm-matrix-table" id="perm-matrix-table"></table>
</div>
</div>
<div id="perm-users-pane" hidden>
<div class="perm-users-layout">
<div class="perm-users-side">
<input type="search" id="perm-user-search" class="settings-text-input" data-i18n-placeholder="settings.admin.perm_matrix.search_users" placeholder="Search members…">
<div id="perm-user-list" class="perm-user-list"></div>
</div>
<div id="perm-user-detail" class="perm-user-detail"></div>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-sm" id="perm-open-classic-btn" data-i18n="settings.admin.perm_matrix.classic">More options</button>
<button type="button" class="btn-sm" id="close-perm-matrix-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Role Management Modal -->
<div class="modal-overlay" id="role-modal" style="display:none">
<div class="modal modal-wide role-management-modal">
<h3>👑 <span data-i18n="modals.role_management.title">Role Management</span></h3>
<div class="role-editor-layout">
<div class="role-sidebar">
<div id="role-list-sidebar" class="role-list-sidebar"></div>
<button class="btn-sm btn-accent btn-full" id="create-role-btn" data-i18n="modals.role_management.new_role_btn" style="margin-top:8px;">+ New Role</button>
</div>
<div class="role-detail" id="role-detail-panel">
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.role_management.select_to_edit">Select a role to edit</p>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="save-role-btn" data-i18n="modals.common.save" style="display:none">Save</button>
<button class="btn-sm" id="close-role-modal-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Role Members Modal (assign/remove role directly from role detail) -->
<div class="modal-overlay" id="role-members-modal" style="display:none">
<div class="modal" style="width:460px;max-height:80vh;display:flex;flex-direction:column;gap:0">
<h3 style="margin-bottom:4px">👥 <span id="role-members-modal-title"></span></h3>
<p class="muted-text" style="font-size:0.75rem;margin-bottom:10px" data-i18n="modals.role_management.members_scope_hint">Server-wide assignment only. For channel-specific roles, use the Role Assignment menu in member management.</p>
<input type="text" id="role-members-search" class="settings-text-input" data-i18n-placeholder="modals.role_management.search_members" placeholder="Search members…" style="margin-bottom:10px;flex-shrink:0">
<div id="role-members-list" style="overflow-y:auto;flex:1;border:1px solid var(--border);border-radius:6px;min-height:100px"></div>
<div class="modal-actions" style="margin-top:12px;flex-shrink:0">
<button class="btn-sm" id="role-members-close-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Assign Role Modal -->
<div class="modal-overlay" id="assign-role-modal" style="display:none">
<div class="modal" style="width:440px">
<h3>👑 <span data-i18n="modals.assign_role.title">Assign Role</span></h3>
<p id="assign-role-user-label" class="muted-text"></p>
<label class="assign-role-field-label" data-i18n="modals.assign_role.role_label">Roles</label>
<p class="assign-role-hint" data-i18n="modals.assign_role.role_hint_multi">A user may hold multiple roles. Their effective permissions are the union of every role they hold, and their displayed badge color comes from the highest role.</p>
<div id="assign-role-checkboxes" class="assign-role-checkboxes" role="group" data-i18n-aria-label="modals.assign_role.assignable_roles" aria-label="Assignable roles"></div>
<label class="assign-role-field-label" style="margin-top:10px" data-i18n="modals.assign_role.scope_label">Scope</label>
<p class="assign-role-hint" data-i18n="modals.assign_role.scope_hint">Server-wide applies to all channels. Channel-specific only applies within that channel.</p>
<select id="assign-role-scope" class="settings-select assign-role-select">
<option value="server" data-i18n="modals.assign_role.server_wide">Server-wide</option>
</select>
<div class="modal-actions">
<button class="btn-sm" id="cancel-assign-role-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="confirm-assign-role-btn" data-i18n="modals.assign_role.assign_btn">Assign</button>
</div>
</div>
</div>
<!-- Centralized Role Assignment (3-pane) Modal -->
<div class="modal-overlay" id="role-assign-center-modal" style="display:none">
<div class="modal role-assign-center-box">
<h3>👑 <span data-i18n="modals.role_assign_center.title">Role Assignment</span></h3>
<p class="muted-text role-assign-center-subtitle" data-i18n="modals.role_assign_center.subtitle">Select a user, then a channel, then configure their role.</p>
<div class="rac-layout">
<!-- Left pane: Users -->
<div class="rac-pane rac-users-pane">
<div class="rac-pane-header">
<span class="rac-pane-title" data-i18n="modals.role_assign_center.users_pane">Users</span>
<input type="text" id="rac-user-search" class="rac-search" data-i18n-placeholder="modals.role_assign_center.search_users_placeholder" placeholder="Search users…">
</div>
<div class="rac-pane-list" id="rac-user-list"></div>
</div>
<!-- Center pane: Channels -->
<div class="rac-pane rac-channels-pane">
<div class="rac-pane-header">
<span class="rac-pane-title" data-i18n="modals.role_assign_center.channels_pane">Channels</span>
</div>
<div class="rac-pane-list" id="rac-channel-list">
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_user_hint">← Select a user</p>
</div>
</div>
<!-- Right pane: Role config -->
<div class="rac-pane rac-config-pane">
<div class="rac-pane-header">
<span class="rac-pane-title" data-i18n="modals.role_assign_center.config_pane">Role Configuration</span>
</div>
<div class="rac-config-body" id="rac-config-body">
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_channel_hint">← Select a channel</p>
</div>
</div>
</div>
<div class="modal-actions rac-footer">
<button class="btn-sm btn-accent" id="rac-manage-roles-btn" style="display:none" data-i18n-title="modals.role_assign_center.manage_roles_title" title="Open role management">⚙️ <span data-i18n="modals.role_assign_center.manage_roles_btn">Manage Roles</span></button>
<div class="rac-footer-right">
<button class="btn-sm" id="rac-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="rac-save-btn" data-i18n="modals.role_assign_center.save_btn" disabled>Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Bot Management Modal -->
<div class="modal-overlay" id="bot-modal" style="display:none">
<div class="modal modal-wide">
<h3>🤖 <span data-i18n="modals.bot_mgmt.title">Bot Management</span></h3>
<div class="role-editor-layout">
<div class="role-sidebar">
<div id="bot-list-sidebar" class="role-list-sidebar"></div>
<button class="btn-sm btn-accent btn-full" id="create-bot-btn" data-i18n="modals.bot_mgmt.new_bot_btn" style="margin-top:8px;">+ New Bot</button>
</div>
<div class="role-detail" id="bot-detail-panel">
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.bot_mgmt.select_or_create">Select a bot to edit, or create a new one</p>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-bot-modal-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Sound Management Modal (Full Popout) -->
<div class="modal-overlay" id="sound-modal" style="display:none">
<div class="modal" style="width:640px;min-height:420px">
<h3>🔊 <span data-i18n="modals.sound_manager.title">Sound Manager</span></h3>
<!-- Tabs -->
<div class="sound-modal-tabs">
<button class="sound-tab active" data-tab="soundboard">🎵 <span data-i18n="modals.sound_manager.soundboard_tab">Soundboard</span></button>
<button class="sound-tab" data-tab="assign">🔔 <span data-i18n="modals.sound_manager.assign_tab">Assign to Events</span></button>
<button class="sound-tab sound-tab-admin" data-tab="manage" style="display:none">⚙️ <span data-i18n="modals.sound_manager.manage_tab">Manage</span></button>
</div>
<!-- TAB: Soundboard (all users) -->
<div class="sound-tab-content active" id="sound-tab-soundboard">
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n="modals.sound_manager.modal_hint">Browse & play custom sounds. Set hotkeys to play them anytime.</small>
<div class="sound-search-row">
<input type="text" id="soundboard-search" data-i18n-placeholder="modals.sound_manager.search_placeholder" placeholder="Search sounds..." class="settings-text-input" style="flex:1">
<button id="soundboard-popout-btn" class="icon-btn small" data-i18n-title="media_runtime.sound.popout" title="Pop out soundboard">⧉</button>
<div style="display:flex;gap:8px;margin-bottom:8px;font-size:0.75rem">
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-show-hidden" style="cursor:pointer"> <span data-i18n="modals.sound_manager.show_hidden">Show hidden</span></label>
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-list-mode" style="cursor:pointer"> <span data-i18n="modals.sound_manager.list_view">List view</span></label>
<label style="display:flex;align-items:center;gap:4px"><input type="checkbox" id="soundboard-sidebar-mode" style="cursor:pointer"> <span data-i18n="settings.soundboard_section.sidebar_mode">Sidebar mode</span></label>
</div>
</div>
<div id="soundboard-grid" class="soundboard-grid">
<p class="muted-text" data-i18n="modals.sound_manager.no_sounds">No sounds available</p>
</div>
</div>
<!-- TAB: Assign to Events (all users) -->
<div class="sound-tab-content" id="sound-tab-assign">
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.sound_manager.assign_hint">Choose which sound plays for each event. Custom sounds uploaded by the admin appear here too.</small>
<div class="sound-assign-list">
<label class="select-row">
<span>📨 <span data-i18n="modals.sound_manager.new_message">New Message</span></span>
<select id="assign-msg-sound" class="sound-assign-select"></select>
</label>
<label class="select-row">
<span>📤 <span data-i18n="modals.sound_manager.msg_sent">Message Sent</span></span>
<select id="assign-sent-sound" class="sound-assign-select"></select>
</label>
<label class="select-row">
<span>📢 <span data-i18n="modals.sound_manager.mention">@Mention</span></span>
<select id="assign-mention-sound" class="sound-assign-select"></select>
</label>
<label class="select-row">
<span>👋 <span data-i18n="modals.sound_manager.user_joined">User Joined</span></span>
<select id="assign-join-sound" class="sound-assign-select"></select>
</label>
<label class="select-row">
<span>🚪 <span data-i18n="modals.sound_manager.user_left">User Left</span></span>
<select id="assign-leave-sound" class="sound-assign-select"></select>
</label>
</div>
</div>
<!-- TAB: Manage (admin only) -->
<div class="sound-tab-content" id="sound-tab-manage">
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="modals.sound_manager.upload_hint">Upload audio files for custom sounds (max 1 MB each, mp3/ogg/wav/webm)</small>
<div class="whitelist-add-row">
<input type="text" id="sound-name-input" data-i18n-placeholder="modals.sound_manager.name_placeholder" placeholder="Sound name..." maxlength="30" class="settings-text-input">
<input type="file" id="sound-file-input" accept="audio/*" style="max-width:120px;font-size:0.6875rem">
<button class="btn-sm btn-accent" id="sound-upload-btn" data-i18n="modals.sound_manager.upload_btn">Upload</button>
</div>
<div id="custom-sounds-list" style="margin-top:12px;">
<p class="muted-text" data-i18n="modals.sound_manager.no_custom_sounds">No custom sounds uploaded</p>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-sound-modal-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- (soundboard sidebar panel is now in-flow before #right-sidebar) -->
<!-- Emoji Management Modal -->
<div class="modal-overlay" id="emoji-modal" style="display:none">
<div class="modal" style="width:520px">
<h3>😎 <span data-i18n="modals.emoji_mgmt.title">Emoji Management</span></h3>
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="settings.admin.emojis_upload_hint">Upload images for custom server emojis (max 256 KB, png/gif/webp). Name must be lowercase, no spaces.</small>
<div class="whitelist-add-row">
<input type="text" id="emoji-name-input" data-i18n-placeholder="modals.emoji_mgmt.name_placeholder" placeholder="emoji_name" maxlength="30" class="settings-text-input">
<input type="file" id="emoji-file-input" accept="image/png,image/gif,image/webp,image/jpeg" style="max-width:120px;font-size:0.6875rem">
<button class="btn-sm btn-accent" id="emoji-upload-btn" data-i18n="modals.emoji_mgmt.upload_btn">Upload</button>
</div>
<div style="margin-top:6px;text-align:center">
<label class="btn-sm" style="cursor:pointer;display:inline-block" data-i18n-title="modals.emoji_mgmt.bulk_title" title="Select multiple images (names auto-generated from filenames)">
📦 <span data-i18n="modals.emoji_mgmt.bulk_upload">Bulk Upload</span>
<input type="file" id="emoji-bulk-input" accept="image/png,image/gif,image/webp,image/jpeg" multiple style="display:none">
</label>
</div>
<div id="emoji-crop-preview-row" style="display:none;align-items:center;gap:10px;margin-top:8px;padding:8px;background:var(--bg-tertiary);border-radius:var(--radius-sm)">
<img id="emoji-crop-thumb" src="" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover;border:1px solid var(--border)">
<span class="settings-hint" style="flex:1" data-i18n="modals.emoji_mgmt.crop_applied">Crop applied, ready to upload</span>
<button class="btn-xs" id="emoji-recrop-btn" data-i18n-title="modals.emoji_mgmt.recrop_title" title="Re-open cropper">✏️ <span data-i18n="modals.emoji_mgmt.adjust_btn">Adjust</span></button>
</div>
<div id="custom-emojis-list" style="margin-top:12px;">
<p class="muted-text" data-i18n="modals.emoji_mgmt.no_emojis">No custom emojis uploaded</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-emoji-modal-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Sticker Management Modal -->
<div class="modal-overlay" id="sticker-modal" style="display:none">
<div class="modal" style="width:520px">
<h3>🖼️ <span data-i18n="modals.sticker_mgmt.title">Sticker Management</span></h3>
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="modals.sticker_mgmt.upload_hint">Upload sticker images (max 1 MB, png/gif/webp). Group stickers into packs by setting the same pack name. Sticker name must be lowercase, no spaces.</small>
<div class="whitelist-add-row">
<input type="text" id="sticker-name-input" data-i18n-placeholder="modals.sticker_mgmt.name_placeholder" placeholder="sticker_name (optional)" maxlength="40" class="settings-text-input">
<input type="text" id="sticker-pack-input" data-i18n-placeholder="modals.sticker_mgmt.pack_placeholder" placeholder="Pack name (optional)" maxlength="40" class="settings-text-input">
<input type="file" id="sticker-file-input" accept="image/png,image/gif,image/webp,image/jpeg" style="max-width:120px;font-size:0.6875rem">
<button class="btn-sm btn-accent" id="sticker-upload-btn" data-i18n="modals.sticker_mgmt.upload_btn">Upload</button>
</div>
<div style="margin-top:6px;text-align:center">
<label class="btn-sm" style="cursor:pointer;display:inline-block" data-i18n-title="modals.sticker_mgmt.bulk_title" title="Select multiple images - names auto-generated from filenames; optional pack applies to all">
📦 <span data-i18n="modals.emoji_mgmt.bulk_upload">Bulk Upload</span>
<input type="file" id="sticker-bulk-input" accept="image/png,image/gif,image/webp,image/jpeg" multiple style="display:none">
</label>
</div>
<div id="stickers-list" style="margin-top:12px;">
<p class="muted-text" data-i18n="modals.sticker_mgmt.no_stickers">No stickers uploaded</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-sticker-modal-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Emoji Crop Modal -->
<div class="modal-overlay" id="emoji-crop-modal" style="display:none">
<div class="modal" style="width:320px;text-align:center">
<h3 style="margin-bottom:6px">✂️ <span data-i18n="modals.emoji_crop.title">Crop Emoji</span></h3>
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.emoji_crop.hint">Drag to reposition · Slider to zoom</small>
<canvas id="emoji-crop-canvas" width="256" height="256" style="border-radius:var(--radius);cursor:grab;display:block;margin:0 auto 10px;border:2px solid var(--accent);max-width:100%;touch-action:none"></canvas>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;padding:0 4px">
<span style="font-size:0.8125rem" data-i18n-title="modals.emoji_crop.zoom_out" title="Zoom out">🔍</span>
<input type="range" id="emoji-crop-zoom" min="100" max="500" value="100" step="1" style="flex:1;accent-color:var(--accent)">
<span style="font-size:0.8125rem" data-i18n-title="modals.emoji_crop.zoom_in" title="Zoom in">🔎</span>
</div>
<div class="modal-actions">
<button class="btn-sm" id="emoji-crop-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="emoji-crop-confirm-btn" data-i18n="modals.emoji_crop.confirm_btn">Use This Crop</button>
</div>
</div>
</div>
<!-- Ferry (Discord bridge) Modal -->
<div class="modal-overlay" id="ferry-modal" style="display:none">
<div class="modal modal-wide">
<h3>🛶 <span data-i18n="modals.ferry.title">Ferry (Discord bridge)</span></h3>
<div id="ferry-modal-body" class="ferry-body"></div>
<div class="modal-actions">
<button class="btn-sm" id="ferry-refresh-btn" data-i18n="modals.ferry.refresh">Reconnect</button>
<button class="btn-sm btn-accent" id="ferry-close-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Audit Log Modal -->
<div class="modal-overlay" id="audit-log-modal" style="display:none">
<div class="modal modal-wide" style="width:760px">
<h3>📜 <span data-i18n="modals.audit_log.title">Audit Log</span></h3>
<p class="modal-desc" data-i18n="modals.audit_log.desc">A record of admin and moderator actions on this server.</p>
<div class="modal-flex-body">
<div class="audit-log-toolbar">
<label class="organize-toolbar-label" data-i18n="modals.audit_log.filter_action">Action:</label>
<select id="audit-log-filter-action">
<option value="" data-i18n="modals.audit_log.all_actions">All actions</option>
</select>
<label class="organize-toolbar-label" data-i18n="modals.audit_log.filter_actor">Actor:</label>
<input type="text" id="audit-log-filter-actor" data-i18n-placeholder="settings.admin.whitelist_username_placeholder" placeholder="Username..." maxlength="40" style="width:140px">
<button class="btn-sm btn-accent" id="audit-log-refresh-btn">🔄 <span data-i18n="modals.audit_log.refresh">Refresh</span></button>
<div class="organize-spacer"></div>
<button class="btn-sm btn-accent" id="audit-log-export-btn">📥 <span data-i18n="modals.audit_log.export">Export JSON</span></button>
</div>
<div id="audit-log-list" class="audit-log-list">
<div class="audit-log-empty" data-i18n="modals.audit_log.loading">Loading...</div>
</div>
<button class="btn-sm audit-log-load-more" id="audit-log-load-more" style="display:none" data-i18n="modals.audit_log.load_more">Load 50 More</button>
<div class="modal-actions">
<button class="btn-sm" id="close-audit-log-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="bans-modal" style="display:none">
<div class="modal modal-wide">
<h3 data-i18n="modals.bans.title">Banned Users</h3>
<div class="modal-flex-body">
<div id="bans-list" class="bans-list modal-grow" style="overflow:auto">
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-bans-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="ip-bans-modal" style="display:none">
<div class="modal modal-wide">
<h3>🚫 <span data-i18n="modals.ip_bans.title">Banned IPs</span></h3>
<div class="modal-flex-body">
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">
<input type="text" id="ip-ban-input" data-i18n-placeholder="modals.ip_bans.ip_placeholder" placeholder="IP address (e.g. 203.0.113.42)" class="settings-text-input" style="flex:1;min-width:160px" maxlength="64">
<input type="text" id="ip-ban-reason-input" data-i18n-placeholder="modals.ip_bans.reason_placeholder" placeholder="Reason (optional)" class="settings-text-input" style="flex:1;min-width:160px" maxlength="200">
<button class="btn-sm btn-accent btn-danger-fill" id="ip-ban-add-btn" data-i18n="modals.ip_bans.add_btn">Ban IP</button>
</div>
<div id="ip-bans-list" class="bans-list modal-grow" style="overflow:auto">
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-ip-bans-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="deleted-users-modal" style="display:none">
<div class="modal modal-wide">
<h3>🗑️ <span data-i18n="modals.deleted_users.title">Deleted Users</span></h3>
<div class="modal-flex-body">
<div id="deleted-users-list" class="bans-list modal-grow" style="overflow:auto">
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
</div>
<div class="modal-actions">
<button class="btn-sm" id="close-deleted-users-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
</div>
<!-- All Members Modal -->
<div class="modal-overlay" id="all-members-modal" style="display:none">
<div class="modal modal-wide" style="max-width:720px;max-height:85vh;display:flex;flex-direction:column">
<h3>👥 <span data-i18n="modals.all_members.title">All Members</span> <span id="all-members-count" class="muted-text" style="font-size:0.8125rem;font-weight:normal"></span></h3>
<div style="margin-bottom:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="all-members-search" data-i18n-placeholder="modals.all_members.search_placeholder" placeholder="Search users..." maxlength="40" class="settings-text-input" style="flex:1;min-width:140px">
<select id="all-members-filter" style="font-size:0.75rem;padding:4px 8px;border-radius:var(--radius-sm);background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border)">
<option value="all" data-i18n="modals.all_members.filter_all">All</option>
<option value="online" data-i18n="modals.all_members.filter_online">Online</option>
<option value="offline" data-i18n="modals.all_members.filter_offline">Offline</option>
<option value="new" data-i18n="modals.all_members.filter_new">New (7 days)</option>
<option value="banned" data-i18n="modals.all_members.filter_banned">Banned</option>
<option value="storage" data-i18n="modals.all_members.filter_storage">Most storage used</option>
</select>
</div>
<div id="all-members-storage-summary" class="settings-hint" style="display:none;margin:-4px 0 8px"></div>
<div id="all-members-list" style="overflow-y:auto;flex:1;min-height:0;display:flex;flex-direction:column;gap:4px">
<p class="muted-text" style="text-align:center;padding:20px" data-i18n="modals.common.loading">Loading...</p>
</div>
<div class="modal-actions" style="justify-content:space-between;flex-wrap:wrap;gap:6px">
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="btn-sm btn-accent" id="aml-view-invite-btn" style="display:none">➕ <span data-i18n="settings.admin.view_invite_btn">Invite Users</span></button>
<button class="btn-sm btn-accent" id="aml-view-bans-btn" style="display:none">📋 <span data-i18n="settings.admin.view_bans_btn">View Bans</span></button>
<button class="btn-sm btn-accent" id="aml-view-deleted-btn" style="display:none">🗑️ <span data-i18n="settings.admin.view_deleted_btn">View Deleted Users</span></button>
<button class="btn-sm btn-danger-fill" id="aml-bulk-cleanup-btn" style="display:none">🧹 <span data-i18n="settings.admin.bulk_cleanup_btn">Cleanup</span></button>
</div>
<button class="btn-sm" id="close-all-members-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Invite Links Modal -->
<div class="modal-overlay" id="invite-links-modal" style="display:none">
<div class="modal modal-wide" style="max-width:640px;max-height:85vh;display:flex;flex-direction:column">
<h3 data-i18n="settings.admin.invite_links_title">🔗 Invite Links</h3>
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n-html="settings.admin.invite_links_modal_hint">Shareable links that drop people straight into the channels you pick. Share <code>https://your-server/?invite=CODE</code> or <code>/invite/CODE</code>. Private channels are never granted by a link.</small>
<div id="invite-codes-list" style="overflow-y:auto;flex:1;min-height:60px;display:flex;flex-direction:column;gap:8px;padding-right:2px">
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.invite_links_loading">Loading invite links…</p>
</div>
<details id="invite-code-create" style="margin-top:12px;border:1px solid var(--border);border-radius:8px;padding:10px;flex:none">
<summary style="cursor:pointer;font-weight:600;font-size:0.9rem" data-i18n="settings.admin.invite_links_create_title">➕ Create a new invite link</summary>
<div style="margin-top:10px;display:flex;flex-direction:column;gap:10px">
<label class="select-row" style="align-items:center">
<span><span data-i18n="settings.admin.invite_links_label">Label</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_label_hint">For your reference, e.g. "Reddit promo"</small></span>
<input type="text" id="invite-new-label" maxlength="60" class="settings-text-input" style="max-width:200px" data-i18n-placeholder="settings.admin.invite_links_label_placeholder" placeholder="Optional name">
</label>
<div>
<h6 style="margin:0 0 4px;font-size:0.82rem;font-weight:600" data-i18n="settings.admin.invite_links.channels_granted">Channels this link grants</h6>
<div id="invite-new-channels" style="margin-top:6px;max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px">
<p class="muted-text" style="margin:4px 0;font-size:0.85rem" data-i18n="settings.admin.loading_channels">Loading channels…</p>
</div>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="btn-sm" id="invite-new-channels-all" type="button" data-i18n="settings.admin.invite_links.select_all">Select all</button>
<button class="btn-sm" id="invite-new-channels-none" type="button" data-i18n="settings.admin.invite_links.select_none">Select none</button>
</div>
</div>
<label class="select-row" style="align-items:center">
<span><span data-i18n="settings.admin.invite_links_max_uses">Max uses</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_max_uses_hint">0 = unlimited. Counts distinct people.</small></span>
<input type="number" id="invite-new-maxuses" min="0" max="100000" value="1" class="settings-number-input">
</label>
<label class="select-row" style="align-items:center">
<span data-i18n="settings.admin.invite_links.expires">Expires</span>
<select id="invite-new-expiry" class="settings-number-input">
<option value="0" data-i18n="settings.admin.invite_links.never">Never</option>
<option value="1" data-i18n="settings.admin.invite_links.after_hour">After 1 hour</option>
<option value="24" data-i18n="settings.admin.invite_links.after_day">After 1 day</option>
<option value="168" data-i18n="settings.admin.invite_links.after_7_days">After 7 days</option>
<option value="720" selected data-i18n="settings.admin.invite_links.after_30_days">After 30 days</option>
</select>
</label>
<label class="select-row" style="align-items:center">
<span><span data-i18n="settings.admin.invite_links_custom_code">Custom code</span> <small class="settings-hint" style="display:block" data-i18n="settings.admin.invite_links_custom_code_hint">Optional. 3-32 chars. Blank = random.</small></span>
<input type="text" id="invite-new-slug" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="e.g. summer2026">
</label>
<div>
<button class="btn-sm btn-accent" id="invite-create-btn" type="button" data-i18n="settings.admin.invite_links_create">Create invite link</button>
</div>
</div>
</details>
<div class="modal-actions" style="justify-content:flex-end">
<button class="btn-sm" id="close-invite-links-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Music Share Modal -->
<div class="modal-overlay" id="music-modal" style="display:none">
<div class="modal">
<h3>🎶 <span data-i18n="modals.listen_together.title">Listen Together</span></h3>
<p class="modal-desc" data-i18n="modals.listen_together.desc">Paste a link from Spotify, YouTube, or SoundCloud to share with everyone in voice</p>
<div class="form-group">
<label data-i18n="modals.listen_together.link_label">Music Link</label>
<input type="text" id="music-link-input" data-i18n-placeholder="modals.listen_together.link_placeholder" placeholder="https://open.spotify.com/track/..." maxlength="500">
</div>
<div class="music-link-preview" id="music-link-preview"></div>
<div class="music-platforms-hint">
<span class="platform-badge spotify">Spotify</span>
<span class="platform-badge youtube">YouTube</span>
<span class="platform-badge soundcloud">SoundCloud</span>
</div>
<div class="modal-actions">
<button class="btn-sm" id="cancel-music-btn" data-i18n="modals.common.cancel">Cancel</button>
<div style="display:flex;flex-direction:column;gap:6px;align-items:stretch">
<button class="btn-sm btn-accent" id="share-music-btn" data-i18n="modals.listen_together.share_btn">Share with Channel</button>
<button class="btn-sm btn-accent" id="share-music-playlist-btn" style="display:none" data-i18n="media.share_playlist">Share Playlist with Channel</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="music-queue-modal" style="display:none">
<div class="modal modal-wide music-queue-modal">
<h3>📋 <span data-i18n="media.music_queue_title">Playback Queue</span></h3>
<p class="modal-desc" id="music-queue-summary" data-i18n="media.music_queue_empty_summary">No queued tracks</p>
<div class="music-queue-table-wrap">
<table class="music-queue-table">
<thead>
<tr>
<th class="music-queue-col-handle"></th>
<th class="music-queue-col-pos" data-i18n="media.music_queue_col_pos">Pos.</th>
<th class="music-queue-col-requested-by" data-i18n="media.music_queue_col_requested_by">Requested By</th>
<th class="music-queue-col-title" data-i18n="media.music_queue_col_title">Title</th>
<th class="music-queue-col-actions"></th>
</tr>
</thead>
<tbody id="music-queue-body">
<tr><td colspan="5" class="music-queue-empty" data-i18n="media.music_queue_is_empty">Queue is empty</td></tr>
</tbody>
</table>
</div>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="shuffle-music-queue-btn" style="display:none" data-i18n="media.music_shuffle_queue">Shuffle Queue</button>
<button class="btn-sm" id="close-music-queue-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Manage Servers Modal -->
<div class="modal-overlay" id="manage-servers-modal" style="display:none">
<div class="modal modal-flex manage-servers-modal-inner">
<h3>⚙ <span data-i18n="modals.manage_servers.title">Manage Servers</span></h3>
<p class="modal-desc" data-i18n="modals.manage_servers.desc">Edit or remove your linked servers</p>
<div id="manage-servers-list" class="manage-servers-list"></div>
<div class="modal-actions" style="justify-content:space-between">
<button class="btn-sm btn-accent" id="manage-servers-add-btn" data-i18n="modals.manage_servers.add_btn">+ Add Server</button>
<button class="btn-sm" id="manage-servers-close-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Channel context menu (appears on "..." hover button) -->
<div id="channel-ctx-menu" class="channel-ctx-menu" style="display:none">
<button class="channel-ctx-item" data-action="mark-read">✅ <span data-i18n="context_menu.channel.mark_read">Mark as Read</span></button>
<button class="channel-ctx-item" data-action="mark-all-read">☑️ <span data-i18n="context_menu.channel.mark_all_read">Mark everything as read</span></button>
<button class="channel-ctx-item" data-action="mute">🔔 <span data-i18n="context_menu.channel.mute">Mute Channel</span></button>
<button class="channel-ctx-item" data-action="copy-channel-link">🔗 <span data-i18n="context_menu.channel.copy_link">Copy Channel Link</span></button>
<button class="channel-ctx-item" data-action="join-voice">🎙️ <span data-i18n="voice.join_ctx">Join Voice</span></button>
<button class="channel-ctx-item" data-action="leave-voice" style="display:none">📴 <span data-i18n="context_menu.channel.disconnect_voice">Disconnect Voice</span></button>
<hr class="channel-ctx-sep">
<button class="channel-ctx-item" data-action="leave-channel">🚪 <span data-i18n="context_menu.channel.leave">Leave Channel</span></button>
<button class="channel-ctx-item" data-action="hide-channel" style="display:none">🙈 <span data-i18n="context_menu.channel.hide">Hide Channel</span></button>
<hr class="channel-ctx-sep rename-or-createSub">
<button class="channel-ctx-item mod-only" data-action="rename-channel">✏️ <span data-i18n="context_menu.channel.rename">Rename Channel</span></button>
<button class="channel-ctx-item mod-only" data-action="create-sub-channel">📁 <span data-i18n="context_menu.channel.create_sub">Create Sub-channel</span></button>
<hr class="channel-ctx-sep admin-only">
<button class="channel-ctx-item admin-only" data-action="organize" style="display:none">📋 <span data-i18n="context_menu.channel.organize">Organize</span></button>
<button class="channel-ctx-item admin-only" data-action="move-to-parent" style="display:none">📦 <span data-i18n="context_menu.channel.move_to">Move to…</span></button>
<button class="channel-ctx-item admin-only" data-action="promote-channel" style="display:none">⬆️ <span data-i18n="context_menu.channel.promote">Promote to Channel</span></button>
<button class="channel-ctx-item admin-only" data-action="channel-functions">⚙️ <span data-i18n="context_menu.channel.functions">Channel Functions</span> <span class="cfn-arrow">▶</span></button>
<div id="channel-functions-panel" class="channel-functions-panel" style="display:none">
<div class="cfn-row" data-fn="voice" data-i18n-title="channel_functions.voice_hint" title="Enable or disable voice chat in this channel">
<span class="cfn-label">🎙️ <span data-i18n="channel_functions.voice">Voice</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="text" data-i18n-title="channel_functions.text_hint" title="Enable or disable text chat in this channel">
<span class="cfn-label">💬 <span data-i18n="channel_functions.text">Text</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="streams" data-i18n-title="channel_functions.streams_hint" title="Allow screen sharing and camera (requires voice)">
<span class="cfn-label">🖥️ <span data-i18n="channel_functions.streams">Streams</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="music" data-i18n-title="channel_functions.music_hint" title="Allow Listen Together / music playback (requires voice)">
<span class="cfn-label">🎵 <span data-i18n="channel_functions.music">Music</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="soundboard" data-i18n-title="channel_functions.soundboard_hint" title="Allow the soundboard in this channel's voice chat">
<span class="cfn-label">🔊 <span data-i18n="channel_functions.soundboard">Soundboard</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="media" data-i18n-title="channel_functions.media_hint" title="Allow image, video, and file uploads in this channel">
<span class="cfn-label">🖼️ <span data-i18n="channel_functions.media">Media</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="reactions" data-i18n-title="channel_functions.reactions_hint" title="Allow emoji reactions on messages in this channel">
<span class="cfn-label">😀 <span data-i18n="channel_functions.reactions">Reactions</span></span>
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
</div>
<div class="cfn-row" data-fn="read-only" data-i18n-title="channel_functions.read_only_hint" title="Only members with permission can send messages">
<span class="cfn-label">🔒 <span data-i18n="channel_functions.read_only">Read Only</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="forum" data-i18n-title="channel_functions.forum_hint" title="Each message is a topic. Replies go in its thread and bump the topic back to the newest end of the channel">
<span class="cfn-label">🗂️ <span data-i18n="channel_functions.forum">Forum</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="forum-tags" data-i18n-title="channel_functions.forum_tags_hint" title="The tags topics in this forum can carry, one per line">
<span class="cfn-label">🏷️ <span data-i18n="channel_functions.forum_tags">Forum tags</span></span>
<span class="cfn-badge cfn-static">…</span>
</div>
<div class="cfn-row" data-fn="forum-votes" data-i18n-title="channel_functions.forum_votes_hint" title="Let members like (and optionally dislike) each post in this forum">
<span class="cfn-label">👍 <span data-i18n="channel_functions.forum_votes">Votes on posts</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="forum-blog" data-i18n-title="channel_functions.forum_blog_hint" title="Each post belongs to its author: only the author adds to the post itself, and everyone else's replies go under Comments">
<span class="cfn-label">📝 <span data-i18n="channel_functions.forum_blog">Blog mode</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="forum-blog-pics-only" data-i18n-title="channel_functions.forum_blog_pics_only_hint" title="No general Comments section under blog posts: people comment on each picture instead">
<span class="cfn-label">🖼️ <span data-i18n="channel_functions.forum_blog_pics_only">Comments on pictures only</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="nsfw" data-i18n-title="channel_functions.nsfw_hint" title="Mark this channel NSFW. Members can hide NSFW channels from their sidebar in Settings">
<span class="cfn-label">🔞 <span data-i18n="channel_functions.nsfw">NSFW</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="private" data-i18n-title="channel_functions.private_hint" title="Only members can see this channel. Turning it on keeps everyone who is already in; turning it off opens it to the whole server, history included">
<span class="cfn-label">🔒 <span data-i18n="channel_functions.private">Private</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="role-gate" data-i18n-title="channel_functions.role_gate_hint" title="Only people holding the chosen roles can open this channel: any one of them, or all of them">
<span class="cfn-label">🎟️ <span data-i18n="channel_functions.role_gate">Required roles</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="save-template" data-i18n-title="channel_functions.save_template_hint" title="Keep this channel's setup as a template in the Create Channel picker">
<span class="cfn-label">💾 <span data-i18n="channel_functions.save_template">Save as template</span></span>
<span class="cfn-badge cfn-off">→</span>
</div>
<div class="cfn-divider"></div>
<div class="cfn-row" data-fn="slow-mode" data-i18n-title="channel_functions.slow_mode_hint" title="Require a cooldown between messages (0 = off)">
<span class="cfn-label">🐢 <span data-i18n="channel_functions.slow_mode">Slow Mode</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="cleanup-exempt" data-i18n-title="channel_functions.cleanup_guard_hint" title="Protect this channel from automatic message cleanup">
<span class="cfn-label">🛡️ <span data-i18n="channel_functions.cleanup_guard">Cleanup Guard</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="welcome" data-i18n-title="channel_functions.welcome_hint" title="Post a saved welcome message here when a new member joins the server">
<span class="cfn-label">👋 <span data-i18n="channel_functions.welcome">Welcome Messages</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="user-limit" data-i18n-title="channel_functions.voice_limit_hint" title="Limit simultaneous voice users (click to set; blank = unlimited)">
<span class="cfn-label">👥 <span data-i18n="channel_functions.voice_limit">Voice Limit</span></span>
<span class="cfn-badge cfn-off">∞</span>
</div>
<div class="cfn-row" data-fn="voice-bitrate" data-i18n-title="channel_functions.voice_bitrate_hint" title="Cap voice audio bitrate in kbps (0 = auto / no cap)">
<span class="cfn-label">🎚️ <span data-i18n="channel_functions.voice_bitrate">Voice Bitrate</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.voice_bitrate_auto">Auto</span>
</div>
<div class="cfn-divider"></div>
<div class="cfn-row" data-fn="self-destruct" data-i18n-title="channel_functions.self_destruct_hint" title="Set a self-destruct timer: channel auto-deletes after the specified hours (0 = off)">
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.self_destruct">Self Destruct</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="announcement" data-i18n-title="channel_functions.announcement_hint" title="Announcement channel: messages use a distinct notification sound and gold highlight">
<span class="cfn-label">📢 <span data-i18n="channel_functions.announcement">Announcement</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row" data-fn="default-role" data-i18n-title="channel_functions.default_role_hint" title="Auto-grant this role (scoped to this channel) to every current and future member">
<span class="cfn-label">👑 <span data-i18n="channel_functions.default_role">Default Role</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.none">None</span>
</div>
<div class="cfn-divider cfn-afk-divider" style="display:none"></div>
<div class="cfn-row cfn-afk-row" data-fn="afk-sub" style="display:none" data-i18n-title="channel_functions.afk_sub_hint" title="Designate a sub-channel as the AFK voice room: idle users are moved here">
<span class="cfn-label">💤 <span data-i18n="channel_functions.afk_sub">AFK Sub</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
<div class="cfn-row cfn-afk-row" data-fn="afk-timeout" style="display:none" data-i18n-title="channel_functions.afk_timeout_hint" title="Minutes of voice inactivity before moving to AFK sub (0 = disabled)">
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.afk_timeout">AFK Timeout</span></span>
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
</div>
</div>
<button class="channel-ctx-item admin-only" data-action="channel-roles">👑 <span data-i18n="context_menu.channel.roles">Roles</span></button>
<button class="channel-ctx-item admin-only" data-action="webhooks">🤖 <span data-i18n="context_menu.channel.webhooks">Webhooks</span></button>
<hr class="channel-ctx-sep admin-only">
<button class="channel-ctx-item admin-only danger" data-action="delete">🗑️ <span data-i18n="context_menu.channel.delete">Delete Channel</span></button>
</div>
<!-- DM context menu -->
<div id="dm-ctx-menu" class="channel-ctx-menu" style="display:none">
<button class="channel-ctx-item" data-action="dm-mark-read">✅ <span data-i18n="context_menu.dm.mark_read">Mark as Read</span></button>
<button class="channel-ctx-item" data-action="mark-all-read">☑️ <span data-i18n="context_menu.channel.mark_all_read">Mark everything as read</span></button>
<button class="channel-ctx-item" data-action="dm-mute">🔔 <span data-i18n="context_menu.dm.mute">Mute DM</span></button>
<hr class="channel-ctx-sep">
<button class="channel-ctx-item" data-action="dm-group-add" style="display:none">➕ <span data-i18n="groups.add_people">Add people</span></button>
<button class="channel-ctx-item danger" data-action="dm-group-leave" style="display:none">🚪 <span data-i18n="groups.leave">Leave group</span></button>
<button class="channel-ctx-item danger" data-action="dm-group-delete" style="display:none">🗑️ <span data-i18n="groups.delete">Delete group</span></button>
<button class="channel-ctx-item danger" data-action="dm-group-delete-all" style="display:none">🗑️ <span data-i18n="groups.delete_all">Delete group for everyone</span></button>
<button class="channel-ctx-item danger" data-action="dm-delete">🗑️ <span data-i18n="context_menu.dm.delete">Delete DM</span></button>
</div>
<!-- Organize Sub-channels Modal -->
<div class="modal-overlay" id="organize-modal" style="display:none">
<div class="modal" style="width:500px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
<button id="organize-back-btn" class="btn-sm" style="display:none;padding:3px 8px;font-size:0.8rem" data-i18n-title="modals.organize.back_title" title="Back to channel list"><span data-i18n="modals.organize.back_btn">← Back</span></button>
<h3 id="organize-modal-title" style="margin:0">📋 <span data-i18n="modals.organize.title">Organize</span></h3>
</div>
<p class="modal-desc" id="organize-modal-parent-name"></p>
<div class="modal-flex-body">
<div class="organize-toolbar">
<label class="organize-toolbar-label" data-i18n="modals.organize.sort_label">Sort:</label>
<select id="organize-global-sort" class="organize-select">
<option value="server_default" data-i18n="modals.organize.server_default">Server Default</option>
<option value="manual" data-i18n="modals.organize.sort_manual">Manual</option>
<option value="alpha" data-i18n="modals.organize.sort_alpha">Alphabetical</option>
<option value="created" data-i18n="modals.organize.sort_newest">Newest First</option>
<option value="oldest" data-i18n="modals.organize.sort_oldest">Oldest First</option>
<option value="dynamic" data-i18n="modals.organize.sort_dynamic">Dynamic</option>
</select>
</div>
<div class="organize-toolbar" id="organize-cat-toolbar" style="display:none">
<label class="organize-toolbar-label" data-i18n="modals.organize.categories_label">Categories:</label>
<select id="organize-cat-sort" class="organize-select">
<option value="manual" data-i18n="modals.organize.cat_manual">Manual Order</option>
<option value="az" data-i18n="modals.organize.cat_az">A → Z</option>
<option value="za" data-i18n="modals.organize.cat_za">Z → A</option>
</select>
</div>
<div id="organize-channel-list" class="organize-list"></div>
<div class="organize-controls">
<button class="btn-sm" id="organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move selected channel up">⬆ Up</button>
<button class="btn-sm" id="organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move selected channel down">⬇ Down</button>
<div class="organize-spacer"></div>
<div class="organize-tag-group">
<input type="text" id="organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.organize.tag_placeholder" placeholder="Tag name…">
<button class="btn-sm organize-tag-btn" id="organize-set-tag" data-i18n-title="modals.organize.tag_set_title" title="Apply tag">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
<button class="btn-sm organize-tag-btn danger" id="organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.organize.tag_remove_title" title="Remove tag">✕</button>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="organize-done-btn" data-i18n="modals.common.done">Done</button>
</div>
</div><!-- /modal-flex-body -->
</div>
</div>
<!-- Move Channel Picker Modal -->
<div class="modal-overlay" id="reparent-modal" style="display:none">
<div class="modal" style="max-width:380px">
<h3 id="reparent-modal-title">📦 <span data-i18n="modals.move_channel.title">Move Channel</span></h3>
<p class="modal-desc" id="reparent-modal-desc"></p>
<div id="reparent-channel-list" class="organize-list" style="max-height:300px;overflow-y:auto"></div>
<div class="modal-actions" style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
<button class="btn-sm" id="reparent-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
</div>
</div>
</div>
<!-- DM Organize Modal -->
<div class="modal-overlay" id="dm-organize-modal" style="display:none">
<div class="modal" style="width:460px">
<h3>📋 <span data-i18n="modals.dm_organize.title">Organize DMs</span></h3>
<p class="modal-desc" data-i18n="modals.dm_organize.desc">Tag your conversations into collapsible categories</p>
<div class="organize-toolbar">
<label class="organize-toolbar-label" data-i18n="modals.dm_organize.sort_label">Sort:</label>
<select id="dm-organize-sort" class="organize-select">
<option value="manual" data-i18n="modals.dm_organize.sort_manual">Manual</option>
<option value="alpha" data-i18n="modals.dm_organize.sort_alpha">Alphabetical</option>
<option value="recent" data-i18n="modals.dm_organize.sort_recent">Recent Activity</option>
</select>
</div>
<div id="dm-organize-list" class="organize-list"></div>
<div class="organize-controls">
<button class="btn-sm" id="dm-organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move up">⬆ Up</button>
<button class="btn-sm" id="dm-organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move down">⬇ Down</button>
<div class="organize-spacer"></div>
<div class="organize-tag-group">
<input type="text" id="dm-organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.dm_organize.category_placeholder" placeholder="Category…">
<button class="btn-sm organize-tag-btn" id="dm-organize-set-tag" data-i18n-title="modals.dm_organize.category_set_title" title="Apply category">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
<button class="btn-sm organize-tag-btn danger" id="dm-organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.dm_organize.category_remove_title" title="Remove category">✕</button>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="dm-organize-done-btn" data-i18n="modals.common.done">Done</button>
</div>
</div>
</div>
<!-- Webhook Management Modal -->
<!-- Poll Creation Modal -->
<div class="modal-overlay" id="poll-modal" style="display:none">
<div class="modal" style="width:440px">
<h3>📊 <span data-i18n="modals.poll.title">Create Poll</span></h3>
<div class="form-group">
<label data-i18n="modals.poll.question_label">Question</label>
<input type="text" id="poll-question-input" data-i18n-placeholder="modals.poll.question_placeholder" placeholder="What do you want to ask?" maxlength="300" autocomplete="off">
</div>
<div class="form-group" style="margin-top:8px">
<label data-i18n="modals.poll.options_label">Options</label>
<div id="poll-options-list" class="poll-options-list">
<div class="poll-option-row"><input type="text" class="poll-option-input" data-i18n-placeholder="modals.poll.option_1" placeholder="Option 1" maxlength="100"><button class="poll-option-remove" data-i18n-title="modals.poll.remove_option" title="Remove" style="display:none">&times;</button></div>
<div class="poll-option-row"><input type="text" class="poll-option-input" data-i18n-placeholder="modals.poll.option_2" placeholder="Option 2" maxlength="100"><button class="poll-option-remove" data-i18n-title="modals.poll.remove_option" title="Remove" style="display:none">&times;</button></div>
</div>
<button class="btn-sm" id="poll-add-option-btn" style="margin-top:6px" data-i18n="modals.poll.add_option_btn">+ Add Option</button>
</div>
<div class="form-group poll-settings-row" style="margin-top:10px">
<label class="poll-checkbox-label"><input type="checkbox" id="poll-multi-vote"> <span data-i18n="modals.poll.multi_vote">Allow multiple votes</span></label>
<label class="poll-checkbox-label"><input type="checkbox" id="poll-anonymous"> <span data-i18n="modals.poll.anonymous">Anonymous votes</span></label>
<label class="poll-checkbox-label" id="poll-columns-wrap" style="display:none"><span data-i18n="modals.poll.columns">Columns</span>
<select id="poll-columns" class="settings-select" style="margin-left:6px;max-width:90px">
<option value="0" data-i18n="modals.poll.columns_auto">One per row</option>
<option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
</select>
</label>
</div>
<div class="modal-actions">
<button class="btn-sm" id="poll-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="poll-create-btn" data-i18n="modals.poll.create_btn">Create Poll</button>
</div>
</div>
</div>
<!-- TEMPORARY (#5649): one-time admin notice that channel access moved to Required roles. Remove after the 4.8.x cycle. -->
<div class="modal-overlay" id="role-gate-notice-modal" style="display:none">
<div class="modal" style="width:520px">
<h3>🔑 <span data-i18n="modals.role_gate_notice.title">Channel access works differently now</span></h3>
<p style="font-size:0.85rem;line-height:1.5" data-i18n="modals.role_gate_notice.p1">Roles no longer carry a list of channels to grant or revoke. Instead, each channel says which roles open it: right-click the channel, Channel Functions, Required roles. Anyone who holds those roles is put in the channel the moment they get them and taken out the moment they lose them.</p>
<p style="font-size:0.85rem;line-height:1.5" data-i18n="modals.role_gate_notice.p2">Nothing you had set up was lost. Any channel a role used to grant has been converted into Required roles on that channel, so the same people still get in. People you added to a channel by hand keep their place.</p>
<p class="muted-text" style="font-size:0.78rem" data-i18n="modals.role_gate_notice.p3">Only admins see this, and only once.</p>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="role-gate-notice-ok" data-i18n="modals.role_gate_notice.ok">Got it</button>
</div>
</div>
</div>
<!-- Scheduled send (#5638): right-click Send, or /schedule -->
<div class="modal-overlay" id="schedule-modal" style="display:none">
<div class="modal" style="width:480px">
<h3>⏰ <span data-i18n="modals.schedule.title">Send later</span></h3>
<p class="muted-text" style="font-size:0.78rem" data-i18n="modals.schedule.hint">The message waits on the server and goes out at the time you pick, whether or not you are online. Not available in direct messages.</p>
<div class="form-group">
<label data-i18n="modals.schedule.message_label">Message</label>
<textarea id="schedule-text" class="settings-text-input" rows="4" maxlength="2000"></textarea>
</div>
<div class="form-group" style="margin-top:8px">
<label data-i18n="modals.schedule.when_label">Send at</label>
<!-- Custom wall-clock fields (mirrors the /time picker) so the entered
time is read in the user's configured timezone rather than the
browser's, which a hardened browser can report incorrectly. -->
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.date_label" aria-label="Date">
<div class="tsm-field">
<label for="sch-year" data-i18n="modals.time.year">Year</label>
<input type="number" id="sch-year" class="tsm-input" min="1970" max="9999" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="sch-month" data-i18n="modals.time.month">Month</label>
<input type="number" id="sch-month" class="tsm-input" min="1" max="12" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="sch-day" data-i18n="modals.time.day">Day</label>
<input type="number" id="sch-day" class="tsm-input" min="1" max="31" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field tsm-cal-field">
<label aria-hidden="true">&nbsp;</label>
<button type="button" id="sch-cal-btn" class="tsm-cal-btn" data-i18n-title="modals.time.calendar" title="Open calendar">📅</button>
<input type="date" id="sch-cal-input" class="tsm-cal-native" tabindex="-1" aria-hidden="true">
</div>
</div>
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.time_label" aria-label="Time">
<div class="tsm-field">
<label for="sch-hour" data-i18n="modals.time.hour">Hour</label>
<input type="number" id="sch-hour" class="tsm-input" min="0" max="23" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="sch-minute" data-i18n="modals.time.minute">Minute</label>
<input type="number" id="sch-minute" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="sch-second" data-i18n="modals.time.second">Second</label>
<input type="number" id="sch-second" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field tsm-meridiem-field">
<label aria-hidden="true">&nbsp;</label>
<div class="tsm-meridiem" role="group" data-i18n-aria-label="modals.time.clock_mode" aria-label="Clock mode">
<button type="button" class="tsm-mer-btn" data-mer="24" data-i18n="modals.time.hour_24">24HR</button>
<button type="button" class="tsm-mer-btn" data-mer="AM" data-i18n="modals.time.am">AM</button>
<button type="button" class="tsm-mer-btn" data-mer="PM" data-i18n="modals.time.pm">PM</button>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm" id="schedule-cancel" data-i18n="modals.common.close">Close</button>
<button class="btn-sm btn-accent" id="schedule-save" data-i18n="modals.schedule.schedule_btn">Schedule</button>
</div>
<small class="settings-group-label" style="margin-top:12px;display:block" data-i18n="modals.schedule.pending">Waiting to send</small>
<div id="schedule-list" style="max-height:220px;overflow-y:auto"></div>
</div>
</div>
<!-- Self-destructing message: how long it lives, asked on send -->
<div class="modal-overlay" id="self-destruct-modal" style="display:none">
<div class="modal sd-modal" role="dialog" aria-modal="true" data-i18n-aria-label="modals.self_destruct.title" aria-label="Self-destructing message">
<button type="button" class="modal-close sd-close" id="sd-close" data-i18n-aria-label="modals.common.close" aria-label="Close">&times;</button>
<div class="sd-flame" aria-hidden="true">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>
</svg>
</div>
<p class="modal-desc sd-desc" data-i18n="modals.self_destruct.hint">This message and all its attachments will automatically delete at the time listed below. Like any deleted message, its attachments stay recoverable by an admin for a short while.</p>
<div class="sd-row">
<input type="text" id="sd-amount" class="settings-text-input sd-amount" inputmode="decimal" autocomplete="off" maxlength="7" data-i18n-aria-label="modals.self_destruct.amount_label" aria-label="How long until it deletes">
<div class="tsm-meridiem" role="group" data-i18n-aria-label="modals.self_destruct.unit_label" aria-label="Unit">
<button type="button" class="tsm-mer-btn" data-sd-unit="minutes" data-i18n="modals.self_destruct.minutes">Minutes</button>
<button type="button" class="tsm-mer-btn" data-sd-unit="hours" data-i18n="modals.self_destruct.hours">Hours</button>
</div>
</div>
<p class="sd-preview" id="sd-preview" aria-live="polite" style="display:none"></p>
<p class="sd-error" id="sd-error" role="alert" style="display:none" data-i18n="modals.self_destruct.invalid">Enter a time from 30 seconds to 24 hours.</p>
<div class="modal-actions">
<button type="button" class="btn-sm" id="sd-cancel" data-i18n="modals.common.cancel">Cancel</button>
<button type="button" class="btn-sm btn-accent sd-send" id="sd-send">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>
</svg>
<span data-i18n="modals.self_destruct.send">Send</span>
</button>
</div>
</div>
</div>
<!-- Timestamp Modal (/time with no args) -->
<div class="modal-overlay" id="time-modal" style="display:none">
<div class="modal tsm-modal" style="width:400px">
<div class="tsm-header">
<div class="tsm-clock" aria-hidden="true">🕐</div>
<p class="modal-desc tsm-subtitle" data-i18n="commands.description.time">Insert a time everyone sees in their own timezone</p>
<p class="tsm-syntax" data-i18n="commands.time_usage">Usage: /time 8pm</p>
</div>
<!-- Date row -->
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.date_label" aria-label="Date">
<div class="tsm-field">
<label for="tsm-year" data-i18n="modals.time.year">Year</label>
<input type="number" id="tsm-year" class="tsm-input" min="1970" max="9999" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="tsm-month" data-i18n="modals.time.month">Month</label>
<input type="number" id="tsm-month" class="tsm-input" min="1" max="12" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="tsm-day" data-i18n="modals.time.day">Day</label>
<input type="number" id="tsm-day" class="tsm-input" min="1" max="31" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field tsm-cal-field">
<label aria-hidden="true">&nbsp;</label>
<button type="button" id="tsm-cal-btn" class="tsm-cal-btn" data-i18n-title="modals.time.calendar" title="Open calendar">📅</button>
<input type="date" id="tsm-cal-input" class="tsm-cal-native" tabindex="-1" aria-hidden="true">
</div>
</div>
<!-- Time row -->
<div class="tsm-row" role="group" data-i18n-aria-label="modals.time.time_label" aria-label="Time">
<div class="tsm-field">
<label for="tsm-hour" data-i18n="modals.time.hour">Hour</label>
<input type="number" id="tsm-hour" class="tsm-input" min="0" max="23" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="tsm-minute" data-i18n="modals.time.minute">Minute</label>
<input type="number" id="tsm-minute" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field">
<label for="tsm-second" data-i18n="modals.time.second">Second</label>
<input type="number" id="tsm-second" class="tsm-input" min="0" max="59" step="1" inputmode="numeric" autocomplete="off">
</div>
<div class="tsm-field tsm-meridiem-field">
<label aria-hidden="true">&nbsp;</label>
<div class="tsm-meridiem" role="group" data-i18n-aria-label="modals.time.clock_mode" aria-label="Clock mode">
<button type="button" class="tsm-mer-btn" data-mer="24" data-i18n="modals.time.hour_24">24HR</button>
<button type="button" class="tsm-mer-btn" data-mer="AM" data-i18n="modals.time.am">AM</button>
<button type="button" class="tsm-mer-btn" data-mer="PM" data-i18n="modals.time.pm">PM</button>
</div>
</div>
</div>
<!-- Styles: every format previewed, each with its own insert -->
<div class="tsm-styles" id="tsm-styles"></div>
</div>
</div>
<!-- Channel Roles Modal -->
<div class="modal-overlay" id="channel-roles-modal" style="display:none">
<div class="modal modal-wide">
<h3>👑 <span data-i18n="modals.channel_roles.title">Channel Roles</span></h3>
<p class="modal-desc" id="channel-roles-channel-name"></p>
<div class="channel-roles-layout">
<!-- Left: Members + Assignment -->
<div class="channel-roles-left">
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.members_title">Members</h4>
<div class="channel-roles-member-list" id="channel-roles-member-list"></div>
<div class="channel-roles-actions" id="channel-roles-actions" style="display:none">
<h4 class="channel-roles-selected-name" id="channel-roles-selected-name"></h4>
<div class="channel-roles-current" id="channel-roles-current-roles"></div>
<div class="channel-roles-assign-area">
<div class="channel-roles-assign-row">
<select id="channel-roles-role-select" class="organize-select">
<option value="" data-i18n="modals.channel_roles.select_role">-- Select Role --</option>
</select>
<select id="channel-roles-scope-select" class="organize-select" style="max-width:140px">
<option value="server" data-i18n="modals.channel_roles.server_wide">🌐 Server-wide</option>
<option value="channel" data-i18n="modals.channel_roles.this_channel">📌 This Channel</option>
</select>
<button class="btn-sm btn-accent" id="channel-roles-assign-btn" data-i18n="modals.channel_roles.assign_btn">Assign</button>
</div>
</div>
</div>
</div>
<!-- Right: Role Configuration -->
<div class="channel-roles-right">
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.role_config_title">Role Configuration</h4>
<div class="channel-roles-role-list" id="channel-roles-role-list"></div>
<button class="btn-sm btn-accent btn-full" id="channel-roles-create-btn" data-i18n="modals.channel_roles.new_role_btn" style="margin-top:6px">+ New Role</button>
<div class="channel-roles-role-detail" id="channel-roles-role-detail">
<p class="muted-text" style="padding:12px;text-align:center;font-size:0.82rem" data-i18n="modals.channel_roles.select_to_configure">Select a role to configure</p>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-sm btn-accent" id="channel-roles-done-btn" data-i18n="modals.common.done">Done</button>
</div>
</div>
</div>
<div class="modal-overlay" id="webhook-modal" style="display:none">
<div class="modal" style="width:520px">
<h3>🤖 <span data-i18n="modals.webhook.title">Webhooks</span></h3>
<p class="modal-desc" id="webhook-modal-channel-name"></p>
<!-- Create new webhook -->
<div class="form-group" style="display:flex;gap:8px;align-items:flex-end">
<div style="flex:1">
<label data-i18n="modals.webhook.name_label">New Webhook Name</label>
<input type="text" id="webhook-name-input" class="input" maxlength="32" data-i18n-placeholder="modals.webhook.name_placeholder" placeholder="My Bot" style="width:100%">
</div>
<button class="btn-sm btn-accent" id="webhook-create-btn" data-i18n="modals.webhook.create_btn" style="height:36px">Create</button>
</div>
<!-- Webhook list -->
<div id="webhook-list" style="margin-top:14px;max-height:280px;overflow-y:auto"></div>
<!-- Token reveal (shown once after creation) -->
<div id="webhook-token-reveal" style="display:none;margin-top:14px;padding:12px;border-radius:8px;background:rgba(0,0,0,0.25);border:1px solid var(--accent-color, #00e5ff)">
<p style="font-size:0.85rem;margin:0 0 8px;opacity:0.7">⚠️ <span data-i18n="modals.webhook.token_warning">Copy this URL now. You won't see the full token again!</span></p>
<div style="display:flex;gap:6px">
<input type="text" id="webhook-url-display" class="input" readonly style="flex:1;font-size:0.8rem;font-family:monospace">
<button class="btn-sm" id="webhook-copy-url-btn">📋 <span data-i18n="modals.webhook.copy_btn">Copy</span></button>
</div>
</div>
<div class="modal-actions" style="margin-top:16px">
<button class="btn-sm" id="webhook-close-btn" data-i18n="modals.common.close">Close</button>
</div>
</div>
</div>
<!-- Sub-channel Subscriptions Panel -->
<div class="modal-overlay" id="sub-panel-modal" style="display:none">
<div class="modal" style="width:520px;max-height:80vh;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<h3 style="margin:0;flex:1">📡 <span data-i18n="modals.sub_panel.title">Sub-channel Subscriptions</span></h3>
<button class="btn-sm" id="sub-panel-close-btn" data-i18n="modals.common.close" style="font-size:1rem;padding:4px 10px">&times;</button>
</div>
<p class="modal-desc" style="margin:0 0 12px;font-size:0.8rem;opacity:0.6" data-i18n="modals.sub_panel.desc">Subscribe to get notifications. Unsubscribed channels still appear but won't notify you.</p>
<div id="sub-panel-content" style="overflow-y:auto;flex:1;min-height:0"></div>
</div>
</div>
<!-- Hidden Channels Modal (admin declutter via "Hide Channel") -->
<div class="modal-overlay" id="hidden-channels-modal" style="display:none">
<div class="modal" style="width:460px;max-height:80vh;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<h3 style="margin:0;flex:1">🙈 <span data-i18n="modals.hidden_channels.title">Hidden Channels</span></h3>
<button class="btn-sm" id="hidden-channels-close-btn" data-i18n="modals.common.close" style="font-size:1rem;padding:4px 10px">&times;</button>
</div>
<p class="modal-desc" style="margin:0 0 12px;font-size:0.8rem;opacity:0.6" data-i18n="modals.hidden_channels.desc">Hidden channels are removed from your sidebar but you still have full access to them. Unhide one to bring it back.</p>
<div id="hidden-channels-content" style="overflow-y:auto;flex:1;min-height:0"></div>
</div>
</div>
<!-- Create Sub-channel Modal -->
<div class="modal-overlay" id="create-sub-modal" style="display:none">
<div class="modal" style="max-width:380px">
<h3 data-i18n="modals.create_sub.title">Create Sub-channel</h3>
<p class="modal-desc" id="create-sub-parent-name"></p>
<label style="display:block;margin:12px 0 6px;font-size:0.85rem;opacity:0.7" data-i18n="modals.create_sub.name_label">Sub-channel name</label>
<input type="text" id="create-sub-name" class="input" maxlength="50" data-i18n-placeholder="modals.create_sub.name_placeholder" placeholder="sub-channel name..." style="width:100%">
<label class="checkbox-row" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="create-sub-private">
<span style="font-size:0.9rem">🔒 <span data-i18n="modals.create_sub.private_label">Private</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.private_hint">(invite-only, hidden from non-members)</span></span>
</label>
<label class="checkbox-row" style="margin:2px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.9rem">
<input type="checkbox" id="create-sub-temporary">
<span>⏱️ <span data-i18n="modals.create_sub.temporary_label">Temporary</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.temporary_hint">(auto-delete)</span></span>
</label>
<div id="sub-temp-duration-row" style="display:none;margin:0 0 8px;padding-left:24px">
<div style="display:flex;gap:6px;align-items:center">
<input type="number" id="create-sub-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" data-i18n-placeholder="modals.create_sub.hours_placeholder" placeholder="Hours">
<span style="font-size:0.75rem;opacity:0.6" data-i18n="modals.create_sub.hours">hours</span>
</div>
</div>
<p class="muted-text" style="font-size:0.75rem;margin:0 0 8px;opacity:0.6">ℹ️ <span data-i18n="modals.create_sub.admin_note">Admins always have access to all channels, including private ones.</span></p>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn-sm" id="create-sub-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
<button class="btn-sm btn-accent" id="create-sub-confirm-btn" data-i18n="modals.create_sub.create_btn">Create</button>
</div>
</div>
</div>
<!-- Stackable effect overlay container -->
<div id="fx-layers"></div>
<!-- Push Notification Error Modal (center-screen, requires acknowledgement) -->
<!-- Activities / Games Launcher Modal -->
<div class="modal-overlay" id="activities-modal" style="display:none">
<div class="modal modal-activities">
<div class="activities-header">
<h3>🎮 <span data-i18n="modals.activities.title">Activities</span></h3>
<button class="settings-close-btn" id="close-activities-btn" data-i18n-title="modals.common.close" title="Close">&times;</button>
</div>
<div class="activities-grid" id="activities-grid">
<!-- Populated dynamically by JS from GAMES_REGISTRY -->
</div>
</div>
</div>
<!-- Iframe Game Container (fullscreen overlay) -->
<div class="game-iframe-overlay" id="game-iframe-overlay" style="display:none">
<div class="game-iframe-header">
<span class="game-iframe-title" id="game-iframe-title" data-i18n="modals.game_overlay.game">Game</span>
<div class="game-iframe-actions">
<label style="display:flex;align-items:center;gap:6px;color:var(--text-secondary);font-size:0.75rem;margin-right:8px;" data-i18n-title="modals.game_overlay.volume_label" title="Game Volume">
🔊 <input type="range" id="game-volume-slider" min="0" max="100" value="80" style="width:80px;accent-color:var(--accent);">
<span id="game-volume-pct" style="min-width:28px;font-size:0.6875rem;">80%</span>
</label>
<button class="btn-sm" id="game-iframe-popout" data-i18n-title="modals.game_overlay.popout_title" title="Open in new window">↗️ <span data-i18n="modals.game_overlay.popout_btn">Pop Out</span></button>
<button class="btn-sm btn-danger" id="game-iframe-close" data-i18n="modals.game_overlay.close_btn" data-i18n-title="modals.game_overlay.close_title" title="Close game">&times; Close</button>
</div>
</div>
<iframe id="game-iframe" class="game-iframe" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" allowfullscreen></iframe>
</div>
<div class="modal-overlay" id="push-error-modal" style="display:none">
<div class="modal" style="width:420px;text-align:center;padding:28px 24px;">
<div style="font-size:3rem;margin-bottom:12px;">🔕</div>
<h3 style="margin:0 0 12px;color:var(--text-primary)" data-i18n="modals.push_error.title">Push Notifications Unavailable</h3>
<p id="push-error-reason" style="color:var(--text-secondary);font-size:0.875rem;line-height:1.5;margin:0 0 16px;"></p>
<p style="color:var(--text-muted);font-size:0.75rem;line-height:1.5;margin:0 0 20px;" data-i18n-html="modals.push_error.recommendation">
<strong>Recommended:</strong> Google Chrome, Microsoft Edge, or Opera on desktop.<br>
iOS requires Safari 16.4+ with Haven added to your Home Screen.
</p>
<button class="btn-accent" id="push-error-dismiss-btn" style="width:100%;padding:10px;font-size:0.875rem;font-weight:600;" data-i18n="modals.push_error.got_it">Got It</button>
</div>
</div>
<!-- E2E Password Prompt Modal -->
<div id="e2e-password-modal" class="modal-overlay" style="display:none">
<div class="modal e2e-password-modal">
<h3>🔐 <span data-i18n="modals.e2e_password.title">Password Required</span></h3>
<p class="e2e-pw-desc" data-i18n="modals.e2e_password.desc">End-to-end encryption requires your password to unlock your keys. Enter your account password to continue.</p>
<div class="e2e-pw-field">
<input type="password" id="e2e-pw-input" data-i18n-placeholder="modals.e2e_password.placeholder" placeholder="Enter your password" autocomplete="current-password" spellcheck="false">
</div>
<div id="e2e-pw-error" class="e2e-pw-error" style="display:none"></div>
<div class="e2e-pw-actions">
<button class="btn-accent" id="e2e-pw-submit-btn" data-i18n="modals.e2e_password.unlock_btn">Unlock</button>
<button class="btn-sm" id="e2e-pw-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
</div>
</div>
</div>
<!-- Desktop App Promo Popup -->
<!-- Android App Promo Modal -->
<div class="modal-overlay" id="android-beta-modal" style="display:none">
<div class="modal android-beta-promo">
<div class="android-beta-promo-icon">📱</div>
<span class="android-beta-promo-badge" data-i18n="modals.android_beta.badge" style="background:var(--accent-bright,#4caf50)">NOW AVAILABLE</span>
<h3 class="android-beta-promo-title"><span data-i18n="modals.android_beta.title">Amni-Haven Android</span></h3>
<p class="android-beta-promo-subtitle" data-i18n="modals.android_beta.subtitle">A native Android app for Haven, built from the ground up by Amnibro!</p>
<ul class="android-beta-promo-features">
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_native">Native Android experience</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_push">Push notifications via Google Play</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_chat">Full chat &amp; voice support</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_built">Built from the ground up for Haven</span></li>
</ul>
<p class="android-beta-promo-note" data-i18n="modals.android_beta.note">Available now on Google Play:</p>
<div class="android-beta-form">
<a id="android-beta-submit" href="https://play.google.com/store/apps/details?id=com.havenapp.mobile&gl=US" target="_blank" rel="noopener" class="btn btn-accent android-beta-submit" style="text-decoration:none;display:inline-block">📲 <span data-i18n="modals.android_beta.request_btn">Get it on Google Play</span></a>
</div>
<p class="android-beta-promo-credit" data-i18n-html="modals.android_beta.credit">Built with ❤️ by <strong>Amnibro</strong>. Thank you for your incredible work on the Amni-Haven Android app!</p>
<div class="android-beta-promo-actions">
<button id="android-beta-later" class="btn android-beta-promo-later" data-i18n="modals.android_beta.later_btn">Maybe later</button>
</div>
<label class="android-beta-promo-dismiss-label">
<input type="checkbox" id="android-beta-dismiss-check"> <span data-i18n="modals.android_beta.dont_show">Don't show this again</span>
</label>
</div>
</div>
<div class="modal-overlay" id="desktop-promo-modal" style="display:none">
<div class="modal desktop-promo">
<div class="desktop-promo-icon">🖥️</div>
<h3 class="desktop-promo-title"><span data-i18n="modals.desktop_promo.title">Haven Desktop App</span> <span class="desktop-promo-badge" data-i18n="modals.desktop_promo.badge">BETA</span></h3>
<p class="desktop-promo-subtitle" data-i18n="modals.desktop_promo.subtitle">Now available for your platform!</p>
<ul class="desktop-promo-features">
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_notifs">Native system notifications</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_tray">Minimize to system tray</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_audio">Per-app audio routing</span></li>
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_launcher">Built-in server launcher</span></li>
</ul>
<p class="desktop-promo-meta" id="desktop-promo-meta">Windows Installer • v1.0.0</p>
<div class="desktop-promo-actions">
<a id="desktop-promo-install" class="btn btn-accent desktop-promo-install" href="https://haven-app.com/#download" target="_blank" rel="noopener">⬇ <span data-i18n="modals.desktop_promo.install_btn">Install Haven</span></a>
<button id="desktop-promo-later" class="btn desktop-promo-later" data-i18n="modals.desktop_promo.later_btn">Maybe later</button>
</div>
<label class="desktop-promo-dismiss-label">
<input type="checkbox" id="desktop-promo-dismiss-check"> <span data-i18n="modals.desktop_promo.dont_show">Don't show this again</span>
</label>
</div>
</div>
<!-- Donors Thank You Modal -->
<div class="modal-overlay" id="donors-modal" style="display:none">
<div class="modal donors-modal-box">
<button class="donors-close-btn" id="donors-close-btn" data-i18n-title="modals.common.close" title="Close">&times;</button>
<div class="donors-heart-icon">❤️</div>
<h2 class="donors-title" data-i18n="modals.donors.title">Thank You</h2>
<p class="donors-subtitle" data-i18n="modals.donors.subtitle">Haven is built with love and kept alive by the generosity of people like you. Every donation, big or small, means the world.</p>
<div class="donors-sort-toggle" id="donors-sort-toggle" style="display:none">
<button class="donors-sort-btn active" data-sort="chronological" data-i18n="modals.donors.sort_chronological">Early Believers</button>
<button class="donors-sort-btn" data-sort="featured" data-i18n="modals.donors.sort_featured">Biggest Thanks!</button>
</div>
<div class="donors-tiers" id="donors-tiers">
<div class="donors-tier">
<h3 class="donors-tier-title donors-sponsor-title">✨ <span data-i18n="modals.donors.sponsors_title">Sponsors</span></h3>
<div class="donors-grid" id="sponsors-grid"></div>
</div>
<div class="donors-tier">
<h3 class="donors-tier-title donors-donor-title">💛 <span data-i18n="modals.donors.donors_title">Donors</span></h3>
<div class="donors-grid" id="donors-grid"></div>
</div>
</div>
<p class="donors-note" data-i18n="modals.donors.note">New donors will be added each release. If you've donated and don't see your name, it'll be in the next update!</p>
<a href="https://ko-fi.com/ancsemi" target="_blank" rel="noopener noreferrer" class="donors-kofi-btn">
<span>☕ <span data-i18n="modals.donors.kofi_btn">Support Haven on Ko-fi</span></span>
</a>
</div>
</div>
<script src="/js/i18n.js"></script>
<script src="/socket.io/socket.io.js"></script>
<!-- Image Lightbox Overlay -->
<div id="image-lightbox" class="image-lightbox" style="display:none">
<button id="lightbox-prev" class="lightbox-nav lightbox-prev" data-i18n-aria-label="media_gallery.previous_image" aria-label="Previous image">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<img id="lightbox-img" class="lightbox-img" src="" alt="">
<button id="lightbox-next" class="lightbox-nav lightbox-next" data-i18n-aria-label="media_gallery.next_image" aria-label="Next image">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
</button>
</div>
<!-- Channel Media Gallery Modal (#5350) -->
<div class="modal-overlay" id="media-gallery-modal" style="display:none">
<div class="modal media-gallery-modal">
<div class="media-gallery-header">
<h3 data-i18n="media_gallery.title">Files & Media</h3>
<button class="icon-btn small" id="media-gallery-close" data-i18n-aria-label="modals.common.close" aria-label="Close">&times;</button>
</div>
<div class="media-gallery-tabs" role="tablist">
<button class="media-tab active" data-tab="photos" role="tab">🖼 <span data-i18n="media_gallery.photos">Photos</span> <span class="media-tab-count" id="media-count-photos">0</span></button>
<button class="media-tab" data-tab="videos" role="tab">🎬 <span data-i18n="media_gallery.videos">Videos</span> <span class="media-tab-count" id="media-count-videos">0</span></button>
<button class="media-tab" data-tab="audios" role="tab">🎵 <span data-i18n="media_gallery.audio">Audio</span> <span class="media-tab-count" id="media-count-audios">0</span></button>
<button class="media-tab" data-tab="files" role="tab">📄 <span data-i18n="media_gallery.files">Files</span> <span class="media-tab-count" id="media-count-files">0</span></button>
<button class="media-tab" data-tab="links" role="tab">🔗 <span data-i18n="media_gallery.links">Links</span> <span class="media-tab-count" id="media-count-links">0</span></button>
</div>
<div class="media-gallery-toolbar">
<label class="media-gallery-sort-label">
<span data-i18n="media_gallery.sort_by">Sort by</span>
<select id="media-gallery-sort" class="media-gallery-sort">
<option value="date-desc" data-i18n="media_gallery.sort_newest">Newest first</option>
<option value="date-asc" data-i18n="media_gallery.sort_oldest">Oldest first</option>
<option value="size-desc" data-i18n="media_gallery.sort_largest">Largest first</option>
<option value="size-asc" data-i18n="media_gallery.sort_smallest">Smallest first</option>
<option value="name-asc" data-i18n="media_gallery.sort_name_az">Name A→Z</option>
<option value="name-desc" data-i18n="media_gallery.sort_name_za">Name Z→A</option>
</select>
</label>
<div class="media-gallery-tagfilter" id="media-gallery-tagfilter-wrap">
<button class="btn-sm" id="media-gallery-tagfilter-btn" data-i18n="media_gallery.filter_tags">Tags</button>
<span class="media-gallery-tagfilter-count" id="media-gallery-tagfilter-count" style="display:none"></span>
</div>
<div class="media-gallery-tagmanage" id="media-gallery-tagmanage" style="display:none">
<button class="btn-sm" id="media-gallery-tagmanage-btn" data-i18n="media_gallery.manage_tags">Manage tags</button>
<div class="media-tagmanage-menu" id="media-tagmanage-menu" style="display:none">
<button type="button" class="media-tagmanage-opt" data-mode="append" data-i18n="media_gallery.append_tags">Append tags</button>
<button type="button" class="media-tagmanage-opt" data-mode="replace" data-i18n="media_gallery.replace_tags">Replace all tags</button>
</div>
</div>
<label class="media-gallery-tile-label" id="media-gallery-tile-wrap">
<span data-i18n="media_gallery.tile_size">Size</span>
<input type="range" id="media-gallery-tile" min="72" max="360" step="8" value="150" data-i18n-aria-label="media_gallery.tile_size" aria-label="Size">
<select id="media-gallery-shape" class="forum-select forum-select-small" data-i18n-aria-label="forum.shape" aria-label="Shape"></select>
</label>
<div class="media-gallery-actions" id="media-gallery-actions" style="display:none">
<button class="btn-sm" id="media-gallery-select-toggle" data-i18n="media_gallery.select">Select</button>
<span class="media-gallery-selection-info" id="media-gallery-selection-info" style="display:none"></span>
<button class="btn-sm" id="media-gallery-select-all" style="display:none" data-i18n="media_gallery.select_all">Select all</button>
<button class="btn-sm btn-danger-fill" id="media-gallery-delete" style="display:none" disabled data-i18n="media_gallery.delete_selected">Delete selected</button>
</div>
</div>
<div class="media-gallery-body" id="media-gallery-body">
<div class="media-gallery-empty muted-text" data-i18n="media_gallery.loading">Loading…</div>
</div>
</div>
</div>
<div class="modal-overlay" id="threads-list-modal" style="display:none">
<div class="modal media-gallery-modal">
<div class="media-gallery-header">
<h3 data-i18n="thread_list.title">Threads</h3>
<button class="icon-btn small" id="threads-list-close" data-i18n-aria-label="modals.common.close" aria-label="Close">&times;</button>
</div>
<div class="media-gallery-toolbar">
<input type="text" id="threads-list-search" class="thread-list-search"
data-i18n-placeholder="thread_list.search_placeholder"
placeholder="Search threads…" autocomplete="off">
<span class="thread-list-count muted-text" id="threads-list-count"></span>
</div>
<div class="media-gallery-body" id="threads-list-body">
<div class="media-gallery-empty muted-text" data-i18n="thread_list.loading">Loading…</div>
</div>
</div>
</div>
<script src="/js/theme.js?v=4.22.9"></script>
<script src="/js/notifications.js?v=4.21.1"></script>
<script src="/js/servers.js?v=4.21.0"></script>
<script src="/js/voice-relay.js?v=4.14.2"></script>
<script src="/js/voice.js?v=4.21.1"></script>
<script src="/js/modmode.js?v=4.21.0"></script>
<script src="/js/e2e-group.js?v=4.21.0"></script>
<script src="/js/e2e.js?v=4.21.0"></script>
<!-- Shared with the server (src/automod.js requires the same file) so both
sides reach identical verdicts on a link. Must load before app.js. -->
<script src="/js/automod-rules.js?v=4.21.0"></script>
<script type="module" src="/js/app.js?v=4.22.25"></script>
<script src="/js/plugin-loader.js?v=4.21.0"></script>
<script src="/js/modules/stream-debug.js?v=4.21.0"></script>
<script src="/js/password-eye.js?v=4.21.0"></script>
</body>
</html>