2026-05-18 15:48:44 +02:00

1208 lines
76 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">
Melde dich mit Discord an, um <strong>Tips einzureichen</strong> und den Status deiner Einsendungen zu sehen.<br><br>
<strong>Admin-Zugriff</strong> wird nur für freigeschaltete Discord-Accounts gewährt.
</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>
Mit Discord anmelden
</a>
<span class="login-or">oder</span>
<button class="btn-sm" id="guest-btn">Als Gast fortfahren (nur lesen)</button>
<div class="login-guest-note">Gäste können nur Ghost Cards, Cheatsheet und Hunt Guide sehen.</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 &nbsp;·&nbsp; Roblox</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">Abmelden</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 — einmal: gefunden &nbsp;·&nbsp; zweimal: ausgeschlossen</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 &amp; 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">+ 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;">Tips einreichen · Fehler melden · Infos ergänzen</div>
<div class="form-section" style="margin-bottom:28px;">
<div class="form-title">Etwas einreichen</div>
<div class="form-grid">
<div class="form-row">
<div class="form-group"><label class="form-label">Geist</label><select class="form-select" id="usr-ghost"><option value="">— Geist auswählen —</option></select></div>
<div class="form-group"><label class="form-label">Art</label>
<select class="form-select" id="usr-type">
<option value="tip">💡 Neuer Tip / Trick</option>
<option value="edit">✏️ Fehler / Korrektur</option>
<option value="new">🆕 Neuer Geist fehlt</option>
</select>
</div>
</div>
<div class="form-row full">
<div class="form-group"><label class="form-label">Beschreibung</label>
<textarea class="form-textarea" id="usr-content" placeholder="Beschreib deinen Tip so genau wie möglich..." rows="4"></textarea>
<span class="form-hint">Je detaillierter, desto besser für den Admin!</span>
</div>
</div>
<div class="form-footer"><button class="btn-blue" id="btn-submit-tip">Einreichen →</button></div>
</div>
</div>
<div class="divider"><div class="divider-line"></div><div class="divider-text">Deine Einreichungen</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">Geist bearbeiten</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="z.B. Target's individual sanity"></div>
</div>
<div class="form-group"><label class="form-label">Evidence (genau 3 auswählen)</label><div class="ev-checks" id="ef-evs"></div></div>
<div class="form-row full"><div class="form-group"><label class="form-label">Kurz-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">Beschreibung (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">+ Tell hinzufügen</button>
</div>
<div class="form-footer">
<button class="btn-sm" id="btn-cancel-edit">Abbrechen</button>
<button class="btn-accent" id="btn-save-ghost">Speichern</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"></button>
<div class="modal-hd">
<div class="modal-title" id="cm-title">Eintrag hinzufügen</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="z.B. GhostHunter99">
</div>
<div class="form-group">
<label class="form-label">Typ</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">Rolle / Titel</label>
<input type="text" class="form-input" id="cf-role" placeholder="z.B. Wiki-Autor, 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 (falls kein 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">Beschreibung</label>
<textarea class="form-textarea" id="cf-desc" rows="2" placeholder="Kurze Beschreibung..."></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 oder 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 Profil 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">Abbrechen</button>
<button class="btn-accent" id="btn-save-credit">Speichern</button>
</div>
</div>
</div>
</div>
</div>
<div class="confirm-bg" id="confirm-bg">
<div class="confirm-box">
<div class="confirm-title" id="confirm-title">Sicher?</div>
<div class="confirm-text" id="confirm-text"></div>
<div class="confirm-btns"><button class="btn-sm">Abbrechen</button><button class="btn-danger" id="confirm-ok">Bestätigen</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 &lt;strong&gt;bold&lt;/strong&gt;"><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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
/* ═══════════════════════════════════════════════════
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';
// Event listener neu setzen (sicher mit clone-replace trick)
const oldBtn = document.getElementById('btn-add-credit');
if (oldBtn) {
const newBtn = oldBtn.cloneNode(true);
newBtn.addEventListener('click', () => openCreditForm(null));
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
}
}
}
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'];
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>