esifawafwu
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
{{-- Shared look of the admin (/admin) and moderator (/mod) dashboards: .adm-* classes --}}
|
||||
<style>
|
||||
/* Admin & moderator dashboards — square, flat, theme colours (same rules as the modals) */
|
||||
.adm {
|
||||
--adm-accent: var(--accent, #0096ff);
|
||||
--adm-text: var(--text-color, var(--white, #fff));
|
||||
--adm-muted: var(--text-muted, #8a8f98);
|
||||
--adm-surface: rgba(255, 255, 255, 0.04);
|
||||
--adm-border: rgba(255, 255, 255, 0.1);
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 60px;
|
||||
text-align: left;
|
||||
color: var(--adm-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.adm *, .adm *::before, .adm *::after { border-radius: 0 !important; }
|
||||
|
||||
.adm-header {
|
||||
display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap;
|
||||
padding-bottom: 18px; margin-bottom: 26px; border-bottom: 1px solid var(--adm-border);
|
||||
}
|
||||
.adm-title { margin: 0; font-size: 1.8em; font-weight: 800; letter-spacing: -0.01em; line-height: 1.1; }
|
||||
.adm-subtitle { margin-top: 6px; font-size: 0.85em; color: var(--adm-muted); }
|
||||
.adm-subtitle strong { color: var(--adm-text); }
|
||||
|
||||
.adm-section { margin-bottom: 34px; }
|
||||
.adm-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.adm-section-title {
|
||||
display: flex; align-items: center; gap: 10px; margin: 0 0 14px;
|
||||
font-size: 0.78em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: var(--adm-muted);
|
||||
}
|
||||
.adm-section-title i { color: var(--adm-accent); }
|
||||
.adm-section-title::after { content: ''; flex: 1; height: 1px; background: var(--adm-border); }
|
||||
.adm-section-head .adm-section-title { flex: 1; }
|
||||
.adm-group-title { margin: 18px 0 8px; font-size: 0.95em; font-weight: 700; color: var(--adm-text); }
|
||||
|
||||
/* Overview tiles */
|
||||
.adm-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
|
||||
.adm-stat {
|
||||
display: flex; flex-direction: column; gap: 2px; padding: 14px 16px;
|
||||
background: var(--adm-surface); border: 1px solid var(--adm-border);
|
||||
color: var(--adm-text); text-decoration: none;
|
||||
}
|
||||
.adm-stat-num { font-size: 1.6em; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
||||
.adm-stat-label { font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--adm-muted); }
|
||||
.adm-stat-label small { text-transform: none; letter-spacing: 0; font-weight: 500; }
|
||||
.adm-stat-link { transition: border-color 0.15s; }
|
||||
.adm-stat-link:hover { border-color: var(--adm-accent); text-decoration: none; color: var(--adm-text); }
|
||||
.adm-stat.is-hot { border-color: var(--adm-accent); border-left: 3px solid var(--adm-accent); }
|
||||
.adm-stat.is-hot .adm-stat-num { color: var(--adm-accent); }
|
||||
|
||||
/* Tool cards */
|
||||
.adm-tools { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; }
|
||||
.adm-tool {
|
||||
display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px;
|
||||
background: var(--adm-surface); border: 1px solid var(--adm-border);
|
||||
color: var(--adm-text); text-decoration: none; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.adm-tool:hover { border-color: var(--adm-accent); background: rgba(255, 255, 255, 0.06); text-decoration: none; color: var(--adm-text); }
|
||||
.adm-tool > i { width: 20px; margin-top: 3px; text-align: center; color: var(--adm-accent); flex-shrink: 0; }
|
||||
.adm-tool-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.adm-tool-text strong { display: flex; align-items: center; gap: 8px; font-size: 0.92em; }
|
||||
.adm-tool-text > span { font-size: 0.78em; color: var(--adm-muted); }
|
||||
.adm-count { padding: 0 6px; font-size: 0.8em; line-height: 1.6; background: var(--adm-accent); color: #000; font-weight: 800; }
|
||||
|
||||
/* Settings panels */
|
||||
.adm-panel { background: var(--adm-surface); border: 1px solid var(--adm-border); }
|
||||
.adm-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px 16px; }
|
||||
.adm-row + .adm-row { border-top: 1px solid var(--adm-border); }
|
||||
.adm-row-wrap { flex-wrap: wrap; }
|
||||
.adm-row-text { min-width: 0; flex: 1 1 260px; }
|
||||
.adm-row-text label { display: block; margin: 0; font-weight: 700; font-size: 0.92em; color: var(--adm-text); }
|
||||
.adm-row-text p { margin: 3px 0 0; font-size: 0.8em; color: var(--adm-muted); }
|
||||
.adm-muted { font-size: 0.78em; font-style: italic; color: var(--adm-muted); }
|
||||
|
||||
.adm-number {
|
||||
width: 76px; height: 36px; padding: 0 8px; text-align: center;
|
||||
background: rgba(0, 0, 0, 0.3); border: 1px solid var(--adm-border); color: var(--adm-text);
|
||||
font: inherit; outline: none; flex-shrink: 0;
|
||||
}
|
||||
.adm-number:focus { border-color: var(--adm-accent); }
|
||||
|
||||
.adm-switch { position: relative; display: inline-block; width: 46px; height: 24px; flex-shrink: 0; cursor: pointer; }
|
||||
.adm-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||
.adm-slider { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.12); border: 1px solid var(--adm-border); transition: background 0.2s, border-color 0.2s; }
|
||||
.adm-slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: var(--adm-muted); transition: transform 0.2s, background 0.2s; }
|
||||
.adm-switch input:checked + .adm-slider { background: var(--adm-accent); border-color: var(--adm-accent); }
|
||||
.adm-switch input:checked + .adm-slider::before { transform: translateX(22px); background: #000; }
|
||||
.adm-switch input:focus-visible + .adm-slider { outline: 2px solid var(--adm-accent); outline-offset: 2px; }
|
||||
|
||||
.adm-brand { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.adm-brand-preview {
|
||||
width: 160px; height: 46px; display: flex; align-items: center; justify-content: center; overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.3); border: 1px solid var(--adm-border);
|
||||
}
|
||||
.adm-brand-preview img { max-height: 40px; max-width: 150px; object-fit: contain; }
|
||||
|
||||
/* Buttons */
|
||||
.adm-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; margin: 0; padding: 0 16px;
|
||||
font: inherit; font-size: 0.82em; font-weight: 700; cursor: pointer; text-decoration: none; white-space: nowrap;
|
||||
border: 1px solid var(--adm-border); background: transparent; color: var(--adm-text);
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.adm-btn:hover { text-decoration: none; }
|
||||
.adm-btn-primary { background: var(--adm-accent); border-color: var(--adm-accent); color: #000; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.adm-btn-primary:hover { background: transparent; color: var(--adm-accent); }
|
||||
.adm-btn-ghost { color: var(--adm-muted); }
|
||||
.adm-btn-ghost:hover { color: var(--adm-text); border-color: var(--adm-accent); }
|
||||
.adm-btn-danger { border-color: rgba(220, 53, 69, 0.5); color: #ff6b6b; }
|
||||
.adm-btn-danger:hover { background: rgba(220, 53, 69, 0.15); }
|
||||
.adm-btn:disabled { opacity: 0.4; cursor: not-allowed; background: transparent; }
|
||||
.adm-btn:focus-visible { outline: 2px solid var(--adm-accent); outline-offset: 2px; }
|
||||
|
||||
.adm-status { font-size: 0.8em; font-weight: 700; min-height: 1em; }
|
||||
.adm-status-row { display: block; padding: 0 16px 12px; }
|
||||
.adm-status-row:empty { display: none; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.adm-row { flex-wrap: wrap; }
|
||||
.adm-tools { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
{{-- Back-to-dashboard bar for admin/mod subpages: /mod… pages (and non-admins) go to /mod, /admin… pages to /admin. Include right after #main opens (AJAX nav only swaps #main). --}}
|
||||
<nav class="adm-back" aria-label="Breadcrumb">
|
||||
@if(session && session.admin && !is_mod_area)
|
||||
<a href="/admin" class="adm-back-link">
|
||||
<span class="adm-back-arrow"><i class="fa-solid fa-arrow-left"></i></span>
|
||||
<span class="adm-back-text"><small>Back to</small>Admin dashboard</span>
|
||||
</a>
|
||||
@else
|
||||
<a href="/mod" class="adm-back-link">
|
||||
<span class="adm-back-arrow"><i class="fa-solid fa-arrow-left"></i></span>
|
||||
<span class="adm-back-text"><small>Back to</small>Moderator dashboard</span>
|
||||
</a>
|
||||
@endif
|
||||
<span class="adm-back-crumb" aria-current="page"></span>
|
||||
</nav>
|
||||
|
||||
@include(snippets/admin-skin)
|
||||
|
||||
<style>
|
||||
.adm-back {
|
||||
--ab-accent: var(--accent, #0096ff);
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
max-width: 1200px; margin: 0 auto; padding: 18px 16px 0; box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
.adm-back, .adm-back * { border-radius: 0 !important; }
|
||||
.adm-back-link {
|
||||
position: relative; display: inline-flex; align-items: stretch; overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text-color, var(--white, #fff)); text-decoration: none;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.adm-back-link::after {
|
||||
content: ''; position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
|
||||
background: var(--ab-accent); transform: scaleX(0); transform-origin: left; transition: transform 0.25s ease;
|
||||
}
|
||||
.adm-back-link:hover, .adm-back-link:focus-visible {
|
||||
border-color: var(--ab-accent); background: color-mix(in srgb, var(--ab-accent) 8%, transparent);
|
||||
color: var(--text-color, var(--white, #fff)); text-decoration: none; outline: none;
|
||||
}
|
||||
.adm-back-link:hover::after, .adm-back-link:focus-visible::after { transform: scaleX(1); }
|
||||
.adm-back-arrow {
|
||||
display: flex; align-items: center; justify-content: center; width: 38px;
|
||||
background: var(--ab-accent); color: #000; font-size: 0.9em;
|
||||
}
|
||||
.adm-back-arrow i { transition: transform 0.2s ease; }
|
||||
.adm-back-link:hover .adm-back-arrow i { transform: translateX(-3px); }
|
||||
.adm-back-text {
|
||||
display: flex; flex-direction: column; justify-content: center; padding: 6px 14px 7px 12px;
|
||||
font-size: 0.86em; font-weight: 700; line-height: 1.2; white-space: nowrap;
|
||||
}
|
||||
.adm-back-text small {
|
||||
font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;
|
||||
color: var(--text-muted, #8a8f98);
|
||||
}
|
||||
.adm-back-crumb {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em;
|
||||
color: var(--text-muted, #8a8f98);
|
||||
}
|
||||
.adm-back-crumb:not(:empty)::before { content: '/ '; color: var(--ab-accent); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.adm-back-link::after, .adm-back-arrow i { transition: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
// Breadcrumb = the page's own heading (first h1/h2 in #main). The rest of the page isn't in the
|
||||
// DOM yet when this runs (parser / AJAX insert), so wait until it is.
|
||||
const fill = () => {
|
||||
const crumb = document.querySelector('#main .adm-back-crumb');
|
||||
const main = document.getElementById('main');
|
||||
if (!crumb || !main) return;
|
||||
const heading = Array.from(main.querySelectorAll('h1, h2')).find(h => !h.closest('.adm-back') && h.textContent.trim());
|
||||
if (heading) crumb.textContent = heading.innerText.trim().slice(0, 60); // innerText already collapses whitespace (no regex: the template engine strips backslashes)
|
||||
};
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fill, { once: true });
|
||||
else setTimeout(fill, 0);
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
{{-- Admin/mod subpage skin (style guide: square, flat, theme colours). Loaded by snippets/admin-back, scoped to pages that show the back bar, except the .mq card pages which have their own design. --}}
|
||||
<style>
|
||||
/* Scope root: #main of an admin subpage. Most rules sit in :where() (zero specificity) so a page's own
|
||||
classes still win; only corners and form-control surfaces are forced because pages hard-code them inline. */
|
||||
#main:has(> .adm-back):not(:has(.mq)) {
|
||||
--as-accent: var(--accent, #0096ff);
|
||||
--as-text: var(--text-color, var(--white, #fff));
|
||||
--as-muted: var(--text-muted, #8a8f98);
|
||||
--as-surface: rgba(255, 255, 255, 0.04);
|
||||
--as-border: rgba(255, 255, 255, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ── Corners: none, anywhere ─────────────────────────────────────────── */
|
||||
#main:has(> .adm-back):not(:has(.mq)) *,
|
||||
#main:has(> .adm-back):not(:has(.mq)) *::before,
|
||||
#main:has(> .adm-back):not(:has(.mq)) *::after { border-radius: 0 !important; }
|
||||
|
||||
/* ── Layout ──────────────────────────────────────────────────────────── */
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) > .container {
|
||||
max-width: 1200px; margin: 0 auto; padding: 20px 16px 60px; box-sizing: border-box; text-align: left;
|
||||
}
|
||||
|
||||
/* ── Headings ────────────────────────────────────────────────────────── */
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(.container > h1, .container > h2, .admin-header-flex h2, .session-page-title) {
|
||||
margin: 0 0 6px; font-size: 1.8em; font-weight: 800; letter-spacing: -0.01em; line-height: 1.15; color: var(--as-text);
|
||||
text-transform: none;
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(h3, h4) { font-weight: 700; color: var(--as-text); }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) .admin-header-flex {
|
||||
display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap;
|
||||
padding-bottom: 16px; margin-bottom: 20px; border-bottom: 1px solid var(--as-border);
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) hr { border: 0; border-top: 1px solid var(--as-border); margin: 20px 0; }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) label { color: var(--as-text); }
|
||||
|
||||
/* ── Cards / panels ──────────────────────────────────────────────────── */
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(.upload-form, .admin-form-container, .admin-motd-form, .settings-section, .admin-card) {
|
||||
background: var(--as-surface); border: 1px solid var(--as-border); padding: 18px; box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── Tables ──────────────────────────────────────────────────────────── */
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) table { width: 100%; border-collapse: collapse; background: transparent; color: var(--as-text); }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(th, thead td) {
|
||||
padding: 10px 12px; text-align: left; font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--as-muted); background: var(--as-surface); border-bottom: 1px solid var(--as-border);
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) tbody td { padding: 10px 12px; border-bottom: 1px solid var(--as-border); vertical-align: middle; }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) tbody tr { background: transparent; transition: background 0.12s; }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) tbody tr:hover { background: var(--as-surface); }
|
||||
|
||||
/* ── Form controls (forced: pages hard-code #333 backgrounds inline) ── */
|
||||
#main:has(> .adm-back):not(:has(.mq)) :is(input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=range]):not([type=color]):not([type=hidden]), select, textarea) {
|
||||
background: var(--as-surface) !important;
|
||||
border: 1px solid var(--as-border) !important;
|
||||
color: var(--as-text) !important;
|
||||
font-family: inherit; box-shadow: none !important; outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=range]):not([type=color]):not([type=hidden]), select) {
|
||||
min-height: 34px; padding: 6px 10px; box-sizing: border-box;
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) textarea { padding: 10px 12px; box-sizing: border-box; }
|
||||
#main:has(> .adm-back):not(:has(.mq)) :is(input, select, textarea):focus { border-color: var(--as-accent) !important; }
|
||||
#main:has(> .adm-back):not(:has(.mq)) select option { background: var(--bg, #111); color: var(--as-text); }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(input[type=checkbox], input[type=radio]) { accent-color: var(--as-accent); }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────── */
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(button, .btn, input[type=submit]) { font-family: inherit; cursor: pointer; }
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(.btn-upload, .btn-primary, button[type=submit], input[type=submit]) {
|
||||
min-height: 36px; padding: 0 18px; border: 1px solid var(--as-accent); background: var(--as-accent); color: #000;
|
||||
font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.82em;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(.btn-upload, .btn-primary, button[type=submit], input[type=submit]):hover:not(:disabled) {
|
||||
background: transparent; color: var(--as-accent);
|
||||
}
|
||||
:where(#main:has(> .adm-back):not(:has(.mq))) :is(button, .btn):disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
#main:has(> .adm-back):not(:has(.mq)) :is(button, a, input, select, textarea):focus-visible { outline: 2px solid var(--as-accent); outline-offset: 2px; }
|
||||
|
||||
/* ── Modals opened from admin pages ──────────────────────────────────── */
|
||||
#main:has(> .adm-back):not(:has(.mq)) :is(.modal-content, .modal-overlay > div:first-child) {
|
||||
background: var(--bg, #111) !important;
|
||||
border: 1px solid var(--as-accent) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
</style>
|
||||
+126
-22
@@ -68,36 +68,45 @@
|
||||
@endif
|
||||
@if(!private_society || session)
|
||||
<div id="report-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-content" style="max-height: 90vh; overflow-y: auto;">
|
||||
<h3>{{ t('report.title') }}</h3>
|
||||
<p style="margin-bottom: 12px; color: var(--text-muted, #aaa); font-size: 0.9em;">Select one or more reasons and optionally add details below.</p>
|
||||
<div class="modal-content report-modal-content">
|
||||
<div class="rm-header">
|
||||
<h3>{{ t('report.title') }}</h3>
|
||||
<p>Select one or more reasons and optionally add details below.</p>
|
||||
</div>
|
||||
<div class="rm-body">
|
||||
<input type="hidden" id="report-item-id">
|
||||
<input type="hidden" id="report-comment-id">
|
||||
<input type="hidden" id="report-user-id">
|
||||
<div id="report-categories" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px;">
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="wrong_rating" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff);">Wrong Rating</strong> <span style="color: var(--text-muted, #aaa);">— Content is marked SFW but should be NSFW, or vice versa.</span></span>
|
||||
<div id="report-categories">
|
||||
<label class="report-category-option">
|
||||
<input type="checkbox" name="report_category" value="wrong_rating" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Wrong Rating</strong><span>Content is marked SFW but should be NSFW, or vice versa.</span></span>
|
||||
</label>
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="spam" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff);">Spam</strong> <span style="color: var(--text-muted, #aaa);">— Repeated, unwanted, or promotional content.</span></span>
|
||||
<label class="report-category-option">
|
||||
<input type="checkbox" name="report_category" value="spam" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Spam</strong><span>Repeated, unwanted, or promotional content.</span></span>
|
||||
</label>
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="duplicate" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff);">Duplicate</strong> <span style="color: var(--text-muted, #aaa);">— This content has already been posted before.</span></span>
|
||||
<label class="report-category-option">
|
||||
<input type="checkbox" name="report_category" value="duplicate" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Duplicate</strong><span>This content has already been posted before.</span></span>
|
||||
</label>
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="copyright" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff);">Copyright</strong> <span style="color: var(--text-muted, #aaa);">— Content infringes on intellectual property rights.</span></span>
|
||||
<label class="report-category-option">
|
||||
<input type="checkbox" name="report_category" value="copyright" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Copyright</strong><span>Content infringes on intellectual property rights.</span></span>
|
||||
</label>
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="illegal" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff); color: var(--danger, #ff4444);">Illegal Content</strong> <span style="color: var(--text-muted, #aaa);">— Content that may violate laws or platform rules.</span></span>
|
||||
<label class="report-category-option rm-danger">
|
||||
<input type="checkbox" name="report_category" value="illegal" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Illegal Content</strong><span>Content that may violate laws or platform rules.</span></span>
|
||||
</label>
|
||||
<label class="report-category-option" style="display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03)); transition: border-color 0.15s, background 0.15s; user-select: none;">
|
||||
<input type="checkbox" name="report_category" value="other" class="report-cat-check" style="accent-color: var(--accent, #e65c00); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0;">
|
||||
<span style="font-size: 0.9em;"><strong style="color: var(--text-color, #fff);">Other</strong> <span style="color: var(--text-muted, #aaa);">— Something else not listed above.</span></span>
|
||||
<label class="report-category-option">
|
||||
<input type="checkbox" name="report_category" value="other" class="report-cat-check">
|
||||
<span class="rm-check"><i class="fa-solid fa-check"></i></span>
|
||||
<span class="rm-text"><strong>Other</strong><span>Something else not listed above.</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<textarea id="report-reason" class="mod-reason" placeholder="{{ t('report.placeholder') }}"></textarea>
|
||||
@@ -105,6 +114,7 @@
|
||||
<div id="modal-report-recaptcha" data-sitekey="{{ recaptcha_site_key }}" style="margin: 10px 0; display: flex; justify-content: center;"></div>
|
||||
@endif
|
||||
<div id="report-error" class="error-msg"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="report-cancel" class="btn-secondary">{{ t('common.cancel') }}</button>
|
||||
<button id="report-submit" class="">{{ t('report.submit') }}</button>
|
||||
@@ -112,6 +122,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Report modal — same style rules as the login/info modals: square, flat, accent primary button */
|
||||
#report-modal .report-modal-content {
|
||||
--rm-accent: var(--accent, #0096ff);
|
||||
--rm-text: var(--text-color, #fff);
|
||||
--rm-muted: var(--text-muted, #8a8f98);
|
||||
--rm-surface: rgba(255, 255, 255, 0.04);
|
||||
--rm-border: rgba(255, 255, 255, 0.1);
|
||||
width: min(460px, 92vw);
|
||||
max-height: min(90vh, 90dvh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid var(--accent) !important;
|
||||
background: var(--bg) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5) !important;
|
||||
color: var(--rm-text);
|
||||
}
|
||||
#report-modal .report-modal-content * { border-radius: 0 !important; }
|
||||
#report-modal .rm-header { flex-shrink: 0; padding: 20px 24px 14px; border-bottom: 1px solid var(--rm-border); }
|
||||
#report-modal .rm-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 16px 24px; }
|
||||
#report-modal .rm-body > :last-child { margin-bottom: 0; }
|
||||
#report-modal .rm-header h3 { margin: 0; font-size: 1.25em; font-weight: 700; color: var(--rm-text); }
|
||||
#report-modal .rm-header p { margin: 6px 0 0; font-size: 0.85em; line-height: 1.5; color: var(--rm-muted); }
|
||||
|
||||
#report-modal #report-categories { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
#report-modal .report-category-option {
|
||||
display: flex; align-items: flex-start; gap: 11px; padding: 10px 12px;
|
||||
background: var(--rm-surface); border: 1px solid var(--rm-border);
|
||||
cursor: pointer; user-select: none; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
#report-modal .report-category-option:hover { border-color: rgba(255, 255, 255, 0.22); }
|
||||
#report-modal .report-category-option:has(.report-cat-check:checked) {
|
||||
border-color: var(--rm-accent);
|
||||
background: color-mix(in srgb, var(--rm-accent) 8%, transparent);
|
||||
}
|
||||
#report-modal .report-cat-check { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||||
#report-modal .rm-check {
|
||||
width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25); background: var(--rm-surface);
|
||||
font-size: 0.7em; color: transparent; transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
#report-modal .report-cat-check:checked + .rm-check { background: var(--rm-accent); border-color: var(--rm-accent); color: #000; }
|
||||
#report-modal .report-cat-check:focus-visible + .rm-check { outline: 2px solid var(--rm-accent); outline-offset: 2px; }
|
||||
#report-modal .rm-text { display: flex; flex-direction: column; gap: 2px; font-size: 0.88em; line-height: 1.4; }
|
||||
#report-modal .rm-text strong { color: var(--rm-text); font-weight: 700; }
|
||||
#report-modal .rm-text span { color: var(--rm-muted); }
|
||||
#report-modal .rm-danger .rm-text strong { color: var(--danger, #ff5c5c); }
|
||||
#report-modal .rm-danger:has(.report-cat-check:checked) { border-color: var(--danger, #ff5c5c); background: rgba(255, 92, 92, 0.08); }
|
||||
#report-modal .rm-danger .report-cat-check:checked + .rm-check { background: var(--danger, #ff5c5c); border-color: var(--danger, #ff5c5c); }
|
||||
|
||||
#report-modal textarea.mod-reason {
|
||||
width: 100%; min-height: 90px; box-sizing: border-box; margin: 0; padding: 10px 12px;
|
||||
background: var(--rm-surface); border: 1px solid var(--rm-border); color: var(--rm-text);
|
||||
font: inherit; font-size: 0.9em; resize: vertical; outline: none; box-shadow: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
#report-modal textarea.mod-reason:focus { border-color: var(--rm-accent); }
|
||||
#report-modal textarea.mod-reason::placeholder { color: var(--rm-muted); }
|
||||
|
||||
#report-modal .modal-actions {
|
||||
flex-shrink: 0; display: flex; justify-content: flex-end; gap: 8px; margin: 0;
|
||||
padding: 14px 24px; border-top: 1px solid var(--rm-border); background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#report-modal { padding: 0 !important; align-items: stretch !important; }
|
||||
#report-modal .report-modal-content {
|
||||
width: 100vw; max-width: none;
|
||||
height: 100vh; height: 100dvh; max-height: none;
|
||||
border: 0 !important; box-shadow: none !important;
|
||||
}
|
||||
#report-modal .modal-actions { padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }
|
||||
#report-modal .modal-actions button { flex: 1; }
|
||||
}
|
||||
#report-modal .modal-actions button {
|
||||
height: 40px; margin: 0; padding: 0 18px; font: inherit; font-size: 0.85em; cursor: pointer;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
transition: background 0.18s, color 0.18s, border-color 0.18s;
|
||||
}
|
||||
#report-modal #report-cancel { background: transparent; border: 1px solid var(--rm-border); color: var(--rm-muted); font-weight: 600; }
|
||||
#report-modal #report-cancel:hover { color: var(--rm-text); border-color: rgba(255, 255, 255, 0.25); background: var(--rm-surface); }
|
||||
#report-modal #report-submit {
|
||||
background: var(--rm-accent); border: 1px solid var(--rm-accent); color: #000;
|
||||
font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
#report-modal #report-submit:hover:not(:disabled) { background: transparent; color: var(--rm-accent); }
|
||||
#report-modal #report-submit:disabled { opacity: 0.6; cursor: progress; }
|
||||
#report-modal button:focus-visible { outline: 2px solid var(--rm-accent); outline-offset: 2px; }
|
||||
</style>
|
||||
|
||||
<div id="warning-modal" class="modal-overlay" style="display:none; z-index: 100100;">
|
||||
<div class="modal-content content-warning-content">
|
||||
<h3 style="color: var(--danger);">{{ t('account_warning.title') }}</h3>
|
||||
|
||||
@@ -107,9 +107,15 @@
|
||||
<div class="infobox-card" id="info-card-directlink">
|
||||
<label class="infobox-card-label"><i class="fa-solid fa-arrow-up-right-from-square"></i> Direct Link</label>
|
||||
<div class="infobox-card-value">
|
||||
@if(item.external_thread_url)
|
||||
<a href="{{ item.external_thread_url }}" target="_blank" rel="noopener noreferrer" class="infobox-link-pill" id="info-file-direct-link" title="{{ item.external_thread_url }}">
|
||||
<i class="fa-solid fa-up-right-from-square"></i> {{ t('info_modal.view_file') || 'Open File' }}
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ item.mime === 'video/youtube' ? 'https://www.youtube.com/watch?v=' + item.dest.replace('yt:', '') : item.dest }}" target="_blank" class="infobox-link-pill" id="info-file-direct-link">
|
||||
<i class="fa-solid fa-up-right-from-square"></i> {{ t('info_modal.view_file') || 'Open File' }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,3 +173,195 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Info modal — same style rules as the login/register/anon modals: square, flat, no glow */
|
||||
#info-modal .infobox-modal-content {
|
||||
--im-accent: var(--accent, #0096ff);
|
||||
--im-text: var(--text-color, #fff);
|
||||
--im-muted: var(--text-muted, #8a8f98);
|
||||
--im-surface: rgba(255, 255, 255, 0.04);
|
||||
--im-border: rgba(255, 255, 255, 0.1);
|
||||
padding: 0;
|
||||
/* Same box as .login-modal-content (f0ckm.css); !important beats the older infobox rules */
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid var(--accent) !important;
|
||||
background: var(--bg) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5) !important;
|
||||
color: var(--im-text);
|
||||
overflow: hidden;
|
||||
}
|
||||
#info-modal .infobox-modal-content * { border-radius: 0 !important; }
|
||||
|
||||
/* Header */
|
||||
#info-modal .infobox-modal-content .infobox-modal-header {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
|
||||
margin: 0; padding: 22px 24px 18px;
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--im-border);
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-modal-header h3 {
|
||||
margin: 0; font-size: 1.25em; font-weight: 700; letter-spacing: -0.01em; color: var(--im-text);
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-sub-heading {
|
||||
display: block; margin-top: 5px; font-size: 0.8em; color: var(--im-muted);
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-sub-heading code {
|
||||
padding: 1px 6px; background: var(--im-surface); border: 1px solid var(--im-border);
|
||||
color: var(--im-accent); font-size: 1em;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-close-icon-btn {
|
||||
flex-shrink: 0; width: 34px; height: 34px; margin: -4px -8px 0 0; padding: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: transparent; border: 1px solid transparent;
|
||||
color: var(--im-muted); font-size: 1.05em; cursor: pointer;
|
||||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-close-icon-btn:hover {
|
||||
color: var(--im-text); background: var(--im-surface); border-color: var(--im-border);
|
||||
}
|
||||
|
||||
/* Body + sections */
|
||||
#info-modal .infobox-modal-content .infobox-modal-body {
|
||||
padding: 20px 24px; display: flex; flex-direction: column; gap: 22px;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-section { margin: 0; padding: 0; background: none; border: 0; }
|
||||
#info-modal .infobox-modal-content .infobox-section-header {
|
||||
display: flex; align-items: center; gap: 8px; margin: 0 0 10px; padding: 0;
|
||||
font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;
|
||||
color: var(--im-muted); background: none; border: 0;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-section-header i { color: var(--im-accent); }
|
||||
#info-modal .infobox-modal-content .infobox-section-header::after {
|
||||
content: ''; flex: 1; height: 1px; background: var(--im-border);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
#info-modal .infobox-modal-content .infobox-cards-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-card {
|
||||
margin: 0; padding: 12px 14px;
|
||||
background: var(--im-surface); border: 1px solid var(--im-border); box-shadow: none;
|
||||
display: flex; flex-direction: column; gap: 8px; min-width: 0;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-card:hover { border-color: rgba(255, 255, 255, 0.18); }
|
||||
#info-modal .infobox-modal-content .infobox-card.full-width { grid-column: 1 / -1; }
|
||||
#info-modal .infobox-modal-content .infobox-card-label {
|
||||
display: flex; align-items: center; gap: 6px; margin: 0;
|
||||
font-size: 0.7em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--im-muted);
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-card-label i { color: var(--im-accent); opacity: 0.85; width: 1em; text-align: center; }
|
||||
#info-modal .infobox-modal-content .infobox-card-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
#info-modal .infobox-modal-content .infobox-card-value { font-size: 0.95em; color: var(--im-text); overflow-wrap: anywhere; }
|
||||
#info-modal .infobox-modal-content .infobox-card-value-row,
|
||||
#info-modal .infobox-modal-content .infobox-expiry-content {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; font-size: 0.92em;
|
||||
}
|
||||
#info-modal .infobox-modal-content .info-expiry-edit-controls { display: flex; gap: 6px; width: 100%; }
|
||||
|
||||
/* Small controls inside cards */
|
||||
#info-modal .infobox-modal-content .infobox-action-btn,
|
||||
#info-modal .infobox-modal-content .copy-hash-btn {
|
||||
height: 28px; margin: 0; padding: 0 10px;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
background: transparent; border: 1px solid var(--im-border);
|
||||
color: var(--im-muted); font: inherit; font-size: 0.78em; font-weight: 600; cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-action-btn:hover,
|
||||
#info-modal .infobox-modal-content .copy-hash-btn:hover {
|
||||
color: var(--im-accent); border-color: var(--im-accent); background: transparent;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-select {
|
||||
flex: 1; min-width: 0; height: 28px; margin: 0; padding: 0 8px;
|
||||
background: rgba(0, 0, 0, 0.35); border: 1px solid var(--im-border);
|
||||
color: var(--im-text); font: inherit; font-size: 0.82em; outline: none;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-select:focus { border-color: var(--im-accent); }
|
||||
|
||||
#info-modal .infobox-modal-content .mime-pill {
|
||||
display: inline-block; padding: 2px 8px;
|
||||
background: rgba(0, 0, 0, 0.35); border: 1px solid var(--im-border);
|
||||
color: var(--im-text); font-size: 0.85em;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-link-pill,
|
||||
#info-modal .infobox-modal-content .infobox-source-link {
|
||||
display: inline-flex; align-items: center; gap: 7px; max-width: 100%;
|
||||
color: var(--im-accent); text-decoration: none; font-weight: 600; font-size: 0.92em;
|
||||
background: none; border: 0; padding: 0;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-link-pill:hover,
|
||||
#info-modal .infobox-modal-content .infobox-source-link:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
#info-modal .infobox-modal-content .info-hash-codeblock {
|
||||
margin: 0; padding: 10px 12px;
|
||||
background: rgba(0, 0, 0, 0.4); border: 1px solid var(--im-border); border-left: 2px solid var(--im-accent);
|
||||
font-size: 0.8em; line-height: 1.5; color: var(--im-text); word-break: break-all;
|
||||
}
|
||||
#info-modal .infobox-modal-content .info-hash-codeblock code { background: none; border: 0; padding: 0; color: inherit; }
|
||||
|
||||
/* Reposts */
|
||||
#info-modal .infobox-modal-content .infobox-reposts-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
#info-modal .infobox-modal-content .repost-pill {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;
|
||||
background: var(--im-surface); border: 1px solid var(--im-border);
|
||||
color: var(--im-text); font-size: 0.82em; text-decoration: none;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
#info-modal .infobox-modal-content .repost-pill:hover { border-color: var(--im-accent); color: var(--im-accent); }
|
||||
#info-modal .infobox-modal-content .repost-pill.exact i { color: var(--im-accent); }
|
||||
|
||||
/* Footer */
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer {
|
||||
display: flex; justify-content: flex-end; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
margin: 0; padding: 16px 24px;
|
||||
background: rgba(0, 0, 0, 0.25); border-top: 1px solid var(--im-border);
|
||||
}
|
||||
#info-modal .infobox-modal-content .btn-infobox-action,
|
||||
#info-modal .infobox-modal-content .btn-infobox-close {
|
||||
height: 40px; margin: 0; padding: 0 18px;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
font: inherit; font-size: 0.85em; cursor: pointer; box-shadow: none;
|
||||
transition: background 0.18s, color 0.18s, border-color 0.18s;
|
||||
}
|
||||
#info-modal .infobox-modal-content .btn-infobox-action {
|
||||
background: transparent; border: 1px solid var(--im-border); color: var(--im-muted); font-weight: 600;
|
||||
}
|
||||
#info-modal .infobox-modal-content .btn-infobox-action:hover { color: var(--im-text); border-color: rgba(255, 255, 255, 0.25); background: var(--im-surface); }
|
||||
#info-modal .infobox-modal-content .btn-infobox-close {
|
||||
background: var(--im-accent); border: 1px solid var(--im-accent); color: #000;
|
||||
font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
#info-modal .infobox-modal-content .btn-infobox-close:hover { background: transparent; color: var(--im-accent); }
|
||||
#info-modal .infobox-modal-content :is(button, a, select):focus-visible { outline: 2px solid var(--im-accent); outline-offset: 2px; }
|
||||
|
||||
/* Fixed header + footer, only the body scrolls; full screen on phones */
|
||||
#info-modal .infobox-modal-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
max-height: min(90vh, 90dvh) !important;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-modal-header,
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer { flex-shrink: 0; }
|
||||
#info-modal .infobox-modal-content .infobox-modal-body {
|
||||
flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; overscroll-behavior: contain;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#info-modal { padding: 0 !important; align-items: stretch !important; }
|
||||
#info-modal .infobox-modal-content {
|
||||
width: 100vw !important; max-width: none !important;
|
||||
height: 100vh !important; height: 100dvh !important; max-height: none !important;
|
||||
border: 0 !important; box-shadow: none !important;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
#info-modal .infobox-modal-content .infobox-modal-header,
|
||||
#info-modal .infobox-modal-content .infobox-modal-body,
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer { padding-left: 16px; padding-right: 16px; }
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer > * { flex: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
{{-- Shared card UI for mod/approve.html and mod/trash.html: styles, confirmation modal, rating toggle and helpers on window.mq (page actions live in each page) --}}
|
||||
<div id="mq-modal" class="mq-modal" style="display:none;">
|
||||
<div class="mq-modal-box">
|
||||
<h3 id="mq-modal-title">Confirm action</h3>
|
||||
<p id="mq-modal-text">Are you sure?</p>
|
||||
<div id="mq-modal-reason-wrap" style="display:none;">
|
||||
<label for="mq-modal-reason">Reason (required)</label>
|
||||
<textarea id="mq-modal-reason" rows="3" placeholder="Enter a reason…"></textarea>
|
||||
</div>
|
||||
<div class="mq-modal-actions">
|
||||
<button type="button" id="mq-modal-cancel" class="mq-btn mq-btn-ghost">Cancel</button>
|
||||
<button type="button" id="mq-modal-confirm" class="mq-btn mq-btn-danger">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Moderation queues — square, flat, theme colours */
|
||||
.mq {
|
||||
--mq-accent: var(--accent, #0096ff);
|
||||
--mq-text: var(--text-color, var(--white, #fff));
|
||||
--mq-muted: var(--text-muted, #8a8f98);
|
||||
--mq-surface: rgba(255, 255, 255, 0.04);
|
||||
--mq-border: rgba(255, 255, 255, 0.1);
|
||||
--mq-danger: #ff5c5c;
|
||||
max-width: 1200px; margin: 0 auto; padding: 24px 16px 60px;
|
||||
text-align: left; color: var(--mq-text); line-height: 1.5;
|
||||
}
|
||||
.mq *, .mq *::before, .mq *::after, .mq-modal *, .mq-modal *::before, .mq-modal *::after { border-radius: 0 !important; }
|
||||
|
||||
.mq-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||
.mq-title { margin: 0; font-size: 1.8em; font-weight: 800; line-height: 1.1; }
|
||||
.mq-subtitle { margin-top: 6px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
|
||||
|
||||
.mq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
|
||||
.mq-card { display: flex; flex-direction: column; background: var(--mq-surface); border: 1px solid var(--mq-border); transition: opacity 0.3s, border-color 0.15s; }
|
||||
.mq-card:hover { border-color: rgba(255, 255, 255, 0.2); }
|
||||
.mq-media { position: relative; aspect-ratio: 16 / 10; background: #000; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.mq-media img, .mq-media video { width: 100%; height: 100%; object-fit: contain; display: block; }
|
||||
.mq-media .embed-responsive { width: 100%; }
|
||||
.mq-trash .mq-media img, .mq-trash .mq-media video { filter: grayscale(50%); }
|
||||
.mq-body { display: flex; flex-direction: column; gap: 8px; padding: 12px; flex: 1; }
|
||||
.mq-meta { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 0.8em; color: var(--mq-muted); }
|
||||
.mq-meta strong { color: var(--mq-text); font-weight: 700; }
|
||||
.mq-reason { padding: 6px 8px; font-size: 0.8em; color: #ffb8b8; background: rgba(255, 92, 92, 0.08); border-left: 2px solid var(--mq-danger); }
|
||||
.mq-tags { display: flex; flex-wrap: wrap; gap: 4px; min-height: 22px; }
|
||||
.mq-tags .badge { margin: 0; }
|
||||
.mq-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: auto; }
|
||||
.mq-actions .mq-btn-rating { grid-column: span 2; }
|
||||
|
||||
.mq-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 34px; margin: 0; padding: 0 12px;
|
||||
font: inherit; font-size: 0.8em; font-weight: 700; cursor: pointer; text-decoration: none; white-space: nowrap;
|
||||
border: 1px solid var(--mq-border); background: transparent; color: var(--mq-text);
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.mq-btn:hover { text-decoration: none; }
|
||||
.mq-btn:disabled { opacity: 0.5; cursor: progress; }
|
||||
.mq-btn-primary { background: var(--mq-accent); border-color: var(--mq-accent); color: #000; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.mq-btn-primary:hover:not(:disabled) { background: transparent; color: var(--mq-accent); }
|
||||
.mq-btn-danger { border-color: rgba(255, 92, 92, 0.5); color: var(--mq-danger); }
|
||||
.mq-btn-danger:hover:not(:disabled) { background: rgba(255, 92, 92, 0.15); }
|
||||
.mq-btn-ghost { color: var(--mq-muted); }
|
||||
.mq-btn-ghost:hover:not(:disabled) { color: var(--mq-text); border-color: rgba(255, 255, 255, 0.25); background: var(--mq-surface); }
|
||||
.mq-btn:focus-visible { outline: 2px solid var(--mq-accent); outline-offset: 2px; }
|
||||
|
||||
.mq-note { margin: 0 0 16px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
.mq-empty { padding: 40px 20px; text-align: center; color: var(--mq-muted); border: 1px dashed var(--mq-border); }
|
||||
.mq-empty strong { display: block; margin-bottom: 4px; color: var(--mq-text); font-size: 1.05em; }
|
||||
|
||||
.mq-pager { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 24px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
|
||||
.mq-modal { position: fixed; inset: 0; z-index: 100100; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(4px); padding: 16px; }
|
||||
.mq-modal-box {
|
||||
width: min(420px, 100%); padding: 22px; text-align: left; color: var(--text-color, #fff);
|
||||
background: var(--bg); border: 1px solid var(--accent); box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.mq-modal-box h3 { margin: 0 0 6px; font-size: 1.15em; font-weight: 700; }
|
||||
.mq-modal-box p { margin: 0 0 14px; font-size: 0.88em; color: var(--text-muted, #8a8f98); }
|
||||
.mq-modal-box label { display: block; margin-bottom: 6px; font-size: 0.78em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted, #8a8f98); }
|
||||
.mq-modal-box textarea {
|
||||
width: 100%; box-sizing: border-box; padding: 10px 12px; margin-bottom: 14px; resize: vertical;
|
||||
background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.1); color: inherit; font: inherit; font-size: 0.9em; outline: none;
|
||||
}
|
||||
.mq-modal-box textarea:focus { border-color: var(--accent); }
|
||||
.mq-modal-box textarea.is-invalid { border-color: #ff5c5c; }
|
||||
.mq-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.mq-modal .mq-btn { height: 38px; padding: 0 16px; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Shared helpers for the moderation card pages. Re-assigned on every (AJAX) page load.
|
||||
window.mq = (() => {
|
||||
const modal = document.getElementById('mq-modal');
|
||||
const title = document.getElementById('mq-modal-title');
|
||||
const text = document.getElementById('mq-modal-text');
|
||||
const reasonWrap = document.getElementById('mq-modal-reason-wrap');
|
||||
const reasonInput = document.getElementById('mq-modal-reason');
|
||||
const btnConfirm = document.getElementById('mq-modal-confirm');
|
||||
const btnCancel = document.getElementById('mq-modal-cancel');
|
||||
|
||||
const csrf = () => (window.f0ckSession && window.f0ckSession.csrf_token) || '';
|
||||
const flash = (msg, type) => { if (window.flashMessage) window.flashMessage(msg, type === 'error' ? 3000 : 2000, type); };
|
||||
|
||||
let pendingAction = null;
|
||||
let needsReason = false;
|
||||
|
||||
const close = () => { modal.style.display = 'none'; pendingAction = null; };
|
||||
const confirm = ({ heading, body, confirmLabel = 'Confirm', withReason = false, action }) => {
|
||||
title.innerText = heading;
|
||||
text.innerText = body;
|
||||
pendingAction = action;
|
||||
needsReason = withReason;
|
||||
btnConfirm.innerText = confirmLabel;
|
||||
btnConfirm.dataset.label = confirmLabel;
|
||||
reasonWrap.style.display = withReason ? 'block' : 'none';
|
||||
reasonInput.value = '';
|
||||
reasonInput.classList.remove('is-invalid');
|
||||
modal.style.display = 'flex';
|
||||
if (withReason) reasonInput.focus();
|
||||
};
|
||||
|
||||
btnConfirm.addEventListener('click', async () => {
|
||||
if (!pendingAction) return;
|
||||
let reason = null;
|
||||
if (needsReason) {
|
||||
reason = reasonInput.value.trim();
|
||||
if (!reason) { reasonInput.classList.add('is-invalid'); reasonInput.focus(); return; }
|
||||
}
|
||||
btnConfirm.disabled = true;
|
||||
btnConfirm.innerText = 'Processing…';
|
||||
try {
|
||||
await pendingAction(reason);
|
||||
close();
|
||||
} catch (e) {
|
||||
flash('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
btnConfirm.disabled = false;
|
||||
btnConfirm.innerText = btnConfirm.dataset.label || 'Confirm';
|
||||
}
|
||||
});
|
||||
btnCancel.addEventListener('click', close);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) close(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.style.display !== 'none') close(); });
|
||||
|
||||
const post = async (url, body) => {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify(body || {})
|
||||
});
|
||||
let data = {};
|
||||
try { data = await res.json(); } catch {}
|
||||
if (!res.ok || !data.success) throw new Error(data.msg || ('Request failed (' + res.status + ')'));
|
||||
return data;
|
||||
};
|
||||
|
||||
const removeCard = (card) => {
|
||||
if (!card) return;
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
card.remove();
|
||||
const grid = document.querySelector('.mq-grid');
|
||||
if (grid && !grid.querySelector('.mq-card')) location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Rating toggle (same on both pages)
|
||||
document.querySelectorAll('.btn-rating-toggle-async').forEach(btn => {
|
||||
btn.addEventListener('click', async e => {
|
||||
e.preventDefault();
|
||||
const tagsContainer = btn.closest('.mq-card')?.querySelector('.mq-tags');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/v2/tags/' + btn.dataset.id + '/toggle', {
|
||||
method: 'PUT',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-Token': csrf() }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.tags) throw new Error(data.msg || 'Unknown error');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '';
|
||||
data.tags.forEach(tag => {
|
||||
let cls = 'badge-light';
|
||||
if (tag.normalized === 'sfw') cls = 'badge-success';
|
||||
else if (tag.normalized === 'nsfw') cls = 'badge-danger';
|
||||
else if (tag.tag.startsWith('>')) cls = 'badge-greentext badge-light';
|
||||
const span = document.createElement('span');
|
||||
span.className = 'badge ' + cls;
|
||||
span.innerText = tag.tag;
|
||||
tagsContainer.appendChild(span);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
flash('Error: ' + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { confirm, post, flash, removeCard };
|
||||
})();
|
||||
</script>
|
||||
+392
-128
@@ -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;">×</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;">×</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;">×</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">×</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>
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{{-- Item visibility modal (shared by item-partial-modern / item-partial-legacy). JS hooks: #visibility-modal, #visibility-item-id, input[name=visibility], #visibility-modal-cancel/-save --}}
|
||||
<div id="visibility-modal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-content vis-modal-content">
|
||||
<div class="vis-header">
|
||||
<h3><i class="fa-solid fa-eye"></i> Change visibility</h3>
|
||||
<p>Choose who can see this post.</p>
|
||||
</div>
|
||||
<div class="vis-body">
|
||||
<form id="visibility-form" onsubmit="return false;">
|
||||
<input type="hidden" id="visibility-item-id" value="{{ item.id }}" />
|
||||
<div class="visibility-options">
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="visibility" value="0" />
|
||||
<span class="vis-radio"></span>
|
||||
<span class="vis-text">
|
||||
<strong><i class="fa-solid fa-globe"></i> Public</strong>
|
||||
<span>Visible to everyone in main feeds, search, and rankings.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="visibility" value="1" />
|
||||
<span class="vis-radio"></span>
|
||||
<span class="vis-text">
|
||||
<strong><i class="fa-solid fa-link"></i> Unlisted</strong>
|
||||
<span>Hidden from main feeds; accessible only via direct link or slug.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="visibility-option">
|
||||
<input type="radio" name="visibility" value="2" />
|
||||
<span class="vis-radio"></span>
|
||||
<span class="vis-text">
|
||||
<strong><i class="fa-solid fa-lock"></i> Private</strong>
|
||||
<span>Only visible to you and site moderators.</span>
|
||||
</span>
|
||||
</label>
|
||||
@if(is_mod_or_admin)
|
||||
<label class="visibility-option vis-danger">
|
||||
<input type="radio" name="visibility" value="3" />
|
||||
<span class="vis-radio"></span>
|
||||
<span class="vis-text">
|
||||
<strong><i class="fa-solid fa-ban"></i> Unavailable (451)</strong>
|
||||
<span>Only visible to administrators. Serves HTTP 451 on direct media for all other users.</span>
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-actions vis-footer">
|
||||
<button type="button" class="btn-secondary" id="visibility-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn-primary" id="visibility-modal-save">Save visibility</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Visibility modal — same rules as the report/info modals: square, flat, fixed header/footer, full screen on phones */
|
||||
#visibility-modal .vis-modal-content {
|
||||
--vm-accent: var(--accent, #0096ff);
|
||||
--vm-text: var(--text-color, #fff);
|
||||
--vm-muted: var(--text-muted, #8a8f98);
|
||||
--vm-surface: rgba(255, 255, 255, 0.04);
|
||||
--vm-border: rgba(255, 255, 255, 0.1);
|
||||
--vm-danger: #ff5c5c;
|
||||
width: min(440px, 92vw);
|
||||
max-height: min(90vh, 90dvh);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
padding: 0; text-align: left;
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid var(--accent) !important;
|
||||
background: var(--bg) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5) !important;
|
||||
color: var(--vm-text);
|
||||
}
|
||||
#visibility-modal .vis-modal-content * { border-radius: 0 !important; }
|
||||
|
||||
#visibility-modal .vis-header { flex-shrink: 0; padding: 20px 24px 14px; border-bottom: 1px solid var(--vm-border); }
|
||||
#visibility-modal .vis-header h3 { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 1.2em; font-weight: 700; color: var(--vm-text); }
|
||||
#visibility-modal .vis-header h3 i { color: var(--vm-accent); font-size: 0.9em; }
|
||||
#visibility-modal .vis-header p { margin: 6px 0 0; font-size: 0.85em; color: var(--vm-muted); }
|
||||
|
||||
#visibility-modal .vis-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 16px 24px; }
|
||||
#visibility-modal .visibility-options { display: flex; flex-direction: column; gap: 6px; }
|
||||
#visibility-modal .visibility-option {
|
||||
display: flex; align-items: flex-start; gap: 12px; margin: 0; padding: 12px 14px;
|
||||
background: var(--vm-surface); border: 1px solid var(--vm-border);
|
||||
cursor: pointer; user-select: none; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
#visibility-modal .visibility-option:hover { border-color: rgba(255, 255, 255, 0.22); }
|
||||
#visibility-modal .visibility-option:has(input:checked) { border-color: var(--vm-accent); background: color-mix(in srgb, var(--vm-accent) 8%, transparent); }
|
||||
#visibility-modal .visibility-option input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||||
#visibility-modal .vis-radio {
|
||||
position: relative; width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28); background: var(--vm-surface); transition: border-color 0.15s;
|
||||
}
|
||||
#visibility-modal .vis-radio::after {
|
||||
content: ''; position: absolute; inset: 4px; background: var(--vm-accent); transform: scale(0); transition: transform 0.15s;
|
||||
}
|
||||
#visibility-modal .visibility-option input:checked + .vis-radio { border-color: var(--vm-accent); }
|
||||
#visibility-modal .visibility-option input:checked + .vis-radio::after { transform: scale(1); }
|
||||
#visibility-modal .visibility-option input:focus-visible + .vis-radio { outline: 2px solid var(--vm-accent); outline-offset: 2px; }
|
||||
#visibility-modal .vis-text { display: flex; flex-direction: column; gap: 3px; font-size: 0.88em; line-height: 1.4; }
|
||||
#visibility-modal .vis-text strong { display: flex; align-items: center; gap: 7px; color: var(--vm-text); font-weight: 700; }
|
||||
#visibility-modal .vis-text strong i { width: 14px; text-align: center; color: var(--vm-muted); }
|
||||
#visibility-modal .visibility-option:has(input:checked) .vis-text strong i { color: var(--vm-accent); }
|
||||
#visibility-modal .vis-text > span { color: var(--vm-muted); }
|
||||
|
||||
#visibility-modal .vis-danger .vis-text strong,
|
||||
#visibility-modal .vis-danger .vis-text strong i { color: var(--vm-danger); }
|
||||
#visibility-modal .vis-danger:has(input:checked) { border-color: var(--vm-danger); background: rgba(255, 92, 92, 0.08); }
|
||||
#visibility-modal .vis-danger input:checked + .vis-radio { border-color: var(--vm-danger); }
|
||||
#visibility-modal .vis-danger .vis-radio::after { background: var(--vm-danger); }
|
||||
|
||||
#visibility-modal .vis-footer {
|
||||
flex-shrink: 0; display: flex; justify-content: flex-end; gap: 8px; margin: 0;
|
||||
padding: 14px 24px; border-top: 1px solid var(--vm-border); background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
#visibility-modal .vis-footer button {
|
||||
height: 40px; margin: 0; padding: 0 18px; font: inherit; font-size: 0.85em; cursor: pointer;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
transition: background 0.18s, color 0.18s, border-color 0.18s;
|
||||
}
|
||||
#visibility-modal #visibility-modal-cancel { background: transparent; border: 1px solid var(--vm-border); color: var(--vm-muted); font-weight: 600; }
|
||||
#visibility-modal #visibility-modal-cancel:hover { color: var(--vm-text); border-color: rgba(255, 255, 255, 0.25); background: var(--vm-surface); }
|
||||
#visibility-modal #visibility-modal-save {
|
||||
background: var(--vm-accent); border: 1px solid var(--vm-accent); color: #000;
|
||||
font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
#visibility-modal #visibility-modal-save:hover:not(:disabled) { background: transparent; color: var(--vm-accent); }
|
||||
#visibility-modal #visibility-modal-save:disabled { opacity: 0.6; cursor: progress; }
|
||||
#visibility-modal .vis-footer button:focus-visible { outline: 2px solid var(--vm-accent); outline-offset: 2px; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#visibility-modal { padding: 0 !important; align-items: stretch !important; }
|
||||
#visibility-modal .vis-modal-content {
|
||||
width: 100vw; max-width: none;
|
||||
height: 100vh; height: 100dvh; max-height: none;
|
||||
border: 0 !important; box-shadow: none !important;
|
||||
}
|
||||
#visibility-modal .vis-footer { padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }
|
||||
#visibility-modal .vis-footer button { flex: 1; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user