esifawafwu

This commit is contained in:
2026-09-28 19:03:31 +02:00
parent 5eb33f97c8
commit dde0a2d271
62 changed files with 3760 additions and 1865 deletions
+392 -128
View File
@@ -69,7 +69,7 @@
@endif
@endif
@if(enable_anonymous_access && session.is_anon)
<a href="#" id="nav-user-anon-identity-btn" 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="#" 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>
@@ -385,7 +385,7 @@ async function loginWithPasskey() {
errEl.textContent = (e && e.message) || 'Passkey login failed.';
}
}
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Login with passkey'; }
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey'; }
}
}
</script>
@@ -396,76 +396,319 @@ async function loginWithPasskey() {
<!-- Login View -->
<div id="modal-login-view">
<h2 style="text-align: center; margin-bottom: 18px;">{{ t('auth.login_title') }}</h2>
<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" style="display: flex; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 20px;">
<button type="button" id="login-tab-creds" onclick="switchLoginTab('creds')"
style="flex: 1; background: none; border: none; border-bottom: 2px solid var(--accent, #0096ff); color: var(--text-color, #fff); padding: 8px 0; font-size: 0.9em; cursor: pointer; font-weight: 600; transition: color 0.15s;">
<i class="fa-solid fa-key" style="margin-right:5px;"></i>Credentials
<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" onclick="switchLoginTab('passkey')"
style="flex: 1; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted, #888); padding: 8px 0; font-size: 0.9em; cursor: pointer; font-weight: 600; transition: color 0.15s;">
<i class="fa-solid fa-fingerprint" style="margin-right:5px;"></i>Passkey
<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">
<form class="login-form" method="post" action="/login" novalidate style="padding: 0; background: none; box-shadow: none; border: none;">
<input type="text" name="username" placeholder="{{ t('auth.username_or_email') }}" autocomplete="off" required />
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder_min') }}" autocomplete="off" required />
<p style="text-align: left; font-size: 0.9em; margin: 0;"><input type="checkbox" id="kmsi-modal" name="kmsi" />
<label for="kmsi-modal">{{ t('auth.stay_signed_in') }}</label>
</p>
<button type="submit">{{ t('auth.login_title') }}</button>
@if(smtp_enabled)
<div style="text-align: center; margin-top: 10px;">
<a href="#" id="modal-forgot-btn" style="font-size: 0.85em; color: var(--accent); text-decoration: underline;">{{ t('auth.forgot_password') }}</a>
<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>
@endif
@if(registration_open || private_society)
<p style="text-align: center; font-size: 0.9em; margin-top: 15px; color: #888;">
{{ t('auth.no_account') }} <a href="#" id="login-to-register" style="color: var(--accent); text-decoration: underline;">{{ t('auth.register_now') }}</a>
</p>
@endif
<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" style="display: none; text-align: center; padding: 8px 0 4px;">
<div style="font-size: 2.6em; margin-bottom: 14px;">🔑</div>
<p style="font-size: 0.88em; color: var(--text-muted, #aaa); margin: 0 0 20px; line-height: 1.55;">
<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"
style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 8px;"
onclick="loginWithPasskey()">
<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" style="display:none; color:#e06c75; font-size:0.82em; margin-top:6px;"></div>
<div id="modal-passkey-error" class="lm-error" style="display:none;"></div>
</div>
@if(enable_anonymous_access)
<div style="margin-top: 16px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 12px; text-align: center;">
<button type="button" id="modal-login-as-anon-btn"
style="background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: #999; border-radius: 4px; padding: 7px 14px; font-size: 0.85em; cursor: pointer; width: 100%; transition: background 0.2s;"
<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' : '';
var tCreds = document.getElementById('login-tab-creds');
var tPk = document.getElementById('login-tab-passkey');
if (tCreds) { tCreds.style.borderBottomColor = isCreds ? 'var(--accent, #0096ff)' : 'transparent'; tCreds.style.color = isCreds ? 'var(--text-color, #fff)' : 'var(--text-muted, #888)'; }
if (tPk) { tPk.style.borderBottomColor = isCreds ? 'transparent' : 'var(--accent, #0096ff)'; tPk.style.color = isCreds ? 'var(--text-muted, #888)' : 'var(--text-color, #fff)'; }
[['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>
@@ -508,40 +751,25 @@ async function loginWithPasskey() {
@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 class="login-modal-content" style="max-width: 500px; width: 92vw; max-height: 90vh; overflow-y: auto; text-align: left; padding: 25px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: var(--bg-primary, #111); box-shadow: 0 10px 40px rgba(0,0,0,0.8); position: relative;">
<button id="anon-passkey-modal-close" style="position: absolute; top: 15px; right: 15px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.4em; cursor: pointer; line-height: 1;">&times;</button>
<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 style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<i class="fa-solid fa-fingerprint" style="color: var(--accent, #0096ff); font-size: 1.3em;"></i>
<h3 style="margin: 0; font-size: 1.25em; color: var(--text-color, #fff);">Anonymous Passkey Identity</h3>
</div>
<p style="margin: 0 0 18px 0; font-size: 0.85em; color: var(--text-muted, #aaa); line-height: 1.4;">
Your anonymous identity is protected by a <strong>passkey</strong> stored in your OS or password manager (e.g. Bitwarden).
No private key ever touches this browser's storage.
</p>
<!-- Identity info -->
<div style="margin-bottom: 14px; padding: 12px; background: rgba(0,0,0,0.35); border-radius: 6px; border: 1px solid rgba(255,255,255,0.07);">
<div style="margin-bottom: 8px;">
<label style="display: block; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 3px;">Short Fingerprint</label>
<code id="anon-pk-fp-display" style="font-family: monospace; font-size: 1em; color: var(--accent, #00d2ff);">—</code>
<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>
<label style="display: block; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 3px;">Credential ID (short)</label>
<code id="anon-pk-cred-display" style="font-family: monospace; font-size: 0.78em; color: var(--text-muted, #aaa); word-break: break-all;">—</code>
</div>
</div>
<!-- Actions -->
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<button type="button" id="anon-pk-add-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 14px;">
<i class="fa-solid fa-plus"></i> Add another passkey
<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>
<p style="margin: 14px 0 0; font-size: 0.8em; color: var(--text-muted, #888); line-height: 1.4;">
To use this identity on another device, simply sign in with your passkey manager (Bitwarden, iCloud Keychain, etc.) — it syncs automatically.
</p>
</div>
</div>
@@ -578,43 +806,45 @@ async function loginWithPasskey() {
@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: 28px; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; background: var(--bg-primary, #111); box-shadow: 0 12px 48px rgba(0,0,0,0.9); position: relative; text-align: 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>
<!-- New user view (no passkey yet) -->
<div id="anon-setup-new">
<div style="font-size: 2.4em; margin-bottom: 12px;">🔑</div>
<h3 style="margin: 0 0 10px; font-size: 1.15em; color: var(--text-color, #fff);">Create an anonymous passkey</h3>
<p style="margin: 0 0 18px; font-size: 0.88em; color: var(--text-muted, #aaa); line-height: 1.55;">
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 style="margin: 0 0 20px; font-size: 0.82em; color: var(--text-muted, #888); line-height: 1.4;">
You can use it across devices if your passkey manager syncs (e.g. Bitwarden).
</p>
<button type="button" id="anon-setup-create-btn" style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 10px;">
<i class="fa-solid fa-fingerprint"></i> Create my passkey
</button>
<div id="anon-setup-new-error" style="display: none; color: #e06c75; font-size: 0.83em; margin-top: 6px;"></div>
<button type="button" id="anon-setup-switch-existing" style="background: none; border: none; color: var(--text-muted, #888); font-size: 0.8em; cursor: pointer; text-decoration: underline; margin-top: 4px;">
Already have one? Use existing passkey
</button>
</div>
<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" style="display: none;">
<div style="font-size: 2.4em; margin-bottom: 12px;">👤</div>
<h3 style="margin: 0 0 10px; font-size: 1.15em; color: var(--text-color, #fff);">Use your anonymous passkey</h3>
<p style="margin: 0 0 20px; font-size: 0.88em; color: var(--text-muted, #aaa); line-height: 1.55;">
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" style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 10px;">
<i class="fa-solid fa-fingerprint"></i> Use my passkey
</button>
<div id="anon-setup-ret-error" style="display: none; color: #e06c75; font-size: 0.83em; margin-top: 6px;"></div>
<button type="button" id="anon-setup-switch-new" style="background: none; border: none; color: var(--text-muted, #888); font-size: 0.8em; cursor: pointer; text-decoration: underline; margin-top: 4px;">
Create a new passkey instead
</button>
<!-- 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>
@@ -733,31 +963,65 @@ async function loginWithPasskey() {
<div id="register-modal" style="display: none;">
<div class="login-modal-content">
<button id="register-modal-close">&times;</button>
<form class="login-form" id="modal-register-form" method="post" action="/register" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.register_title') }}</h2>
<div id="register-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<input type="text" name="username" placeholder="{{ t('auth.username_placeholder') }}" autocomplete="off" required />
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder') }}" autocomplete="off" required minlength="20"
title="Must be at least 20 characters long." />
<input type="password" name="password_confirm" placeholder="{{ t('auth.confirm_password') }}" autocomplete="off" required minlength="20" />
@if(!registration_open)
<input type="text" name="token" placeholder="{{ t('auth.invite_token') }}" autocomplete="off" required />
@elseif(registration_require_mail_andor_token)
<input type="email" name="email" placeholder="{{ t('auth.email_placeholder') }}" autocomplete="off" required />
@endif
<input type="text" name="email_confirm_field" style="display: none !important;" tabindex="-1" autocomplete="off" />
<p style="text-align: left; font-size: 0.9em; margin: 0; color: #fff;">
<input type="checkbox" id="tos-modal" name="tos" required />
<label for="tos-modal">@if(private_society){{ t('auth.tos_private') }}@else{{ t('auth.tos_public') }} <a href="/terms" target="_blank" style="color: var(--accent); text-decoration: underline;">{{ t('auth.tos_terms') }}</a>, <a href="/rules" target="_blank" style="color: var(--accent); text-decoration: underline;">{{ t('auth.tos_rules') }}</a> {{ t('auth.tos_age') }}@endif</label>
</p>
@if(recaptcha_enabled)
<div id="modal-register-recaptcha" style="margin: 10px 0;"></div>
@endif
<button type="submit">{{ t('auth.create_account') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="register-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline;">{{ t('auth.back_to_login') }}</a>
<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>
<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>