2958 lines
93 KiB
HTML
2958 lines
93 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Blair — Ghost Dashboard</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Share+Tech+Mono&display=swap"
|
|
rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #0a0a0f;
|
|
--bg2: #111118;
|
|
--bg3: #16161f;
|
|
--bg4: #1d1d2a;
|
|
--bg5: #22222e;
|
|
--border: rgba(255, 255, 255, .07);
|
|
--border2: rgba(255, 255, 255, .13);
|
|
--border3: rgba(255, 255, 255, .22);
|
|
--text: #e8e4d8;
|
|
--muted: #7a7568;
|
|
--muted2: #a09890;
|
|
--accent: #c8a96e;
|
|
--accent2: #e8c98e;
|
|
--red: #c04040;
|
|
--red2: #e05050;
|
|
--green: #4a9c6a;
|
|
--green2: #5ab87e;
|
|
--blue: #4a7ac0;
|
|
--blue2: #6a9add;
|
|
--purple: #8a5ac0;
|
|
--purple2: #a87add;
|
|
--discord: #5865F2;
|
|
--orange2: #e09860;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'Crimson Pro', Georgia, serif;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image: radial-gradient(ellipse 80% 60% at 20% 10%, rgba(100, 40, 140, .06) 0%, transparent 60%), radial-gradient(ellipse 60% 80% at 80% 90%, rgba(40, 80, 160, .05) 0%, transparent 60%);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.wrap {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 0 24px 80px;
|
|
}
|
|
|
|
/* HEADER */
|
|
.site-header {
|
|
padding: 34px 0 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.header-inner {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.site-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 40px;
|
|
font-weight: 700;
|
|
letter-spacing: 8px;
|
|
color: var(--accent);
|
|
text-transform: uppercase;
|
|
line-height: 1;
|
|
}
|
|
|
|
.site-sub {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 4px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.auth-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
padding: 7px 13px;
|
|
}
|
|
|
|
.auth-avatar {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.auth-name {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 1.5px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.auth-role {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
padding: 2px 7px;
|
|
border-radius: 2px;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.role-admin {
|
|
background: rgba(192, 64, 64, .15);
|
|
color: var(--red2);
|
|
border: 1px solid rgba(192, 64, 64, .3);
|
|
}
|
|
|
|
.role-user {
|
|
background: rgba(74, 122, 192, .12);
|
|
color: var(--blue2);
|
|
border: 1px solid rgba(74, 122, 192, .3);
|
|
}
|
|
|
|
.mode-pills {
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
}
|
|
|
|
.mode-pill {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: none;
|
|
border: none;
|
|
color: var(--muted);
|
|
padding: 7px 14px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
position: relative;
|
|
}
|
|
|
|
.mode-pill:hover {
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.mode-pill.active {
|
|
background: var(--bg5);
|
|
color: var(--accent);
|
|
border: 1px solid var(--border2);
|
|
}
|
|
|
|
.mode-pill.ap.active {
|
|
background: rgba(192, 64, 64, .12);
|
|
color: var(--red2);
|
|
border-color: rgba(192, 64, 64, .3);
|
|
}
|
|
|
|
.mode-pill.up.active {
|
|
background: rgba(74, 122, 192, .12);
|
|
color: var(--blue2);
|
|
border-color: rgba(74, 122, 192, .3);
|
|
}
|
|
|
|
.pill-badge {
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -6px;
|
|
background: var(--red);
|
|
color: #fff;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
border-radius: 10px;
|
|
padding: 1px 5px;
|
|
min-width: 16px;
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
|
|
.pill-badge.show {
|
|
display: block;
|
|
}
|
|
|
|
/* TABS */
|
|
.top-nav {
|
|
display: flex;
|
|
gap: 2px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 28px;
|
|
padding-top: 6px;
|
|
}
|
|
|
|
.nav-btn {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--muted);
|
|
padding: 10px 18px 12px;
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
margin-bottom: -1px;
|
|
}
|
|
|
|
.nav-btn:hover {
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.nav-btn.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
.panel {
|
|
display: none;
|
|
}
|
|
|
|
.panel.active {
|
|
display: block;
|
|
}
|
|
|
|
/* EV FILTER */
|
|
.filter-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.filter-label-sm {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
flex-basis: 100%;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.ev-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 7px 14px;
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 1.5px;
|
|
color: var(--muted2);
|
|
text-transform: uppercase;
|
|
transition: all .18s;
|
|
user-select: none;
|
|
}
|
|
|
|
.ev-btn:hover {
|
|
border-color: var(--border3);
|
|
color: var(--text);
|
|
}
|
|
|
|
.ev-btn.ef {
|
|
background: rgba(200, 169, 110, .12);
|
|
border-color: var(--accent);
|
|
color: var(--accent2);
|
|
}
|
|
|
|
.ev-btn.ex {
|
|
background: rgba(192, 64, 64, .12);
|
|
border-color: var(--red);
|
|
color: var(--red2);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.ev-num {
|
|
background: var(--bg4);
|
|
border-radius: 10px;
|
|
padding: 1px 7px;
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.ev-btn.ef .ev-num {
|
|
background: rgba(200, 169, 110, .2);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.result-bar {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
flex-basis: 100%;
|
|
display: none;
|
|
}
|
|
|
|
.result-bar span {
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* GHOST GRID */
|
|
.ghost-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(295px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.ghost-card {
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: all .22s;
|
|
}
|
|
|
|
.ghost-card:hover {
|
|
border-color: var(--border2);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.ghost-card.hl {
|
|
border-color: rgba(200, 169, 110, .45);
|
|
box-shadow: 0 0 0 1px rgba(200, 169, 110, .12) inset;
|
|
}
|
|
|
|
.ghost-card.dm {
|
|
opacity: .2;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card-hd {
|
|
padding: 15px 17px 11px;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.card-name {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--accent2);
|
|
letter-spacing: .8px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.card-num {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.sb {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
padding: 3px 8px;
|
|
border-radius: 2px;
|
|
flex-shrink: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sb-lo {
|
|
background: rgba(74, 156, 106, .15);
|
|
color: var(--green2);
|
|
border: 1px solid rgba(74, 156, 106, .3);
|
|
}
|
|
|
|
.sb-mi {
|
|
background: rgba(200, 169, 110, .12);
|
|
color: var(--accent2);
|
|
border: 1px solid rgba(200, 169, 110, .25);
|
|
}
|
|
|
|
.sb-hi {
|
|
background: rgba(192, 64, 64, .15);
|
|
color: var(--red2);
|
|
border: 1px solid rgba(192, 64, 64, .3);
|
|
}
|
|
|
|
.sb-va {
|
|
background: rgba(138, 90, 192, .12);
|
|
color: var(--purple2);
|
|
border: 1px solid rgba(138, 90, 192, .3);
|
|
}
|
|
|
|
.card-evs {
|
|
padding: 11px 17px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.ev-pill {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
padding: 3px 8px;
|
|
border-radius: 2px;
|
|
background: var(--bg4);
|
|
border: 1px solid var(--border2);
|
|
color: var(--muted2);
|
|
transition: all .18s;
|
|
}
|
|
|
|
.ev-pill.pf {
|
|
background: rgba(200, 169, 110, .15);
|
|
border-color: rgba(200, 169, 110, .4);
|
|
color: var(--accent2);
|
|
}
|
|
|
|
.ev-pill.px {
|
|
background: rgba(192, 64, 64, .12);
|
|
border-color: rgba(192, 64, 64, .35);
|
|
color: var(--red2);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.card-tip {
|
|
padding: 11px 17px 13px;
|
|
font-size: 13.5px;
|
|
color: var(--muted2);
|
|
line-height: 1.5;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* CHEAT TABLE */
|
|
.cheat-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.cheat-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13.5px;
|
|
}
|
|
|
|
.cheat-table th {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
padding: 10px 11px;
|
|
border-bottom: 1px solid var(--border2);
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--bg);
|
|
}
|
|
|
|
.cheat-table th:first-child {
|
|
text-align: left;
|
|
min-width: 105px;
|
|
}
|
|
|
|
.cheat-table td {
|
|
padding: 8px 11px;
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.cheat-table td:first-child {
|
|
text-align: left;
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 13px;
|
|
color: var(--accent2);
|
|
cursor: pointer;
|
|
letter-spacing: .5px;
|
|
}
|
|
|
|
.cheat-table td:first-child:hover {
|
|
color: var(--accent);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.cheat-table tr:hover td {
|
|
background: rgba(255, 255, 255, .02);
|
|
}
|
|
|
|
.chk {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.tip-cell {
|
|
text-align: left;
|
|
font-size: 12px;
|
|
color: var(--muted2);
|
|
font-style: italic;
|
|
min-width: 200px;
|
|
max-width: 360px;
|
|
}
|
|
|
|
.scc {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
padding: 2px 7px;
|
|
border-radius: 2px;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* GUIDE */
|
|
.guide-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.guide-card {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 15px 17px;
|
|
}
|
|
|
|
.guide-card-title {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.guide-card-body {
|
|
font-size: 13.5px;
|
|
color: var(--muted2);
|
|
line-height: 1.75;
|
|
}
|
|
|
|
.guide-card-body strong {
|
|
color: var(--text);
|
|
}
|
|
|
|
.divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin: 28px 0 18px;
|
|
}
|
|
|
|
.divider-line {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.divider-text {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 4px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.acc-l {
|
|
border-left: 2px solid var(--accent);
|
|
}
|
|
|
|
.red-l {
|
|
border-left: 2px solid var(--red);
|
|
}
|
|
|
|
.blue-l {
|
|
border-left: 2px solid var(--blue);
|
|
}
|
|
|
|
.purple-l {
|
|
border-left: 2px solid var(--purple);
|
|
}
|
|
|
|
/* MODALS */
|
|
.modal-bg {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(5, 5, 10, .88);
|
|
z-index: 200;
|
|
overflow-y: auto;
|
|
padding: 36px 18px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.modal-bg.open {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.modal-box {
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 6px;
|
|
max-width: 760px;
|
|
width: 100%;
|
|
position: relative;
|
|
animation: mIn .2s ease;
|
|
}
|
|
|
|
.modal-box.wide {
|
|
max-width: 920px;
|
|
}
|
|
|
|
@keyframes mIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
.modal-close {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 16px;
|
|
background: none;
|
|
border: none;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
padding: 4px 8px;
|
|
transition: color .15s;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
.modal-hd {
|
|
padding: 26px 30px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.modal-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: 3px;
|
|
}
|
|
|
|
.modal-sub {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 22px 30px 30px;
|
|
}
|
|
|
|
.wiki-sec {
|
|
margin-bottom: 22px;
|
|
}
|
|
|
|
.wiki-sec:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.wiki-hd {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-bottom: 8px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.wiki-text {
|
|
font-size: 14.5px;
|
|
color: var(--muted2);
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.wiki-text strong {
|
|
color: var(--text);
|
|
}
|
|
|
|
.wiki-ev-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 7px;
|
|
}
|
|
|
|
.wiki-ev-p {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
padding: 5px 13px;
|
|
border-radius: 3px;
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.tells-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
}
|
|
|
|
.tell {
|
|
display: flex;
|
|
gap: 11px;
|
|
align-items: flex-start;
|
|
padding: 9px 13px;
|
|
background: var(--bg3);
|
|
border-radius: 3px;
|
|
border-left: 2px solid var(--accent);
|
|
}
|
|
|
|
.tell-ico {
|
|
flex-shrink: 0;
|
|
font-size: 13px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.tell-text {
|
|
font-size: 13.5px;
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.tell-text strong {
|
|
color: var(--text);
|
|
}
|
|
|
|
.sanity-blk {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
background: var(--bg3);
|
|
border-radius: 3px;
|
|
padding: 9px 15px;
|
|
border: 1px solid var(--border2);
|
|
}
|
|
|
|
.sanity-pct {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 22px;
|
|
}
|
|
|
|
.sanity-note {
|
|
font-size: 13px;
|
|
color: var(--muted2);
|
|
}
|
|
|
|
/* ADMIN */
|
|
.admin-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.admin-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: var(--red2);
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.admin-sub {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 12px 15px;
|
|
}
|
|
|
|
.stat-val {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 22px;
|
|
color: var(--accent2);
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.admin-ghost-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.agr {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 12px 16px;
|
|
transition: border-color .18s;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.agr:hover {
|
|
border-color: var(--border3);
|
|
}
|
|
|
|
.agr-name {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 14px;
|
|
color: var(--accent2);
|
|
letter-spacing: .5px;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.agr-evs {
|
|
display: flex;
|
|
gap: 5px;
|
|
flex-wrap: wrap;
|
|
flex: 1;
|
|
}
|
|
|
|
.agr-ev {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
padding: 2px 7px;
|
|
background: var(--bg4);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 2px;
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.agr-meta {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agr-btns {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.icon-btn {
|
|
background: none;
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
transition: all .18s;
|
|
}
|
|
|
|
.icon-btn.ibe:hover {
|
|
color: var(--accent2);
|
|
border-color: rgba(200, 169, 110, .4);
|
|
}
|
|
|
|
.icon-btn.ibd:hover {
|
|
color: var(--red2);
|
|
border-color: rgba(192, 64, 64, .4);
|
|
}
|
|
|
|
.sub-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.sub-item {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 15px 18px;
|
|
}
|
|
|
|
.sub-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.sub-ghost {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 13px;
|
|
color: var(--accent2);
|
|
}
|
|
|
|
.sub-type {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
padding: 2px 8px;
|
|
border-radius: 2px;
|
|
border: 1px solid;
|
|
}
|
|
|
|
.stt {
|
|
color: var(--blue2);
|
|
border-color: rgba(74, 122, 192, .35);
|
|
background: rgba(74, 122, 192, .1);
|
|
}
|
|
|
|
.ste {
|
|
color: var(--orange2);
|
|
border-color: rgba(192, 120, 64, .35);
|
|
background: rgba(192, 120, 64, .1);
|
|
}
|
|
|
|
.stn {
|
|
color: var(--green2);
|
|
border-color: rgba(74, 156, 106, .35);
|
|
background: rgba(74, 156, 106, .1);
|
|
}
|
|
|
|
.sub-time {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.sub-content {
|
|
font-size: 13.5px;
|
|
color: var(--muted2);
|
|
line-height: 1.6;
|
|
margin-bottom: 10px;
|
|
font-style: italic;
|
|
}
|
|
|
|
.sub-actions {
|
|
display: flex;
|
|
gap: 7px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.badge-count {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--red);
|
|
color: #fff;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
border-radius: 10px;
|
|
padding: 1px 6px;
|
|
margin-left: 6px;
|
|
min-width: 18px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px 24px;
|
|
color: var(--muted);
|
|
font-style: italic;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.wl-section {
|
|
background: var(--bg3);
|
|
border: 1px solid rgba(192, 64, 64, .25);
|
|
border-radius: 4px;
|
|
padding: 18px 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.wl-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 14px;
|
|
color: var(--red2);
|
|
letter-spacing: 1px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.wl-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: var(--bg4);
|
|
border-radius: 3px;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border2);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.wl-name {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 11px;
|
|
color: var(--accent2);
|
|
}
|
|
|
|
.wl-id {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 11px;
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.wl-date {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* FORMS */
|
|
.form-section {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 20px 22px;
|
|
}
|
|
|
|
.form-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 15px;
|
|
color: var(--accent2);
|
|
letter-spacing: 1px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.form-row.full {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.form-label {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.form-input,
|
|
.form-select,
|
|
.form-textarea {
|
|
background: var(--bg4);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
color: var(--text);
|
|
font-family: 'Crimson Pro', Georgia, serif;
|
|
font-size: 14px;
|
|
padding: 8px 12px;
|
|
transition: border-color .18s;
|
|
width: 100%;
|
|
}
|
|
|
|
.form-input:focus,
|
|
.form-select:focus,
|
|
.form-textarea:focus {
|
|
outline: none;
|
|
border-color: var(--border3);
|
|
}
|
|
|
|
.form-textarea {
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.form-select option {
|
|
background: var(--bg3);
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
font-style: italic;
|
|
}
|
|
|
|
.ev-checks {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.ev-check-lbl {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
color: var(--muted2);
|
|
cursor: pointer;
|
|
padding: 5px 10px;
|
|
background: var(--bg4);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 2px;
|
|
transition: all .18s;
|
|
user-select: none;
|
|
}
|
|
|
|
.ev-check-lbl:hover {
|
|
border-color: var(--border3);
|
|
color: var(--text);
|
|
}
|
|
|
|
.ev-check-lbl input {
|
|
display: none;
|
|
}
|
|
|
|
.ev-check-lbl.ck {
|
|
background: rgba(200, 169, 110, .12);
|
|
border-color: rgba(200, 169, 110, .4);
|
|
color: var(--accent2);
|
|
}
|
|
|
|
.tells-editor {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.tell-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.tell-row .form-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.tell-ico-in {
|
|
width: 58px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.remove-btn {
|
|
background: none;
|
|
border: 1px solid rgba(192, 64, 64, .3);
|
|
border-radius: 3px;
|
|
color: var(--red2);
|
|
cursor: pointer;
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
transition: all .18s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.remove-btn:hover {
|
|
background: rgba(192, 64, 64, .12);
|
|
}
|
|
|
|
.add-tell-btn {
|
|
background: none;
|
|
border: 1px dashed var(--border2);
|
|
border-radius: 3px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
padding: 7px;
|
|
font-size: 11px;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
transition: all .18s;
|
|
width: 100%;
|
|
}
|
|
|
|
.add-tell-btn:hover {
|
|
border-color: var(--border3);
|
|
color: var(--muted2);
|
|
}
|
|
|
|
.form-footer {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
margin-top: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* BUTTONS */
|
|
.btn-sm {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: none;
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
padding: 7px 14px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-sm:hover {
|
|
color: var(--text);
|
|
border-color: var(--border3);
|
|
}
|
|
|
|
.btn-accent {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: rgba(200, 169, 110, .12);
|
|
border: 1px solid rgba(200, 169, 110, .35);
|
|
border-radius: 3px;
|
|
padding: 8px 16px;
|
|
color: var(--accent2);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-accent:hover {
|
|
background: rgba(200, 169, 110, .22);
|
|
}
|
|
|
|
.btn-danger {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: rgba(192, 64, 64, .12);
|
|
border: 1px solid rgba(192, 64, 64, .35);
|
|
border-radius: 3px;
|
|
padding: 8px 16px;
|
|
color: var(--red2);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: rgba(192, 64, 64, .22);
|
|
}
|
|
|
|
.btn-success {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: rgba(74, 156, 106, .12);
|
|
border: 1px solid rgba(74, 156, 106, .35);
|
|
border-radius: 3px;
|
|
padding: 8px 16px;
|
|
color: var(--green2);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: rgba(74, 156, 106, .22);
|
|
}
|
|
|
|
.btn-blue {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: rgba(74, 122, 192, .12);
|
|
border: 1px solid rgba(74, 122, 192, .35);
|
|
border-radius: 3px;
|
|
padding: 8px 16px;
|
|
color: var(--blue2);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-blue:hover {
|
|
background: rgba(74, 122, 192, .22);
|
|
}
|
|
|
|
.btn-discord {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 11px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: var(--discord);
|
|
border: none;
|
|
border-radius: 3px;
|
|
padding: 11px 22px;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
transition: opacity .18s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn-discord:hover {
|
|
opacity: .88;
|
|
}
|
|
|
|
.btn-discord svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.btn-logout {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
background: none;
|
|
border: 1px solid var(--border2);
|
|
border-radius: 3px;
|
|
padding: 5px 10px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.btn-logout:hover {
|
|
color: var(--red2);
|
|
border-color: rgba(192, 64, 64, .4);
|
|
}
|
|
|
|
/* LOGIN */
|
|
.login-screen {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--bg);
|
|
z-index: 500;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.login-screen.show {
|
|
display: flex;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.login-box {
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 8px;
|
|
padding: 48px 52px;
|
|
max-width: 440px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.login-logo {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 48px;
|
|
font-weight: 700;
|
|
letter-spacing: 10px;
|
|
color: var(--accent);
|
|
line-height: 1;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.login-sub {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 4px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
margin-bottom: 36px;
|
|
}
|
|
|
|
.login-desc {
|
|
font-size: 14px;
|
|
color: var(--muted2);
|
|
line-height: 1.7;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.login-desc strong {
|
|
color: var(--text);
|
|
}
|
|
|
|
.login-or {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
margin: 18px 0;
|
|
display: block;
|
|
}
|
|
|
|
.login-guest-note {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-top: 18px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* COMMUNITY */
|
|
.user-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: var(--blue2);
|
|
letter-spacing: 2px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.usi {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 4px;
|
|
padding: 13px 16px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.usi-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.usi-st {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
padding: 2px 8px;
|
|
border-radius: 2px;
|
|
border: 1px solid;
|
|
}
|
|
|
|
.ssp {
|
|
color: var(--accent2);
|
|
border-color: rgba(200, 169, 110, .35);
|
|
background: rgba(200, 169, 110, .1);
|
|
}
|
|
|
|
.ssa {
|
|
color: var(--green2);
|
|
border-color: rgba(74, 156, 106, .35);
|
|
background: rgba(74, 156, 106, .1);
|
|
}
|
|
|
|
.ssr {
|
|
color: var(--red2);
|
|
border-color: rgba(192, 64, 64, .35);
|
|
background: rgba(192, 64, 64, .1);
|
|
}
|
|
|
|
/* LOADING */
|
|
.loading {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(5, 5, 10, .7);
|
|
z-index: 400;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.loading.show {
|
|
display: flex;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.spinner {
|
|
width: 36px;
|
|
height: 36px;
|
|
border: 2px solid var(--border2);
|
|
border-top-color: var(--accent);
|
|
border-radius: 50%;
|
|
animation: spin .8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.loading-txt {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 10px;
|
|
letter-spacing: 3px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* CONFIRM */
|
|
.confirm-bg {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(5, 5, 10, .88);
|
|
z-index: 300;
|
|
align-items: center;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.confirm-bg.open {
|
|
display: flex;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.confirm-box {
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 6px;
|
|
padding: 28px 32px;
|
|
max-width: 380px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.confirm-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 16px;
|
|
color: var(--red2);
|
|
letter-spacing: 1px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.confirm-text {
|
|
font-size: 14px;
|
|
color: var(--muted2);
|
|
margin-bottom: 22px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.confirm-btns {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* TOAST */
|
|
.toast-wrap {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.toast {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 11px;
|
|
letter-spacing: 1px;
|
|
padding: 11px 18px;
|
|
border-radius: 4px;
|
|
border-left: 3px solid;
|
|
animation: tIn .25s ease;
|
|
pointer-events: all;
|
|
max-width: 340px;
|
|
}
|
|
|
|
@keyframes tIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
.toast-success {
|
|
background: rgba(74, 156, 106, .18);
|
|
border-color: var(--green2);
|
|
color: var(--green2);
|
|
}
|
|
|
|
.toast-danger {
|
|
background: rgba(192, 64, 64, .18);
|
|
border-color: var(--red2);
|
|
color: var(--red2);
|
|
}
|
|
|
|
.toast-info {
|
|
background: rgba(74, 122, 192, .18);
|
|
border-color: var(--blue2);
|
|
color: var(--blue2);
|
|
}
|
|
|
|
|
|
/* ═══════════ CREDITS PAGE ═══════════ */
|
|
.credits-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.credit-card {
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 6px;
|
|
padding: 22px 24px;
|
|
transition: all .22s;
|
|
}
|
|
|
|
.credit-card:hover {
|
|
border-color: var(--border3);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.credit-card.partner {
|
|
border-color: rgba(200, 169, 110, .3);
|
|
background: linear-gradient(135deg, var(--bg2) 0%, rgba(200, 169, 110, .04) 100%);
|
|
}
|
|
|
|
.credit-avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
background: var(--bg4);
|
|
border: 2px solid var(--border2);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.credit-avatar-placeholder {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: var(--bg4);
|
|
border: 2px solid var(--border2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 22px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.credit-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.credit-name {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--accent2);
|
|
letter-spacing: .5px;
|
|
}
|
|
|
|
.credit-role {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.credit-role.partner-role {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.credit-desc {
|
|
font-size: 13.5px;
|
|
color: var(--muted2);
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.credit-links {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.credit-link {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
padding: 4px 10px;
|
|
border-radius: 2px;
|
|
background: var(--bg4);
|
|
border: 1px solid var(--border2);
|
|
color: var(--muted2);
|
|
text-decoration: none;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.credit-link:hover {
|
|
color: var(--text);
|
|
border-color: var(--border3);
|
|
}
|
|
|
|
.credit-link.discord {
|
|
background: rgba(88, 101, 242, .12);
|
|
border-color: rgba(88, 101, 242, .35);
|
|
color: #7289da;
|
|
}
|
|
|
|
.credit-link.discord:hover {
|
|
background: rgba(88, 101, 242, .22);
|
|
}
|
|
|
|
.credit-link.roblox {
|
|
background: rgba(192, 64, 64, .1);
|
|
border-color: rgba(192, 64, 64, .3);
|
|
color: var(--red2);
|
|
}
|
|
|
|
.credit-link.roblox:hover {
|
|
background: rgba(192, 64, 64, .2);
|
|
}
|
|
|
|
.credit-link.website {
|
|
background: rgba(74, 122, 192, .1);
|
|
border-color: rgba(74, 122, 192, .3);
|
|
color: var(--blue2);
|
|
}
|
|
|
|
.credit-link.website:hover {
|
|
background: rgba(74, 122, 192, .2);
|
|
}
|
|
|
|
.partner-badge {
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 9px;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
padding: 2px 8px;
|
|
border-radius: 2px;
|
|
background: rgba(200, 169, 110, .15);
|
|
border: 1px solid rgba(200, 169, 110, .35);
|
|
color: var(--accent2);
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.credits-section-title {
|
|
font-family: 'Cinzel', serif;
|
|
font-size: 14px;
|
|
color: var(--accent);
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.credits-empty {
|
|
text-align: center;
|
|
padding: 40px 24px;
|
|
color: var(--muted);
|
|
font-style: italic;
|
|
font-size: 14px;
|
|
background: var(--bg3);
|
|
border: 1px dashed var(--border2);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.add-credit-form {
|
|
background: var(--bg3);
|
|
border: 1px solid var(--border2);
|
|
border-radius: 6px;
|
|
padding: 22px 24px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
@media(max-width:640px) {
|
|
.site-title {
|
|
font-size: 28px;
|
|
letter-spacing: 4px;
|
|
}
|
|
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.ghost-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.modal-hd,
|
|
.modal-body {
|
|
padding-left: 18px;
|
|
padding-right: 18px;
|
|
}
|
|
|
|
.login-box {
|
|
padding: 32px 24px;
|
|
}
|
|
|
|
.login-logo {
|
|
font-size: 34px;
|
|
letter-spacing: 6px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<!-- LOGIN -->
|
|
<div class="login-screen show" id="login-screen">
|
|
<div class="login-box">
|
|
<div class="login-logo">BLAIR</div>
|
|
<div class="login-sub">Ghost Investigation Dashboard</div>
|
|
<div class="login-desc">
|
|
Login with discord to submit <Strong>Tips</strong> and check the status of your submissions.<br><br>
|
|
<strong>Admin access</strong> will only be granted to unlocked Discord accounts.
|
|
</div>
|
|
<a class="btn-discord" href="/auth/discord" style="text-decoration:none;">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path
|
|
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057.101 18.08.114 18.1.134 18.114a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z" />
|
|
</svg>
|
|
Login with Discord
|
|
</a>
|
|
<span class="login-or">or</span>
|
|
<button class="btn-sm" id="guest-btn">Login with discord</button>
|
|
<div class="login-guest-note">Guests can only view Ghost Cards, Cheatsheet and Hunt Guide.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOADING -->
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<div class="loading-txt" id="loading-txt">Lade...</div>
|
|
</div>
|
|
|
|
<!-- APP -->
|
|
<div class="wrap" id="app" style="display:none;">
|
|
<header class="site-header">
|
|
<div class="header-inner">
|
|
<div>
|
|
<div class="site-title">BLAIR</div>
|
|
<div class="site-sub">Ghost Investigation Dashboard · Blair Cheatsheet</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="auth-chip" id="auth-chip" style="display:none;">
|
|
<img class="auth-avatar" id="auth-av" src="" alt="">
|
|
<span class="auth-name" id="auth-nm"></span>
|
|
<span class="auth-role" id="auth-rl"></span>
|
|
<button class="btn-logout">Logout</button>
|
|
</div>
|
|
<span id="guest-badge"
|
|
style="display:none;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:var(--muted);">GAST</span>
|
|
<div class="mode-pills">
|
|
<button class="mode-pill active" id="pill-viewer">👁 Viewer</button>
|
|
<button class="mode-pill up" id="pill-user" style="display:none;">✦ Community<span class="pill-badge"
|
|
id="badge-user"></span></button>
|
|
<button class="mode-pill ap" id="pill-admin" style="display:none;">⚙ Admin<span class="pill-badge"
|
|
id="badge-admin"></span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- VIEWER -->
|
|
<div id="mode-viewer">
|
|
<nav class="top-nav" id="vnav">
|
|
<button class="nav-btn active">Ghost Cards</button>
|
|
<button class="nav-btn">Cheatsheet</button>
|
|
<button class="nav-btn">Hunt Guide</button>
|
|
<button class="nav-btn">Credits</button>
|
|
</nav>
|
|
<div class="panel active" id="vtab-cards">
|
|
<div class="filter-row" id="ev-filter">
|
|
<div class="filter-label-sm">Evidence Filter — single click: found · double click: excluded</div>
|
|
</div>
|
|
<div class="ghost-grid" id="ghost-grid"></div>
|
|
</div>
|
|
<div class="panel" id="vtab-cheat">
|
|
<div class="cheat-wrap">
|
|
<table class="cheat-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Ghost</th>
|
|
<th>EMF</th>
|
|
<th>❄</th>
|
|
<th>✍</th>
|
|
<th>UV</th>
|
|
<th>◎</th>
|
|
<th>📻</th>
|
|
<th>SLS</th>
|
|
<th>Sanity</th>
|
|
<th>Tips</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="cheat-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="panel" id="vtab-guide">
|
|
<div style="max-width:800px;">
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Hunt Mechanics</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="guide-grid">
|
|
<div class="guide-card">
|
|
<div class="guide-card-title">Grace Period</div>
|
|
<div class="guide-card-body">Easy — 5s<br>Medium — 4s<br>Hard — 3s<br><strong
|
|
style="color:var(--red2)">Nightmare — 2s</strong></div>
|
|
</div>
|
|
<div class="guide-card">
|
|
<div class="guide-card-title">Sanity Drain/min</div>
|
|
<div class="guide-card-body">Easy — 2%<br>Medium — 2%<br>Hard — 3%<br><strong
|
|
style="color:var(--red2)">Nightmare — ~5%</strong></div>
|
|
</div>
|
|
<div class="guide-card">
|
|
<div class="guide-card-title">Hunt Triggers</div>
|
|
<div class="guide-card-body">Sanity ≤ ghost threshold<br>Cursed objects bypass sanity<br>Spirit Board — no
|
|
goodbye = hunt<br>Strigoi tap = 1/8 hunt chance</div>
|
|
</div>
|
|
<div class="guide-card">
|
|
<div class="guide-card-title">Survival Priority</div>
|
|
<div class="guide-card-body">1. Crucifix in ghost room<br>2. Incense (stun/cleanse)<br>3. Break line of
|
|
sight<br>4. Hide in closet/vehicle</div>
|
|
</div>
|
|
</div>
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Salt Tips</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="guide-card" style="margin-bottom:24px;">
|
|
<div class="guide-card-body"
|
|
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px 20px;">
|
|
<div><strong>Wraith</strong> — NEVER steps in salt.</div>
|
|
<div><strong>Harrow</strong> — Salt outside room = not Harrow.</div>
|
|
<div><strong>Revenant</strong> — Normal steps = no Revenant.</div>
|
|
<div><strong>Jiangshi</strong> — Skips the 2nd footstep.</div>
|
|
<div><strong>Yurei</strong> — Takes extra time to step on salt.</div>
|
|
<div><strong>Demon</strong> — Fast, accelerating steps.</div>
|
|
<div><strong>Strigoi</strong> — 4-finger UV handprint.</div>
|
|
<div><strong>Oni</strong> — Fast salt steps; opens/closes doors.</div>
|
|
</div>
|
|
</div>
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Cursed Objects</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="guide-grid">
|
|
<div class="guide-card red-l">
|
|
<div class="guide-card-title">Spirit Board</div>
|
|
<div class="guide-card-body">No "Goodbye" = forced hunt. ZoZo overrides 50%.</div>
|
|
</div>
|
|
<div class="guide-card purple-l">
|
|
<div class="guide-card-title">Tarot Cards</div>
|
|
<div class="guide-card-body">Death (hunt 10%), Moon (→0% sanity), Sun (→100%), Fool (negates prev).</div>
|
|
</div>
|
|
<div class="guide-card acc-l">
|
|
<div class="guide-card-title">Summoning Circle</div>
|
|
<div class="guide-card-body">Light all 5 candles → ghost manifests → hunt after 5s.</div>
|
|
</div>
|
|
<div class="guide-card blue-l">
|
|
<div class="guide-card-title">Music Box</div>
|
|
<div class="guide-card-body">Triggers cursed hunt once/match. Can lure ghost during hunts.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" id="vtab-credits">
|
|
<div style="max-width:1000px;">
|
|
<div
|
|
style="display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px;flex-wrap:wrap;">
|
|
<div>
|
|
<div
|
|
style="font-family:'Cinzel',serif;font-size:26px;font-weight:700;color:var(--accent);letter-spacing:3px;">
|
|
Credits & Partner</div>
|
|
<div
|
|
style="font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-top:5px;">
|
|
Danksagungen · Partner · Community</div>
|
|
</div>
|
|
<div id="credits-admin-btn" style="display:none;">
|
|
<button class="btn-accent" id="btn-add-credit" onclick="openCreditForm(null)">+ Eintrag
|
|
hinzufügen</button>
|
|
</div>
|
|
</div>
|
|
<div id="credits-partners-section">
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Partner</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="credits-grid" id="credits-partners-grid"></div>
|
|
</div>
|
|
<div id="credits-credits-section">
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Credits</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="credits-grid" id="credits-credits-grid"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADMIN -->
|
|
<div id="mode-admin" style="display:none;margin-top:24px;">
|
|
<div class="admin-header">
|
|
<div>
|
|
<div class="admin-title">⚙ Admin Dashboard</div>
|
|
<div class="admin-sub">Ghost Database Management · Nur für Whitelist-Admins</div>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
|
<button class="btn-accent" id="btn-new-ghost">+ New Ghost</button>
|
|
<button class="btn-sm" id="btn-export">↓ Export</button>
|
|
</div>
|
|
</div>
|
|
<div class="stats-row" id="admin-stats"></div>
|
|
<div class="wl-section">
|
|
<div class="wl-title">🔐 Admin Whitelist</div>
|
|
<div id="wl-list" style="margin-bottom:12px;"></div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
|
<input type="text" class="form-input" id="wl-id" placeholder="Discord ID (18-stellig)" style="width:210px;">
|
|
<input type="text" class="form-input" id="wl-name" placeholder="Username (optional)" style="width:160px;">
|
|
<button class="btn-success" id="btn-add-admin">+ Hinzufügen</button>
|
|
</div>
|
|
<div class="form-hint" style="margin-top:8px;">Discord → Einstellungen → Erweitert → Entwicklermodus →
|
|
Rechtsklick auf Nutzer → ID kopieren</div>
|
|
</div>
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text" id="sub-title">Community Submissions</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="sub-list" id="admin-sub-list" style="margin-bottom:28px;"></div>
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Ghost Database</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div class="admin-ghost-list" id="admin-ghost-list"></div>
|
|
</div>
|
|
|
|
<!-- COMMUNITY -->
|
|
<div id="mode-user" style="display:none;margin-top:24px;max-width:820px;">
|
|
<div class="user-title">✦ Community Hub</div>
|
|
<div
|
|
style="font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin:4px 0 22px;">
|
|
submit tips · report errors · add information</div>
|
|
<div class="form-section" style="margin-bottom:28px;">
|
|
<div class="form-title">Submit something</div>
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Ghost</label><select class="form-select" id="usr-ghost">
|
|
<option value="">— select ghost —</option>
|
|
</select></div>
|
|
<div class="form-group"><label class="form-label">Type</label>
|
|
<select class="form-select" id="usr-type">
|
|
<option value="tip">💡 New tip / trick</option>
|
|
<option value="edit">✏️ Error / correction</option>
|
|
<option value="new">🆕 Ghost missing</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group"><label class="form-label">Description</label>
|
|
<textarea class="form-textarea" id="usr-content"
|
|
placeholder="Describe your tip as detailed as possible..." rows="4"></textarea>
|
|
<span class="form-hint">The more detailed, the better for the admin!</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-footer"><button class="btn-blue" id="btn-submit-tip">Submit →</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="divider">
|
|
<div class="divider-line"></div>
|
|
<div class="divider-text">Your submissions</div>
|
|
<div class="divider-line"></div>
|
|
</div>
|
|
<div id="user-sub-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ghost View Modal -->
|
|
<div class="modal-bg" id="ghost-modal">
|
|
<div class="modal-box"><button class="modal-close" id="close-ghost-modal">✕</button>
|
|
<div class="modal-hd">
|
|
<div class="modal-title" id="gm-title"></div>
|
|
<div class="modal-sub" id="gm-sub"></div>
|
|
</div>
|
|
<div class="modal-body" id="gm-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ghost Editor Modal -->
|
|
<div class="modal-bg" id="edit-modal">
|
|
<div class="modal-box wide"><button class="modal-close" id="close-edit-modal">✕</button>
|
|
<div class="modal-hd">
|
|
<div class="modal-title" id="em-title">Edit Ghost</div>
|
|
<div class="modal-sub" id="em-sub">Admin · Ghost Editor</div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Name</label><input type="text" class="form-input"
|
|
id="ef-name"></div>
|
|
<div class="form-group"><label class="form-label">Sanity Threshold (%)</label><input type="number"
|
|
class="form-input" id="ef-sanity" min="0" max="100"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label class="form-label">Hunt Type</label>
|
|
<select class="form-select" id="ef-hunt">
|
|
<option value="mid">Mid (50%)</option>
|
|
<option value="low">Low (≤35%)</option>
|
|
<option value="high">High (≥80%)</option>
|
|
<option value="variable">Variable</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group"><label class="form-label">Sanity Note</label><input type="text" class="form-input"
|
|
id="ef-ss" placeholder="e.g. Target's individual sanity"></div>
|
|
</div>
|
|
<div class="form-group"><label class="form-label">Evidence (select exactly 3)</label>
|
|
<div class="ev-checks" id="ef-evs"></div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group"><label class="form-label">Short Tip (Ghost Card)</label><textarea
|
|
class="form-textarea" id="ef-tip" rows="2"></textarea></div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group"><label class="form-label">Description (Wiki)</label><textarea class="form-textarea"
|
|
id="ef-desc" rows="3"></textarea></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Identification Tells</label>
|
|
<div class="tells-editor" id="ef-tells"></div>
|
|
<button class="add-tell-btn" id="btn-add-tell">+ Add tell</button>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button class="btn-sm" id="btn-cancel-edit">Cancel</button>
|
|
<button class="btn-accent" id="btn-save-ghost">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Credits Editor Modal -->
|
|
<div class="modal-bg" id="credits-modal">
|
|
<div class="modal-box" style="max-width:600px;">
|
|
<button class="modal-close" id="close-credits-modal" onclick="closeModal('credits-modal')">✕</button>
|
|
<div class="modal-hd">
|
|
<div class="modal-title" id="cm-title">Add Entry</div>
|
|
<div class="modal-sub">Admin · Credits Editor</div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-grid">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-input" id="cf-name" placeholder="e.g. GhostHunter99">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" id="cf-type">
|
|
<option value="credit">✦ Credit</option>
|
|
<option value="partner">⭐ Partner</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Role / Title</label>
|
|
<input type="text" class="form-input" id="cf-role" placeholder="e.g. Wiki-Author, Community Manager">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Avatar URL (optional)</label>
|
|
<input type="text" class="form-input" id="cf-avatar" placeholder="https://...">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Emoji / Icon (if no Avatar)</label>
|
|
<input type="text" class="form-input" id="cf-emoji" placeholder="👻" maxlength="4" style="width:80px;">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-textarea" id="cf-desc" rows="2" placeholder="Short description..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Discord (optional)</label>
|
|
<input type="text" class="form-input" id="cf-discord" placeholder="Username or server link">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Website (optional)</label>
|
|
<input type="text" class="form-input" id="cf-website" placeholder="https://...">
|
|
</div>
|
|
</div>
|
|
<div class="form-row full">
|
|
<div class="form-group">
|
|
<label class="form-label">Roblox Profile URL (optional)</label>
|
|
<input type="text" class="form-input" id="cf-roblox" placeholder="https://www.roblox.com/users/...">
|
|
</div>
|
|
</div>
|
|
<div class="form-footer">
|
|
<button class="btn-sm" id="btn-cancel-credits" onclick="closeModal('credits-modal')">Cancel</button>
|
|
<button class="btn-accent" id="btn-save-credit" onclick="saveCreditEntry()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="confirm-bg" id="confirm-bg">
|
|
<div class="confirm-box">
|
|
<div class="confirm-title" id="confirm-title">Sure?</div>
|
|
<div class="confirm-text" id="confirm-text"></div>
|
|
<div class="confirm-btns"><button class="btn-sm">Cancel</button><button class="btn-danger"
|
|
id="confirm-ok">Confirm</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="toast-wrap" id="toast-wrap"></div>
|
|
|
|
<script>
|
|
/* ── CONSTANTS ── */
|
|
const EV_KEYS = ["EMF", "Freezing", "Writing", "UV", "Orbs", "SpiritBox", "SLS"];
|
|
const EV_LABELS = { EMF: "EMF 5", Freezing: "Freezing", Writing: "Writing", UV: "UV", Orbs: "Orbs", SpiritBox: "Spirit Box", SLS: "SLS" };
|
|
|
|
/* ── STATE ── */
|
|
let ghosts = [], currentUser = null, ev_state = {}, editingId = null, confirmCb = null;
|
|
EV_KEYS.forEach(k => ev_state[k] = 0);
|
|
|
|
/* ── INIT ── */
|
|
async function init() {
|
|
// Gast-Button Event hier setzen (kein inline onclick nötig)
|
|
document.getElementById('guest-btn').addEventListener('click', guestMode);
|
|
|
|
showLoading("Lade...");
|
|
try {
|
|
const r = await fetch('/auth/me');
|
|
if (!r.ok) throw new Error('auth failed');
|
|
const { user } = await r.json();
|
|
currentUser = user;
|
|
|
|
if (!user) {
|
|
hideLoading();
|
|
return; // login-screen ist schon sichtbar (show class per default)
|
|
}
|
|
|
|
await loadGhosts();
|
|
setupUI();
|
|
hideLoading();
|
|
hideLogin(); // erst jetzt verstecken
|
|
document.getElementById('app').style.display = 'block';
|
|
} catch (e) {
|
|
hideLoading();
|
|
console.error('Init error:', e);
|
|
// Login screen bleibt sichtbar
|
|
}
|
|
}
|
|
|
|
function guestMode() {
|
|
hideLogin();
|
|
showLoading("Lade...");
|
|
loadGhosts().then(() => {
|
|
hideLoading();
|
|
setupGuestUI();
|
|
document.getElementById('app').style.display = 'block';
|
|
}).catch(e => {
|
|
hideLoading();
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
/* ── UI SETUP ── */
|
|
function setupUI() {
|
|
const u = currentUser;
|
|
const chip = document.getElementById('auth-chip');
|
|
chip.style.display = 'flex';
|
|
const av = document.getElementById('auth-av');
|
|
if (u.avatar) { av.src = u.avatar; av.style.display = 'block'; } else { av.style.display = 'none'; }
|
|
document.getElementById('auth-nm').textContent = u.username;
|
|
const rl = document.getElementById('auth-rl');
|
|
if (u.isAdmin) { rl.textContent = 'Admin'; rl.className = 'auth-role role-admin'; }
|
|
else { rl.textContent = 'User'; rl.className = 'auth-role role-user'; }
|
|
document.getElementById('pill-user').style.display = 'block';
|
|
if (u.isAdmin) document.getElementById('pill-admin').style.display = 'block';
|
|
buildCards(); buildCheat();
|
|
populateGhostSelect();
|
|
loadCredits();
|
|
if (u.isAdmin) updateAdminBadge();
|
|
// Credits-Tab sofort vorbereiten falls aktiv
|
|
if (document.getElementById('vtab-credits')?.classList.contains('active')) renderCredits();
|
|
}
|
|
|
|
function setupGuestUI() {
|
|
document.getElementById('guest-badge').style.display = 'block';
|
|
buildCards(); buildCheat();
|
|
loadCredits();
|
|
}
|
|
|
|
/* ── AUTH ── */
|
|
async function doLogout() {
|
|
await fetch('/auth/logout', { method: 'POST' });
|
|
location.reload();
|
|
}
|
|
|
|
/* ── FETCH HELPERS ── */
|
|
async function api(method, path, body) {
|
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
if (body) opts.body = JSON.stringify(body);
|
|
const r = await fetch(path, opts);
|
|
const d = await r.json();
|
|
if (!r.ok) throw new Error(d.error || 'Fehler');
|
|
return d;
|
|
}
|
|
|
|
/* ── GHOSTS ── */
|
|
async function loadGhosts() {
|
|
const data = await api('GET', '/api/ghosts');
|
|
ghosts = data.map(row => ({
|
|
...row,
|
|
evidence: Array.isArray(row.evidence) ? row.evidence : (typeof row.evidence === 'string' ? JSON.parse(row.evidence) : []),
|
|
tells: Array.isArray(row.tells) ? row.tells : (typeof row.tells === 'string' ? JSON.parse(row.tells) : []),
|
|
}));
|
|
}
|
|
|
|
function sbC(g) { return g.hunt === 'high' ? 'sb-hi' : g.hunt === 'low' ? 'sb-lo' : g.hunt === 'variable' ? 'sb-va' : 'sb-mi'; }
|
|
|
|
function buildFilter() {
|
|
const bar = document.getElementById('ev-filter'), lbl = bar.querySelector('.filter-label-sm');
|
|
bar.innerHTML = ''; bar.appendChild(lbl);
|
|
EV_KEYS.forEach(ev => {
|
|
const cnt = ghosts.filter(g => g.evidence.includes(ev)).length;
|
|
const b = document.createElement('button'); b.className = 'ev-btn'; b.dataset.ev = ev;
|
|
b.innerHTML = `${EV_LABELS[ev]} <span class="ev-num">${cnt}</span>`;
|
|
b.onclick = () => { ev_state[ev] = (ev_state[ev] + 1) % 3; b.classList.toggle('ef', ev_state[ev] === 1); b.classList.toggle('ex', ev_state[ev] === 2); applyFilter(); };
|
|
bar.appendChild(b);
|
|
});
|
|
const rb = document.createElement('button'); rb.className = 'btn-sm'; rb.textContent = 'Reset';
|
|
rb.onclick = () => { EV_KEYS.forEach(k => ev_state[k] = 0); document.querySelectorAll('.ev-btn').forEach(b => b.classList.remove('ef', 'ex')); applyFilter(); };
|
|
bar.appendChild(rb);
|
|
const ri = document.createElement('div'); ri.className = 'result-bar'; ri.id = 'result-bar'; bar.appendChild(ri);
|
|
}
|
|
|
|
function applyFilter() {
|
|
const found = EV_KEYS.filter(k => ev_state[k] === 1), excl = EV_KEYS.filter(k => ev_state[k] === 2);
|
|
let m = 0;
|
|
document.querySelectorAll('.ghost-card').forEach(c => {
|
|
const g = ghosts[c.dataset.idx]; if (!g) return;
|
|
const ok = !found.some(e => !g.evidence.includes(e)) && !excl.some(e => g.evidence.includes(e));
|
|
const act = found.length || excl.length;
|
|
c.classList.toggle('hl', act && ok); c.classList.toggle('dm', act && !ok);
|
|
if (ok && act) m++;
|
|
c.querySelectorAll('.ev-pill').forEach(p => { p.classList.toggle('pf', ev_state[p.dataset.ev] === 1); p.classList.toggle('px', ev_state[p.dataset.ev] === 2); });
|
|
});
|
|
const ri = document.getElementById('result-bar');
|
|
if (ri) { ri.style.display = (found.length || excl.length) ? 'block' : 'none'; ri.innerHTML = `<span>${m}</span> possible ghost${m !== 1 ? 's' : ''} matching evidence`; }
|
|
}
|
|
|
|
function buildCards() {
|
|
buildFilter();
|
|
const grid = document.getElementById('ghost-grid'); grid.innerHTML = '';
|
|
ghosts.forEach((g, i) => {
|
|
const d = document.createElement('div'); d.className = 'ghost-card'; d.dataset.idx = i;
|
|
d.innerHTML = `<div class="card-hd"><div><div class="card-name">${g.name}</div><div class="card-num">Ghost #${String(i + 1).padStart(2, '0')}</div></div><div class="sb ${sbC(g)}">${g.sanity}%${g.hunt === 'variable' ? '↕' : ''}</div></div><div class="card-evs">${g.evidence.map(e => `<span class="ev-pill" data-ev="${e}">${EV_LABELS[e] || e}</span>`).join('')}</div><div class="card-tip">${g.tip || ''}</div>`;
|
|
d.onclick = () => openViewModal(i); grid.appendChild(d);
|
|
});
|
|
}
|
|
|
|
function buildCheat() {
|
|
const tb = document.getElementById('cheat-tbody'); tb.innerHTML = '';
|
|
ghosts.forEach((g, i) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td onclick="openViewModal(${i})">${g.name}</td>${EV_KEYS.map(e => `<td>${g.evidence.includes(e) ? '<span class="chk">✓</span>' : ''}</td>`).join('')}<td><span class="scc ${sbC(g)}">${g.sanity}%${g.hunt === 'variable' ? '↕' : ''}</span></td><td class="tip-cell">${(g.tip || '').split(' · ').map(t => `<div>· ${t}</div>`).join('')}</td>`;
|
|
tb.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function populateGhostSelect() {
|
|
const sel = document.getElementById('usr-ghost');
|
|
sel.innerHTML = '<option value="">— Geist auswählen —</option>' + ghosts.map(g => `<option value="${g.name}">${g.name}</option>`).join('');
|
|
}
|
|
|
|
function rebuildAll() { buildCards(); buildCheat(); populateGhostSelect(); }
|
|
|
|
/* ── VIEW MODAL ── */
|
|
function openViewModal(idx) {
|
|
const g = ghosts[idx]; if (!g) return;
|
|
const sc = g.sanity >= 80 ? 'var(--red2)' : g.sanity <= 35 ? 'var(--green2)' : 'var(--accent2)';
|
|
document.getElementById('gm-title').textContent = g.name.toUpperCase();
|
|
document.getElementById('gm-sub').textContent = 'Evidence · ' + g.evidence.map(e => EV_LABELS[e] || e).join(' · ');
|
|
document.getElementById('gm-body').innerHTML = `
|
|
<div class="wiki-sec"><div class="wiki-hd">Overview</div><div class="wiki-text">${g.description || 'No description yet.'}</div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Evidence</div><div class="wiki-ev-row">${g.evidence.map(e => `<span class="wiki-ev-p">${EV_LABELS[e] || e}</span>`).join('')}</div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Hunt Threshold</div><div class="sanity-blk"><span class="sanity-pct" style="color:${sc}">${g.sanity}%${g.hunt === 'variable' ? ' ↕' : ''}</span><span class="sanity-note">${g.sanity_special || ''}</span></div></div>
|
|
<div class="wiki-sec"><div class="wiki-hd">Identification Tells</div><div class="tells-list">${(g.tells || []).map(t => `<div class="tell"><span class="tell-ico">${t.icon}</span><span class="tell-text">${t.text}</span></div>`).join('')}</div></div>`;
|
|
openModal('ghost-modal');
|
|
}
|
|
|
|
/* ── MODE ── */
|
|
function setMode(m) {
|
|
['viewer', 'admin', 'user'].forEach(x => document.getElementById('mode-' + x).style.display = x === m ? 'block' : 'none');
|
|
document.querySelectorAll('.mode-pill').forEach(p => p.classList.remove('active'));
|
|
const mp = { viewer: document.querySelectorAll('.mode-pill')[0], user: document.getElementById('pill-user'), admin: document.getElementById('pill-admin') };
|
|
if (mp[m]) mp[m].classList.add('active');
|
|
if (m === 'admin') renderAdmin();
|
|
if (m === 'user') { populateGhostSelect(); renderUserSubs(); }
|
|
}
|
|
|
|
function switchTab(t) {
|
|
['cards', 'cheat', 'guide', 'credits'].forEach((x, i) => {
|
|
const el = document.getElementById('vtab-' + x);
|
|
if (el) el.classList.toggle('active', x === t);
|
|
const btn = document.querySelectorAll('#vnav .nav-btn')[i];
|
|
if (btn) btn.classList.toggle('active', x === t);
|
|
});
|
|
if (t === 'credits') renderCredits();
|
|
}
|
|
|
|
/* ── ADMIN ── */
|
|
async function renderAdmin() {
|
|
// Stats
|
|
try {
|
|
const s = await api('GET', '/admin/stats');
|
|
document.getElementById('admin-stats').innerHTML = `
|
|
<div class="stat-card"><div class="stat-val">${s.ghosts}</div><div class="stat-label">Ghosts</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.total}</div><div class="stat-label">Submissions</div></div>
|
|
<div class="stat-card"><div class="stat-val" style="color:${s.pending ? 'var(--accent2)' : 'var(--muted2)'}">${s.pending}</div><div class="stat-label">Pending</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.approved}</div><div class="stat-label">Approved</div></div>
|
|
<div class="stat-card"><div class="stat-val">${s.rejected}</div><div class="stat-label">Rejected</div></div>`;
|
|
// badge
|
|
const b = document.getElementById('badge-admin'); b.textContent = s.pending; b.classList.toggle('show', s.pending > 0);
|
|
} catch { }
|
|
|
|
// Whitelist
|
|
try {
|
|
const wl = await api('GET', '/admin/whitelist');
|
|
const wlEl = document.getElementById('wl-list');
|
|
if (!wl.length) { wlEl.innerHTML = '<div style="font-size:13px;color:var(--muted);font-style:italic;">Noch keine weiteren Admins.</div>'; }
|
|
else { wlEl.innerHTML = wl.map(w => `<div class="wl-row"><span style="font-size:16px">⚙</span><span class="wl-name">${escH(w.discord_username || '—')}</span><span class="wl-id">${w.discord_id}</span><span class="wl-date">${fmtTime(w.added_at)}</span><button class="icon-btn ibd" onclick="removeAdmin('${w.discord_id}')" title="Entfernen">🗑</button></div>`).join(''); }
|
|
} catch { }
|
|
|
|
// Submissions (pending)
|
|
try {
|
|
const subs = await api('GET', '/admin/submissions?status=pending');
|
|
const sl = document.getElementById('admin-sub-list');
|
|
document.getElementById('sub-title').innerHTML = `Community Submissions${subs.length ? `<span class="badge-count">${subs.length}</span>` : ''}`;
|
|
if (!subs.length) { sl.innerHTML = '<div class="empty-state">Keine ausstehenden Einreichungen 📭</div>'; }
|
|
else {
|
|
sl.innerHTML = ''; subs.forEach(sub => {
|
|
const tl = sub.type === 'tip' ? '💡 Tip' : sub.type === 'edit' ? '✏️ Korrektur' : '🆕 Neuer Geist';
|
|
const tc = sub.type === 'tip' ? 'stt' : sub.type === 'edit' ? 'ste' : 'stn';
|
|
const d = document.createElement('div'); d.className = 'sub-item';
|
|
d.innerHTML = `<div class="sub-meta"><span class="sub-ghost">${escH(sub.ghost_name || 'Allgemein')}</span><span class="sub-type ${tc}">${tl}</span><span style="font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted)">von ${escH(sub.username || 'Anonym')}</span><span class="sub-time">${fmtTime(sub.created_at)}</span></div><div class="sub-content">${escH(sub.content)}</div><div class="sub-actions"><button class="btn-success" onclick="approveSub(${sub.id})">✓ Annehmen</button><button class="btn-danger" onclick="rejectSub(${sub.id})">✕ Ablehnen</button><button class="btn-accent" onclick="injectSub(${sub.id},'${escH(sub.ghost_name)}')">→ Bearbeiten & Einfügen</button></div>`;
|
|
sl.appendChild(d);
|
|
});
|
|
}
|
|
} catch { }
|
|
|
|
// Ghost list
|
|
const gl = document.getElementById('admin-ghost-list'); gl.innerHTML = '';
|
|
ghosts.forEach(g => {
|
|
const row = document.createElement('div'); row.className = 'agr';
|
|
row.innerHTML = `<div class="agr-name">${g.name}</div><div class="agr-evs">${g.evidence.map(e => `<span class="agr-ev">${EV_LABELS[e] || e}</span>`).join('')}</div><div class="agr-meta">von ${g.updated_by || '—'}</div><div class="agr-btns"><button class="icon-btn ibe" onclick="openGhostForm('${g.id}')" title="Bearbeiten">✏</button><button class="icon-btn ibd" onclick="confirmDel('${g.id}')" title="Löschen">🗑</button></div>`;
|
|
gl.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function updateAdminBadge() {
|
|
try { const s = await api('GET', '/admin/stats'); const b = document.getElementById('badge-admin'); b.textContent = s.pending; b.classList.toggle('show', s.pending > 0); } catch { }
|
|
}
|
|
|
|
async function addAdmin() {
|
|
const id = document.getElementById('wl-id').value.trim(), name = document.getElementById('wl-name').value.trim();
|
|
if (!id || id.length < 10) { toast("Gültige Discord ID eingeben!", "danger"); return; }
|
|
try { await api('POST', '/admin/whitelist', { discord_id: id, discord_username: name || id }); toast("Admin hinzugefügt ✓", "success"); document.getElementById('wl-id').value = ''; document.getElementById('wl-name').value = ''; renderAdmin(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
}
|
|
|
|
async function removeAdmin(id) {
|
|
showConfirm("Admin entfernen?", "Diese Person verliert sofort den Admin-Zugriff.", async () => {
|
|
try { await api('DELETE', `/admin/whitelist/${id}`); toast("Admin entfernt.", "danger"); renderAdmin(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
});
|
|
}
|
|
|
|
async function approveSub(id) {
|
|
try { await api('PATCH', `/admin/submissions/${id}`, { status: 'approved' }); toast("Angenommen ✓", "success"); renderAdmin(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
}
|
|
async function rejectSub(id) {
|
|
try { await api('PATCH', `/admin/submissions/${id}`, { status: 'rejected' }); toast("Abgelehnt.", "danger"); renderAdmin(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
}
|
|
async function injectSub(id, ghostName) {
|
|
try { await api('PATCH', `/admin/submissions/${id}`, { status: 'approved' }); } catch { }
|
|
const g = ghosts.find(x => x.name === ghostName);
|
|
openGhostForm(g?.id || null);
|
|
setTimeout(() => toast("💡 Submission-Info anzeigen: prüfe den Tip!", "info"), 300);
|
|
renderAdmin();
|
|
}
|
|
|
|
/* ── GHOST EDITOR ── */
|
|
function openGhostForm(id) {
|
|
const g = id ? ghosts.find(x => x.id === id) : null;
|
|
editingId = id || null;
|
|
document.getElementById('em-title').textContent = g ? `Bearbeiten: ${g.name}` : 'Neuen Geist erstellen';
|
|
document.getElementById('em-sub').textContent = g ? `Admin · Ghost Editor · ID: ${g.id}` : 'Admin · Ghost Editor · Neu';
|
|
document.getElementById('ef-name').value = g?.name || '';
|
|
document.getElementById('ef-sanity').value = g?.sanity ?? 50;
|
|
document.getElementById('ef-hunt').value = g?.hunt || 'mid';
|
|
document.getElementById('ef-ss').value = g?.sanity_special || '';
|
|
document.getElementById('ef-tip').value = g?.tip || '';
|
|
document.getElementById('ef-desc').value = g?.description || '';
|
|
const evDiv = document.getElementById('ef-evs'); evDiv.innerHTML = '';
|
|
EV_KEYS.forEach(ev => {
|
|
const ck = g?.evidence.includes(ev);
|
|
const lbl = document.createElement('label'); lbl.className = 'ev-check-lbl' + (ck ? ' ck' : '');
|
|
lbl.innerHTML = `<input type="checkbox" value="${ev}"${ck ? ' checked' : ''}>${EV_LABELS[ev]}`;
|
|
lbl.querySelector('input').onchange = e => lbl.classList.toggle('ck', e.target.checked);
|
|
evDiv.appendChild(lbl);
|
|
});
|
|
const td = document.getElementById('ef-tells'); td.innerHTML = '';
|
|
(g?.tells || [{ icon: '💡', text: '' }]).forEach(t => addTellRow(t.icon, t.text));
|
|
openModal('edit-modal');
|
|
}
|
|
|
|
function addTellRow(icon = '💡', text = '') {
|
|
const td = document.getElementById('ef-tells');
|
|
const row = document.createElement('div'); row.className = 'tell-row';
|
|
row.innerHTML = `<input type="text" class="form-input tell-ico-in" value="${escH(icon)}" placeholder="🔥" maxlength="4"><input type="text" class="form-input" value="${escH(text)}" placeholder="Tell text... use <strong>bold</strong>"><button class="remove-btn" onclick="this.parentElement.remove()">✕</button>`;
|
|
td.appendChild(row);
|
|
}
|
|
|
|
async function saveGhost() {
|
|
const name = document.getElementById('ef-name').value.trim();
|
|
if (!name) { toast("Name darf nicht leer sein!", "danger"); return; }
|
|
const evs = Array.from(document.querySelectorAll('#ef-evs input:checked')).map(i => i.value);
|
|
if (evs.length !== 3) { toast(`Genau 3 Evidenzen auswählen (aktuell: ${evs.length})!`, "danger"); return; }
|
|
const tells = Array.from(document.querySelectorAll('.tell-row')).map(row => { const ins = row.querySelectorAll('input'); return { icon: ins[0].value || '💡', text: ins[1].value }; }).filter(t => t.text.trim());
|
|
const data = {
|
|
id: editingId || name.toLowerCase().replace(/[^a-z0-9]/g, '') + Date.now().toString(36),
|
|
name, evidence: evs, sanity: parseInt(document.getElementById('ef-sanity').value) || 50,
|
|
hunt: document.getElementById('ef-hunt').value,
|
|
sanity_special: document.getElementById('ef-ss').value,
|
|
tip: document.getElementById('ef-tip').value,
|
|
description: document.getElementById('ef-desc').value,
|
|
tells
|
|
};
|
|
showLoading("Speichere...");
|
|
try {
|
|
if (editingId) { await api('PUT', `/admin/ghosts/${editingId}`, data); }
|
|
else { await api('POST', '/admin/ghosts', data); }
|
|
toast(`✓ ${name} gespeichert!`, "success");
|
|
closeModal('edit-modal');
|
|
await loadGhosts(); rebuildAll(); renderAdmin();
|
|
} catch (e) { toast(e.message, "danger"); }
|
|
hideLoading();
|
|
}
|
|
|
|
async function confirmDel(id) {
|
|
const g = ghosts.find(x => x.id === id); if (!g) return;
|
|
showConfirm(`${g.name} löschen?`, `Dauerhaft aus der Datenbank entfernen.`, async () => {
|
|
showLoading("Lösche...");
|
|
try { await api('DELETE', `/admin/ghosts/${id}`); toast(`${g.name} gelöscht.`, "danger"); await loadGhosts(); rebuildAll(); renderAdmin(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
hideLoading();
|
|
});
|
|
}
|
|
|
|
/* ── COMMUNITY ── */
|
|
async function submitTip() {
|
|
const ghost = document.getElementById('usr-ghost').value, type = document.getElementById('usr-type').value, content = document.getElementById('usr-content').value.trim();
|
|
if (!content) { toast("Bitte Inhalt eingeben!", "danger"); return; }
|
|
showLoading("Sende...");
|
|
try { await api('POST', '/api/submissions', { ghost_name: ghost, type, content }); toast("Einreichung gesendet ✓", "success"); document.getElementById('usr-content').value = ''; renderUserSubs(); }
|
|
catch (e) { toast(e.message, "danger"); }
|
|
hideLoading();
|
|
}
|
|
|
|
async function renderUserSubs() {
|
|
const ul = document.getElementById('user-sub-list');
|
|
try {
|
|
const subs = await api('GET', '/api/submissions/mine');
|
|
if (!subs.length) { ul.innerHTML = '<div class="empty-state">📭 Du hast noch nichts eingereicht.</div>'; return; }
|
|
ul.innerHTML = '';
|
|
subs.forEach(sub => {
|
|
const tl = sub.type === 'tip' ? '💡 Tip' : sub.type === 'edit' ? '✏️ Korrektur' : '🆕 Neuer Geist';
|
|
const sc = sub.status === 'approved' ? 'ssa' : sub.status === 'rejected' ? 'ssr' : 'ssp';
|
|
const sl = sub.status === 'approved' ? '✓ Angenommen' : sub.status === 'rejected' ? '✕ Abgelehnt' : '⏳ Ausstehend';
|
|
const d = document.createElement('div'); d.className = 'usi';
|
|
d.innerHTML = `<div class="usi-meta"><span style="font-family:'Cinzel',serif;font-size:13px;color:var(--accent2)">${escH(sub.ghost_name || 'Allgemein')}</span><span style="font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--muted2)">${tl}</span><span class="usi-st ${sc}">${sl}</span><span class="sub-time">${fmtTime(sub.created_at)}</span></div><div style="font-size:13.5px;color:var(--muted2);line-height:1.55;font-style:italic">${escH(sub.content)}</div>${sub.admin_note ? `<div style="font-size:12px;color:var(--accent2);margin-top:6px">Admin: ${escH(sub.admin_note)}</div>` : ''}`;
|
|
ul.appendChild(d);
|
|
});
|
|
const approved = subs.filter(s => s.status === 'approved').length;
|
|
const ub = document.getElementById('badge-user'); ub.textContent = approved; ub.classList.toggle('show', approved > 0);
|
|
} catch { ul.innerHTML = '<div class="empty-state">Fehler beim Laden.</div>'; }
|
|
}
|
|
|
|
/* ── EXPORT ── */
|
|
function exportGhosts() { const b = new Blob([JSON.stringify({ ghosts }, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = 'blair-ghosts.json'; a.click(); toast("Exportiert ↓", "info"); }
|
|
|
|
/* ── MODAL / CONFIRM / TOAST ── */
|
|
function openModal(id) { document.getElementById(id).classList.add('open'); document.body.style.overflow = 'hidden'; }
|
|
function closeModal(id) { document.getElementById(id).classList.remove('open'); document.body.style.overflow = ''; }
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal('ghost-modal'); closeModal('edit-modal'); closeConfirm(); } });
|
|
function showConfirm(title, text, cb) { document.getElementById('confirm-title').textContent = title; document.getElementById('confirm-text').textContent = text; confirmCb = cb; document.getElementById('confirm-bg').classList.add('open'); }
|
|
function closeConfirm() { document.getElementById('confirm-bg').classList.remove('open'); confirmCb = null; }
|
|
document.getElementById('confirm-ok').onclick = () => { if (confirmCb) confirmCb(); closeConfirm(); };
|
|
function toast(msg, type = 'info') { const w = document.getElementById('toast-wrap'), t = document.createElement('div'); t.className = `toast toast-${type}`; t.textContent = msg; w.appendChild(t); setTimeout(() => { t.style.cssText = 'opacity:0;transition:opacity .3s'; setTimeout(() => t.remove(), 350); }, 3400); }
|
|
|
|
/* ── LOADING / LOGIN ── */
|
|
function showLoading(msg = 'Lade...') { document.getElementById('loading-txt').textContent = msg; document.getElementById('loading').classList.add('show'); }
|
|
function hideLoading() { document.getElementById('loading').classList.remove('show'); }
|
|
function showLogin() { document.getElementById('login-screen').classList.add('show'); }
|
|
function hideLogin() { document.getElementById('login-screen').classList.remove('show'); }
|
|
|
|
/* ── UTILS ── */
|
|
function fmtTime(ts) { if (!ts) return '—'; const d = new Date(ts); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); }
|
|
function escH(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
CREDITS
|
|
═══════════════════════════════════════════════════ */
|
|
let credits = [];
|
|
let editingCreditId = null;
|
|
|
|
// Load from localStorage (no separate API needed — admin manages locally)
|
|
function loadCredits() {
|
|
try { credits = JSON.parse(localStorage.getItem('blair_credits') || '[]'); }
|
|
catch { credits = []; }
|
|
}
|
|
function saveCredits() {
|
|
localStorage.setItem('blair_credits', JSON.stringify(credits));
|
|
}
|
|
|
|
function renderCredits() {
|
|
loadCredits();
|
|
const partners = credits.filter(c => c.type === 'partner');
|
|
const crds = credits.filter(c => c.type === 'credit');
|
|
|
|
const partnerGrid = document.getElementById('credits-partners-grid');
|
|
const creditsGrid = document.getElementById('credits-credits-grid');
|
|
const partnerSec = document.getElementById('credits-partners-section');
|
|
const creditsSec = document.getElementById('credits-credits-section');
|
|
|
|
// Partners
|
|
if (!partners.length) {
|
|
partnerGrid.innerHTML = `<div class="credits-empty">${currentUser?.isAdmin ? 'Noch keine Partner eingetragen. Nutze "+ Eintrag hinzufügen" oben rechts.' : 'Noch keine Partner eingetragen.'}</div>`;
|
|
} else {
|
|
partnerGrid.innerHTML = '';
|
|
partners.forEach(c => partnerGrid.appendChild(buildCreditCard(c)));
|
|
}
|
|
|
|
// Credits
|
|
if (!crds.length) {
|
|
creditsGrid.innerHTML = `<div class="credits-empty">${currentUser?.isAdmin ? 'Noch keine Credits eingetragen. Nutze "+ Eintrag hinzufügen" oben rechts.' : 'Noch keine Credits eingetragen.'}</div>`;
|
|
} else {
|
|
creditsGrid.innerHTML = '';
|
|
crds.forEach(c => creditsGrid.appendChild(buildCreditCard(c)));
|
|
}
|
|
|
|
// Show add button for admins — immer neu verdrahten nach jedem render
|
|
const adminBtn = document.getElementById('credits-admin-btn');
|
|
if (adminBtn) {
|
|
adminBtn.style.display = currentUser?.isAdmin ? 'block' : 'none';
|
|
// onclick is set directly in HTML
|
|
}
|
|
}
|
|
|
|
function buildCreditCard(c) {
|
|
const card = document.createElement('div');
|
|
card.className = 'credit-card' + (c.type === 'partner' ? ' partner' : '');
|
|
|
|
const avatarHtml = c.avatar
|
|
? `<img class="credit-avatar" src="${escH(c.avatar)}" alt="${escH(c.name)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">`
|
|
+ `<div class="credit-avatar-placeholder" style="display:none">${c.emoji || '👤'}</div>`
|
|
: `<div class="credit-avatar-placeholder">${c.emoji || '👤'}</div>`;
|
|
|
|
const linksHtml = [
|
|
c.discord ? `<a class="credit-link discord" href="${c.discord.startsWith('http') ? escH(c.discord) : '#'}" target="_blank" rel="noopener">Discord</a>` : '',
|
|
c.website ? `<a class="credit-link website" href="${escH(c.website)}" target="_blank" rel="noopener">Website</a>` : '',
|
|
c.roblox ? `<a class="credit-link roblox" href="${escH(c.roblox)}" target="_blank" rel="noopener">Roblox</a>` : '',
|
|
].filter(Boolean).join('');
|
|
|
|
const adminBtns = currentUser?.isAdmin
|
|
? `<div style="display:flex;gap:6px;margin-top:10px;">
|
|
<button class="icon-btn ibe" onclick="openCreditForm('${c.id}')" title="Bearbeiten">✏</button>
|
|
<button class="icon-btn ibd" onclick="confirmDeleteCredit('${c.id}')" title="Löschen">🗑</button>
|
|
</div>` : '';
|
|
|
|
card.innerHTML = `
|
|
<div class="credit-header">
|
|
${avatarHtml}
|
|
<div style="flex:1;min-width:0;">
|
|
<div class="credit-name">${escH(c.name)}</div>
|
|
<div class="credit-role ${c.type === 'partner' ? 'partner-role' : ''}">${escH(c.role || '')}</div>
|
|
</div>
|
|
${c.type === 'partner' ? '<span class="partner-badge">Partner</span>' : ''}
|
|
</div>
|
|
${c.desc ? `<div class="credit-desc">${escH(c.desc)}</div>` : ''}
|
|
${linksHtml ? `<div class="credit-links">${linksHtml}</div>` : ''}
|
|
${adminBtns}`;
|
|
|
|
return card;
|
|
}
|
|
|
|
function openCreditForm(id) {
|
|
const c = id ? credits.find(x => x.id === id) : null;
|
|
editingCreditId = id || null;
|
|
document.getElementById('cm-title').textContent = c ? `Bearbeiten: ${c.name}` : 'Eintrag hinzufügen';
|
|
document.getElementById('cf-name').value = c?.name || '';
|
|
document.getElementById('cf-type').value = c?.type || 'credit';
|
|
document.getElementById('cf-role').value = c?.role || '';
|
|
document.getElementById('cf-avatar').value = c?.avatar || '';
|
|
document.getElementById('cf-emoji').value = c?.emoji || '';
|
|
document.getElementById('cf-desc').value = c?.desc || '';
|
|
document.getElementById('cf-discord').value = c?.discord || '';
|
|
document.getElementById('cf-website').value = c?.website || '';
|
|
document.getElementById('cf-roblox').value = c?.roblox || '';
|
|
openModal('credits-modal');
|
|
}
|
|
|
|
function saveCreditEntry() {
|
|
const name = document.getElementById('cf-name').value.trim();
|
|
if (!name) { toast("Name darf nicht leer sein!", "danger"); return; }
|
|
|
|
const entry = {
|
|
id: editingCreditId || 'c_' + Date.now().toString(36),
|
|
name,
|
|
type: document.getElementById('cf-type').value,
|
|
role: document.getElementById('cf-role').value.trim(),
|
|
avatar: document.getElementById('cf-avatar').value.trim(),
|
|
emoji: document.getElementById('cf-emoji').value.trim() || '👤',
|
|
desc: document.getElementById('cf-desc').value.trim(),
|
|
discord: document.getElementById('cf-discord').value.trim(),
|
|
website: document.getElementById('cf-website').value.trim(),
|
|
roblox: document.getElementById('cf-roblox').value.trim(),
|
|
};
|
|
|
|
if (editingCreditId) {
|
|
const idx = credits.findIndex(c => c.id === editingCreditId);
|
|
if (idx > -1) credits[idx] = entry;
|
|
} else {
|
|
credits.push(entry);
|
|
}
|
|
|
|
saveCredits();
|
|
closeModal('credits-modal');
|
|
renderCredits();
|
|
toast(`✓ ${name} gespeichert!`, "success");
|
|
}
|
|
|
|
function confirmDeleteCredit(id) {
|
|
const c = credits.find(x => x.id === id);
|
|
if (!c) return;
|
|
showConfirm(`${c.name} entfernen?`, 'Dieser Eintrag wird aus Credits & Partner entfernt.', () => {
|
|
credits = credits.filter(x => x.id !== id);
|
|
saveCredits();
|
|
renderCredits();
|
|
toast(`${c.name} entfernt.`, "danger");
|
|
});
|
|
}
|
|
|
|
/* ── BOOT ── */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
// ── Mode pills ──
|
|
document.getElementById('pill-viewer').addEventListener('click', () => setMode('viewer'));
|
|
const pu = document.getElementById('pill-user');
|
|
const pa = document.getElementById('pill-admin');
|
|
if (pu) pu.addEventListener('click', () => setMode('user'));
|
|
if (pa) pa.addEventListener('click', () => setMode('admin'));
|
|
|
|
// ── Viewer tabs ──
|
|
document.querySelectorAll('#vnav .nav-btn').forEach((btn, i) => {
|
|
const tabs = ['cards', 'cheat', 'guide', 'credits'];
|
|
btn.addEventListener('click', () => switchTab(tabs[i]));
|
|
});
|
|
|
|
// ── Auth ──
|
|
const logoutBtn = document.querySelector('.btn-logout');
|
|
if (logoutBtn) logoutBtn.addEventListener('click', doLogout);
|
|
|
|
// ── Admin actions ──
|
|
const newGhost = document.getElementById('btn-new-ghost');
|
|
const exportBtn = document.getElementById('btn-export');
|
|
const addAdminBtn = document.getElementById('btn-add-admin');
|
|
if (newGhost) newGhost.addEventListener('click', () => openGhostForm(null));
|
|
if (exportBtn) exportBtn.addEventListener('click', exportGhosts);
|
|
if (addAdminBtn) addAdminBtn.addEventListener('click', addAdmin);
|
|
|
|
// ── Community ──
|
|
const submitBtn = document.getElementById('btn-submit-tip');
|
|
if (submitBtn) submitBtn.addEventListener('click', submitTip);
|
|
|
|
// ── Ghost view modal ──
|
|
document.getElementById('ghost-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('ghost-modal')) closeModal('ghost-modal');
|
|
});
|
|
document.getElementById('close-ghost-modal').addEventListener('click', () => closeModal('ghost-modal'));
|
|
|
|
// ── Edit modal ──
|
|
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('edit-modal')) closeModal('edit-modal');
|
|
});
|
|
document.getElementById('close-edit-modal').addEventListener('click', () => closeModal('edit-modal'));
|
|
document.getElementById('btn-cancel-edit').addEventListener('click', () => closeModal('edit-modal'));
|
|
document.getElementById('btn-add-tell').addEventListener('click', addTellRow);
|
|
document.getElementById('btn-save-ghost').addEventListener('click', saveGhost);
|
|
|
|
// ── Confirm dialog ──
|
|
document.querySelector('.confirm-bg .btn-sm').addEventListener('click', closeConfirm);
|
|
document.getElementById('confirm-ok').addEventListener('click', () => {
|
|
if (confirmCb) confirmCb();
|
|
closeConfirm();
|
|
});
|
|
|
|
// ── Keyboard shortcuts ──
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
closeModal('ghost-modal');
|
|
closeModal('edit-modal');
|
|
closeConfirm();
|
|
}
|
|
});
|
|
|
|
// ── Guest button ──
|
|
const guestBtn = document.getElementById('guest-btn');
|
|
if (guestBtn) guestBtn.addEventListener('click', guestMode);
|
|
|
|
// ── Credits ──
|
|
document.getElementById('close-credits-modal').addEventListener('click', () => closeModal('credits-modal'));
|
|
document.getElementById('btn-cancel-credits').addEventListener('click', () => closeModal('credits-modal'));
|
|
document.getElementById('btn-save-credit').addEventListener('click', saveCreditEntry);
|
|
document.getElementById('credits-modal').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('credits-modal')) closeModal('credits-modal');
|
|
});
|
|
const addCreditBtn = document.getElementById('btn-add-credit');
|
|
if (addCreditBtn) addCreditBtn.addEventListener('click', () => openCreditForm(null));
|
|
|
|
init();
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |