Files
Haven/public/theme-preview.html
T

4299 lines
443 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>
<!-- Generated by scripts/theme-preview-capture.js from a running Haven. A static copy of the app with sample content, for theme authors: open it from a checkout in a browser or a live CSS editor, type the path of a .theme.css file in the toolbar, and see it applied. Not used by the app itself. See docs/theme-authoring.md. -->
<html lang="en" data-haven-theme-api="1" data-toggle-style="switch" data-density="cozy" data-haven-density="cozy" data-channel-scroll="separate" data-layout-toggles-bound="1" data-emojisize="normal" data-roledisplay="colored-name" data-toolbaricons="mono" data-theme="haven"><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">
<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">
<style id="haven-password-eye-style">
.haven-pw-wrap { position: relative; display: block; width: 100%; }
/* 3.20.2: width:100% + box-sizing fixes the gap on the login form
where the input was collapsing to its intrinsic ~173px and the
eye floated several px to the right of it. Padding bumped from
36 → 30 to bring the eye visually closer to the field too. */
.haven-pw-wrap input { padding-right: 30px !important; width: 100% !important; box-sizing: border-box !important; }
.haven-pw-eye {
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
background: transparent; border: none; cursor: pointer;
color: var(--text-muted, #888); padding: 2px; border-radius: 4px;
display: inline-flex; align-items: center; justify-content: center;
line-height: 0;
}
.haven-pw-eye:hover { color: var(--text-primary, #fff); background: rgba(255,255,255,0.06); }
.haven-pw-eye:focus-visible { outline: 2px solid var(--accent, #6c8cff); outline-offset: 1px; }
</style></head>
<body data-haven-page="app" class="embed-size-medium">
<div id="app" data-haven-region="app-shell" data-status-pos="bottom">
<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" data-panel-pos="left">
<div class="server-icon home active" id="home-server" data-i18n-title="app.sidebar.this_server" title="Haven">
<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" data-panel-pos="left">
<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" data-original-text="Haven">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">×</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">forumtester</span>
<span class="login-name" id="login-name" data-i18n-title="app.profile.login_username" title="Your login username">@forumtester</span>
</div><button id="user-status-btn" class="status-picker-btn" type="button" title="Set status" aria-label="Set status"><span id="user-status-dot" class="user-dot status-picker-dot"></span></button>
<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"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
</button>
<div id="status-picker" class="status-picker" style="display: none;">
<div class="status-option" data-status="online"><span class="user-dot"></span> Online</div>
<div class="status-option" data-status="away"><span class="user-dot away"></span> Away</div>
<div class="status-option" data-status="dnd"><span class="user-dot dnd"></span> Do Not Disturb</div>
<div class="status-option" data-status="invisible"><span class="user-dot invisible"></span> Invisible</div>
<div class="status-text-row">
<input type="text" id="status-text-input" placeholder="Custom status..." maxlength="128">
</div>
<div class="status-activity-row">
<div class="status-activity-label">Share Activity</div>
<label class="status-activity-toggle" title="Let others see what you're playing or listening to. Never shared while you're Invisible.">
<span>Music Activity</span>
<input type="checkbox" id="status-music-activity">
</label>
<label class="status-activity-toggle" title="Let others see what you're playing or listening to. Never shared while you're Invisible.">
<span>Game Activity</span>
<input type="checkbox" id="status-game-activity">
</label>
</div>
</div></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"><span class="mobile-servers-empty">No servers added yet</span></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: block;">
<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"><option value="b:chat">💬 Chat</option><option value="b:announcements">📢 Announcements</option><option value="b:forum">🗂️ Forum</option><option value="b:team">🔒 Private team</option><option value="b:event">🎉 Event (24h)</option><option value="b:slow">🐢 Slow chat</option><option value="b:text_only">📝 Text only</option></select>
<button id="new-channel-template-del" class="btn-sm" type="button" data-i18n-title="app.sidebar.templates.delete_title" title="Delete 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-delete)" 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)" 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" id="organize-channels-btn" data-i18n-title="app.actions.organize_channels" title="Organize channels" style="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 class="channel-item" data-code="e950d401" draggable="true">
<span class="channel-hash">🗂️</span>
<span class="channel-name">gallery</span>
<button class="channel-more-btn" title="Channel options">⋯</button>
</div><div class="channel-item active" data-code="456b447e" draggable="true">
<span class="channel-hash">#</span>
<span class="channel-name">general</span>
<button class="channel-more-btn" title="Channel options">⋯</button>
</div><div class="channel-item temp-channel-create-btn" title="Create Temporary Voice Channel" style="opacity: 0.5; cursor: pointer; padding: 4px 12px; font-size: 0.8rem; display: flex; align-items: center; gap: 6px;"><span style="font-size:0.9rem">➕</span><span>Create Temp Channel</span></div></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" style="display: none;">
<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="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">×</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" style="background: rgb(121, 61, 242);">🎨</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="Theme">🎨</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="">👥</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>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</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"># general</h3>
</div>
<div class="header-actions-box" id="header-actions-box" style="display: flex;">
<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">456b447e</span>
<button id="copy-code-btn" class="icon-btn small" data-i18n-title="header.copy_code" title="Copy code" style="display: inline-flex;">📋</button>
<button id="channel-code-settings-btn" class="icon-btn small" data-i18n-title="header.channel_code_settings" title="Channel code settings" style="display: inline-flex;">⚙️</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)">🔍</button>
<button id="pinned-toggle-btn" class="icon-btn small" data-i18n-title="header.pinned_messages" title="Pinned messages" data-pin-count="0">📌</button>
<button id="gallery-toggle-btn" class="icon-btn small" data-i18n-title="header.media_gallery" title="View files and media">🖼</button>
<button id="threads-toggle-btn" class="icon-btn small" data-i18n-title="header.thread_list" title="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" style="display: inline-flex;">☰</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">🔐</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" style="display: none;">
<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"></polygon></svg>
</button>
<button id="search-close-btn" class="icon-btn small" data-i18n-title="header.search_close" title="Close search">×</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="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">‹</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: inline-flex;">
<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">×</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: inline-flex;" data-wired="1">
<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">×</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="All Members" aria-label="All 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"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</button>
<!-- Dynamic indicators (music, streams) inject here via JS -->
<button id="voice-join-btn" class="btn-voice" style="display: inline-flex;"><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><div id="channel-topic-bar" class="channel-topic-bar" title="" style="cursor: pointer; color: var(--text-muted);"><span class="channel-topic-text">Click to set a topic...</span><button type="button" class="channel-topic-toggle" title="Hide the topic bar" aria-label="Hide the topic bar">▴</button></div><div id="dm-cleanup-notice" class="dm-cleanup-notice" style="display: none;"></div>
<!-- Welcome Screen -->
<div id="no-channel-msg" class="welcome-screen" data-haven-region="welcome" style="display: none;">
<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,
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: flex;">
<!-- 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">×</button>
</div>
</div>
<div id="pinned-list" class="pinned-list"></div>
</div>
<div class="messages" id="messages" data-haven-region="message-list" style="--forum-shape: 1 / 1;"><div class="message" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:42:58Z" data-time-short="06:42 PM" data-msg-id="4">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:42 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:02)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:43:23Z" data-time-short="06:43 PM" data-msg-id="5" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:43 PM</span>
<div class="message-body">
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:03)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:44:24Z" data-time-short="06:44 PM" data-msg-id="6" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:44 PM</span>
<div class="message-body">
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:03)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:47Z" data-time-short="06:46 PM" data-msg-id="7">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Hey! Is this the place where we test <strong>themes</strong>?</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:48Z" data-time-short="06:46 PM" data-msg-id="8">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="chat-heading chat-h1">Welcome to Haven</div><br>This is a <em>sample</em> conversation for theme authors. It has <u>underline</u>, <del>strikethrough</del>, <mark class="chat-highlight">highlight</mark>, <code class="inline-code">inline code</code>, <span class="spoiler">a spoiler</span> and <span style="color:#FF00EF"> coloured text </span>, plus a link to <a href="https://example.com." target="_blank" rel="noopener noreferrer nofollow">https://example.com.</a></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:48Z" data-time-short="06:46 PM" data-msg-id="9" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content"><blockquote class="chat-blockquote"><div class="chat-blockquote-body">A quoted line<br>and a second one</div></blockquote><br><ul class="chat-list"><li>a bullet</li><li>another bullet</li></ul><br><br><ol class="chat-list" start="1"><li>first</li><li>second</li></ol></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:49Z" data-time-short="06:46 PM" data-msg-id="10">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="code-block" data-lang="js"><span class="code-block-lang">js</span><pre><code>const haven = "themeable";
console.log(haven);</code></pre></div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:49Z" data-time-short="06:46 PM" data-msg-id="11" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content"><div class="chat-table-wrap"><table class="chat-table"><thead><tr><th>Column A</th><th>Column B</th></tr></thead></table></div>| one | two |</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-has-image message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:51Z" data-time-short="06:46 PM" data-msg-id="12">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20480%20300%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%236b4fdb%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2343b581%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22480%22%20height%3D%22300%22%20fill%3D%22url(%23g)%22%2F%3E%3Ctext%20x%3D%22240%22%20y%3D%22162%22%20text-anchor%3D%22middle%22%20font-family%3D%22sans-serif%22%20font-size%3D%2234%22%20font-weight%3D%22700%22%20fill%3D%22%23fff%22%3Esample%20image%3C%2Ftext%3E%3C%2Fsvg%3E" class="chat-image" alt="image" data-lazy="pending" decoding="async"></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:52Z" data-time-short="06:46 PM" data-msg-id="13">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Nice picture 😄 and a mention for <span class="mention mention-self">@forumtester</span> so the highlight shows.</div>
<div class="reactions-row"><button class="reaction-badge own" data-emoji="👍" data-users="[&quot;forumtester&quot;,&quot;nova&quot;]" title="forumtester, nova">👍 2</button><button class="reaction-badge own" data-emoji="🎉" data-users="[&quot;forumtester&quot;]" title="forumtester">🎉 1</button></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:53Z" data-time-short="06:46 PM" data-msg-id="14">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="reply-banner" data-reply-msg-id="13">
<span class="reply-line" style="background:#f47fff"></span>
<span class="reply-author" style="color:#f47fff">nova</span>
<span class="reply-preview">Nice picture 😄 and a mention for @forumtester so the highlight shows.</span>
</div>
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Replying to the one above, so the reply banner shows.</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:54Z" data-time-short="06:46 PM" data-msg-id="15" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content">📊 Poll: Which theme do you use?</div>
<div class="poll-widget" data-msg-id="15">
<div class="poll-question">Which theme do you use?</div>
<div class="poll-options"><button class="poll-option" data-msg-id="15" data-option="0" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Braid</span>
<span class="poll-option-count">0 (0%)</span>
</button><button class="poll-option" data-msg-id="15" data-option="1" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Compact</span>
<span class="poll-option-count">0 (0%)</span>
</button><button class="poll-option" data-msg-id="15" data-option="2" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Matrix</span>
<span class="poll-option-count">0 (0%)</span>
</button></div>
<div class="poll-footer">0 votes</div>
</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="new-messages-divider" id="new-messages-divider"><span>NEW MESSAGES</span></div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:55Z" data-time-short="06:46 PM" data-msg-id="16">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">And a plain closing line from the other person.</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div></div>
<button id="jump-to-bottom" class="jump-to-bottom visible" data-i18n-title="app.actions.jump_to_bottom" title="Jump to bottom" style="display: none;">
<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"></polyline>
</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">×</button>
</div>
<div id="image-queue-bar" class="image-queue-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" style="width: 100%;"></div></div>
<span class="upload-progress-text" id="upload-progress-text" data-i18n="header.uploading">100%</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">×</button>
</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" 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">
<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"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<path d="M21 15l-5-5L5 21"></path>
</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">
<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"></circle>
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
<circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"></circle>
<circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"></circle>
</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"><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">
<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>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="2" y1="20" x2="22" y2="20"></line>
</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">
<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"></circle>
<line x1="12" y1="12" x2="12" y2="7"></line>
<line x1="12" y1="12" x2="15.5" y2="13.5"></line>
</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">
<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>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
</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">
<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"></rect>
<path d="M5 11a7 7 0 0 0 14 0"></path>
<line x1="12" y1="18" x2="12" y2="21"></line>
<line x1="9" y1="21" x2="15" y2="21"></line>
</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:01</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 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">
<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"></path>
</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; right: 260px; top: 114px;" data-i18n-title="media.toggle_soundboard" title="Toggle soundboard panel"><span class="sb-toggle-mark" aria-hidden="true">♪</span><span class="sb-toggle-arrow">❮</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">🎵 <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" style="right: 260px;">❯</button>
<!-- ─── Right Sidebar ───────────────────────────── -->
<aside class="right-sidebar" id="right-sidebar" data-haven-region="context-sidebar" data-panel-pos="right">
<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">×</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">×</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=""><span data-i18n="voice.join">🎤 Join Voice</span></button>
<div id="voice-users" class="user-list"><p class="muted-text">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="">
<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">×</button>
</div>
<div id="online-users" class="user-list"><div class="user-group-label">Online (1)</div>
<div class="user-item" data-user-id="1">
<div class="user-avatar-wrapper"><div class="user-item-avatar avatar-circle" style="background-color:#82aaff">F</div><span class="user-status-dot"></span></div>
<div class="user-item-text">
<div class="user-item-line">
<span class="user-item-name" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
</div>
</div>
<div class="user-admin-actions"><button class="user-action-btn user-dm-btn" data-dm-uid="1" title="Notes to self (DM yourself)">📝</button></div>
</div>
</div>
</div>
</div>
<!-- Invite users button pinned under user list -->
<div class="panel sidebar-invite-panel" id="sidebar-invite-panel" style="display: block;">
<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"></circle>
<path d="M5,26c0-4.971,4.029-9,9-9 c1.864,0,3.596,0.567,5.032,1.537"></path>
<circle cx="24" cy="24" r="7"></circle>
<line x1="24" y1="28" x2="24" y2="20"></line>
<line x1="20" y1="24" x2="28" y2="24"></line>
</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" style="left: 11.6%;"></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" data-mod-pos="right-sidebar">
<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"></circle>
<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"></path>
</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="">
<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">0</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">general</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" style="cursor: pointer;">
<span class="label" data-i18n="status_bar.online">Online</span>
<span class="value" id="status-online-count">1</span>
</div>
<span class="spacer"></span>
<div class="status-item status-url-item" id="status-url-item" style="display: none;">
<span class="value status-url-text url-hidden" 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 server address">👁‍🗨</button>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="value" id="status-clock">18:51:05</span>
</div>
<div class="divider"></div>
<div class="status-item">
<span class="value" id="status-version">v4.8.0</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>
<!-- Hidden audio container for voice streams -->
<!-- Toast notifications -->
<!-- Move Messages Toolbar (floating above messages) -->
<!-- Move Messages Channel Picker Modal -->
<!-- Thread Panel (slides in from right) -->
<!-- Pinned Messages PiP floating panel (draggable overlay) -->
<!-- DM PiP floating panel (overlay; doesn't switch the active channel) -->
<div id="thread-panel" class="thread-panel" data-haven-region="thread-panel" style="display:none" data-parent-id="">
<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">×</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">×</button>
</div>
<div id="thread-image-queue-bar" class="image-queue-bar" style="display:none"></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"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><circle cx="9" cy="10" r="1.2" fill="currentColor" stroke="none"></circle><circle cx="15" cy="10" r="1.2" fill="currentColor" stroke="none"></circle></svg></span></button>
<textarea 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><path d="M22 2L15 22L11 13L2 9L22 2Z"></path>
</svg>
</button>
</div>
</div>
<!-- First-Time Setup Wizard (Admin only) -->
<!-- Channel Code Settings Modal (Admin) -->
<!-- Add Server Modal -->
<!-- Timezone / Time Format Modal -->
<!-- Rename / Profile Modal -->
<!-- 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. -->
<!-- Admin Action Modal (Kick/Ban/Mute/Delete) -->
<!-- Settings Popout Modal -->
<div class="modal-overlay" id="settings-modal" style="display:none">
<div class="modal modal-settings" data-haven-region="settings" data-modal-controls-injected="1">
<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 type="button" class="modal-expand-btn modal-expand-btn-inline" title="Expand / Restore">⛶</button><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">×</button>
</div>
<div class="settings-layout">
<nav class="settings-nav" id="settings-nav"><div class="settings-search-wrap"><input type="search" id="settings-search" class="settings-search" placeholder="Search settings (Ctrl+F)" autocomplete="off"></div>
<!-- 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</span></div>
<div class="settings-nav-item" data-target="section-zoom">🔍 <span data-i18n="settings.nav.zoom">Zoom</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">Toolbar 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" style="display: none;">🏞️ <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-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-branding" style="display:none">🏠 <span data-i18n="settings.nav.branding">Branding</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-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">Auto-Mod</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-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-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>
<div class="lang-picker"><button type="button" class="lang-picker-btn" aria-haspopup="listbox" aria-expanded="false"><span class="lang-flag lang-flag-text lang-flag-auto" aria-hidden="true">A</span><span class="lang-name">Automatic (English)</span><span class="lang-caret">▾</span></button><div class="lang-picker-list" role="listbox" hidden=""><button type="button" class="lang-picker-item active" role="option" data-value="auto" aria-selected="true"><span class="lang-flag lang-flag-text lang-flag-auto" aria-hidden="true">A</span><span class="lang-name">Automatic (English)</span></button><button type="button" class="lang-picker-item" role="option" data-value="en" aria-selected="false"><img class="lang-flag" src="emoji/flags/gb.svg" alt=""><span class="lang-name">English</span></button><button type="button" class="lang-picker-item" role="option" data-value="fr" aria-selected="false"><img class="lang-flag" src="emoji/flags/fr.svg" alt=""><span class="lang-name">Français</span></button><button type="button" class="lang-picker-item" role="option" data-value="de" aria-selected="false"><img class="lang-flag" src="emoji/flags/de.svg" alt=""><span class="lang-name">Deutsch</span></button><button type="button" class="lang-picker-item" role="option" data-value="es" aria-selected="false"><img class="lang-flag" src="emoji/flags/es.svg" alt=""><span class="lang-name">Español</span></button><button type="button" class="lang-picker-item" role="option" data-value="pl" aria-selected="false"><img class="lang-flag" src="emoji/flags/pl.svg" alt=""><span class="lang-name">Polski</span></button><button type="button" class="lang-picker-item" role="option" data-value="ru" aria-selected="false"><img class="lang-flag" src="emoji/flags/ru.svg" alt=""><span class="lang-name">Русский</span></button><button type="button" class="lang-picker-item" role="option" data-value="zh" aria-selected="false"><img class="lang-flag" src="emoji/flags/cn.svg" alt=""><span class="lang-name">中文</span></button><button type="button" class="lang-picker-item" role="option" data-value="pt" aria-selected="false"><img class="lang-flag" src="emoji/flags/br.svg" alt=""><span class="lang-name">Português (Brasil)</span></button></div></div><select id="language-select" class="settings-select lang-select-hidden" data-haven-picker="1">
<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-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>
<!-- Font Size -->
<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.zoom.title">Zoom</span></h5>
<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>
<!-- 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">Toolbar 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>
</div>
<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 class="toolbar-order-item">
<span class="toolbar-order-item-label">React</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="react" disabled="" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="react" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Reply</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="reply" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="reply" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Quote</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="quote" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="quote" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Thread</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="thread" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="thread" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Pin / Unpin</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="pin" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="pin" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Protect / Unprotect</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="archive" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="archive" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Edit</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="edit" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="edit" title="Move down">▼</button>
</div>
</div><div class="toolbar-order-item">
<span class="toolbar-order-item-label">Delete</span>
<div class="toolbar-order-item-controls">
<button type="button" class="toolbar-order-move" data-dir="up" data-key="delete" title="Move up">▲</button>
<button type="button" class="toolbar-order-move" data-dir="down" data-key="delete" disabled="" title="Move down">▼</button>
</div>
</div></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>
</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">Suppress all notifications, sounds, and taskbar badge from this server.</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.</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>
<optgroup label="🎙️ Built-in" data-custom-group="1"><option value="custom:AOL - Door Open" data-custom="1" data-url="/sounds/aol_door_open.mp3">AOL - Door Open</option><option value="custom:AOL - Door Close" data-custom="1" data-url="/sounds/aol_door_close.mp3">AOL - Door Close</option><option value="custom:AOL - You've Got Mail" data-custom="1" data-url="/sounds/aol_got_mail.mp3">AOL - You've Got Mail</option><option value="custom:AOL - Message" data-custom="1" data-url="/sounds/aol_message.mp3">AOL - Message</option><option value="custom:AOL - Files Done" data-custom="1" data-url="/sounds/aol_filesdone.mp3">AOL - Files Done</option></optgroup><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>
<optgroup label="🎙️ Built-in" data-custom-group="1"><option value="custom:AOL - Door Open" data-custom="1" data-url="/sounds/aol_door_open.mp3">AOL - Door Open</option><option value="custom:AOL - Door Close" data-custom="1" data-url="/sounds/aol_door_close.mp3">AOL - Door Close</option><option value="custom:AOL - You've Got Mail" data-custom="1" data-url="/sounds/aol_got_mail.mp3">AOL - You've Got Mail</option><option value="custom:AOL - Message" data-custom="1" data-url="/sounds/aol_message.mp3">AOL - Message</option><option value="custom:AOL - Files Done" data-custom="1" data-url="/sounds/aol_filesdone.mp3">AOL - Files Done</option></optgroup><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>
<optgroup label="🎙️ Built-in" data-custom-group="1"><option value="custom:AOL - Door Open" data-custom="1" data-url="/sounds/aol_door_open.mp3">AOL - Door Open</option><option value="custom:AOL - Door Close" data-custom="1" data-url="/sounds/aol_door_close.mp3">AOL - Door Close</option><option value="custom:AOL - You've Got Mail" data-custom="1" data-url="/sounds/aol_got_mail.mp3">AOL - You've Got Mail</option><option value="custom:AOL - Message" data-custom="1" data-url="/sounds/aol_message.mp3">AOL - Message</option><option value="custom:AOL - Files Done" data-custom="1" data-url="/sounds/aol_filesdone.mp3">AOL - Files Done</option></optgroup><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>
<optgroup label="🎙️ Built-in" data-custom-group="1"><option value="custom:AOL - Door Open" data-custom="1" data-url="/sounds/aol_door_open.mp3">AOL - Door Open</option><option value="custom:AOL - Door Close" data-custom="1" data-url="/sounds/aol_door_close.mp3">AOL - Door Close</option><option value="custom:AOL - You've Got Mail" data-custom="1" data-url="/sounds/aol_got_mail.mp3">AOL - You've Got Mail</option><option value="custom:AOL - Message" data-custom="1" data-url="/sounds/aol_message.mp3">AOL - Message</option><option value="custom:AOL - Files Done" data-custom="1" data-url="/sounds/aol_filesdone.mp3">AOL - Files Done</option></optgroup><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>
<optgroup label="🎙️ Built-in" data-custom-group="1"><option value="custom:AOL - Door Open" data-custom="1" data-url="/sounds/aol_door_open.mp3">AOL - Door Open</option><option value="custom:AOL - Door Close" data-custom="1" data-url="/sounds/aol_door_close.mp3">AOL - Door Close</option><option value="custom:AOL - You've Got Mail" data-custom="1" data-url="/sounds/aol_got_mail.mp3">AOL - You've Got Mail</option><option value="custom:AOL - Message" data-custom="1" data-url="/sounds/aol_message.mp3">AOL - Message</option><option value="custom:AOL - Files Done" data-custom="1" data-url="/sounds/aol_filesdone.mp3">AOL - Files Done</option></optgroup><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" disabled="">
</label>
<small class="settings-hint" id="push-notif-status" data-i18n="settings.push_section.checking">Registration failed</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="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">
<div class="connection-block">
<div class="connection-row is-unavailable">
<span class="connection-icon">🎵</span>
<span class="connection-info">
<span class="connection-name">Last.fm</span>
<span class="connection-sub">Not set up yet</span>
</span>
<button class="btn-sm connection-setup" data-provider="lastfm">Set up</button>
</div>
<div class="connection-setup-form" data-provider="lastfm" hidden="">
<a class="connection-help" href="https://www.last.fm/api/account/create" target="_blank" rel="noopener noreferrer">Get a Last.fm API key ↗</a>
<ol class="connection-steps"><li>Sign in with a Last.fm account and fill in the short form (name it "Haven"; the other fields can be anything).</li><li>Copy the <b>API key</b> it shows you and paste it below. Ignore the shared secret. Haven does not need it.</li></ol>
<label class="connection-field">
<span>API Key</span>
<span class="haven-pw-wrap"><input type="password" autocomplete="off" spellcheck="false" data-env-key="LASTFM_API_KEY" placeholder="32 hex characters" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</label>
<div class="connection-setup-actions">
<button class="btn-sm btn-accent connection-save" data-provider="lastfm">Save</button>
<button class="btn-sm connection-cancel" data-provider="lastfm">Cancel</button>
</div>
<small class="settings-hint">Saved to the server's .env automatically. No restart needed.</small>
</div>
</div>
<div class="connection-block">
<div class="connection-row is-unavailable">
<span class="connection-icon">🎮</span>
<span class="connection-info">
<span class="connection-name">Steam</span>
<span class="connection-sub">Not set up yet</span>
</span>
<button class="btn-sm connection-setup" data-provider="steam">Set up</button>
</div>
<div class="connection-setup-form" data-provider="steam" hidden="">
<a class="connection-help" href="https://steamcommunity.com/dev/apikey" target="_blank" rel="noopener noreferrer">Open the Steam key page ↗</a>
<ol class="connection-steps"><li>Sign in with your Steam account.</li><li>Where it asks for a domain, any value works (Steam does not check it). Put <code>localhost</code>.</li><li>Copy the key it gives you and paste it below.</li></ol>
<label class="connection-field">
<span>API Key</span>
<span class="haven-pw-wrap"><input type="password" autocomplete="off" spellcheck="false" data-env-key="STEAM_API_KEY" placeholder="32 hex characters" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</label>
<div class="connection-setup-actions">
<button class="btn-sm btn-accent connection-save" data-provider="steam">Save</button>
<button class="btn-sm connection-cancel" data-provider="steam">Cancel</button>
</div>
<small class="settings-hint">Saved to the server's .env automatically. No restart needed.</small>
</div>
</div>
<details class="connection-advanced">
<summary>Other options</summary>
<div class="connection-block">
<div class="connection-row is-unavailable">
<span class="connection-icon">🎧</span>
<span class="connection-info">
<span class="connection-name">Spotify</span>
<span class="connection-sub">Not set up yet</span>
</span>
<button class="btn-sm connection-setup" data-provider="spotify">Set up</button>
</div>
<div class="connection-setup-form" data-provider="spotify" hidden="">
<a class="connection-help" href="https://developer.spotify.com/dashboard" target="_blank" rel="noopener noreferrer">Open the Spotify developer dashboard ↗</a>
<ol class="connection-steps"><li>Use the link above. It goes to <b>developer.spotify.com</b>.</li><li>Sign in with your Spotify account. <b>This needs Spotify Premium.</b> As of 2026 the Web API is no longer available on free accounts, so the rest of these steps will not work without it (#5528). Accept the developer terms if it asks.</li><li>Click <b>Create app</b>. You are not building software. This just registers a name. Call it "Haven".</li><li>Paste this into <b>Redirect URI</b>:<code class="setup-uri">https://your-haven-address/connect/spotify/callback</code></li><li>Tick <b>Web API</b>, save, then open <b>Settings</b> on the app you just made.</li><li>Copy <b>Client ID</b>, then click <b>View client secret</b> and copy that too.</li></ol>
<label class="connection-field">
<span>Client ID</span>
<span class="haven-pw-wrap"><input type="password" autocomplete="off" spellcheck="false" data-env-key="SPOTIFY_CLIENT_ID" placeholder="32 hex characters" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</label>
<label class="connection-field">
<span>Client Secret</span>
<span class="haven-pw-wrap"><input type="password" autocomplete="off" spellcheck="false" data-env-key="SPOTIFY_CLIENT_SECRET" placeholder="32 hex characters" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</label>
<div class="connection-setup-actions">
<button class="btn-sm btn-accent connection-save" data-provider="spotify">Save</button>
<button class="btn-sm connection-cancel" data-provider="spotify">Cancel</button>
</div>
<small class="settings-hint">Saved to the server's .env automatically. No restart needed.</small>
</div>
</div>
</details></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, and forum topics marked 🔞 leave their forum, until you turn this off. Handy on a phone in public.</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">
<span class="haven-pw-wrap"><input type="password" id="current-password" data-i18n-placeholder="settings.password_section.current_placeholder" placeholder="Current password" maxlength="128" autocomplete="current-password" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</div>
<div class="form-group compact">
<span class="haven-pw-wrap"><input type="password" id="new-password" data-i18n-placeholder="settings.password_section.new_placeholder" placeholder="New password (8+ chars)" maxlength="128" autocomplete="new-password" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</div>
<div class="form-group compact">
<span class="haven-pw-wrap"><input type="password" id="confirm-password" data-i18n-placeholder="settings.password_section.confirm_placeholder" placeholder="Confirm new password" maxlength="128" autocomplete="new-password" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</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>
<!-- 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 &amp; 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">
<span class="haven-pw-wrap"><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" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</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">
<span class="haven-pw-wrap"><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" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</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>
<!-- Plugins & Themes Section -->
<div class="settings-section" id="section-plugins">
<h5 class="settings-section-title">🧩 <span data-i18n="settings.plugins_section.title">Plugins &amp; 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" style="display: none;">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 class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Braid Layout</div>
<div class="plugin-card-desc">Vastly simplified two-edge layout: folds the server rail into the sidebar, docks the full voice controls bottom-left, tucks header extras into a kebab menu, merges message runs into cards, and calms the chrome. One-key toggle (Ctrl+Shift+B) between Braid and the classic layout. Suspends itself while Mod Mode edits the layout. Pairs with the Braid / Braid Light themes, and respects every other theme: cosmetic shape rules use :where() so any [data-theme] override wins.</div>
<div class="plugin-card-meta">Amnibro • v1.9</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div><div class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Compact Layout</div>
<div class="plugin-card-desc">Reversible desktop layout that folds the server rail into the navigation sidebar and docks account and voice controls in its footer. Preserves Haven's tablet, mobile, and message-density behavior.</div>
<div class="plugin-card-meta">bernardokcosta • v1.0.0</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div><div class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Message Timestamps</div>
<div class="plugin-card-desc">Adds relative timestamps (e.g. "2m ago") next to every message</div>
<div class="plugin-card-meta">Haven • v1.0.0</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div></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 class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Braid Light</div>
<div class="plugin-card-desc">Braid's airy light look: white surfaces, deep green accent, soft rounded corners. Pairs with the Braid Layout plugin.</div>
<div class="plugin-card-meta">Amnibro • v1.0 • Legacy theme</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div><div class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Braid</div>
<div class="plugin-card-desc">Mint-on-slate with soft rounded corners and a calm, decluttered look. Pairs with the Braid Layout plugin.</div>
<div class="plugin-card-meta">Amnibro • v1.0 • Legacy theme</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div><div class="plugin-card">
<div class="plugin-card-info">
<div class="plugin-card-name">Compact</div>
<div class="plugin-card-desc">Dense graphite chrome with narrow panels and crisp cyan accents. Respects the selected message density.</div>
<div class="plugin-card-meta">bernardokcosta • v1.0 • Theme API 1</div>
</div>
<label class="plugin-toggle">
<input type="checkbox">
<span class="plugin-toggle-slider"></span>
</label>
</div></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">
<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-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: block;">
<!-- 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>
<!-- 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" data-env-placeholder="HAVEN">
</label><small id="env-hint-server_name" class="settings-hint env-setting-hint">In use from this server's environment (SERVER_NAME): Haven. Saving something here replaces it.</small>
<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">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>
<!-- 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" data-loaded="1"><label style="display: flex; align-items: center; gap: 8px; font-size: 0.8125rem; cursor: pointer;"><input type="checkbox" data-file="braid-light.theme.css"><span>Braid Light</span><span style="font-size: 0.6875rem; color: var(--text-muted);">Braid's airy light look: white surfaces, deep green accent, soft rounded corners. Pairs with the Braid Layout plugin.</span></label><label style="display: flex; align-items: center; gap: 8px; font-size: 0.8125rem; cursor: pointer;"><input type="checkbox" data-file="braid.theme.css"><span>Braid</span><span style="font-size: 0.6875rem; color: var(--text-muted);">Mint-on-slate with soft rounded corners and a calm, decluttered look. Pairs with the Braid Layout plugin.</span></label><label style="display: flex; align-items: center; gap: 8px; font-size: 0.8125rem; cursor: pointer;"><input type="checkbox" data-file="compact.theme.css"><span>Compact</span><span style="font-size: 0.6875rem; color: var(--text-muted);">Dense graphite chrome with narrow panels and crisp cyan accents. Respects the selected message density.</span></label></div>
</div>
</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="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
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>
<span class="haven-pw-wrap"><input type="password" id="turn-password-input" class="settings-text-input" autocomplete="new-password" style="flex:1" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</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>
<!-- 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.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">Create and manage roles. Assign roles to users from the user list context menu.</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">Manage Roles</span></button>
<button class="btn-sm" id="post-role-menu-btn" data-i18n-title="settings.admin.role_menu.hint" title="Posts a message listing roles with an emoji each. Reacting with the emoji, or clicking the button under it, gives the reader the role; undoing it takes the role away.">🎭 <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">Default (highest channel role)</option><option value="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">Cached: 1 image, 0.0 MB <button class="btn-sm" id="media-cache-clear" style="margin-left:6px">Clear</button></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; opacity: 1;">
<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 class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
archive.org <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="archive.org">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
bsky.app <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="bsky.app">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
giphy.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="giphy.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
github.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="github.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
gitlab.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="gitlab.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
imgur.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="imgur.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
last.fm <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="last.fm">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
reddit.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="reddit.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
soundcloud.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="soundcloud.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
spotify.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="spotify.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
stackoverflow.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="stackoverflow.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
steamcommunity.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="steamcommunity.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
steampowered.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="steampowered.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
tenor.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="tenor.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
twitch.tv <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="twitch.tv">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
twitter.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="twitter.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
wikipedia.org <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="wikipedia.org">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
x.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="x.com">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
youtu.be <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="youtu.be">✕</button>
</div>
<div class="whitelist-item">
<span class="whitelist-username">
<strong style="color:var(--accent, #5865f2)">
ALLOW
</strong>
youtube.com <span class="muted-text">+ subdomains</span>
<span class="muted-text"> (Seeded default)</span>
</span>
<button class="btn-sm btn-danger-sm automod-domain-remove-btn" data-domain="youtube.com">✕</button>
</div>
</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_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"><p class="muted-text">Nothing blocked yet.</p></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">No whitelisted users</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.4;">-</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>
<span class="haven-pw-wrap"><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" data-eye-attached="1"><button type="button" class="haven-pw-eye" data-i18n-aria-label="auth.aria.show_password" aria-label="Show password" tabindex="-1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button></span>
</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.4;">-</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"><label style="display:flex;align-items:center;gap:6px;padding:3px 4px;font-size:0.85rem">
<input type="checkbox" class="default-join-channel-cb" data-cid="1" checked="">
<span>#gallery</span>
</label><label style="display:flex;align-items:center;gap:6px;padding:3px 4px;font-size:0.85rem">
<input type="checkbox" class="default-join-channel-cb" data-cid="2" checked="">
<span>#general</span>
</label></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>
<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"><label style="display:flex;align-items:center;gap:6px;padding:3px 4px;font-size:0.85rem">
<input type="checkbox" class="guest-channel-cb" data-cid="1">
<span>#gallery</span>
</label><label style="display:flex;align-items:center;gap:6px;padding:3px 4px;font-size:0.85rem">
<input type="checkbox" class="guest-channel-cb" data-cid="2">
<span>#general</span>
</label></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_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>
<!-- 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 Selected</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 (no passwords / 2FA / emails)</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 / settings alone are usually small. User passwords, 2FA secrets, and emails are never exported.</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">A backup can be restored here when it was made with Messages ticked: that is the whole database. Uploaded files is optional; when the backup has none, the uploads folder is left as it is. A channels/users/settings-only backup has no database to restore from.</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"><small class="settings-hint">No auto-backups yet.</small></div>
</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 -->
<!-- Role Management Modal -->
<!-- Role Members Modal (assign/remove role directly from role detail) -->
<!-- Assign Role Modal -->
<!-- Centralized Role Assignment (3-pane) Modal -->
<!-- Bot Management Modal -->
<!-- Sound Management Modal (Full Popout) -->
<!-- (soundboard sidebar panel is now in-flow before #right-sidebar) -->
<!-- Emoji Management Modal -->
<!-- Sticker Management Modal -->
<!-- Emoji Crop Modal -->
<!-- Ferry (Discord bridge) Modal -->
<!-- Audit Log Modal -->
<!-- All Members Modal -->
<!-- Invite Links Modal -->
<!-- Music Share Modal -->
<!-- Manage Servers Modal -->
<!-- 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="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="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="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="Holding the chosen roles is what gets you into this channel: any one of them, or all of them. People are added the moment they have the roles and taken out when they lose them. Anyone added by hand stays a member but only sees the channel while they hold the roles.">
<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="Role automatically assigned to channel members">
<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="dm-mute">🔔 <span data-i18n="context_menu.dm.mute">Mute DM</span></button>
<hr class="channel-ctx-sep">
<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 -->
<!-- Move Channel Picker Modal -->
<!-- DM Organize Modal -->
<!-- Webhook Management Modal -->
<!-- Poll Creation Modal -->
<!-- TEMPORARY (#5649): one-time admin notice that channel access moved to Required roles. Remove after the 4.8.x cycle. -->
<!-- Scheduled send (#5638): right-click Send, or /schedule -->
<!-- Timestamp Modal (/time with no args) -->
<!-- Channel Roles Modal -->
<!-- Sub-channel Subscriptions Panel -->
<!-- Hidden Channels Modal (admin declutter via "Hide Channel") -->
<!-- Create Sub-channel Modal -->
<!-- Stackable effect overlay container -->
<div id="fx-layers"></div>
<!-- Push Notification Error Modal (center-screen, requires acknowledgement) -->
<!-- Activities / Games Launcher Modal -->
<!-- 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">✕ Close</button>
</div>
</div>
<iframe id="game-iframe" class="game-iframe" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" allowfullscreen=""></iframe>
</div>
<!-- E2E Password Prompt Modal -->
<!-- Desktop App Promo Popup -->
<!-- Android App Promo Modal -->
<!-- Donors Thank You Modal -->
<!-- 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"></polyline></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"></polyline></svg>
</button>
</div>
<!-- Channel Media Gallery Modal (#5350) -->
<!-- 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. -->
<div id="profile-popup" class="profile-popup" data-anim-play="1" style="left: 484.984px; top: 8px;">
<div class="profile-popup-banner" style="background:linear-gradient(135deg, #82aaff44, #82aaff22)">
<button class="profile-popup-close" title="Close">×</button>
</div>
<div class="profile-popup-avatar-wrapper">
<div class="profile-popup-avatar profile-popup-avatar-fallback avatar-circle" style="background-color:#82aaff">F</div>
<span class="profile-popup-status-dot " title="Online"></span>
</div>
<div class="profile-popup-body">
<div class="profile-popup-names">
<span class="profile-popup-displayname">forumtester</span>
<span class="profile-popup-username">@forumtester</span>
</div>
<div id="profile-popup-status-slot"></div>
<div class="profile-popup-bio profile-bio-empty">No bio yet. Click Edit Profile to add one</div>
<div class="profile-popup-divider"></div>
<div id="profile-popup-activity-slot"></div>
<div class="profile-popup-section-label">Roles</div><div class="profile-popup-roles"><span class="profile-popup-role" style="border-color:#e74c3c; color:#e74c3c"><span class="profile-role-dot" style="background:#e74c3c"></span>Admin</span></div>
<div class="profile-popup-section-label">Member Since</div><div class="profile-popup-join-date">Sep 15, 2026</div>
<div class="profile-popup-actions"><button class="profile-popup-action-btn profile-edit-btn" id="profile-popup-edit-btn">✏️ Edit Profile</button><button class="profile-popup-action-btn profile-dm-btn" data-dm-uid="1" title="Notes to self">📝 Notes to self</button></div>
</div>
</div><template id="preview-chat"><div class="messages" id="messages" data-haven-region="message-list" style="--forum-shape: 1 / 1;"><div class="message" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:42:58Z" data-time-short="06:42 PM" data-msg-id="4">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:42 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:02)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:43:23Z" data-time-short="06:43 PM" data-msg-id="5" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:43 PM</span>
<div class="message-body">
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:03)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:44:24Z" data-time-short="06:44 PM" data-msg-id="6" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:44 PM</span>
<div class="message-body">
<div class="message-content"><div class="file-attachment voice-message">
<div class="file-info">🎤 <span class="file-name">Voice message</span> <span class="file-size">(0:03)</span></div>
<audio controls="" preload="metadata" src="" class="file-audio" data-haven-setup="1"></audio>
</div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:47Z" data-time-short="06:46 PM" data-msg-id="7">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Hey! Is this the place where we test <strong>themes</strong>?</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:48Z" data-time-short="06:46 PM" data-msg-id="8">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="chat-heading chat-h1">Welcome to Haven</div><br>This is a <em>sample</em> conversation for theme authors. It has <u>underline</u>, <del>strikethrough</del>, <mark class="chat-highlight">highlight</mark>, <code class="inline-code">inline code</code>, <span class="spoiler">a spoiler</span> and <span style="color:#FF00EF"> coloured text </span>, plus a link to <a href="https://example.com." target="_blank" rel="noopener noreferrer nofollow">https://example.com.</a></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:48Z" data-time-short="06:46 PM" data-msg-id="9" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content"><blockquote class="chat-blockquote"><div class="chat-blockquote-body">A quoted line<br>and a second one</div></blockquote><br><ul class="chat-list"><li>a bullet</li><li>another bullet</li></ul><br><br><ol class="chat-list" start="1"><li>first</li><li>second</li></ol></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:49Z" data-time-short="06:46 PM" data-msg-id="10">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><div class="code-block" data-lang="js"><span class="code-block-lang">js</span><pre><code>const haven = "themeable";
console.log(haven);</code></pre></div></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:49Z" data-time-short="06:46 PM" data-msg-id="11" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content"><div class="chat-table-wrap"><table class="chat-table"><thead><tr><th>Column A</th><th>Column B</th></tr></thead></table></div>| one | two |</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="message message-has-image message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:51Z" data-time-short="06:46 PM" data-msg-id="12">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content"><img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20480%20300%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%236b4fdb%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2343b581%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22480%22%20height%3D%22300%22%20fill%3D%22url(%23g)%22%2F%3E%3Ctext%20x%3D%22240%22%20y%3D%22162%22%20text-anchor%3D%22middle%22%20font-family%3D%22sans-serif%22%20font-size%3D%2234%22%20font-weight%3D%22700%22%20fill%3D%22%23fff%22%3Esample%20image%3C%2Ftext%3E%3C%2Fsvg%3E" class="chat-image" alt="image" data-lazy="pending" decoding="async"></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:52Z" data-time-short="06:46 PM" data-msg-id="13">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Nice picture 😄 and a mention for <span class="mention mention-self">@forumtester</span> so the highlight shows.</div>
<div class="reactions-row"><button class="reaction-badge own" data-emoji="👍" data-users="[&quot;forumtester&quot;,&quot;nova&quot;]" title="forumtester, nova">👍 2</button><button class="reaction-badge own" data-emoji="🎉" data-users="[&quot;forumtester&quot;]" title="forumtester">🎉 1</button></div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message message-user-sep" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:53Z" data-time-short="06:46 PM" data-msg-id="14">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#82aaff">F</div>
<div class="message-body">
<div class="reply-banner" data-reply-msg-id="13">
<span class="reply-line" style="background:#f47fff"></span>
<span class="reply-author" style="color:#f47fff">nova</span>
<span class="reply-preview">Nice picture 😄 and a mention for @forumtester so the highlight shows.</span>
</div>
<div class="message-header">
<span class="message-author" style="color:#e74c3c">forumtester</span>
<span class="user-role-badge msg-role-badge" style="color:#e74c3c" title="Admin">Admin</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">Replying to the one above, so the reply banner shows.</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div><div class="message-compact" data-user-id="1" data-username="forumtester" data-time="2026-09-15T22:46:54Z" data-time-short="06:46 PM" data-msg-id="15" data-avatar-shape="circle" data-animate-profile="trigger">
<span class="compact-time">06:46 PM</span>
<div class="message-body">
<div class="message-content">📊 Poll: Which theme do you use?</div>
<div class="poll-widget" data-msg-id="15">
<div class="poll-question">Which theme do you use?</div>
<div class="poll-options"><button class="poll-option" data-msg-id="15" data-option="0" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Braid</span>
<span class="poll-option-count">0 (0%)</span>
</button><button class="poll-option" data-msg-id="15" data-option="1" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Compact</span>
<span class="poll-option-count">0 (0%)</span>
</button><button class="poll-option" data-msg-id="15" data-option="2" title="">
<div class="poll-option-bar" style="width:0%"></div>
<span class="poll-option-text">Matrix</span>
<span class="poll-option-count">0 (0%)</span>
</button></div>
<div class="poll-footer">0 votes</div>
</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="edit" title="Edit"><span class="tb-icon tb-icon-emoji" aria-hidden="true">✏️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l4.5-1 9-9-3.5-3.5-9 9L4 20z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M13.5 6.5l3.5 3.5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div><div class="new-messages-divider" id="new-messages-divider"><span>NEW MESSAGES</span></div><div class="message message-user-sep" data-user-id="2" data-username="nova" data-time="2026-09-15T22:46:55Z" data-time-short="06:46 PM" data-msg-id="16">
<div class="message-row">
<div class="message-avatar avatar-circle" style="background-color:#f47fff">N</div>
<div class="message-body">
<div class="message-header">
<span class="message-author" style="color:#f47fff">nova</span>
<span class="message-time">Today at 06:46 PM</span>
<span class="message-header-spacer"></span>
</div>
<div class="message-content">And a plain closing line from the other person.</div>
</div>
<div class="msg-toolbar"><div class="msg-toolbar-group"><button data-action="react" title="React"><span class="tb-icon tb-icon-emoji" aria-hidden="true">😀</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke-width="1.8"></circle><path d="M8.5 14.5c1 1.2 2.2 1.8 3.5 1.8s2.5-.6 3.5-1.8" stroke-width="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></button><button data-action="reply" title="Reply"><span class="tb-icon tb-icon-emoji" aria-hidden="true">↩️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8L4 12L10 16" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="quote" title="Quote"><span class="tb-icon tb-icon-emoji" aria-hidden="true">💬</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7H5v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 7h-4v6h4l-2 4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div><div class="msg-toolbar-more"><button class="msg-toolbar-more-btn" type="button" aria-label="More Actions"><span class="tb-icon tb-icon-emoji" aria-hidden="true">⋯</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"></circle><circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none"></circle></svg></span></button><div class="msg-toolbar-overflow"><button data-action="thread" title="Thread"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🧵</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9h8" stroke-width="1.8" stroke-linecap="round"></path><path d="M8 13h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M6 6h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-8l-4 3v-3H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="pin" title="Pin"><span class="tb-icon tb-icon-emoji" aria-hidden="true">📌</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l-2 5v4l2 2H8l2-2V9L8 4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M12 15v5" stroke-width="1.8" stroke-linecap="round"></path></svg></span></button><button data-action="archive" title="Protect from cleanup"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🛡️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v11H4z" stroke-width="1.8" stroke-linejoin="round"></path><path d="M9 11h6" stroke-width="1.8" stroke-linecap="round"></path><path d="M3 7l2-3h14l2 3" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="delete" title="Delete"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🗑️</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14" stroke-width="1.8" stroke-linecap="round"></path><path d="M9 7V5h6v2" stroke-width="1.8" stroke-linecap="round"></path><path d="M7 7l1 12h8l1-12" stroke-width="1.8" stroke-linejoin="round"></path></svg></span></button><button data-action="copy-link" title="Copy link to message"><span class="tb-icon tb-icon-emoji" aria-hidden="true">🔗</span><span class="tb-icon tb-icon-mono" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></button></div></div></div>
<button class="msg-dots-btn" aria-label="Message actions">⋯</button>
</div>
</div></div></template><template id="preview-forum"><div class="messages forum-view" id="messages" data-haven-region="message-list" data-forum-tile="medium" style="--forum-tile: 11rem; --forum-shape: 1 / 1;"><div class="forum-toolbar" id="forum-toolbar">
<div class="forum-toolbar-row">
<button type="button" class="btn-sm btn-accent forum-new-post" id="forum-new-post">✏️ New Post</button>
<div class="forum-sort-view">
<select id="forum-sort" class="forum-select" title="Sort">
<option value="active" selected="">Recently active</option>
<option value="created">Date posted</option>
</select>
<div class="forum-view-toggle" role="group">
<button type="button" class="forum-view-btn active" data-view="list" title="List">☰</button>
<button type="button" class="forum-view-btn" data-view="gallery" title="Gallery">▦</button>
<button type="button" class="forum-view-btn" data-view="feed" title="Feed">▤</button>
</div>
<label class="forum-tile-size" hidden="">
<span>Tile size</span>
<input type="range" id="forum-tile-size" min="7" max="28" step="0.5" value="11" aria-label="Tile size">
</label>
<label class="forum-tile-size forum-tile-shape" hidden="">
<span>Shape</span>
<select id="forum-shape" class="forum-select forum-select-small" aria-label="Shape"><option value="square" selected="">Square</option><option value="4:3">4:3 wide</option><option value="3:4">3:4 tall</option><option value="3:2">3:2 wide</option><option value="2:3">2:3 tall</option><option value="16:9">16:9 wide</option><option value="9:16">9:16 tall</option></select>
</label>
<button type="button" class="btn-sm forum-mark-read" id="forum-mark-read" title="Clear the unread dots on every topic in this forum">Mark all read</button>
<button type="button" class="btn-sm forum-set-default" id="forum-set-default" title="Make the current view and tile size the layout everyone opens this forum in. Anyone who picks their own afterwards keeps it.">Set as default</button>
<button type="button" class="btn-sm forum-lock-layout" id="forum-lock-layout" title="Lock the layout: only people who can change this channel's settings can switch the view or tile shape. The size slider stays for everyone.">🔓</button>
</div>
</div>
</div><div class="forum-topics" id="forum-topics"><div class="forum-topic forum-topic-nsfw" data-msg-id="3" data-user-id="1" data-time="2026-09-15T21:57:31Z" data-username="forumtester">
<div class="forum-topic-avatar avatar-circle" style="background:#82aaff">F</div>
<div class="forum-topic-thumb" data-nsfw-label="NSFW, click to show"><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="chat-image forum-thumb-img" alt="" data-lazy="pending" decoding="async"></div>
<div class="forum-topic-body">
<div class="forum-topic-tags"><span class="forum-tag forum-tag-nsfw" title="NSFW">🔞</span></div>
<div class="forum-topic-title">Spicy picture</div>
<div class="forum-topic-snippet message-content">Picture preview text</div>
<div class="forum-topic-meta">
<span class="message-author forum-topic-author">forumtester</span>
<span class="forum-topic-replies" data-thread-parent="3">Reply to this topic</span>
<span class="forum-topic-when" title="9/15/2026, 5:57:31 PM">49m ago</span>
<button type="button" class="forum-topic-edit" title="Edit post">✎</button>
</div>
</div></div><div class="forum-topic" data-msg-id="2" data-user-id="1" data-time="2026-09-15T21:57:07Z" data-username="forumtester">
<div class="forum-topic-avatar avatar-circle" style="background:#82aaff">F</div>
<div class="forum-topic-thumb forum-topic-thumb-empty"><span>⬡</span></div>
<div class="forum-topic-body">
<div class="forum-topic-tags"></div>
<div class="forum-topic-title">Normal topic</div>
<div class="forum-topic-snippet message-content">Plain topic body text</div>
<div class="forum-topic-meta">
<span class="message-author forum-topic-author">forumtester</span>
<span class="forum-topic-replies" data-thread-parent="2">Reply to this topic</span>
<span class="forum-topic-when" title="9/15/2026, 5:57:07 PM">49m ago</span>
<button type="button" class="forum-topic-edit" title="Edit post">✎</button>
</div>
</div></div><div class="forum-topic forum-topic-nsfw" data-msg-id="1" data-user-id="1" data-time="2026-09-15T21:57:07Z" data-username="forumtester">
<div class="forum-topic-avatar avatar-circle" style="background:#82aaff">F</div>
<div class="forum-topic-thumb forum-topic-thumb-empty" data-nsfw-label="NSFW, click to show"><span>⬡</span></div>
<div class="forum-topic-body">
<div class="forum-topic-tags"><span class="forum-tag forum-tag-nsfw" title="NSFW">🔞</span></div>
<div class="forum-topic-title">Spicy topic</div>
<div class="forum-topic-snippet message-content">https://picsum.photos/seed/haven/400/300 A preview line that should be blurred until clicked</div>
<div class="forum-topic-meta">
<span class="message-author forum-topic-author">forumtester</span>
<span class="forum-topic-replies" data-thread-parent="1">Reply to this topic</span>
<span class="forum-topic-when" title="9/15/2026, 5:57:07 PM">49m ago</span>
<button type="button" class="forum-topic-edit" title="Edit post">✎</button>
</div>
</div></div></div><button class="btn-sm forum-load-more" id="forum-load-more" style="display: none;">Load more topics</button></div></template><style id="theme-preview-tools-style">
#theme-preview-tools { position: fixed; top: 8px; right: 8px; z-index: 2147483647; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 10px; background: rgba(12, 12, 20, 0.92); color: #eee; border: 1px solid #444; border-radius: 10px; font: 12px/1.3 system-ui, sans-serif; box-shadow: 0 8px 24px rgba(0,0,0,0.5); max-width: 46rem; }
#theme-preview-tools strong { margin-right: 4px; }
#theme-preview-tools label { display: flex; align-items: center; gap: 4px; }
#theme-preview-tools input { width: 15rem; padding: 4px 6px; border: 1px solid #555; border-radius: 6px; background: #1a1a24; color: #eee; font: inherit; }
#theme-preview-tools button { padding: 4px 8px; border: 1px solid #555; border-radius: 6px; background: #22222e; color: #eee; font: inherit; cursor: pointer; }
#theme-preview-tools button.active { background: #6b4fdb; border-color: #6b4fdb; color: #fff; }
#theme-preview-tools .sep { width: 1px; height: 18px; background: #444; }
#theme-preview-tools .hide { margin-left: auto; opacity: 0.7; }
#theme-preview-tools.folded > :not(.hide) { display: none; }
</style><div id="theme-preview-tools">
<strong>Theme preview</strong>
<label>Theme file <input id="preview-theme-path" placeholder="../themes/braid.theme.css" spellcheck="false"></label>
<button type="button" data-apply="1">Apply</button>
<button type="button" data-clear="1">Clear</button>
<span class="sep"></span>
<button type="button" data-view="chat" class="active">Chat</button>
<button type="button" data-view="forum">Forum</button>
<button type="button" data-toggle="#settings-modal">Settings modal</button>
<button type="button" data-toggle="#profile-popup">Profile card</button>
<button type="button" class="hide" data-fold="1">Hide</button></div><script src="theme-preview.js"></script></body></html>