Files
f0ckm/views/snippets/navbar.html
T
2026-09-28 19:03:31 +02:00

1868 lines
85 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.
@if(session)
<!-- logged in -->
<nav class="navbar navbar-expand-lg">
<!-- Brand: always visible, flush left -->
<a class="navbar-brand" href="/">
@if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" onload="if(window.syncNavbarHeight)window.syncNavbarHeight();" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;">
@else
<span class="f0ck">{{ custom_navbar_brand_text || domain }}</span>
@endif
</a>
<!-- Nav links: desktop always-on, mobile hidden until toggled -->
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
@if(!session.is_anon || (enable_anonymous_access && anon_permissions.upload))
<a id="nav-upload-link" style="cursor:pointer;"><i class="fa-solid fa-angle-up"></i> {{ t('nav.upload') }}</a>
@endif
@if(meme_creator)
<a id="nav-meme-link" href="/meme"><i class="fa-regular fa-image"></i> {{ t('nav.meme') }}</a>
@endif
@if(halls_enabled)
<div class="nav-user-dropdown nav-halls-dropdown">
<a href="/halls" class="nav-halls-btn" title="{{ t('nav.halls') }}">
<i class="fa-solid fa-layer-group"></i>
</a>
</div>
@endif
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
@if(session.can_chan)
<a href="/4/wsg/catalogue" id="nav-chan-link" title="4chan Viewer & Rehost" style="color: #4ade80;"><i class="fa-solid fa-clover"></i></a>
@endif
@if(abyss_enabled)
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
@endif
<a href="#" id="nav-search-btn" title="{{ t('nav.search') }}"><i class="fa-solid fa-magnifying-glass"></i></a>
@if(!/^\/\d$/.test(url.pathname))
<a href="/random" id="nav-random" title="{{ t('nav.random') }}"><i class="fa-solid fa-shuffle"></i></a>
@endif
</div>
</div>
<!-- Right controls: always visible, flush right -->
<div class="nav-right-group">
<!-- Avatar dropdown -->
<div class="nav-user-dropdown">
<button class="nav-user-btn nav-avatar-btn" id="nav-user-toggle">
@if(session.is_anon)
<span class="nav-avatar-icon" style="font-size: 1.2em; margin-right: 6px; display: inline-flex; align-items: center; cursor: pointer;" onclick="event.stopPropagation(); const u = '{{ session.login.toLowerCase() }}'; if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/' + u, true); } else { window.location.href='/user/' + u; }" title="{{ session.login }}"><i class="fa-solid fa-user-secret"></i></span>
@elseif(session.avatar_file)
<img class="nav-avatar-img" src="/a/{{ session.avatar_file }}" onerror="this.src='/a/default.png'" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
@elseif(session.avatar && session.avatar > 0)
<img class="nav-avatar-img" src="/t/{{ session.avatar }}.webp" onerror="this.src='/a/default.png'" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
@else
<img class="nav-avatar-img" src="/a/default.png" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
@endif
<span class="nav-avatar-name" id="nav-display-name" @if(session.username_color)style="color: {{ session.username_color }}"@endif>{!! session.display_name || session.user !!}</span>
<span class="nav-avatar-caret">▾</span>
</button>
<div class="nav-user-menu" id="nav-user-menu">
@if(session.is_anon)
<a href="/user/{{ session.login.toLowerCase() }}">{{ t('nav.profile') }}</a>
@else
<a href="/user/{!! session.user.toLowerCase() !!}">{{ t('nav.profile') }}</a>
@if(userhalls_enabled)
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
@endif
@endif
@if(enable_anonymous_access && session.is_anon)
<a href="#" id="nav-user-anon-identity-btn" style="white-space: nowrap;" onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }">Passkey Identity</a>
@endif
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
<div class="nav-user-divider"></div>
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
</div>
</div>
<!-- Notification bell -->
<div id="nav-notifications" class="nav-item-rel">
<a href="#" id="nav-notif-btn" title="Notifications">
<i class="fa-solid fa-bell"></i>
<span class="notif-count" style="display:none">0</span>
</a>
<div id="notif-dropdown" class="notif-dropdown">
<div class="notif-header">
<div class="notif-tabs">
@if(enable_comments)
<button class="notif-tab active" data-tab="user">{{ t('nav.notif_tab_user') }} <span class="notif-tab-badge" id="notif-tab-badge-user" style="display:none">0</span></button>
<button class="notif-tab" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="notif-tab-badge-system" style="display:none">0</span></button>
@else
<button class="notif-tab active" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="notif-tab-badge-system" style="display:none">0</span></button>
@endif
</div>
<button id="mark-all-read" title="{{ t('nav.mark_all_read') }}"><i class="fa-solid fa-check-double"></i></button>
</div>
@if(enable_comments)
<div class="notif-list" data-active-tab="user">
@else
<div class="notif-list" data-active-tab="system">
@endif
<div class="notif-empty">{{ t('nav.no_notifications') }}</div>
</div>
<div class="notif-footer">
<a href="/notifications" class="view-all-notifs">{{ t('nav.view_all_notifications') }}</a>
</div>
@if(enable_comments)
<div class="submanage">
<a href="/subscriptions">{{ t('nav.manage_subscriptions') }}</a>
</div>
@endif
</div>
</div>
<!-- Quick Favs -->
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" title="Favorites" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
<!-- DM -->
@if(private_messages && !session.is_anon)
<div id="nav-dm" class="nav-item-rel">
<a href="/messages" id="nav-dm-btn" title="Direct Messages">
<i class="fa-solid fa-envelope"></i>
<span id="dm-badge" class="notif-count-dm" style="display:none">0</span>
</a>
</div>
@endif
<!-- Quick Settings -->
<a href="/settings" title="Settings" class="desktop-only"><i class="fa-solid fa-gear"></i></a>
<!-- Filter -->
@if(!session.is_anon || (enable_anonymous_access && anon_permissions.filter))
<a href="#" id="nav-filter-btn" title="Filter" style="position: relative; display: inline-flex; align-items: center; justify-content: center; z-index: 1000;">
<i class="fa-solid fa-filter"></i>
@if(session.mode == 0)
<span id="nav-filter-badge" class="filter-badge filter-badge-sfw">SFW</span>
@elseif(session.mode == 1)
<span id="nav-filter-badge" class="filter-badge filter-badge-nsfw">NSFW</span>
@elseif(session.mode == 4)
<span id="nav-filter-badge" class="filter-badge filter-badge-nsfl">NSFL</span>
@elseif(session.mode == 2)
<span id="nav-filter-badge" class="filter-badge filter-badge-unt">UNT</span>
@elseif(session.mode == 3)
<span id="nav-filter-badge" class="filter-badge filter-badge-all">ALL</span>
@else
<span id="nav-filter-badge" class="filter-badge filter-badge-sfw">SFW</span>
@endif
</a>
@endif
<!-- Logout -->
<a href="/logout" title="Logout" class="desktop-only"><i class="fa-solid fa-right-from-bracket"></i></a>
<!-- Hamburger: mobile only -->
<button class="navbar-toggler" type="button" id="nav-toggler"
onclick="document.getElementById('navbarContent').classList.toggle('show'); this.classList.toggle('is-open')">
<span></span><span></span><span></span>
</button>
</div><!-- /.nav-right-group -->
<!-- MOTD: below the nav row -->
<div class="motd-container" id="motd-container" @if(typeof motd==='undefined' || motd==='' ) style="display:none" @endif>
<span class="motd-content" id="motd-display"></span>
<div id="motd-data" style="display:none">@if(typeof motd !== 'undefined'){!! motd !!}@endif</div>
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
</div>
</nav>
@if(session.admin || session._is_impersonating)
<!-- Admin Bar (Bottom, hotkey-activated) -->
<div class="admin-bar" id="admin-bar">
@if(session._is_impersonating)
<!-- Impersonation Banner -->
<div class="admin-bar-impersonate-notice">
<span class="admin-bar-imp-icon"><i class="fa-solid fa-user-secret"></i></span>
<span class="admin-bar-imp-text">Viewing as <strong>{!! session.user !!}</strong></span>
<span class="admin-bar-imp-hint">— all actions are performed as this user</span>
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
<i class="fa-solid fa-door-open"></i> Exit
</button>
<button class="admin-bar-close-btn" id="admin-bar-close-imp" title="Close admin bar (^ or ~)" aria-label="Close admin bar" style="margin-left: 8px;">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@else
<!-- Normal Admin Bar -->
<div class="admin-bar-inner">
<div class="admin-bar-brand">
<i class="fa-solid fa-shield-halved"></i>
<span>Admin</span>
</div>
<div class="admin-bar-links">
<a href="/admin" class="admin-bar-link" title="Admin panel">
<i class="fa-solid fa-cog"></i> Admin
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/admin/approve" class="admin-bar-link" title="Pending approvals">
<i class="fa-solid fa-clock"></i> Pending
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/mod" class="admin-bar-link" title="Moderator panel">
<i class="fa-solid fa-gavel"></i> Mod
</a>
<a href="/admin/audit" class="admin-bar-link" title="Audit log">
<i class="fa-solid fa-scroll"></i> Audit
</a>
</div>
<!-- Preview As role toggle (right-aligned) -->
<div class="admin-bar-preview" id="admin-bar-preview-wrap" style="margin-left: auto;">
<span class="admin-bar-preview-label"><i class="fa-solid fa-eye" style="opacity:0.6;"></i> Preview:</span>
<div class="admin-bar-preview-pills" id="admin-bar-preview-pills">
<button class="admin-bar-preview-pill active" data-role="admin" title="View as yourself (admin)">Admin</button>
<button class="admin-bar-preview-pill" data-role="mod" title="Simulate moderator view">Mod</button>
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
</div>
</div>
<button class="admin-bar-close-btn" id="admin-bar-close" title="Close admin bar (^ or ~)" aria-label="Close admin bar">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@endif
</div>
@endif
<!-- Floating exit-preview pill (visible only during role preview) -->
@if(session.admin)
<div id="admin-preview-exit-pill" style="display:none;">
<i class="fa-solid fa-eye"></i>
<span id="admin-preview-exit-label">Previewing as User</span>
<button id="admin-preview-exit-btn" title="Return to admin view"><i class="fa-solid fa-xmark"></i> Exit</button>
</div>
@endif
@else
<!-- not logged in -->
@if(!private_society)
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="/">
@if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" onload="if(window.syncNavbarHeight)window.syncNavbarHeight();" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;">
@else
<span class="f0ck">{{ custom_navbar_brand_text || domain }}</span>
@endif
</a>
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
<a href="/tags" title="Tags"><i class="fa-solid fa-tags"></i></a>
@if(halls_enabled)
<div class="">
<a href="/halls" class="nav-halls-btn" title="Halls">
<i class="fa-solid fa-layer-group"></i>
</a>
<div class="nav-user-menu">
<a href="/halls" style="font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 5px;"><i class="fa-solid fa-building-columns"></i> Overview</a>
@if(typeof halls !== 'undefined' && halls.length > 0)
@each(halls as h)
<a href="/h/{{ h.slug }}">{{ h.name }}</a>
@endeach
@else
<a href="/h/legendary">Legendary</a>
@endif
</div>
</div>
@endif
@if(abyss_enabled)
<a href="/abyss" title="Abyss"><i class="fa-solid fa-dice-d6"></i></a>
@endif
@if(!/^\/\d$/.test(url.pathname))
<a href="/random" id="nav-random" title="Random"><i class="fa-solid fa-shuffle"></i></a>
@endif
<a href="#" id="nav-search-btn" title="Search"><i class="fa-solid fa-magnifying-glass"></i></a>
</div>
</div>
<div class="nav-right-group">
<div class="nav-user-dropdown">
<button class="nav-user-btn" id="nav-visitor-toggle">
<i id="nav-visitor-icon" class="fa-solid @if(enable_anonymous_access && session && session.is_anon) fa-user-secret @else fa-user @endif"></i>
<span id="nav-anon-label">@if(enable_anonymous_access && session && session.is_anon)anonymous@else guest @endif</span>
<span class="nav-avatar-caret">▾</span>
</button>
<div class="nav-user-menu" id="nav-visitor-menu" style="min-width: max-content; width: max-content; white-space: nowrap;">
@if(enable_anonymous_access && session && session.is_anon)
<a href="/favs" id="nav-guest-favs-link" class="mobile-only">{{ t('nav.favs') }}</a>
@endif
@if(enable_anonymous_access)
<a href="#" id="nav-anon-identity-btn" @if(!session || !session.is_anon) style="display:none;" @endif onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-fingerprint"></i> Passkey Identity</a>
<a href="/settings" id="nav-anon-settings-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
@endif
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
<a href="#" id="nav-register-btn">{{ t('nav.register') }}</a>
@if(enable_anonymous_access)
<div class="nav-user-divider" id="nav-anon-divider" @if(!session || !session.is_anon) style="display:none;" @endif></div>
<a href="#" id="nav-anon-logout-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-right-from-bracket"></i> Logout (Guest)</a>
@endif
</div>
</div>
@if(enable_anonymous_access && session && session.is_anon)
<!-- Quick Favs for Anon -->
<a href="/favs" id="nav-guest-favs" title="{{ t('nav.favs') }}" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
@endif
<button class="navbar-toggler" type="button" id="nav-toggler"
onclick="document.getElementById('navbarContent').classList.toggle('show'); this.classList.toggle('is-open')">
<span></span><span></span><span></span>
</button>
</div>
<div class="motd-container" id="motd-container" @if(typeof motd==='undefined' || motd==='' ) style="display:none" @endif>
<span class="motd-content" id="motd-display"></span>
<div id="motd-data" style="display:none">@if(typeof motd !== 'undefined'){!! motd !!}@endif</div>
</div>
</nav>
@endif
@endif
<script>
// Passkey login for registered users — fires only on explicit button click
async function loginWithPasskey() {
const btn = document.getElementById('modal-login-passkey-btn');
const errEl = document.getElementById('modal-passkey-error');
if (errEl) errEl.style.display = 'none';
if (!window.PublicKeyCredential) {
if (errEl) { errEl.style.display = ''; errEl.textContent = 'Passkeys are not supported in this browser.'; }
return;
}
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Waiting for passkey...'; }
function b64urlToArr(s) { return Uint8Array.from(atob(s.split('-').join('+').split('_').join('/')), c=>c.charCodeAt(0)); }
function arrToB64url(buf) { return btoa(String.fromCharCode(...new Uint8Array(buf))).split('+').join('-').split('/').join('_').replace(/=+$/,''); }
try {
const beginRes = await fetch('/api/v2/settings/passkeys/login/begin', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' });
const beginData = await beginRes.json();
if (!beginData.success) throw new Error(beginData.msg || 'Server error');
const opts = beginData.options;
const cred = await navigator.credentials.get({ publicKey: {
challenge: b64urlToArr(opts.challenge),
rpId: opts.rpId,
allowCredentials: (opts.allowCredentials || []).map(c => ({ type: c.type, id: b64urlToArr(c.id) })),
userVerification: opts.userVerification || 'preferred',
timeout: opts.timeout || 60000
}});
const finishRes = await fetch('/api/v2/settings/passkeys/login/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: opts.challenge,
credentialId: arrToB64url(cred.rawId),
clientDataJSON: arrToB64url(cred.response.clientDataJSON),
authenticatorData: arrToB64url(cred.response.authenticatorData),
signature: arrToB64url(cred.response.signature)
})
});
const finishData = await finishRes.json();
if (!finishData.success) throw new Error(finishData.msg || 'Authentication failed');
window.location.reload();
} catch (e) {
console.warn('[PASSKEY] Login failed:', e);
if (errEl) {
errEl.style.display = '';
if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey request was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it or check extension permissions.';
} else {
errEl.textContent = (e && e.message) || 'Passkey login failed.';
}
}
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey'; }
}
}
</script>
<div id="login-modal" style="display: none;">
<div class="login-modal-content">
<button id="login-modal-close">&times;</button>
<!-- Login View -->
<div id="modal-login-view">
<div class="lm-header">
<h2 class="lm-title">{{ t('auth.login_title') }}</h2>
<div class="lm-subtitle">{{ domain }}</div>
</div>
<!-- Tab bar -->
<div id="login-tabs" class="lm-tabs" role="tablist">
<button type="button" id="login-tab-creds" class="lm-tab is-active" role="tab" aria-selected="true" onclick="switchLoginTab('creds')">
<i class="fa-solid fa-key"></i>Credentials
</button>
<button type="button" id="login-tab-passkey" class="lm-tab" role="tab" aria-selected="false" onclick="switchLoginTab('passkey')">
<i class="fa-solid fa-fingerprint"></i>Passkey
</button>
</div>
<!-- Tab: Credentials -->
<div id="login-panel-creds" class="lm-panel">
<form class="login-form lm-form" method="post" action="/login" novalidate>
<label class="lm-field">
<i class="fa-solid fa-user lm-field-icon"></i>
<input class="lm-input" type="text" name="username" placeholder="{{ t('auth.username_or_email') }}" aria-label="{{ t('auth.username_or_email') }}" autocomplete="username" autocapitalize="off" spellcheck="false" required />
</label>
<label class="lm-field">
<i class="fa-solid fa-lock lm-field-icon"></i>
<input class="lm-input" type="password" name="password" id="modal-login-password" placeholder="{{ t('auth.password_placeholder_min') }}" aria-label="{{ t('auth.password_placeholder_min') }}" autocomplete="current-password" required />
<button type="button" class="lm-reveal" onclick="toggleLoginPassword(this)" aria-label="Show password" title="Show password">
<i class="fa-solid fa-eye"></i>
</button>
</label>
<div class="lm-row">
<label class="lm-check" for="kmsi-modal">
<input type="checkbox" id="kmsi-modal" name="kmsi" />
<span class="lm-check-box"><i class="fa-solid fa-check"></i></span>
<span>{{ t('auth.stay_signed_in') }}</span>
</label>
@if(smtp_enabled)
<a href="#" id="modal-forgot-btn" class="lm-link lm-link-muted">{{ t('auth.forgot_password') }}</a>
@endif
</div>
<button type="submit" class="lm-btn lm-btn-primary">
{{ t('auth.login_title') }} <i class="fa-solid fa-arrow-right"></i>
</button>
</form>
</div>
<!-- Tab: Passkey -->
<div id="login-panel-passkey" class="lm-panel lm-panel-passkey" style="display: none;">
<div class="lm-passkey-icon"><i class="fa-solid fa-fingerprint"></i></div>
<p class="lm-passkey-text">
Use a saved passkey from Bitwarden, iCloud Keychain, or your OS to sign in without a password.
</p>
<button type="button" id="modal-login-passkey-btn" class="lm-btn lm-btn-primary" onclick="loginWithPasskey()">
<i class="fa-solid fa-fingerprint"></i> Use my passkey
</button>
<div id="modal-passkey-error" class="lm-error" style="display:none;"></div>
</div>
@if(enable_anonymous_access)
<div class="lm-anon">
<div class="lm-divider"><span>or</span></div>
<button type="button" id="modal-login-as-anon-btn" class="lm-btn lm-btn-ghost"
onclick="event.preventDefault(); if(window.f0ckAnonPasskey) window.f0ckAnonPasskey.openSetupModal(); else { const m=document.getElementById('anon-setup-modal'); if(m) m.style.display='flex'; }">
<i class="fa-solid fa-user-secret"></i> Login as anonymous
</button>
</div>
@endif
@if(registration_open || private_society)
<p class="lm-footer">
{{ t('auth.no_account') }} <a href="#" id="login-to-register" class="lm-link">{{ t('auth.register_now') }}</a>
</p>
@endif
</div>
<style>
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) {
--lm-accent: var(--accent, #0096ff);
--lm-text: var(--text-color, #fff);
--lm-muted: var(--text-muted, #8a8f98);
--lm-surface: rgba(255, 255, 255, 0.04);
--lm-border: rgba(255, 255, 255, 0.1);
--lm-radius: 0;
padding: 28px 28px 24px;
font-family: inherit;
color: var(--lm-text);
}
#login-modal .login-modal-content,
#register-modal .login-modal-content { border-radius: 0; }
/* Header */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-header { text-align: center; margin-bottom: 22px; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-title {
margin: 0; font-size: 1.5em; font-weight: 700; letter-spacing: -0.01em; color: var(--lm-text);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-subtitle {
margin-top: 4px; font-size: 0.82em; color: var(--lm-muted); letter-spacing: 0.02em;
}
/* Segmented tabs */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-tabs {
display: flex; gap: 4px; padding: 4px; margin-bottom: 20px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--lm-border);
border-radius: var(--lm-radius);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-tab {
flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
padding: 8px 0; margin: 0;
background: transparent; border: 0; border-radius: var(--lm-radius);
color: var(--lm-muted); font: inherit; font-size: 0.88em; font-weight: 600;
cursor: pointer; transition: background 0.18s, color 0.18s, box-shadow 0.18s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-tab:hover { color: var(--lm-text); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-tab.is-active {
color: var(--lm-text);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 2px 8px rgba(0, 0, 0, 0.35);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-tab.is-active i { color: var(--lm-accent); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-panel { animation: lm-fade 0.2s ease; }
@keyframes lm-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
/* Form */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) form.lm-form {
display: flex; flex-direction: column; gap: 12px;
padding: 0; margin: 0; background: none; border: 0; box-shadow: none; width: 100%;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-field { position: relative; display: block; margin: 0; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-field-icon {
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
color: var(--lm-muted); font-size: 0.9em; pointer-events: none; transition: color 0.15s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) input.lm-input {
width: 100%; box-sizing: border-box; height: 46px; margin: 0;
padding: 0 42px 0 40px;
background: var(--lm-surface);
border: 1px solid var(--lm-border);
border-radius: var(--lm-radius);
color: var(--lm-text); font: inherit; font-size: 0.95em;
outline: none; box-shadow: none;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) input.lm-input::placeholder { color: var(--lm-muted); opacity: 0.8; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) input.lm-input:hover { border-color: rgba(255, 255, 255, 0.18); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) input.lm-input:focus {
border-color: var(--lm-accent);
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--lm-accent) 22%, transparent);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-field:focus-within .lm-field-icon { color: var(--lm-accent); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-reveal {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
width: 34px; height: 34px; margin: 0; padding: 0;
display: flex; align-items: center; justify-content: center;
background: transparent; border: 0; border-radius: var(--lm-radius);
color: var(--lm-muted); cursor: pointer; transition: color 0.15s, background 0.15s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-reveal:hover { color: var(--lm-text); background: rgba(255, 255, 255, 0.06); }
/* Remember me + forgot */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-row {
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
font-size: 0.85em; margin: 2px 0 4px;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check {
display: inline-flex; align-items: center; gap: 9px; cursor: pointer; color: var(--lm-muted); user-select: none;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check-box {
width: 18px; height: 18px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.25); border-radius: var(--lm-radius);
background: var(--lm-surface); font-size: 0.7em; color: transparent;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check:hover .lm-check-box { border-color: var(--lm-accent); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check input:checked + .lm-check-box { background: var(--lm-accent); border-color: var(--lm-accent); color: #000; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check input:focus-visible + .lm-check-box { box-shadow: 0 0 0 3px color-mix(in srgb, var(--lm-accent) 30%, transparent); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-check input:checked ~ span:last-child { color: var(--lm-text); }
/* Links */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-link {
color: var(--lm-accent); text-decoration: none; font-weight: 600; transition: opacity 0.15s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-link:hover { text-decoration: underline; text-underline-offset: 3px; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-link-muted { color: var(--lm-muted); font-weight: 500; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-link-muted:hover { color: var(--lm-text); }
/* Buttons */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn {
width: 100%; height: 46px; margin: 0; padding: 0 16px;
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
border-radius: var(--lm-radius); font: inherit; font-size: 0.95em; font-weight: 700;
cursor: pointer; transition: transform 0.12s, box-shadow 0.18s, background 0.18s, border-color 0.18s, filter 0.18s;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-primary {
margin-top: 6px;
border: 1px solid var(--lm-accent); color: #000;
background: var(--lm-accent);
box-shadow: none;
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 800;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-primary:hover { background: transparent; color: var(--lm-accent); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-primary:active { transform: translateY(1px); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-primary .fa-arrow-right { font-size: 0.85em; transition: transform 0.18s; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-primary:hover .fa-arrow-right { transform: translateX(3px); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn:disabled { opacity: 0.6; cursor: progress; filter: none; transform: none; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn:focus-visible { outline: 2px solid var(--lm-accent); outline-offset: 2px; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-ghost {
background: var(--lm-surface); border: 1px solid var(--lm-border); color: var(--lm-muted); font-weight: 600;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-btn-ghost:hover { color: var(--lm-text); border-color: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.07); }
/* Passkey panel */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-panel-passkey { text-align: center; padding-top: 4px; }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-passkey-icon {
width: 76px; height: 76px; margin: 0 auto 16px;
display: flex; align-items: center; justify-content: center;
border-radius: var(--lm-radius); font-size: 2.2em; color: var(--lm-accent);
background: radial-gradient(circle, color-mix(in srgb, var(--lm-accent) 20%, transparent) 0%, transparent 70%);
border: 1px solid color-mix(in srgb, var(--lm-accent) 30%, transparent);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-passkey-text {
margin: 0 auto 20px; max-width: 320px; font-size: 0.88em; line-height: 1.6; color: var(--lm-muted);
}
/* Errors (also covers the .flash-error the login JS injects) */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-error,
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .flash-error {
margin: 10px 0 0; padding: 10px 12px; text-align: left;
font-size: 0.84em; line-height: 1.45; color: #ff8a8a;
background: rgba(224, 108, 117, 0.1);
border: 1px solid rgba(224, 108, 117, 0.3);
border-radius: var(--lm-radius);
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) form .flash-error { margin: 0; }
#register-modal #modal-register-view #register-status:empty { display: none; }
#register-modal #modal-register-view #register-status.flash-success {
padding: 10px 12px; font-size: 0.84em; line-height: 1.45; color: #7ee2a0;
background: rgba(74, 222, 128, 0.1);
border: 1px solid rgba(74, 222, 128, 0.3);
border-radius: var(--lm-radius);
}
/* Anon setup */
#anon-setup-modal #anon-setup-view .lm-note {
margin: 0 0 20px; padding: 9px 12px; text-align: left;
font-size: 0.8em; line-height: 1.5; color: var(--lm-muted);
background: var(--lm-surface); border: 1px solid var(--lm-border); border-left: 2px solid var(--lm-accent);
}
#anon-setup-modal #anon-setup-view .lm-note i { color: var(--lm-accent); margin-right: 4px; }
#anon-setup-modal #anon-setup-view .lm-textbtn {
margin: 0; padding: 0; background: none; border: 0;
font: inherit; font-weight: 600; color: var(--lm-accent); cursor: pointer;
}
#anon-setup-modal #anon-setup-view .lm-textbtn:hover { text-decoration: underline; text-underline-offset: 3px; }
/* Anon passkey identity */
#anon-passkey-modal #anon-passkey-view .lm-stat {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
margin: 0 0 16px; padding: 12px 14px;
background: var(--lm-surface); border: 1px solid var(--lm-border);
}
#anon-passkey-modal #anon-passkey-view .lm-stat-label {
font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--lm-muted);
}
#anon-passkey-modal #anon-passkey-view .lm-stat code { color: var(--lm-accent); font-size: 0.95em; }
/* Register-only */
#register-modal #modal-register-view .lm-check-multiline { align-items: flex-start; line-height: 1.5; }
#register-modal #modal-register-view .lm-check-multiline .lm-check-box { margin-top: 1px; }
#register-modal #modal-register-view .lm-recaptcha { display: flex; justify-content: center; }
/* Divider + footer */
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-divider {
display: flex; align-items: center; gap: 12px; margin: 20px 0 14px;
color: var(--lm-muted); font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.12em;
}
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-divider::before,
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-divider::after { content: ''; flex: 1; height: 1px; background: var(--lm-border); }
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) .lm-footer {
margin: 20px 0 0; padding-top: 16px; border-top: 1px solid var(--lm-border);
text-align: center; font-size: 0.88em; color: var(--lm-muted);
}
@media (prefers-reduced-motion: reduce) {
:is(#login-modal #modal-login-view, #register-modal #modal-register-view, #anon-setup-modal #anon-setup-view, #anon-passkey-modal #anon-passkey-view) * { animation: none !important; transition: none !important; }
}
</style>
<script>
function switchLoginTab(tab) {
var isCreds = tab === 'creds';
document.getElementById('login-panel-creds').style.display = isCreds ? '' : 'none';
document.getElementById('login-panel-passkey').style.display = isCreds ? 'none' : '';
[['login-tab-creds', isCreds], ['login-tab-passkey', !isCreds]].forEach(function (t) {
var el = document.getElementById(t[0]);
if (!el) return;
el.classList.toggle('is-active', t[1]);
el.setAttribute('aria-selected', t[1] ? 'true' : 'false');
});
}
function toggleLoginPassword(btn) {
var input = btn.parentNode.querySelector('input');
if (!input) return;
var show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.innerHTML = show ? '<i class="fa-solid fa-eye-slash"></i>' : '<i class="fa-solid fa-eye"></i>';
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
btn.title = show ? 'Hide password' : 'Show password';
input.focus();
}
</script>
@if(smtp_enabled)
<!-- Forgot Password View -->
<div id="modal-forgot-view" style="display: none;">
<form class="login-form" id="forgot-password-form" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.forgot_title') }}</h2>
<p style="text-align: center; margin-bottom: 20px; font-size: 0.9em; color: #ccc;">{{ t('auth.forgot_desc') }}</p>
<input type="email" id="forgot-email" placeholder="{{ t('auth.email_address') }}" required />
<div id="forgot-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<button type="submit">{{ t('auth.send_reset') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="forgot-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline;">{{ t('auth.back_to_login') }}</a>
</div>
</form>
</div>
@endif
<!-- Reset Password View -->
<div id="modal-reset-view" style="display: none;">
<form class="login-form" id="reset-password-form" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.reset_title') }}</h2>
<p style="text-align: center; margin-bottom: 20px; font-size: 0.9em; color: #ccc;">{{ t('auth.reset_desc') }}</p>
<input type="hidden" id="reset-token" />
<input type="password" id="reset-password" placeholder="{{ t('auth.new_password_min') }}" required minlength="20" autocomplete="new-password" />
<input type="password" id="reset-password-confirm" placeholder="{{ t('auth.confirm_new_password') }}" required minlength="20" autocomplete="new-password" />
<div id="reset-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<button type="submit">{{ t('auth.update_password') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="reset-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline; display: none;">{{ t('auth.back_to_login') }}</a>
</div>
</form>
</div>
</div>
</div>
@if(enable_anonymous_access)
<!-- Anonymous Passkey Identity Modal -->
<div id="anon-passkey-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
<div style="max-width: 380px; width: 92vw; max-height: 90vh; overflow-y: auto; padding: 0; border: 1px solid var(--accent); border-radius: 0; background: var(--bg); box-shadow: 0 0 20px rgba(0,0,0,0.5); position: relative;">
<button id="anon-passkey-modal-close" style="position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.3em; cursor: pointer; line-height: 1;">&times;</button>
<div id="anon-passkey-view">
<div class="lm-header">
<h2 class="lm-title">Passkeys</h2>
<div class="lm-subtitle"><span id="anon-pk-count">–</span> / <span id="anon-pk-max">4</span> in use</div>
</div>
<div class="lm-stat">
<span class="lm-stat-label">Fingerprint</span>
<code id="anon-pk-fp-display">—</code>
</div>
<button type="button" id="anon-pk-add-btn" class="lm-btn lm-btn-primary">
<i class="fa-solid fa-plus"></i> Add passkey
</button>
<div id="anon-pk-error" class="lm-error" style="display: none;"></div>
</div>
</div>
</div>
<script>
(function(){
// Keep legacy modal ID working for any nav onclick handlers that reference anon-ssh-modal
var legacyAlias = document.getElementById('anon-ssh-modal');
if (!legacyAlias) {
// Create a minimal alias element that delegates to the passkey modal
var alias = document.createElement('div');
alias.id = 'anon-ssh-modal';
alias.style.display = 'none';
Object.defineProperty(alias.style, 'display', {
set: function(v) { if (v === 'flex' || v === 'block') { var m = document.getElementById('anon-passkey-modal'); if (m) m.style.display = 'flex'; } }
});
document.body.appendChild(alias);
}
var modalClose = document.getElementById('anon-passkey-modal-close');
if (modalClose) {
modalClose.addEventListener('click', function() {
var m = document.getElementById('anon-passkey-modal');
if (m) m.style.display = 'none';
});
}
var overlay = document.getElementById('anon-passkey-modal');
if (overlay) {
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.style.display = 'none'; });
}
})();
</script>
@endif
@if(enable_anonymous_access)
<!-- Anonymous Passkey Setup Modal — shown when user clicks "Login as anonymous" -->
<div id="anon-setup-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
<div style="max-width: 420px; width: 92vw; padding: 0; border: 1px solid var(--accent); border-radius: 0; background: var(--bg); box-shadow: 0 0 20px rgba(0,0,0,0.5); position: relative;">
<button id="anon-setup-modal-close" style="position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.3em; cursor: pointer; line-height: 1;">&times;</button>
<div id="anon-setup-view">
<!-- New user view (no passkey yet) -->
<div id="anon-setup-new" class="lm-panel lm-panel-passkey">
<div class="lm-passkey-icon"><i class="fa-solid fa-fingerprint"></i></div>
<h2 class="lm-title">Create an anonymous passkey</h2>
<p class="lm-passkey-text" style="margin-top: 12px;">
Your browser will ask you to save a <strong>passkey</strong> — a secure credential stored in Bitwarden, iCloud Keychain, or your OS.
No account, no email, no password. Your comments and favorites are tied to this passkey.
</p>
<p class="lm-note">
<i class="fa-solid fa-rotate"></i> You can use it across devices if your passkey manager syncs (e.g. Bitwarden).
</p>
<button type="button" id="anon-setup-create-btn" class="lm-btn lm-btn-primary">
<i class="fa-solid fa-fingerprint"></i> Create my passkey
</button>
<div id="anon-setup-new-error" class="lm-error" style="display: none;"></div>
<p class="lm-footer">
<button type="button" id="anon-setup-switch-existing" class="lm-textbtn">Already have one? Use existing passkey</button>
</p>
</div>
<!-- Returning user view (has passkey) -->
<div id="anon-setup-returning" class="lm-panel lm-panel-passkey" style="display: none;">
<div class="lm-passkey-icon"><i class="fa-solid fa-user-secret"></i></div>
<h2 class="lm-title">Use your anonymous passkey</h2>
<p class="lm-passkey-text" style="margin-top: 12px;">
Pick your saved passkey from Bitwarden, iCloud Keychain, or your OS to continue as the same anonymous user.
</p>
<button type="button" id="anon-setup-auth-btn" class="lm-btn lm-btn-primary">
<i class="fa-solid fa-fingerprint"></i> Use my passkey
</button>
<div id="anon-setup-ret-error" class="lm-error" style="display: none;"></div>
<p class="lm-footer">
<button type="button" id="anon-setup-switch-new" class="lm-textbtn">Create a new passkey instead</button>
</p>
</div>
</div>
</div>
</div>
<script>
(function() {
function closeSetupModal() {
var m = document.getElementById('anon-setup-modal'); if (m) m.style.display = 'none';
}
var closeBtn = document.getElementById('anon-setup-modal-close');
if (closeBtn) closeBtn.addEventListener('click', closeSetupModal);
var overlay = document.getElementById('anon-setup-modal');
if (overlay) overlay.addEventListener('click', function(e) { if (e.target === overlay) closeSetupModal(); });
var switchToExisting = document.getElementById('anon-setup-switch-existing');
var switchToNew = document.getElementById('anon-setup-switch-new');
if (switchToExisting) switchToExisting.addEventListener('click', function() {
document.getElementById('anon-setup-new').style.display = 'none';
document.getElementById('anon-setup-returning').style.display = '';
});
if (switchToNew) switchToNew.addEventListener('click', function() {
document.getElementById('anon-setup-returning').style.display = 'none';
document.getElementById('anon-setup-new').style.display = '';
});
var createBtn = document.getElementById('anon-setup-create-btn');
if (createBtn) createBtn.addEventListener('click', async function() {
var errEl = document.getElementById('anon-setup-new-error');
if (errEl) errEl.style.display = 'none';
createBtn.disabled = true;
createBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving passkey...';
try {
await window.f0ckAnonPasskey.doRegister();
} catch(e) {
console.warn('[ANON_PASSKEY] Registration failed:', e);
if (errEl) {
errEl.style.display = '';
if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it or disable passkey interception.';
} else {
errEl.textContent = (e && e.message) || 'Something went wrong.';
}
}
createBtn.disabled = false;
createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create my passkey';
}
});
var authBtn = document.getElementById('anon-setup-auth-btn');
if (authBtn) authBtn.addEventListener('click', async function() {
var errEl = document.getElementById('anon-setup-ret-error');
if (errEl) errEl.style.display = 'none';
authBtn.disabled = true;
authBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Waiting for passkey...';
try {
await window.f0ckAnonPasskey.doAuthenticate();
} catch(e) {
console.warn('[ANON_PASSKEY] Authentication failed:', e);
if (errEl) {
errEl.style.display = '';
if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey request was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.';
} else {
errEl.textContent = (e && e.message) || 'Authentication failed.';
}
}
authBtn.disabled = false;
authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey';
}
});
})();
</script>
@endif
@if(!private_society || session)
<!-- Shortcuts Modal -->
<div id="shortcuts-modal" style="display: none;">
<div class="login-modal-content">
<button id="shortcuts-modal-close">&times;</button>
<div class="shortcuts-content">
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('shortcuts.title') }}</h2>
<div class="shortcut-list">
<div class="shortcut-item"><span><kbd>k</kbd></span><span>{{ t('shortcuts.search') }}</span></div>
<div class="shortcut-item"><span><kbd>m</kbd></span><span>{{ t('shortcuts.main_page') }}</span></div>
<div class="shortcut-item"><span><kbd>r</kbd></span><span>{{ t('shortcuts.random') }}</span></div>
<div class="shortcut-item"><span><kbd>z</kbd></span><span>{{ t('shortcuts.shuffle') }}</span></div>
<div class="shortcut-item"><span><kbd>u</kbd></span><span>{{ t('shortcuts.quick_upload') }}</span></div>
<div class="shortcut-item"><span><kbd>c</kbd></span><span>{{ t('shortcuts.toggle_comments') }}</span></div>
<div class="shortcut-item"><span><kbd>h</kbd></span><span>{{ t('shortcuts.toggle_sidebar') }}</span></div>
<div class="shortcut-item"><span><kbd>Ctrl</kbd> + <kbd>&#46;</kbd></span><span>{{ t('shortcuts.focus_comment') }}</span></div>
<div class="shortcut-item"><span><kbd>Ctrl</kbd> + <kbd>Enter</kbd></span><span>{{ t('shortcuts.send_comment') }}</span></div>
<div class="shortcut-item"><span><kbd>s</kbd></span><span>{{ t('shortcuts.flash_yank') }}</span></div>
@if(session)
<div class="shortcut-item"><span><kbd>e</kbd></span><span>{{ t('shortcuts.tag_exclude') }}</span></div>
<div class="shortcut-item"><span><kbd>i</kbd></span><span>{{ t('shortcuts.tag_input') }}</span></div>
@endif
<div class="shortcut-item"><span><kbd>l</kbd></span><span>{{ t('shortcuts.toggle_bg') }}</span></div>
<div class="shortcut-item"><span><kbd id="shortcut-theme">t</kbd></span><span>{{ t('shortcuts.cycle_themes') }}</span></div>
@if(session && session.admin)
<div class="shortcut-item"><span><kbd>x</kbd></span><span>{{ t('shortcuts.delete') }}</span></div>
<div class="shortcut-item"><span><kbd>p</kbd></span><span>{{ t('shortcuts.toggle_rating') }}</span></div>
<div class="shortcut-item"><span><kbd>^</kbd> / <kbd>~</kbd></span><span>Toggle Admin Bar</span></div>
@endif
<div class="shortcut-item"><span><kbd>Space</kbd></span><span>{{ t('shortcuts.play_pause') }}</span></div>
<div class="shortcut-item"><span><kbd>←</kbd> <kbd>→</kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
<div class="shortcut-item"><span><kbd>↑</kbd> <kbd>↓</kbd></span><span>{{ t('shortcuts.volume') }}</span></div>
<div class="shortcut-item"><span><kbd>Scroll</kbd></span><span>{{ t('shortcuts.scroll_nav') }}</span></div>
<div class="shortcut-item"><span><kbd>Shift</kbd> + <kbd>Scroll</kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
<div class="shortcut-item"><span><kbd>,</kbd> <kbd>.</kbd></span><span>{{ t('shortcuts.frame_skip') }}</span></div>
<div class="shortcut-item"><span><kbd>?</kbd></span><span>{{ t('shortcuts.show_help') }}</span></div>
</div>
</div>
</div>
</div>
<div id="flash-container"></div>
@endif
<!-- Register Modal -->
<div id="register-modal" style="display: none;">
<div class="login-modal-content">
<button id="register-modal-close">&times;</button>
<div id="modal-register-view">
<div class="lm-header">
<h2 class="lm-title">{{ t('auth.register_title') }}</h2>
<div class="lm-subtitle">{{ domain }}</div>
</div>
<form class="login-form lm-form" id="modal-register-form" method="post" action="/register" novalidate>
<div id="register-status"></div>
<label class="lm-field">
<i class="fa-solid fa-user lm-field-icon"></i>
<input class="lm-input" type="text" name="username" placeholder="{{ t('auth.username_placeholder') }}" aria-label="{{ t('auth.username_placeholder') }}" autocomplete="username" autocapitalize="off" spellcheck="false" required />
</label>
<label class="lm-field">
<i class="fa-solid fa-lock lm-field-icon"></i>
<input class="lm-input" type="password" name="password" placeholder="{{ t('auth.password_placeholder') }}" aria-label="{{ t('auth.password_placeholder') }}" autocomplete="new-password" required minlength="20"
title="Must be at least 20 characters long." />
<button type="button" class="lm-reveal" onclick="toggleLoginPassword(this)" aria-label="Show password" title="Show password">
<i class="fa-solid fa-eye"></i>
</button>
</label>
<label class="lm-field">
<i class="fa-solid fa-lock lm-field-icon"></i>
<input class="lm-input" type="password" name="password_confirm" placeholder="{{ t('auth.confirm_password') }}" aria-label="{{ t('auth.confirm_password') }}" autocomplete="new-password" required minlength="20" />
<button type="button" class="lm-reveal" onclick="toggleLoginPassword(this)" aria-label="Show password" title="Show password">
<i class="fa-solid fa-eye"></i>
</button>
</label>
@if(!registration_open)
<label class="lm-field">
<i class="fa-solid fa-ticket lm-field-icon"></i>
<input class="lm-input" type="text" name="token" placeholder="{{ t('auth.invite_token') }}" aria-label="{{ t('auth.invite_token') }}" autocomplete="off" spellcheck="false" required />
</label>
@elseif(registration_require_mail_andor_token)
<label class="lm-field">
<i class="fa-solid fa-envelope lm-field-icon"></i>
<input class="lm-input" type="email" name="email" placeholder="{{ t('auth.email_placeholder') }}" aria-label="{{ t('auth.email_placeholder') }}" autocomplete="email" required />
</label>
@endif
<input type="text" name="email_confirm_field" style="display: none !important;" tabindex="-1" autocomplete="off" />
<div class="lm-row">
<label class="lm-check lm-check-multiline" for="tos-modal">
<input type="checkbox" id="tos-modal" name="tos" required />
<span class="lm-check-box"><i class="fa-solid fa-check"></i></span>
<span>@if(private_society){{ t('auth.tos_private') }}@else{{ t('auth.tos_public') }} <a href="/terms" target="_blank" class="lm-link">{{ t('auth.tos_terms') }}</a>, <a href="/rules" target="_blank" class="lm-link">{{ t('auth.tos_rules') }}</a> {{ t('auth.tos_age') }}@endif</span>
</label>
</div>
@if(recaptcha_enabled)
<div id="modal-register-recaptcha" class="lm-recaptcha"></div>
@endif
<button type="submit" class="lm-btn lm-btn-primary">{{ t('auth.create_account') }}</button>
</form>
<p class="lm-footer">
<a href="#" id="register-to-login" class="lm-link">{{ t('auth.back_to_login') }}</a>
</p>
</div>
</div>
</div>
<!-- Global Drag & Drop Feature for Logged-in Users -->
@if(session)
<!-- Global Drop Overlay -->
<div id="drop-overlay">
<div class="overlay-content">
<i class="fa-solid fa-cloud-arrow-up" style="font-size: 5rem;"></i>
<h2>{{ t('upload.drop_anywhere') }}</h2>
</div>
</div>
<!-- Upload Drag Modal -->
<div id="upload-drag-modal">
<div class="modal-content">
<button class="modal-close" id="drag-modal-close" title="Cancel Upload">&times;</button>
<div class="modal-body">
<div class="upload-container" style="padding: 0; animation: none; opacity: 1;">
<div class="upload-title">{{ t('upload.title') }}</div>
<div class="upload-limit-info">
@if(session.uploads_remaining === undefined)
<span class="limit-unlimited"><i class="fa-solid fa-infinity"></i> {{ t('upload.unlimited') }}</span>
@elseif(session.uploads_remaining === 0)
<span class="limit-exhausted"><i class="fa-solid fa-triangle-exclamation"></i> {{ t('upload.limit_reached') }} (0/{{ upload_limit }})</span>
@else
<span class="limit-remaining"><i class="fa-solid fa-upload"></i> {{ session.uploads_remaining }}/{{ upload_limit }} {{ t('upload.remaining') }}</span>
@endif
</div>
@include(snippets/upload-form)
</div>
</div>
</div>
</div>
@endif
@if(recaptcha_enabled && !session)
<script>
(function() {
var _rcModalWidgetId = null;
var _rcLoaded = false;
function renderModalRecaptcha() {
var el = document.getElementById('modal-register-recaptcha');
if (!el || !window.grecaptcha) return;
if (_rcModalWidgetId !== null) {
try { grecaptcha.reset(_rcModalWidgetId); } catch(e) {}
} else {
_rcModalWidgetId = grecaptcha.render(el, { sitekey: '{{ recaptcha_site_key }}', theme: 'dark' });
}
}
window.onRecaptchaModalReady = function() {
_rcLoaded = true;
renderModalRecaptcha();
if (typeof window.renderReportRecaptcha === 'function') window.renderReportRecaptcha();
};
document.addEventListener('DOMContentLoaded', function() {
var modal = document.getElementById('register-modal');
if (!modal) return;
var obs = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.attributeName === 'style') {
if (modal.style.display !== 'none' && _rcLoaded) renderModalRecaptcha();
}
});
});
obs.observe(modal, { attributes: true });
});
})();
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaModalReady&render=explicit" async defer></script>
@endif
@if(session && (session.admin || session._is_impersonating))
<style>
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
.admin-bar {
position: fixed;
bottom: 0;
left: var(--admin-bar-left, 0px);
right: var(--admin-bar-right, 300px);
width: auto;
font-family: var(--font, system-ui, sans-serif);
font-size: 0.76rem;
z-index: 100080 !important;
box-shadow: 0 -3px 14px rgba(0, 0, 0, 0.5);
border-top: 1px solid rgba(255, 190, 0, 0.28);
border-bottom: none;
transform: translateY(100%);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.22s ease,
left 0.3s ease-in-out,
right 0.3s ease-in-out;
}
.admin-bar.open {
transform: translateY(0);
opacity: 1;
visibility: visible;
pointer-events: auto;
}
body.onara-modal-open .admin-bar {
z-index: 100080 !important;
filter: none !important;
}
body.onara-modal-open .admin-bar.open {
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
}
/* Sidebar boundaries via CSS */
body.sidebar-right-hidden .admin-bar {
right: 0 !important;
}
@media (max-width: 999px) {
.admin-bar {
right: min(280px, 80vw);
}
body.sidebar-right-hidden .admin-bar {
right: 0 !important;
}
}
body:not(:has(.global-sidebar-right, .item-sidebar-right, .index-sidebar-right)) .admin-bar {
right: 0 !important;
}
@media (min-width: 1000px) {
body.layout-modern:not(.sidebar-left-hidden):not(.comments-hidden):has(.item-sidebar-left) .admin-bar {
left: min(350px, 30vw);
}
body.layout-modern.sidebar-left-hidden .admin-bar,
body.layout-modern.comments-hidden .admin-bar,
body:has(.item-layout-container.sidebar-hidden) .admin-bar {
left: 0 !important;
}
}
/* Lift bottom pagination smoothly if admin bar is open */
body.admin-bar-open .pagination-container-fluid {
bottom: 32px;
}
.pagination-container-fluid {
transition: right 0.3s ease-in-out, bottom 0.3s ease-in-out !important;
}
.admin-bar-close-btn {
background: transparent;
border: none;
color: rgba(255, 210, 80, 0.45);
cursor: pointer;
padding: 3px 7px;
font-size: 0.8rem;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
transition: color 0.15s, background 0.15s;
flex-shrink: 0;
line-height: 1;
}
.admin-bar-close-btn:hover {
color: #ffd844;
background: rgba(255, 200, 0, 0.15);
}
/* Normal admin bar */
.admin-bar-inner {
background: linear-gradient(90deg, #1a1200 0%, #1e1600 40%, #141000 100%);
display: flex;
align-items: center;
gap: 6px;
padding: 0 14px;
height: 32px;
overflow: hidden;
}
.admin-bar-brand {
display: flex;
align-items: center;
gap: 5px;
color: #f5c400;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
font-size: 0.72rem;
opacity: 0.9;
white-space: nowrap;
padding-right: 4px;
}
.admin-bar-brand i { font-size: 0.78rem; }
.admin-bar-links {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.admin-bar-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: rgba(255, 210, 80, 0.75) !important;
text-decoration: none !important;
padding: 2px 8px;
border-radius: 3px;
font-size: 0.74rem;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
position: relative;
}
.admin-bar-link:hover {
background: rgba(255, 200, 0, 0.12);
color: #ffd844 !important;
}
.admin-bar-badge {
background: #e05020;
color: #fff;
border-radius: 9px;
font-size: 0.65rem;
font-weight: 700;
padding: 0 5px;
min-width: 16px;
text-align: center;
line-height: 16px;
display: inline-block;
}
.admin-bar-sep {
flex: 1;
height: 14px;
border-left: 1px solid rgba(255, 200, 0, 0.12);
margin: 0 4px;
flex-basis: 0;
min-width: 0;
}
/* View-as widget */
.admin-bar-viewas {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
flex-shrink: 0;
}
.admin-bar-viewas-input-wrap {
position: relative;
}
.admin-bar-input {
background: rgba(255, 200, 0, 0.07);
border: 1px solid rgba(255, 200, 0, 0.2);
border-radius: 4px;
color: #ffd844;
font-size: 0.73rem;
font-family: var(--font, system-ui, sans-serif);
padding: 2px 8px;
width: 160px;
outline: none;
transition: border-color 0.15s, width 0.2s;
}
.admin-bar-input::placeholder { color: rgba(255,210,80,0.35); }
.admin-bar-input:focus {
border-color: rgba(255, 200, 0, 0.55);
width: 210px;
}
.admin-bar-viewas-btn {
background: rgba(255, 200, 0, 0.12);
border: 1px solid rgba(255, 200, 0, 0.3);
border-radius: 4px;
color: #ffd844;
cursor: pointer;
padding: 2px 8px;
font-size: 0.72rem;
transition: background 0.15s, border-color 0.15s;
height: 22px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.admin-bar-viewas-btn:hover {
background: rgba(255, 200, 0, 0.22);
border-color: rgba(255, 200, 0, 0.55);
}
.admin-bar-viewas-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Autocomplete suggestions */
.admin-viewas-suggestions {
position: absolute;
top: calc(100% + 3px);
left: 0;
right: 0;
background: #1a1600;
border: 1px solid rgba(255, 200, 0, 0.3);
border-radius: 4px;
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
z-index: 100090 !important;
display: none;
overflow: hidden;
min-width: 180px;
}
.admin-viewas-suggestions.open { display: block; }
.admin-viewas-suggestion {
padding: 5px 10px;
color: rgba(255, 210, 80, 0.85);
cursor: pointer;
font-size: 0.76rem;
transition: background 0.1s;
display: flex;
align-items: center;
gap: 6px;
}
.admin-viewas-suggestion:hover,
.admin-viewas-suggestion.active {
background: rgba(255, 200, 0, 0.14);
color: #ffd844;
}
.admin-viewas-suggestion i { opacity: 0.5; font-size: 0.72rem; }
/* Impersonation banner */
.admin-bar-impersonate-notice {
background: linear-gradient(90deg, #1a0800 0%, #200c00 40%, #180a00 100%);
border-top: 1px solid rgba(255, 120, 0, 0.35);
display: flex;
align-items: center;
gap: 8px;
padding: 0 14px;
height: 30px;
overflow: hidden;
animation: admin-imp-pulse 3s ease-in-out infinite;
}
@keyframes admin-imp-pulse {
0%, 100% { border-top-color: rgba(255, 120, 0, 0.35); }
50% { border-top-color: rgba(255, 160, 0, 0.7); }
}
.admin-bar-imp-icon {
color: #ff8c00;
font-size: 0.85rem;
animation: admin-imp-wiggle 4s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes admin-imp-wiggle {
0%, 90%, 100% { transform: rotate(0deg); }
92% { transform: rotate(-8deg); }
96% { transform: rotate(8deg); }
}
.admin-bar-imp-text {
color: #ffb84d;
font-size: 0.77rem;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.admin-bar-imp-text strong { color: #ffcc00; }
.admin-bar-imp-hint {
color: rgba(255, 160, 60, 0.55);
font-size: 0.71rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.admin-bar-exit-btn {
margin-left: auto;
flex-shrink: 0;
background: rgba(255, 80, 0, 0.18);
border: 1px solid rgba(255, 100, 0, 0.45);
border-radius: 4px;
color: #ff8c40;
cursor: pointer;
padding: 2px 10px;
font-size: 0.73rem;
font-family: var(--font, system-ui, sans-serif);
font-weight: 600;
letter-spacing: 0.02em;
transition: background 0.15s, border-color 0.15s, color 0.15s;
display: inline-flex;
align-items: center;
gap: 5px;
height: 22px;
}
.admin-bar-exit-btn:hover {
background: rgba(255, 80, 0, 0.35);
border-color: rgba(255, 120, 0, 0.7);
color: #ffaa66;
}
/* Preview As role pills */
.admin-bar-preview {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.admin-bar-preview-label {
color: rgba(255,210,80,0.55);
font-size: 0.72rem;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.admin-bar-preview-pills {
display: flex;
gap: 2px;
}
.admin-bar-preview-pill {
background: rgba(255,200,0,0.07);
border: 1px solid rgba(255,200,0,0.18);
border-radius: 3px;
color: rgba(255,210,80,0.55);
cursor: pointer;
font-size: 0.71rem;
font-family: var(--font, system-ui, sans-serif);
padding: 1px 7px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.admin-bar-preview-pill:hover {
background: rgba(255,200,0,0.16);
color: #ffd844;
border-color: rgba(255,200,0,0.4);
}
.admin-bar-preview-pill.active {
background: rgba(255,200,0,0.22);
color: #ffd844;
border-color: rgba(255,200,0,0.55);
font-weight: 600;
}
/* Floating exit-preview pill */
#admin-preview-exit-pill {
position: fixed;
bottom: 18px;
right: 18px;
z-index: 99999;
background: #1a1200;
border: 1px solid rgba(255,200,0,0.45);
border-radius: 20px;
color: #ffd844;
font-size: 0.76rem;
font-family: var(--font, system-ui, sans-serif);
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
animation: preview-pill-in 0.2s ease;
}
@keyframes preview-pill-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
#admin-preview-exit-pill span { font-weight: 600; }
#admin-preview-exit-btn {
background: rgba(255,200,0,0.15);
border: 1px solid rgba(255,200,0,0.35);
border-radius: 10px;
color: #ffd844;
cursor: pointer;
font-size: 0.71rem;
font-family: var(--font, system-ui, sans-serif);
padding: 1px 10px;
display: inline-flex;
align-items: center;
gap: 4px;
transition: background 0.15s;
}
#admin-preview-exit-btn:hover { background: rgba(255,200,0,0.3); }
/* Body class rules: simulate role views */
body.preview-as-user .admin-bar,
body.preview-as-mod .admin-bar { display: none !important; }
/* Hide all owner/admin/mod actions in user preview */
body.preview-as-user #a_delete,
body.preview-as-user #a_rethumb,
body.preview-as-user .removetag,
body.preview-as-user .can-cycle,
body.preview-as-user .info-title-edit-wrap,
body.preview-as-user #info-title-save,
body.preview-as-user #info-title-status,
body.preview-as-user #info-visibility-edit-btn,
body.preview-as-user #info-rethumb-btn,
body.preview-as-user #info-set-expiry-btn,
body.preview-as-user .infobox-action-btn,
body.preview-as-user .btn-infobox-action,
body.preview-as-user .admin-delete-btn,
body.preview-as-user .admin-edit-btn,
body.preview-as-user .admin-pin-btn,
body.preview-as-user .delete-btn,
body.preview-as-user .poll-delete-btn,
body.preview-as-user .item_title_text { display: inline !important; cursor: default !important; pointer-events: none !important; }
body.preview-as-user .item_title_text.no-title { display: none !important; }
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */
body.preview-as-mod #a_rethumb,
body.preview-as-mod .removetag,
body.preview-as-mod #info-rethumb-btn,
body.preview-as-mod .btn-infobox-action { display: none !important; }
@media (max-width: 640px) {
.admin-bar-imp-hint { display: none; }
.admin-bar-links a span:not(.admin-bar-badge) { display: none; }
.admin-bar-input { width: 110px; }
.admin-bar-input:focus { width: 140px; }
.admin-bar-preview-label { display: none; }
}
</style>
<script>
(function() {
'use strict';
/* ── Admin bar: Sidebar Width Awareness & Hotkey Toggle ────────────────── */
const adminBar = document.getElementById('admin-bar');
const closeBtn = document.getElementById('admin-bar-close');
const closeImpBtn = document.getElementById('admin-bar-close-imp');
function syncAdminBarSidebars() {
if (!adminBar) return;
// Right sidebar detection
const rightSb = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
const isRightHidden = !rightSb ||
document.body.classList.contains('sidebar-right-hidden') ||
window.getComputedStyle(rightSb).display === 'none' ||
window.getComputedStyle(rightSb).visibility === 'hidden';
let rw = 0;
if (!isRightHidden && rightSb) {
rw = rightSb.offsetWidth || (window.innerWidth <= 999 ? Math.min(280, Math.round(window.innerWidth * 0.8)) : 300);
}
// Left sidebar detection (modern layout item page)
const leftSb = document.querySelector('.item-sidebar-left');
const isLeftHidden = !leftSb ||
document.body.classList.contains('sidebar-left-hidden') ||
document.body.classList.contains('comments-hidden') ||
window.innerWidth < 1000 ||
window.getComputedStyle(leftSb).display === 'none' ||
window.getComputedStyle(leftSb).visibility === 'hidden' ||
!!document.querySelector('.item-layout-container.sidebar-hidden');
let lw = 0;
if (!isLeftHidden && leftSb) {
lw = leftSb.offsetWidth || Math.min(350, Math.round(window.innerWidth * 0.3));
}
adminBar.style.setProperty('--admin-bar-right', rw + 'px');
adminBar.style.setProperty('--admin-bar-left', lw + 'px');
adminBar.style.right = rw + 'px';
adminBar.style.left = lw + 'px';
}
function openAdminBar() {
if (!adminBar) return;
syncAdminBarSidebars();
adminBar.classList.add('open');
document.body.classList.add('admin-bar-open');
}
function closeAdminBar() {
if (!adminBar) return;
adminBar.classList.remove('open');
document.body.classList.remove('admin-bar-open');
}
function toggleAdminBar() {
if (!adminBar) return;
if (adminBar.classList.contains('open')) {
closeAdminBar();
} else {
openAdminBar();
}
}
if (closeBtn) closeBtn.addEventListener('click', closeAdminBar);
if (closeImpBtn) closeImpBtn.addEventListener('click', closeAdminBar);
window.addEventListener('keydown', (e) => {
// Never trigger if focus is inside an input, textarea, select or contenteditable
const target = e.target;
const tag = (target && target.tagName) ? target.tagName.toUpperCase() : '';
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (target && target.isContentEditable)) {
return;
}
// Hotkey: '^' or '~' (also match Backquote key without modifier keys)
const isHotkey = (
e.key === '^' ||
e.key === '~' ||
(e.code === 'Backquote' && !e.ctrlKey && !e.metaKey && !e.altKey)
);
if (isHotkey) {
e.preventDefault();
toggleAdminBar();
return;
}
// Escape closes admin bar if open
if (e.key === 'Escape' && adminBar && adminBar.classList.contains('open')) {
closeAdminBar();
}
});
// Watch body class changes for sidebar toggles
if (window.MutationObserver) {
const bodyObserver = new MutationObserver(() => {
syncAdminBarSidebars();
});
bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
}
window.addEventListener('resize', syncAdminBarSidebars, { passive: true });
document.addEventListener('f0ck:contentLoaded', syncAdminBarSidebars);
document.addEventListener('transitionend', (e) => {
if (e.target && (e.target.classList.contains('global-sidebar-right') ||
e.target.classList.contains('item-sidebar-right') ||
e.target.classList.contains('item-sidebar-left'))) {
syncAdminBarSidebars();
}
});
/* ── Admin bar: View-As (impersonation) ────────────────────────────────── */
const input = document.getElementById('admin-viewas-input');
const suggestBox = document.getElementById('admin-viewas-suggestions');
const viewAsBtn = document.getElementById('admin-viewas-btn');
const exitBtn = document.getElementById('admin-bar-exit-impersonate');
let _suggestTimer = null;
let _suggestions = [];
let _activeIdx = -1;
function closeSuggestions() {
if (suggestBox) { suggestBox.innerHTML = ''; suggestBox.classList.remove('open'); }
_suggestions = [];
_activeIdx = -1;
}
function renderSuggestions(list) {
if (!suggestBox) return;
suggestBox.innerHTML = '';
if (!list || list.length === 0) { suggestBox.classList.remove('open'); return; }
list.forEach((u, i) => {
const el = document.createElement('div');
el.className = 'admin-viewas-suggestion';
el.innerHTML = '<i class="fa-solid fa-user"></i>' + u.user;
el.dataset.username = u.user;
el.addEventListener('mousedown', (e) => { e.preventDefault(); selectSuggestion(u.user); });
el.addEventListener('mouseenter', () => { setActive(i); });
suggestBox.appendChild(el);
});
_suggestions = list;
_activeIdx = -1;
suggestBox.classList.add('open');
}
function setActive(idx) {
const items = suggestBox ? suggestBox.querySelectorAll('.admin-viewas-suggestion') : [];
items.forEach((el, i) => el.classList.toggle('active', i === idx));
_activeIdx = idx;
}
function selectSuggestion(username) {
if (input) input.value = username;
closeSuggestions();
}
function fetchSuggestions(q) {
if (!q || q.length < 1) { closeSuggestions(); return; }
fetch('/api/v2/admin/users/search?q=' + encodeURIComponent(q), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(r => r.json()).then(data => {
if (Array.isArray(data)) renderSuggestions(data);
}).catch(() => {});
}
if (input) {
input.addEventListener('input', () => {
clearTimeout(_suggestTimer);
const q = input.value.trim();
if (!q) { closeSuggestions(); return; }
_suggestTimer = setTimeout(() => fetchSuggestions(q), 200);
});
input.addEventListener('keydown', (e) => {
const items = suggestBox ? suggestBox.querySelectorAll('.admin-viewas-suggestion') : [];
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive(Math.min(_activeIdx + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActive(Math.max(_activeIdx - 1, -1));
} else if (e.key === 'Enter') {
e.preventDefault();
if (_activeIdx >= 0 && _suggestions[_activeIdx]) {
selectSuggestion(_suggestions[_activeIdx].user);
} else {
startImpersonation();
}
} else if (e.key === 'Escape') {
closeSuggestions();
input.blur();
}
});
input.addEventListener('blur', () => {
setTimeout(closeSuggestions, 150);
});
}
function startImpersonation() {
const username = input ? input.value.trim() : '';
if (!username) return;
if (viewAsBtn) { viewAsBtn.disabled = true; }
const csrf = (typeof window.f0ckSession !== 'undefined' && window.f0ckSession.csrf_token)
? window.f0ckSession.csrf_token
: (document.querySelector('meta[name="csrf-token"]')?.content || '');
const params = new URLSearchParams();
params.append('username', username);
if (csrf) params.append('csrf_token', csrf);
fetch('/api/v2/admin/impersonate', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
}).then(r => r.json()).then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Impersonation failed: ' + (data.msg || 'Unknown error'));
if (viewAsBtn) viewAsBtn.disabled = false;
}
}).catch(e => {
alert('Network error: ' + e.message);
if (viewAsBtn) viewAsBtn.disabled = false;
});
}
if (viewAsBtn) {
viewAsBtn.addEventListener('click', startImpersonation);
}
/* ── Exit impersonation ─────────────────────────────────────────────────── */
if (exitBtn) {
exitBtn.addEventListener('click', () => {
exitBtn.disabled = true;
exitBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Exiting…';
const csrf = (typeof window.f0ckSession !== 'undefined' && window.f0ckSession.csrf_token)
? window.f0ckSession.csrf_token
: (document.querySelector('meta[name="csrf-token"]')?.content || '');
const params = new URLSearchParams();
if (csrf) params.append('csrf_token', csrf);
fetch('/api/v2/admin/stop-impersonate', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
}).then(() => {
window.location.reload();
}).catch(() => {
window.location.reload();
});
});
}
/* ── Admin bar: Preview As role toggle ──────────────────────────────────── */
(function() {
const STORAGE_KEY = 'f0ck_preview_role';
const pills = document.querySelectorAll('.admin-bar-preview-pill');
const exitPill = document.getElementById('admin-preview-exit-pill');
const exitLabel = document.getElementById('admin-preview-exit-label');
const exitBtn = document.getElementById('admin-preview-exit-btn');
const roleLabels = { admin: 'Admin', mod: 'Moderator', user: 'User' };
function applyRole(role) {
document.body.classList.remove('preview-as-user', 'preview-as-mod');
if (role === 'user') document.body.classList.add('preview-as-user');
if (role === 'mod') document.body.classList.add('preview-as-mod');
// Update active pill
pills.forEach(p => p.classList.toggle('active', p.dataset.role === role));
// Show/hide exit pill
if (exitPill) {
if (role !== 'admin') {
if (exitLabel) exitLabel.textContent = 'Previewing as ' + roleLabels[role];
exitPill.style.display = 'flex';
} else {
exitPill.style.display = 'none';
}
}
}
// Init from localStorage
const saved = localStorage.getItem(STORAGE_KEY) || 'admin';
applyRole(saved);
// Pill click
pills.forEach(pill => {
pill.addEventListener('click', () => {
const role = pill.dataset.role;
localStorage.setItem(STORAGE_KEY, role);
applyRole(role);
});
});
// Exit button
if (exitBtn) {
exitBtn.addEventListener('click', () => {
localStorage.setItem(STORAGE_KEY, 'admin');
applyRole('admin');
});
}
})();
})();
</script>
@endif