snapshot current state before gitea sync
This commit is contained in:
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ssh:*)",
|
||||
"Bash(sips:*)",
|
||||
"Bash(python3:*)",
|
||||
"WebFetch(domain:manual.uberspace.de)",
|
||||
"WebFetch(domain:lab.uberspace.de)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(rsync:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
250
app.js
Normal file
250
app.js
Normal file
@@ -0,0 +1,250 @@
|
||||
const words = [
|
||||
"Kekse",
|
||||
"Schach",
|
||||
"Giraffe",
|
||||
"Spaghetti",
|
||||
"Rakete",
|
||||
"Blume",
|
||||
"Kino",
|
||||
"Drachen",
|
||||
"Buch",
|
||||
"Kaffee",
|
||||
"Berg",
|
||||
"Pizza",
|
||||
"Wolke",
|
||||
"Bus",
|
||||
"Zitrone",
|
||||
"Gitarre",
|
||||
"Meer",
|
||||
"Bananen",
|
||||
"Loewe",
|
||||
"Regen",
|
||||
];
|
||||
|
||||
const state = {
|
||||
players: [],
|
||||
currentIndex: 0,
|
||||
imposterIndex: -1,
|
||||
secretWord: "",
|
||||
revealed: false,
|
||||
discussionIndex: 0,
|
||||
};
|
||||
|
||||
const elements = {
|
||||
playerInput: document.querySelector("#player-input"),
|
||||
addPlayer: document.querySelector("#add-player"),
|
||||
playerList: document.querySelector("#player-list"),
|
||||
startGame: document.querySelector("#start-game"),
|
||||
clearPlayers: document.querySelector("#clear-players"),
|
||||
setupHint: document.querySelector("#setup-hint"),
|
||||
setup: document.querySelector("#setup"),
|
||||
round: document.querySelector("#round"),
|
||||
discussion: document.querySelector("#discussion"),
|
||||
done: document.querySelector("#done"),
|
||||
roundCount: document.querySelector("#round-count"),
|
||||
restart: document.querySelector("#restart"),
|
||||
currentPlayer: document.querySelector("#current-player"),
|
||||
roundScreen: document.querySelector("#round-screen"),
|
||||
handoff: document.querySelector("#handoff"),
|
||||
revealCard: document.querySelector("#reveal-card"),
|
||||
revealWord: document.querySelector("#reveal-word"),
|
||||
discussionCount: document.querySelector("#discussion-count"),
|
||||
discussionPlayer: document.querySelector("#discussion-player"),
|
||||
discussionNext: document.querySelector("#discussion-next"),
|
||||
discussionRestart: document.querySelector("#discussion-restart"),
|
||||
imposterFound: document.querySelector("#imposter-found"),
|
||||
playAgain: document.querySelector("#play-again"),
|
||||
editPlayers: document.querySelector("#edit-players"),
|
||||
};
|
||||
|
||||
const MIN_PLAYERS = 3;
|
||||
const STORAGE_KEY = "imposterPlayers";
|
||||
|
||||
const updatePlayerList = () => {
|
||||
elements.playerList.innerHTML = "";
|
||||
state.players.forEach((player, index) => {
|
||||
const item = document.createElement("li");
|
||||
const name = document.createElement("span");
|
||||
name.textContent = player;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "Entfernen";
|
||||
remove.addEventListener("click", () => {
|
||||
state.players.splice(index, 1);
|
||||
updatePlayerList();
|
||||
});
|
||||
item.append(name, remove);
|
||||
elements.playerList.appendChild(item);
|
||||
});
|
||||
|
||||
const ready = state.players.length >= MIN_PLAYERS;
|
||||
elements.startGame.disabled = !ready;
|
||||
elements.setupHint.textContent = ready
|
||||
? `${state.players.length} Spieler bereit.`
|
||||
: `Mindestens ${MIN_PLAYERS} Spieler.`;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.players));
|
||||
};
|
||||
|
||||
const addPlayer = () => {
|
||||
const raw = elements.playerInput.value.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
state.players.push(raw);
|
||||
elements.playerInput.value = "";
|
||||
updatePlayerList();
|
||||
elements.playerInput.focus();
|
||||
};
|
||||
|
||||
const startRound = () => {
|
||||
state.currentIndex = 0;
|
||||
state.imposterIndex = Math.floor(Math.random() * state.players.length);
|
||||
state.secretWord = words[Math.floor(Math.random() * words.length)];
|
||||
state.revealed = false;
|
||||
elements.revealCard.classList.add("flip-card--instant");
|
||||
elements.setup.classList.add("hidden");
|
||||
elements.discussion.classList.add("hidden");
|
||||
elements.done.classList.add("hidden");
|
||||
elements.round.classList.remove("hidden");
|
||||
updateRoundView();
|
||||
requestAnimationFrame(() => {
|
||||
elements.revealCard.classList.remove("flip-card--instant");
|
||||
});
|
||||
};
|
||||
|
||||
const updateRoundView = () => {
|
||||
const total = state.players.length;
|
||||
const current = state.currentIndex + 1;
|
||||
elements.roundCount.textContent = `Phase 1: Zuweisung \u2014 Spieler ${current} / ${total}`;
|
||||
elements.currentPlayer.textContent = state.players[state.currentIndex];
|
||||
elements.roundScreen.classList.remove("hidden");
|
||||
const isImposter = state.currentIndex === state.imposterIndex;
|
||||
elements.revealWord.textContent = isImposter ? "IMPOSTER" : state.secretWord;
|
||||
elements.revealCard.classList.remove("flipped");
|
||||
state.revealed = false;
|
||||
};
|
||||
|
||||
const toggleReveal = () => {
|
||||
elements.revealCard.classList.toggle("flipped");
|
||||
state.revealed = !state.revealed;
|
||||
};
|
||||
|
||||
const handoffPlayer = () => {
|
||||
elements.revealCard.classList.add("flip-card--instant");
|
||||
elements.revealCard.classList.remove("flipped");
|
||||
state.currentIndex += 1;
|
||||
if (state.currentIndex >= state.players.length) {
|
||||
startDiscussion();
|
||||
return;
|
||||
}
|
||||
updateRoundView();
|
||||
requestAnimationFrame(() => {
|
||||
elements.revealCard.classList.remove("flip-card--instant");
|
||||
});
|
||||
};
|
||||
|
||||
const startDiscussion = () => {
|
||||
state.discussionIndex = 0;
|
||||
elements.round.classList.add("hidden");
|
||||
elements.done.classList.add("hidden");
|
||||
elements.discussion.classList.remove("hidden");
|
||||
updateDiscussionView();
|
||||
};
|
||||
|
||||
const updateDiscussionView = () => {
|
||||
const total = state.players.length;
|
||||
const current = state.discussionIndex + 1;
|
||||
elements.discussionCount.textContent = `Phase 2: Runde \u2014 Spieler ${current} / ${total}`;
|
||||
elements.discussionPlayer.textContent = state.players[state.discussionIndex];
|
||||
};
|
||||
|
||||
const nextDiscussion = () => {
|
||||
if (state.players.length === 0) {
|
||||
return;
|
||||
}
|
||||
state.discussionIndex = (state.discussionIndex + 1) % state.players.length;
|
||||
updateDiscussionView();
|
||||
};
|
||||
|
||||
const endDiscussion = () => {
|
||||
elements.discussion.classList.add("hidden");
|
||||
elements.done.classList.remove("hidden");
|
||||
};
|
||||
|
||||
const resetToSetup = () => {
|
||||
elements.round.classList.add("hidden");
|
||||
elements.discussion.classList.add("hidden");
|
||||
elements.done.classList.add("hidden");
|
||||
elements.setup.classList.remove("hidden");
|
||||
};
|
||||
|
||||
const clearPlayers = () => {
|
||||
state.players = [];
|
||||
updatePlayerList();
|
||||
};
|
||||
|
||||
const loadPlayers = () => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed)) {
|
||||
state.players = parsed.filter((name) => typeof name === "string");
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
elements.addPlayer.addEventListener("click", addPlayer);
|
||||
elements.playerInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
addPlayer();
|
||||
}
|
||||
});
|
||||
|
||||
elements.clearPlayers.addEventListener("click", clearPlayers);
|
||||
elements.startGame.addEventListener("click", startRound);
|
||||
elements.handoff.addEventListener("click", handoffPlayer);
|
||||
elements.revealCard.addEventListener("click", toggleReveal);
|
||||
elements.discussionNext.addEventListener("click", nextDiscussion);
|
||||
elements.discussionRestart.addEventListener("click", startRound);
|
||||
elements.imposterFound.addEventListener("click", endDiscussion);
|
||||
elements.restart.addEventListener("click", startRound);
|
||||
elements.playAgain.addEventListener("click", startRound);
|
||||
elements.editPlayers.addEventListener("click", resetToSetup);
|
||||
|
||||
loadPlayers();
|
||||
updatePlayerList();
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", async () => {
|
||||
const reg = await navigator.serviceWorker.register("./sw.js");
|
||||
|
||||
const showUpdate = () => {
|
||||
document.querySelector("#update-banner").classList.remove("hidden");
|
||||
document.querySelector("#update-btn").addEventListener("click", () => {
|
||||
reg.waiting.postMessage("SKIP_WAITING");
|
||||
});
|
||||
};
|
||||
|
||||
if (reg.waiting) {
|
||||
showUpdate();
|
||||
}
|
||||
|
||||
reg.addEventListener("updatefound", () => {
|
||||
const newSW = reg.installing;
|
||||
newSW.addEventListener("statechange", () => {
|
||||
if (newSW.state === "installed" && navigator.serviceWorker.controller) {
|
||||
showUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
BIN
apple-touch-icon.png
Normal file
BIN
apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
icon-192.png
Normal file
BIN
icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
icon-512.png
Normal file
BIN
icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
8
icon.svg
Normal file
8
icon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Imposter">
|
||||
<rect width="512" height="512" rx="120" fill="#ff7b54"/>
|
||||
<circle cx="256" cy="210" r="110" fill="#fffaf0"/>
|
||||
<rect x="140" y="300" width="232" height="120" rx="60" fill="#fffaf0"/>
|
||||
<circle cx="230" cy="210" r="20" fill="#1a1a1a"/>
|
||||
<circle cx="282" cy="210" r="20" fill="#1a1a1a"/>
|
||||
<path d="M200 260c24 26 88 26 112 0" stroke="#1a1a1a" stroke-width="18" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
145
index.html
Normal file
145
index.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#f8f3e7" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>Imposter</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Unbounded:wght@300;500;700&family=Space+Grotesk:wght@400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app">
|
||||
<section class="screen" id="setup">
|
||||
<header class="hero">
|
||||
<div class="hero__tag">Handy weiterreichen</div>
|
||||
<h1>Imposter</h1>
|
||||
<p>
|
||||
Trage Spieler ein, starte die Runde, und zeige jedem sein
|
||||
Wort oder "IMPOSTER". Danach findet ihr heraus, wer das Wort
|
||||
nicht kennt.
|
||||
</p>
|
||||
</header>
|
||||
<div class="card">
|
||||
<h2>Spieler</h2>
|
||||
<div class="input-row">
|
||||
<input
|
||||
id="player-input"
|
||||
type="text"
|
||||
placeholder="Name eingeben"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button id="add-player" class="btn">Hinzufuegen</button>
|
||||
</div>
|
||||
<ul id="player-list" class="player-list"></ul>
|
||||
<div class="actions">
|
||||
<button id="clear-players" class="btn btn--ghost">
|
||||
Liste leeren
|
||||
</button>
|
||||
<button id="start-game" class="btn btn--primary">
|
||||
Runde starten
|
||||
</button>
|
||||
</div>
|
||||
<p id="setup-hint" class="hint">Mindestens 3 Spieler.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen hidden" id="round">
|
||||
<div class="card">
|
||||
<div class="round-header">
|
||||
<div class="round-count" id="round-count">Spieler 1 / 1</div>
|
||||
<button id="restart" class="btn btn--ghost">Neue Runde</button>
|
||||
</div>
|
||||
<div id="round-screen" class="pass-screen">
|
||||
<h2>Phase 1: Zuweisung</h2>
|
||||
<div class="player-chip" id="current-player">Spieler</div>
|
||||
<div class="flip-card" id="reveal-card">
|
||||
<div class="flip-card__inner">
|
||||
<div class="flip-card__front">
|
||||
<div class="reveal-label">Antippen zum Aufdecken</div>
|
||||
</div>
|
||||
<div class="flip-card__back">
|
||||
<div class="reveal-label">Dein Hinweis</div>
|
||||
<div class="reveal-word" id="reveal-word">WORT</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Tippe auf die Karte, um dein Wort zu sehen. Gib das Handy
|
||||
danach weiter.
|
||||
</p>
|
||||
<div class="actions actions--end">
|
||||
<button id="handoff" class="btn btn--primary btn--big">
|
||||
Weitergeben
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen hidden" id="discussion">
|
||||
<div class="card">
|
||||
<div class="round-header">
|
||||
<div class="round-count" id="discussion-count">
|
||||
Diskussion 1 / 1
|
||||
</div>
|
||||
<button id="discussion-restart" class="btn btn--ghost">
|
||||
Neue Runde
|
||||
</button>
|
||||
</div>
|
||||
<div class="pass-screen">
|
||||
<h2>Wer ist dran?</h2>
|
||||
<div
|
||||
class="player-chip player-chip--large"
|
||||
id="discussion-player"
|
||||
>
|
||||
Spieler
|
||||
</div>
|
||||
<p class="hint">Gib das Handy weiter.</p>
|
||||
<div class="actions">
|
||||
<button id="imposter-found" class="btn btn--ghost">
|
||||
Imposter gefunden
|
||||
</button>
|
||||
<button
|
||||
id="discussion-next"
|
||||
class="btn btn--primary btn--big"
|
||||
>
|
||||
Weitergeben
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen hidden" id="done">
|
||||
<div class="card">
|
||||
<h2>Runde beendet</h2>
|
||||
<p>Optional: Neue Runde starten oder Spieler bearbeiten.</p>
|
||||
<div class="actions">
|
||||
<button id="edit-players" class="btn btn--ghost">
|
||||
Spieler bearbeiten
|
||||
</button>
|
||||
<button id="play-again" class="btn btn--primary">
|
||||
Neue Runde
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="update-banner hidden" id="update-banner">
|
||||
Neue Version verfügbar
|
||||
<button id="update-btn" class="btn btn--primary">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
manifest.json
Normal file
22
manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Imposter",
|
||||
"short_name": "Imposter",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#f8f3e7",
|
||||
"theme_color": "#f8f3e7",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
371
styles.css
Normal file
371
styles.css
Normal file
@@ -0,0 +1,371 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f8f3e7;
|
||||
--bg-alt: #f0e6d1;
|
||||
--ink: #1a1a1a;
|
||||
--accent: #ff7b54;
|
||||
--accent-dark: #d95c38;
|
||||
--muted: #5b5b5b;
|
||||
--card: #fffaf0;
|
||||
--shadow: 0 20px 45px rgba(26, 26, 26, 0.12);
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
background-color: var(--bg);
|
||||
background: radial-gradient(circle at top, #fff4dc, var(--bg));
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.screen > .card {
|
||||
width: min(560px, 100%);
|
||||
}
|
||||
|
||||
#round > .card,
|
||||
#discussion > .card,
|
||||
#done > .card {
|
||||
min-height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#setup {
|
||||
justify-content: flex-start;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(560px, 100%);
|
||||
}
|
||||
|
||||
.hero__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-self: start;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
color: #fffaf0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: "Unbounded", "Trebuchet MS", sans-serif;
|
||||
margin: 0;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 5vw, 3.2rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.5rem, 3.5vw, 2rem);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid rgba(26, 26, 26, 0.08);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(26, 26, 26, 0.2);
|
||||
font-size: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: var(--bg-alt);
|
||||
color: var(--ink);
|
||||
transition: box-shadow 0.2s ease;
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
box-shadow: 0 10px 20px rgba(26, 26, 26, 0.15);
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: var(--accent);
|
||||
color: #fffaf0;
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
border: 1px dashed rgba(26, 26, 26, 0.25);
|
||||
}
|
||||
|
||||
.btn--big {
|
||||
padding: 14px 20px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.player-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 16px 0 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.player-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(26, 26, 26, 0.08);
|
||||
}
|
||||
|
||||
.player-list button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--accent-dark);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actions--end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.round-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.round-count {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pass-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pass-screen .actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.player-chip {
|
||||
padding: 10px 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-alt);
|
||||
font-weight: 600;
|
||||
font-size: clamp(1.2rem, 3.8vw, 1.8rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-chip--large {
|
||||
font-size: clamp(1.8rem, 5vw, 2.8rem);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.flip-card {
|
||||
perspective: 600px;
|
||||
width: min(320px, 80vw);
|
||||
height: 180px;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.flip-card__inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-card.flipped .flip-card__inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card--instant .flip-card__inner {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.flip-card__front,
|
||||
.flip-card__back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
background: linear-gradient(135deg, #fff4dc, #ffe2c0);
|
||||
box-shadow: inset 0 0 0 1px rgba(26, 26, 26, 0.08);
|
||||
}
|
||||
|
||||
.flip-card__back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.reveal-label {
|
||||
font-size: 0.95rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.reveal-word {
|
||||
font-family: "Unbounded", sans-serif;
|
||||
font-size: clamp(1.8rem, 6vw, 3rem);
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16px 0 32px;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.input-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.round-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.35s ease;
|
||||
}
|
||||
|
||||
|
||||
.pass-screen .btn {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.update-banner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
background: var(--ink);
|
||||
color: #fffaf0;
|
||||
font-weight: 600;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.update-banner .btn {
|
||||
min-height: 40px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
45
sw.js
Normal file
45
sw.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const CACHE_NAME = "imposter-static-v4";
|
||||
const ASSETS = [
|
||||
"./",
|
||||
"./index.html",
|
||||
"./styles.css",
|
||||
"./app.js",
|
||||
"./manifest.json",
|
||||
"./icon-192.png",
|
||||
"./icon-512.png",
|
||||
"./apple-touch-icon.png",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((key) => key !== CACHE_NAME)
|
||||
.map((key) => caches.delete(key)),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(
|
||||
caches
|
||||
.match(event.request)
|
||||
.then((response) => response || fetch(event.request)),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user