import React, { useEffect, useState } from "react";
const API = "/api";
const CHIP_LIST = ["AL", "JG", "JN", "SN", "TL"];
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();
}
/**
* Backend erlaubt: null | "i" | "m" | "s"
* Rotation:
* null -> i -> m -> s (Popup) -> null
*/
function cycleTag(tag) {
if (!tag) return "i";
if (tag === "i") return "m";
if (tag === "m") return "s";
return null; // "s" -> null
}
/* ========= Chip localStorage (Frontend-only) ========= */
function chipStorageKey(gameId, entryId) {
return `chip:${gameId}:${entryId}`;
}
function getChipLS(gameId, entryId) {
try {
return localStorage.getItem(chipStorageKey(gameId, entryId));
} catch {
return null;
}
}
function setChipLS(gameId, entryId, chip) {
try {
localStorage.setItem(chipStorageKey(gameId, entryId), chip);
} catch {}
}
function clearChipLS(gameId, entryId) {
try {
localStorage.removeItem(chipStorageKey(gameId, entryId));
} catch {}
}
/* ========= Admin Panel ========= */
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 (
Admin Dashboard
Vorhandene User
{users.map((u) => (
{u.email}
{u.role}
{u.disabled ? "disabled" : "active"}
))}
{open && (
e.stopPropagation()}>
setEmail(e.target.value)}
placeholder="Email"
style={styles.input}
autoFocus
/>
setPassword(e.target.value)}
placeholder="Initial Passwort"
type="password"
style={styles.input}
/>
{msg &&
{msg}
}
Tipp: Klick auf Item: Grün → Rot → Grau → Leer
)}
);
}
/* ========= App ========= */
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);
// Chip popup
const [chipOpen, setChipOpen] = useState(false);
const [chipEntry, setChipEntry] = 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
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,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
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: ${stylesTokens.pageBg};
color: ${stylesTokens.textMain};
}
body {
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
#root { background: transparent; }
* { -webkit-tap-highlight-color: transparent; }
`;
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);
};
// Notiz-Button: i -> m -> (Popup) s -> null
const toggleTag = async (entry) => {
const next = cycleTag(entry.note_tag);
// Wenn wir zu "s" gehen würden -> Chip Popup öffnen, aber NICHT ins Backend schreiben
if (next === "s") {
setChipEntry(entry);
setChipOpen(true);
return;
}
// Wenn wir auf null gehen, Chip lokal löschen (weil s -> —)
if (next === null) {
clearChipLS(gameId, entry.entry_id);
}
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: next }),
});
await reloadSheet();
};
// Chip wählen:
// Backend: note_tag = "s"
// Frontend: Chip in localStorage
const chooseChip = async (chip) => {
if (!chipEntry) return;
// UI sofort schließen -> fühlt sich besser an
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
// local speichern
setChipLS(gameId, entry.entry_id, chip);
try {
// Backend bekommt nur "s"
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: "s" }),
});
} finally {
await reloadSheet();
}
};
// X im Modal:
// Backend zurück auf null und lokalen Chip löschen
const closeChipModalToDash = async () => {
if (!chipEntry) {
setChipOpen(false);
return;
}
// UI sofort schließen
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
// Frontend-only Chip entfernen
clearChipLS(gameId, entry.entry_id);
try {
// Backend zurück auf —
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: null }),
});
} finally {
await reloadSheet();
}
};
// Anzeige im Tag-Button:
// - "s" wird zu "s.AL" (aus localStorage), sonst "s"
const displayTag = (entry) => {
const t = entry.note_tag;
if (!t) return "—";
if (t === "s") {
const chip = getChipLS(gameId, entry.entry_id);
return chip ? `s.${chip}` : "s"; // <-- genau wie gewünscht
}
return t; // i oder m
};
// --- helpers ---
const getRowBg = (status) => {
if (status === 1) return "rgba(255, 35, 35, 0.16)";
if (status === 2) return "rgba(0, 190, 80, 0.16)";
if (status === 3) return "rgba(140, 140, 140, 0.12)";
return "rgba(255,255,255,0.06)";
};
const getNameColor = (status) => {
if (status === 1) return "#ffb3b3";
if (status === 2) return "#baf3c9";
if (status === 3) return "rgba(233,216,166,0.78)";
return stylesTokens.textMain;
};
const getStatusSymbol = (status) => {
if (status === 2) return "✓";
if (status === 1) return "✕";
if (status === 3) return "?";
return "–";
};
const getStatusBadge = (status) => {
if (status === 2) return { color: "#baf3c9", background: "rgba(0,190,80,0.18)" };
if (status === 1) return { color: "#ffb3b3", background: "rgba(255,35,35,0.18)" };
if (status === 3)
return { color: "rgba(233,216,166,0.85)", background: "rgba(140,140,140,0.14)" };
return { color: "rgba(233,216,166,0.75)", background: "rgba(255,255,255,0.08)" };
};
const closeHelp = () => setHelpOpen(false);
// ===== Login =====
if (!me) {
return (
Zauber-Detektiv Notizbogen
Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen
Deine Notizen bleiben privat – jeder Spieler sieht nur seinen eigenen Zettel.
);
}
// ===== Main =====
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 || [] },
]
: [];
return (
{me.role === "admin" &&
}
Spiel
{helpOpen && (
e.stopPropagation()}>
1) Namen anklicken (Status)
Tippe auf einen Namen, um den Status zu wechseln. Reihenfolge:
✓
Grün = bestätigt / fix richtig
✕
Rot = ausgeschlossen / fix falsch
?
Grau = unsicher / „vielleicht“
–
Leer = unknown / noch nicht bewertet
2) i / m / s Button (Notiz)
Rechts pro Zeile gibt es einen Button, der durch diese Werte rotiert:
i
i = „Ich habe diese Geheimkarte“
m
m = „Geheimkarte aus dem mittleren Deck“
s
s = „Ein anderer Spieler hat diese Karte“ (Chip Auswahl)
Tipp: Jeder Spieler sieht nur seine eigenen Notizen – andere Spieler können nicht in deinen
Zettel schauen.
)}
{/* Chip Popup */}
{chipOpen && (
e.stopPropagation()}>
Chip auswählen:
{CHIP_LIST.map((c) => (
))}
Tipp: Wenn du wieder auf den Notiz-Button klickst, geht’s von s.XX zurück auf —.
)}
{sections.map((sec) => (
{sec.title}
{sec.entries.map((e) => {
// UI "rot" wenn note_tag i oder s (Backend s wird als s.XX angezeigt)
const isIorMorS = e.note_tag === "i" || e.note_tag === "m" || e.note_tag === "s";
const effectiveStatus = e.status === 0 && isIorMorS ? 1 : e.status;
const badge = getStatusBadge(effectiveStatus);
return (
cycleStatus(e)}
style={{
...styles.name,
textDecoration: effectiveStatus === 1 ? "line-through" : "none",
color: getNameColor(effectiveStatus),
opacity: effectiveStatus === 1 ? 0.8 : 1,
}}
title="Klick: Grün → Rot → Grau → Leer"
>
{e.label}
{getStatusSymbol(effectiveStatus)}
);
})}
))}
);
}
/* ===== Theme Tokens ===== */
const stylesTokens = {
pageBg: "#0b0b0c",
panelBg: "rgba(20, 20, 22, 0.55)",
panelBorder: "rgba(233, 216, 166, 0.14)",
textMain: "rgba(245, 239, 220, 0.92)",
textDim: "rgba(233, 216, 166, 0.70)",
textGold: "#e9d8a6",
goldLine: "rgba(233, 216, 166, 0.18)",
};
/* ===== 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: stylesTokens.panelBg,
border: `1px solid ${stylesTokens.panelBorder}`,
boxShadow: "0 12px 30px rgba(0,0,0,0.45)",
backdropFilter: "blur(6px)",
},
card: {
borderRadius: 18,
overflow: "hidden",
border: `1px solid ${stylesTokens.panelBorder}`,
background: "rgba(18, 18, 20, 0.50)",
boxShadow: "0 18px 40px rgba(0,0,0,0.50), inset 0 1px 0 rgba(255,255,255,0.06)",
},
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: stylesTokens.textGold,
background: "linear-gradient(180deg, rgba(32,32,36,0.92), rgba(14,14,16,0.92))",
borderBottom: `1px solid ${stylesTokens.goldLine}`,
textTransform: "uppercase",
textShadow: "0 1px 0 rgba(0,0,0,0.6)",
},
row: {
display: "grid",
gridTemplateColumns: "1fr 54px 68px",
gap: 10,
padding: "12px 14px",
alignItems: "center",
borderBottom: "1px solid rgba(233,216,166,0.08)",
borderLeft: "4px solid rgba(0,0,0,0)",
},
name: {
cursor: "pointer",
userSelect: "none",
fontWeight: 800,
letterSpacing: 0.2,
color: stylesTokens.textMain,
},
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(233,216,166,0.18)`,
fontWeight: 1100,
fontSize: 16,
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
},
tagBtn: {
padding: "8px 0",
fontWeight: 1000,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
},
helpBtn: {
padding: "10px 12px",
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
fontWeight: 1000,
cursor: "pointer",
whiteSpace: "nowrap",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
},
input: {
width: "100%",
padding: 10,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(10,10,12,0.55)",
color: stylesTokens.textMain,
outline: "none",
fontSize: 16,
},
primaryBtn: {
padding: "10px 12px",
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.28)`,
background: "linear-gradient(180deg, rgba(233,216,166,0.24), rgba(233,216,166,0.10))",
color: stylesTokens.textGold,
fontWeight: 1000,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)",
},
secondaryBtn: {
padding: "10px 12px",
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.05)",
color: stylesTokens.textMain,
fontWeight: 900,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
},
// Admin
adminWrap: {
marginTop: 14,
padding: 12,
borderRadius: 16,
border: `1px solid rgba(233,216,166,0.14)`,
background: "rgba(18, 18, 20, 0.40)",
boxShadow: "0 12px 30px rgba(0,0,0,0.45)",
backdropFilter: "blur(6px)",
},
adminTop: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
},
adminTitle: {
fontWeight: 1000,
color: stylesTokens.textGold,
},
userRow: {
display: "grid",
gridTemplateColumns: "1fr 80px 90px",
gap: 8,
padding: 10,
borderRadius: 12,
background: "rgba(255,255,255,0.06)",
border: `1px solid rgba(233,216,166,0.10)`,
},
// Modal
modalOverlay: {
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.65)",
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(233,216,166,0.18)`,
background: "linear-gradient(180deg, rgba(20,20,24,0.92), rgba(12,12,14,0.86))",
boxShadow: "0 18px 55px rgba(0,0,0,0.70)",
padding: 14,
backdropFilter: "blur(6px)",
animation: "popIn 160ms ease-out",
color: stylesTokens.textMain,
},
modalHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 10,
},
modalCloseBtn: {
width: 38,
height: 38,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
fontWeight: 1000,
cursor: "pointer",
lineHeight: "38px",
textAlign: "center",
},
// Help
helpBody: {
marginTop: 10,
paddingTop: 4,
maxHeight: "70vh",
overflow: "auto",
},
helpSectionTitle: {
fontWeight: 1000,
color: stylesTokens.textGold,
marginTop: 10,
marginBottom: 6,
},
helpText: {
color: stylesTokens.textMain,
opacity: 0.92,
lineHeight: 1.35,
},
helpList: {
marginTop: 10,
display: "grid",
gap: 8,
},
helpListRow: {
display: "grid",
gridTemplateColumns: "42px 1fr",
gap: 10,
alignItems: "center",
color: stylesTokens.textMain,
},
helpBadge: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 38,
height: 38,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
fontWeight: 1100,
fontSize: 18,
},
helpMiniTag: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 38,
height: 38,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
fontWeight: 1100,
},
helpDivider: {
margin: "14px 0",
height: 1,
background: "rgba(233,216,166,0.12)",
},
// Login
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(233,216,166,0.18)`,
background: "rgba(18, 18, 20, 0.55)",
boxShadow: "0 18px 60px rgba(0,0,0,0.70)",
backdropFilter: "blur(8px)",
animation: "popIn 240ms ease-out",
color: stylesTokens.textMain,
},
loginTitle: {
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
fontWeight: 1000,
fontSize: 26,
color: stylesTokens.textGold,
textAlign: "center",
letterSpacing: 0.6,
},
loginSubtitle: {
marginTop: 6,
textAlign: "center",
color: stylesTokens.textMain,
opacity: 0.9,
fontSize: 15,
lineHeight: 1.4,
},
loginFieldWrap: {
width: "100%",
display: "flex",
justifyContent: "center",
},
loginInput: {
width: "100%",
padding: 10,
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(10,10,12,0.60)",
color: stylesTokens.textMain,
outline: "none",
fontSize: 16,
},
loginBtn: {
padding: "12px 14px",
borderRadius: 14,
border: `1px solid rgba(233,216,166,0.28)`,
background: "linear-gradient(180deg, rgba(233,216,166,0.24), rgba(233,216,166,0.10))",
color: stylesTokens.textGold,
fontWeight: 1000,
fontSize: 16,
cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)",
},
loginHint: {
marginTop: 18,
fontSize: 13,
opacity: 0.78,
textAlign: "center",
color: stylesTokens.textDim,
lineHeight: 1.35,
},
candleGlowLayer: {
position: "absolute",
inset: 0,
pointerEvents: "none",
background: `
radial-gradient(circle at 20% 25%, rgba(255, 200, 120, 0.16), rgba(0,0,0,0) 40%),
radial-gradient(circle at 80% 30%, rgba(255, 210, 140, 0.12), rgba(0,0,0,0) 42%),
radial-gradient(circle at 55% 75%, rgba(255, 180, 100, 0.08), 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(233,216,166,0.18)`,
background: "rgba(10,10,12,0.60)",
color: stylesTokens.textMain,
outline: "none",
minWidth: 0,
fontSize: 16,
},
pwToggleBtn: {
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0 12px 12px 0",
border: `1px solid rgba(233,216,166,0.18)`,
borderLeft: "none",
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
cursor: "pointer",
fontWeight: 900,
padding: 0,
},
// Background
bgFixed: {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100dvh",
zIndex: -1,
pointerEvents: "none",
transform: "translateZ(0)",
backfaceVisibility: "hidden",
willChange: "transform",
},
bgMap: {
position: "absolute",
inset: 0,
backgroundImage: 'url("/bg/marauders-map-blur.jpg")',
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
filter: "saturate(0.9) contrast(1.05) brightness(0.55)",
},
chipGrid: {
marginTop: 12,
display: "grid",
gridTemplateColumns: "repeat(5, minmax(0, 1fr))",
gap: 8,
},
chipBtn: {
padding: "10px 14px",
borderRadius: 12,
border: "1px solid rgba(233,216,166,0.18)",
background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold,
fontWeight: 1000,
cursor: "pointer",
minWidth: 64,
},
};