Files
cluedo-hp-webapp/frontend/src/App.jsx
nessi e6d32bc151 Remove redundant background style settings.
The background color settings for `document.body` and `document.documentElement` were removed as they are not needed. This simplifies the global styling without affecting functionality.
2026-02-03 13:11:55 +01:00

1187 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from "react";
const API = "/api";
async function api(path, opts = {}) {
const res = await fetch(API + path, {
credentials: "include",
headers: { "Content-Type": "application/json" },
...opts,
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
function cycleTag(tag) {
if (!tag) return "i";
if (tag === "i") return "m";
if (tag === "m") return "s";
return null;
}
function AdminPanel() {
const [users, setUsers] = useState([]);
const [open, setOpen] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("user");
const [msg, setMsg] = useState("");
const loadUsers = async () => {
const u = await api("/admin/users");
setUsers(u);
};
useEffect(() => {
loadUsers().catch(() => {});
}, []);
const resetForm = () => {
setEmail("");
setPassword("");
setRole("user");
};
const createUser = async () => {
setMsg("");
try {
await api("/admin/users", {
method: "POST",
body: JSON.stringify({ email, password, role }),
});
setMsg("✅ User erstellt.");
await loadUsers();
resetForm();
setOpen(false);
} catch (e) {
setMsg("❌ Fehler: " + (e?.message || "unknown"));
}
};
const closeModal = () => {
setOpen(false);
setMsg("");
};
return (
<div style={styles.adminWrap}>
<div style={styles.adminTop}>
<div style={styles.adminTitle}>Admin Dashboard</div>
<button onClick={() => setOpen(true)} style={styles.primaryBtn}>
+ User anlegen
</button>
</div>
<div style={{ marginTop: 12, fontWeight: 900, color: "#20140c" }}>
Vorhandene User
</div>
<div style={{ marginTop: 8, display: "grid", gap: 8 }}>
{users.map((u) => (
<div key={u.id} style={styles.userRow}>
<div>{u.email}</div>
<div style={{ textAlign: "center", fontWeight: 900 }}>{u.role}</div>
<div style={{ textAlign: "center", opacity: 0.85 }}>
{u.disabled ? "disabled" : "active"}
</div>
</div>
))}
</div>
{open && (
<div style={styles.modalOverlay} onMouseDown={closeModal}>
<div
style={styles.modalCard}
onMouseDown={(e) => e.stopPropagation()}
>
<div style={styles.modalHeader}>
<div style={{ fontWeight: 1000, color: "#20140c" }}>
Neuen User anlegen
</div>
<button
onClick={closeModal}
style={styles.modalCloseBtn}
aria-label="Schließen"
>
</button>
</div>
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
style={styles.input}
autoFocus
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Initial Passwort"
type="password"
style={styles.input}
/>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
style={styles.input}
>
<option value="user">user</option>
<option value="admin">admin</option>
</select>
{msg && <div style={{ opacity: 0.9 }}>{msg}</div>}
<div
style={{
display: "flex",
gap: 8,
justifyContent: "flex-end",
marginTop: 4,
}}
>
<button
onClick={() => {
resetForm();
setMsg("");
}}
style={styles.secondaryBtn}
>
Leeren
</button>
<button onClick={createUser} style={styles.primaryBtn}>
User erstellen
</button>
</div>
<div style={{ fontSize: 12, opacity: 0.75 }}>
Tipp: Klick auf Item: Grün Rot Grau Leer
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default function App() {
const [me, setMe] = useState(null);
const [loginEmail, setLoginEmail] = useState("");
const [loginPassword, setLoginPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [games, setGames] = useState([]);
const [gameId, setGameId] = useState(null);
const [sheet, setSheet] = useState(null);
const [pulseId, setPulseId] = useState(null);
const [helpOpen, setHelpOpen] = useState(false);
const load = async () => {
const m = await api("/auth/me");
setMe(m);
const gs = await api("/games");
setGames(gs);
if (gs[0] && !gameId) setGameId(gs[0].id);
};
// ✅ Google Fonts laden
useEffect(() => {
if (document.getElementById("hp-fonts")) return;
const pre1 = document.createElement("link");
pre1.id = "hp-fonts-pre1";
pre1.rel = "preconnect";
pre1.href = "https://fonts.googleapis.com";
const pre2 = document.createElement("link");
pre2.id = "hp-fonts-pre2";
pre2.rel = "preconnect";
pre2.href = "https://fonts.gstatic.com";
pre2.crossOrigin = "anonymous";
const link = document.createElement("link");
link.id = "hp-fonts";
link.rel = "stylesheet";
link.href =
"https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700;900&family=IM+Fell+English:ital,wght@0,400;0,0.700;1,400&display=swap";
document.head.appendChild(pre1);
document.head.appendChild(pre2);
document.head.appendChild(link);
}, []);
// ✅ Keyframes
useEffect(() => {
if (document.getElementById("hp-anim-style")) return;
const style = document.createElement("style");
style.id = "hp-anim-style";
style.innerHTML = `
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes rowPulse { 0%{ transform: scale(1); } 50%{ transform: scale(1.01); } 100%{ transform: scale(1); } }
@keyframes candleGlow {
0% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); }
35% { opacity: .85; transform: translateY(-2px) scale(1.02); filter: blur(18px); }
70% { opacity: .62; transform: translateY(1px) scale(1.01); filter: blur(17px); }
100% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); }
}
`;
document.head.appendChild(style);
}, []);
// ✅ html/body reset
useEffect(() => {
document.documentElement.style.height = "100%";
document.body.style.height = "100%";
document.documentElement.style.margin = "0";
document.body.style.margin = "0";
document.documentElement.style.padding = "0";
document.body.style.padding = "0";
}, []);
// ✅ Global CSS (Hover komplett weg + sauberes Scroll-Verhalten)
useEffect(() => {
if (document.getElementById("hp-global-style")) return;
const style = document.createElement("style");
style.id = "hp-global-style";
style.innerHTML = `
html, body {
overscroll-behavior-y: none;
-webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #1c140d;
}
body {
overflow-x: hidden;
// touch-action: pan-y;
-webkit-overflow-scrolling: touch;
}
#root { background: transparent; }
/*
.hp-row:hover,
.hp-row:active,
.hp-row:focus,
.hp-row:focus-within {
background: inherit !important;
filter: none !important;
box-shadow: none !important;
outline: none !important;
}
.hp-row *:hover,
.hp-row *:active,
.hp-row *:focus {
filter: none !important;
box-shadow: none !important;
outline: none !important;
}
.hp-row, .hp-row * {
-webkit-tap-highlight-color: transparent !important;
-webkit-touch-callout: none;
}
button:hover,
button:active,
button:focus {
filter: none !important;
box-shadow: none !important;
outline: none !important;
}*/
`;
document.head.appendChild(style);
}, []);
useEffect(() => {
(async () => {
try {
await load();
} catch {
// not logged in
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
(async () => {
if (!gameId) return;
try {
const sh = await api(`/games/${gameId}/sheet`);
setSheet(sh);
} catch {
// ignore
}
})();
}, [gameId]);
const doLogin = async () => {
await api("/auth/login", {
method: "POST",
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
});
await load();
};
const doLogout = async () => {
await api("/auth/logout", { method: "POST" });
setMe(null);
setGames([]);
setGameId(null);
setSheet(null);
};
const newGame = async () => {
const g = await api("/games", {
method: "POST",
body: JSON.stringify({ name: "Spiel " + new Date().toLocaleString() }),
});
const gs = await api("/games");
setGames(gs);
setGameId(g.id);
};
const reloadSheet = async () => {
if (!gameId) return;
const sh = await api(`/games/${gameId}/sheet`);
setSheet(sh);
};
const cycleStatus = async (entry) => {
let next = 0;
if (entry.status === 0) next = 2;
else if (entry.status === 2) next = 1;
else if (entry.status === 1) next = 3;
else next = 0;
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ status: next }),
});
await reloadSheet();
setPulseId(entry.entry_id);
setTimeout(() => setPulseId(null), 220);
};
const toggleTag = async (entry) => {
const next = cycleTag(entry.note_tag);
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: next }),
});
await reloadSheet();
};
if (!me) {
return (
<div style={styles.loginPage}>
<div style={styles.bgFixed} aria-hidden="true">
<div style={styles.bgMap} />
</div>
<div style={styles.candleGlowLayer} aria-hidden="true" />
<div style={styles.loginCard}>
<div style={styles.loginTitle}>Zauber-Detektiv Notizbogen</div>
<div style={styles.loginSubtitle}>
Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen
</div>
<div style={{ marginTop: 18, display: "grid", gap: 12 }}>
<div style={styles.loginFieldWrap}>
<input
value={loginEmail}
onChange={(e) => setLoginEmail(e.target.value)}
placeholder="Email"
style={styles.loginInput}
inputMode="email"
autoComplete="username"
/>
</div>
<div style={styles.loginFieldWrap}>
<div style={styles.inputRow}>
<input
value={loginPassword}
onChange={(e) => setLoginPassword(e.target.value)}
placeholder="Passwort"
type={showPw ? "text" : "password"}
style={styles.inputInRow}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPw((v) => !v)}
style={styles.pwToggleBtn}
aria-label={showPw ? "Passwort verstecken" : "Passwort anzeigen"}
title={showPw ? "Verstecken" : "Anzeigen"}
>
{showPw ? "🙈" : "👁"}
</button>
</div>
</div>
<button onClick={doLogin} style={styles.loginBtn}>
Anmelden
</button>
</div>
<div style={styles.loginHint}>
Deine Notizen bleiben privat jeder Spieler sieht nur seinen eigenen
Zettel.
</div>
</div>
</div>
);
}
const sections = sheet
? [
{
key: "suspect",
title: "VERDÄCHTIGE PERSON",
entries: sheet.suspect || [],
},
{ key: "item", title: "GEGENSTAND", entries: sheet.item || [] },
{ key: "location", title: "ORT", entries: sheet.location || [] },
]
: [];
const getRowBg = (status) => {
if (status === 1) return "rgba(255, 35, 35, 0.16)"; // rot kräftiger
if (status === 2) return "rgba(0, 190, 80, 0.16)"; // grün kräftiger
if (status === 3) return "rgba(80, 80, 80, 0.18)"; // grau kräftiger
return "rgba(255,255,255,0.14)"; // neutral
};
const getNameColor = (status) => {
if (status === 1) return "#9f0f0f";
if (status === 2) return "#0b6a1e";
if (status === 3) return "#3f3f3f";
return "#20140c";
};
const getStatusSymbol = (status) => {
if (status === 2) return "✓";
if (status === 1) return "✕";
if (status === 3) return "?";
return "";
};
const getStatusBadge = (status) => {
if (status === 2)
return { color: "#0b6a1e", background: "rgba(0,170,60,0.10)" };
if (status === 1)
return { color: "#b10000", background: "rgba(255,0,0,0.10)" };
if (status === 3)
return { color: "#444444", background: "rgba(120,120,120,0.12)" };
return {
color: "rgba(30,20,12,0.60)",
background: "rgba(255,255,255,0.26)",
};
};
const closeHelp = () => setHelpOpen(false);
return (
<div style={styles.page}>
<div style={styles.bgFixed} aria-hidden="true">
<div style={styles.bgMap} />
</div>
<div style={styles.shell}>
<div style={styles.topBar}>
<div>
<div style={{ fontWeight: 900, color: "#20140c" }}>{me.email}</div>
<div style={{ fontSize: 12, opacity: 0.75 }}>{me.role}</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={doLogout} style={styles.secondaryBtn}>
Logout
</button>
<button onClick={newGame} style={styles.primaryBtn}>
+ Neues Spiel
</button>
</div>
</div>
{me.role === "admin" && <AdminPanel />}
<div style={{ marginTop: 14 }}>
<div style={styles.card}>
<div style={styles.sectionHeader}>Spiel</div>
<div style={styles.cardBody}>
<select
value={gameId || ""}
onChange={(e) => setGameId(e.target.value)}
style={{ ...styles.input, flex: 1 }}
>
{games.map((g) => (
<option key={g.id} value={g.id}>
{g.name}
</option>
))}
</select>
<button
onClick={() => setHelpOpen(true)}
style={styles.helpBtn}
title="Hilfe"
aria-label="Hilfe"
>
Hilfe
</button>
</div>
</div>
</div>
{helpOpen && (
<div style={styles.modalOverlay} onMouseDown={closeHelp}>
<div
style={styles.modalCard}
onMouseDown={(e) => e.stopPropagation()}
>
<div style={styles.modalHeader}>
<div style={{ fontWeight: 1000, color: "#20140c" }}>Hilfe</div>
<button
onClick={closeHelp}
style={styles.modalCloseBtn}
aria-label="Schließen"
>
</button>
</div>
<div style={styles.helpBody}>
<div style={styles.helpSectionTitle}>
1) Namen anklicken (Status)
</div>
<div style={styles.helpText}>
Tippe auf einen Namen, um den Status zu wechseln. Reihenfolge:
</div>
<div style={styles.helpList}>
<div style={styles.helpListRow}>
<span
style={{
...styles.helpBadge,
background: "rgba(0,170,60,0.12)",
color: "#0b6a1e",
}}
>
</span>
<div>
<b>Grün</b> = bestätigt / fix richtig
</div>
</div>
<div style={styles.helpListRow}>
<span
style={{
...styles.helpBadge,
background: "rgba(255,0,0,0.10)",
color: "#b10000",
}}
>
</span>
<div>
<b>Rot</b> = ausgeschlossen / fix falsch
</div>
</div>
<div style={styles.helpListRow}>
<span
style={{
...styles.helpBadge,
background: "rgba(120,120,120,0.14)",
color: "#444",
}}
>
?
</span>
<div>
<b>Grau</b> = unsicher / vielleicht
</div>
</div>
<div style={styles.helpListRow}>
<span
style={{
...styles.helpBadge,
background: "rgba(255,255,255,0.30)",
color: "#20140c",
}}
>
</span>
<div>
<b>Leer</b> = unknown / noch nicht bewertet
</div>
</div>
</div>
<div style={styles.helpDivider} />
<div style={styles.helpSectionTitle}>
2) i / m / s Button (Notiz)
</div>
<div style={styles.helpText}>
Rechts pro Zeile gibt es einen Button, der durch diese Werte
rotiert:
</div>
<div style={styles.helpList}>
<div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>i</span>
<div>
<b>i</b> = Ich habe diese Geheimkarte
</div>
</div>
<div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>m</span>
<div>
<b>m</b> = Geheimkarte aus dem mittleren Deck
</div>
</div>
<div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>s</span>
<div>
<b>s</b> = Ein anderer Spieler hat diese Karte
</div>
</div>
<div style={styles.helpListRow}>
<span style={styles.helpMiniTag}></span>
<div>
<b></b> = keine Notiz
</div>
</div>
</div>
<div style={styles.helpDivider} />
<div style={styles.helpText}>
Tipp: Jeder Spieler sieht nur seine eigenen Notizen andere
Spieler können nicht in deinen Zettel schauen.
</div>
</div>
</div>
</div>
)}
<div style={{ marginTop: 14, display: "grid", gap: 14 }}>
{sections.map((sec) => (
<div key={sec.key} style={styles.card}>
<div style={styles.sectionHeader}>{sec.title}</div>
<div style={{ display: "grid" }}>
{sec.entries.map((e) => {
const badge = getStatusBadge(e.status);
return (
<div
key={e.entry_id}
className="hp-row"
style={{
...styles.row,
background: getRowBg(e.status),
animation:
pulseId === e.entry_id
? "rowPulse 220ms ease-out"
: "none",
borderLeft:
e.status === 2 ? "4px solid rgba(0,190,80,0.55)" :
e.status === 1 ? "4px solid rgba(255,35,35,0.55)" :
e.status === 3 ? "4px solid rgba(90,90,90,0.55)" :
"4px solid rgba(0,0,0,0)",
}}
>
<div
onClick={() => cycleStatus(e)}
style={{
...styles.name,
textDecoration:
e.status === 1 ? "line-through" : "none",
color: getNameColor(e.status),
opacity: e.status === 1 ? 0.75 : 1,
}}
title="Klick: Grün → Rot → Grau → Leer"
>
{e.label}
</div>
<div style={styles.statusCell}>
<span
style={{
...styles.statusBadge,
color: badge.color,
background: badge.background,
}}
>
{getStatusSymbol(e.status)}
</span>
</div>
<button
onClick={() => toggleTag(e)}
style={styles.tagBtn}
title="i → m → s → leer"
>
{e.note_tag || "—"}
</button>
</div>
);
})}
</div>
</div>
))}
</div>
<div style={{ height: 24 }} />
</div>
</div>
);
}
/* ===== Styles ===== */
const styles = {
page: {
minHeight: "100dvh",
background: "transparent",
position: "relative",
zIndex: 1,
},
shell: {
fontFamily: '"IM Fell English", system-ui',
padding: 16,
maxWidth: 680,
margin: "0 auto",
},
topBar: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 10,
padding: 12,
borderRadius: 16,
background: "rgba(255,255,255,0.30)",
border: "1px solid rgba(0,0,0,0.14)",
boxShadow: "0 12px 28px rgba(0,0,0,0.14)",
backdropFilter: "blur(4px)",
},
card: {
borderRadius: 18,
overflow: "hidden",
border: "1px solid rgba(0,0,0,0.16)",
background: "rgba(255,255,255,0.26)",
boxShadow:
"0 18px 40px rgba(0,0,0,0.16), inset 0 1px 0 rgba(255,255,255,0.55)",
},
cardBody: {
padding: 12,
display: "flex",
gap: 10,
alignItems: "center",
},
sectionHeader: {
padding: "11px 14px",
fontWeight: 1000,
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
letterSpacing: 1.0,
color: "rgba(25,16,10,0.95)",
background:
"linear-gradient(180deg, rgba(210,170,90,0.95), rgba(150,110,35,0.95))",
borderBottom: "1px solid rgba(0,0,0,0.22)",
textTransform: "uppercase",
textShadow: "0 1px 0 rgba(255,255,255,0.20)",
},
row: {
display: "grid",
gridTemplateColumns: "1fr 54px 68px",
gap: 10,
padding: "12px 14px",
alignItems: "center",
borderBottom: "1px solid rgba(0,0,0,0.08)",
borderLeft: "4px solid rgba(0,0,0,0)", // default
},
name: {
cursor: "pointer",
userSelect: "none",
fontWeight: 800,
letterSpacing: 0.2,
},
statusCell: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
statusBadge: {
width: 34,
height: 34,
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
borderRadius: 999,
border: "1px solid rgba(0,0,0,0.22)",
fontWeight: 1100,
fontSize: 16,
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
tagBtn: {
padding: "8px 0",
fontWeight: 1000,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.22)",
background: "rgba(255,255,255,0.42)",
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
helpBtn: {
padding: "10px 12px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.22)",
background: "rgba(255,255,255,0.46)",
fontWeight: 1000,
cursor: "pointer",
whiteSpace: "nowrap",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
input: {
width: "100%",
padding: 10,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.20)",
background: "rgba(255,255,255,0.55)",
outline: "none",
fontSize: 16,
},
primaryBtn: {
padding: "10px 12px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.22)",
background:
"linear-gradient(180deg, rgba(246,226,179,0.95), rgba(202,164,90,0.95))",
fontWeight: 1000,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
secondaryBtn: {
padding: "10px 12px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.20)",
background: "rgba(255,255,255,0.46)",
fontWeight: 900,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
adminWrap: {
marginTop: 14,
padding: 12,
borderRadius: 16,
border: "1px solid rgba(0,0,0,0.16)",
background: "rgba(255,255,255,0.22)",
boxShadow: "0 12px 28px rgba(0,0,0,0.12)",
},
adminTop: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
},
adminTitle: {
fontWeight: 1000,
color: "#20140c",
},
userRow: {
display: "grid",
gridTemplateColumns: "1fr 80px 90px",
gap: 8,
padding: 10,
borderRadius: 12,
background: "rgba(255,255,255,0.50)",
border: "1px solid rgba(0,0,0,0.08)",
},
modalOverlay: {
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.45)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 16,
zIndex: 9999,
animation: "fadeIn 160ms ease-out",
},
modalCard: {
width: "100%",
maxWidth: 560,
borderRadius: 18,
border: "1px solid rgba(0,0,0,0.25)",
background:
"linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.42))",
boxShadow: "0 18px 50px rgba(0,0,0,0.35)",
padding: 14,
backdropFilter: "blur(6px)",
animation: "popIn 160ms ease-out",
},
modalHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 10,
},
modalCloseBtn: {
width: 38,
height: 38,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.25)",
background: "rgba(255,255,255,0.60)",
fontWeight: 1000,
cursor: "pointer",
lineHeight: "38px",
textAlign: "center",
},
helpBody: {
marginTop: 10,
paddingTop: 4,
maxHeight: "70vh",
overflow: "auto",
},
helpSectionTitle: {
fontWeight: 1000,
color: "#20140c",
marginTop: 10,
marginBottom: 6,
},
helpText: {
color: "#20140c",
opacity: 0.9,
lineHeight: 1.35,
},
helpList: {
marginTop: 10,
display: "grid",
gap: 8,
},
helpListRow: {
display: "grid",
gridTemplateColumns: "42px 1fr",
gap: 10,
alignItems: "center",
},
helpBadge: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 38,
height: 38,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.18)",
fontWeight: 1100,
fontSize: 18,
},
helpMiniTag: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 38,
height: 38,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.18)",
background: "rgba(255,255,255,0.55)",
fontWeight: 1100,
},
helpDivider: {
margin: "14px 0",
height: 1,
background: "rgba(0,0,0,0.12)",
},
loginPage: {
minHeight: "100dvh",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
overflow: "hidden",
padding: 20,
background: "transparent",
zIndex: 1,
},
loginCard: {
width: "100%",
maxWidth: 420,
padding: 26,
borderRadius: 22,
position: "relative",
zIndex: 2,
border: "1px solid rgba(0,0,0,0.20)",
background: "rgba(255,255,255,0.28)",
boxShadow: "0 18px 55px rgba(0,0,0,0.28)",
backdropFilter: "blur(8px)",
animation: "popIn 240ms ease-out",
},
loginTitle: {
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
fontWeight: 1000,
fontSize: 26,
color: "#20140c",
textAlign: "center",
letterSpacing: 0.6,
},
loginSubtitle: {
marginTop: 6,
textAlign: "center",
opacity: 0.85,
fontSize: 15,
lineHeight: 1.4,
},
loginFieldWrap: {
width: "100%",
display: "flex",
justifyContent: "center",
},
loginInput: {
width: "100%",
padding: 10,
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.20)",
background: "rgba(255,255,255,0.55)",
outline: "none",
fontSize: 16,
},
loginBtn: {
padding: "12px 14px",
borderRadius: 14,
border: "1px solid rgba(0,0,0,0.26)",
background:
"linear-gradient(180deg, rgba(246,226,179,0.95), rgba(202,164,90,0.95))",
fontWeight: 1000,
fontSize: 16,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.55)",
},
loginHint: {
marginTop: 18,
fontSize: 13,
opacity: 0.72,
textAlign: "center",
lineHeight: 1.35,
},
candleGlowLayer: {
position: "absolute",
inset: 0,
pointerEvents: "none",
background: `
radial-gradient(circle at 20% 25%, rgba(255, 200, 120, 0.22), rgba(0,0,0,0) 40%),
radial-gradient(circle at 80% 30%, rgba(255, 210, 140, 0.18), rgba(0,0,0,0) 42%),
radial-gradient(circle at 55% 75%, rgba(255, 180, 100, 0.12), rgba(0,0,0,0) 45%)
`,
animation: "candleGlow 3.8s ease-in-out infinite",
mixBlendMode: "multiply",
},
inputRow: {
display: "flex",
alignItems: "stretch",
width: "100%",
},
inputInRow: {
flex: 1,
padding: 10,
borderRadius: "12px 0 0 12px",
border: "1px solid rgba(0,0,0,0.20)",
background: "rgba(255,255,255,0.55)",
outline: "none",
minWidth: 0,
fontSize: 16,
},
pwToggleBtn: {
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0 12px 12px 0",
border: "1px solid rgba(0,0,0,0.20)",
borderLeft: "none",
background: "rgba(255,255,255,0.60)",
cursor: "pointer",
fontWeight: 900,
padding: 0,
},
// ✅ Background (FIXED) — unter Content (zIndex -1) und ohne blur() Filter
bgFixed: {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
// iOS dynamic viewport support:
height: "100dvh",
zIndex: 0,
pointerEvents: "none",
// verhindert “zucken” bei repaint:
transform: "translateZ(0)",
backfaceVisibility: "hidden",
willChange: "transform",
},
bgMap: {
position: "absolute",
inset: 0,
backgroundImage: 'url("/public/bg/marauders-map-blur.jpg")',
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
},
bgInkVignette: {
position: "absolute",
inset: 0,
background: `
radial-gradient(circle at 50% 45%,
rgba(0,0,0,0.08) 0%,
rgba(0,0,0,0.18) 48%,
rgba(0,0,0,0.55) 78%,
rgba(0,0,0,0.72) 100%
),
linear-gradient(180deg,
rgba(245, 226, 185, 0.42),
rgba(214, 180, 120, 0.32)
)
`,
mixBlendMode: "multiply",
transform: "translateZ(0)",
backfaceVisibility: "hidden",
},
};