My previous 'no scrolling' commit didn't actually work — two real bugs: 1. Stale assets: nginx cached css/js for a week (max-age=604800), so after a redeploy returning visitors kept the OLD stylesheet/script and never saw the fit logic. Switched assets to Cache-Control:no-cache (tiny files, 304s) and added ?v= query strings to bust the existing week-long cache. 2. Bad centering geometry: grid-centering pins an over-tall card to the top, and .winner-card re-declared position:relative, overriding the fix. Now the card is position:fixed at 50%/50% and fitActiveScreen() folds the fit scale into one transform: translate(-50%,-50%) scale(fit). Removed the winner-card position override and switched the screen fade to opacity-only (a transform on .screen would capture the fixed card and break centering). Verified in real Chromium against the container — setup, game, AND winner all show fits:true / scrolls:false at 1366x640 (short laptop), 375x667, and 360x640 (small phone). No scrolling on any screen at any size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
798 lines
27 KiB
CSS
798 lines
27 KiB
CSS
/* =========================================================================
|
|
ULTIMATE TIC-TAC-TOE — per-player camps + switchable stages
|
|
------------------------------------------------------------------------
|
|
Two orthogonal systems:
|
|
1. STAGE (global vibe) → [data-stage="sparkle|shred|mixed"] on <html>
|
|
Controls background, backdrop FX, fonts, surface (light/dark), winner.
|
|
2. CAMP (per player) → .camp-sparkle / .camp-shred on player elements
|
|
Controls a player's accent color + glow on their display, score, and
|
|
their pieces on the board. A player's camp comes from their avatar, so
|
|
one player can be Sparkle (🦄) while the other is Shred (💀).
|
|
========================================================================= */
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--radius-lg: 28px;
|
|
--radius-md: 16px;
|
|
--radius-sm: 12px;
|
|
--maxw: 940px;
|
|
--transition: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
|
|
/* Per-camp identity colors — constant across all stages so a player's
|
|
flavor travels with their avatar. */
|
|
--sparkle-color: #f368e0;
|
|
--sparkle-glow: rgba(243, 104, 224, 0.55);
|
|
--sparkle-tint: rgba(243, 104, 224, 0.14);
|
|
--shred-color: #ff2d2d;
|
|
--shred-glow: rgba(255, 45, 45, 0.6);
|
|
--shred-tint: rgba(255, 45, 45, 0.13);
|
|
}
|
|
|
|
/* ---------- Stage themes ---------- */
|
|
[data-stage="sparkle"] {
|
|
--bg: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #667eea 100%);
|
|
--bg-size: 400% 400%;
|
|
--surface: rgba(255, 255, 255, 0.95);
|
|
--surface-2: rgba(255, 255, 255, 0.55);
|
|
--surface-border: rgba(255, 255, 255, 0.85);
|
|
--card-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), 0 0 40px rgba(246, 79, 254, 0.5), inset 0 0 60px rgba(255, 255, 255, 0.5);
|
|
--accent1: #f093fb;
|
|
--accent2: #4facfe;
|
|
--accent-hot: #f5576c;
|
|
--text: #5a3d7a;
|
|
--text-strong: #764ba2;
|
|
--text-muted: #9b86b3;
|
|
--heading-font: 'Fredoka', 'Segoe UI', sans-serif;
|
|
--body-font: 'Fredoka', 'Segoe UI', Tahoma, sans-serif;
|
|
--heading-spacing: 0px;
|
|
--heading-transform: none;
|
|
--cell-bg: #ffffff;
|
|
--board-bg: linear-gradient(135deg, rgba(240, 147, 251, 0.2), rgba(79, 172, 254, 0.2));
|
|
--input-bg: rgba(255, 255, 255, 0.9);
|
|
--title-gradient: linear-gradient(45deg, #f093fb, #f5576c, #4facfe, #f093fb);
|
|
}
|
|
|
|
[data-stage="shred"] {
|
|
--bg: linear-gradient(135deg, #0a0a0c 0%, #1a0205 30%, #2b0508 50%, #160003 75%, #0a0a0c 100%);
|
|
--bg-size: 300% 300%;
|
|
--surface: rgba(18, 16, 18, 0.94);
|
|
--surface-2: rgba(255, 255, 255, 0.06);
|
|
--surface-border: rgba(255, 45, 45, 0.55);
|
|
--card-shadow: 0 20px 60px rgba(0, 0, 0, 0.75), 0 0 45px rgba(255, 26, 26, 0.45), inset 0 0 70px rgba(255, 26, 26, 0.06);
|
|
--accent1: #ff1a1a;
|
|
--accent2: #ff6a00;
|
|
--accent-hot: #ff2d2d;
|
|
--text: #ededed;
|
|
--text-strong: #ffffff;
|
|
--text-muted: #9a9aa0;
|
|
--heading-font: 'Metal Mania', 'Bebas Neue', 'Arial Black', Impact, sans-serif;
|
|
--body-font: 'Bebas Neue', 'Arial Narrow', 'Segoe UI', sans-serif;
|
|
--heading-spacing: 2px;
|
|
--heading-transform: uppercase;
|
|
--cell-bg: #161417;
|
|
--board-bg: linear-gradient(135deg, rgba(255, 26, 26, 0.12), rgba(0, 0, 0, 0.4));
|
|
--input-bg: rgba(0, 0, 0, 0.45);
|
|
--title-gradient: linear-gradient(45deg, #ff1a1a, #ff6a00, #ffd000, #ff1a1a);
|
|
}
|
|
|
|
/* Mixed: a neutral battleground where BOTH camps' colors pop. */
|
|
[data-stage="mixed"] {
|
|
--bg: linear-gradient(135deg, #1a0b2e 0%, #2b0a1e 30%, #1d1033 55%, #2a0815 78%, #140a24 100%);
|
|
--bg-size: 320% 320%;
|
|
--surface: rgba(24, 18, 34, 0.93);
|
|
--surface-2: rgba(255, 255, 255, 0.07);
|
|
--surface-border: rgba(243, 104, 224, 0.4);
|
|
--card-shadow: 0 20px 60px rgba(0, 0, 0, 0.7), 0 0 45px rgba(243, 104, 224, 0.3), 0 0 60px rgba(255, 45, 45, 0.18), inset 0 0 70px rgba(255, 255, 255, 0.04);
|
|
--accent1: #f368e0;
|
|
--accent2: #ff2d2d;
|
|
--accent-hot: #ff6a00;
|
|
--text: #f0e9f5;
|
|
--text-strong: #ffffff;
|
|
--text-muted: #b9a9c9;
|
|
--heading-font: 'Bebas Neue', 'Fredoka', sans-serif;
|
|
--body-font: 'Fredoka', 'Segoe UI', sans-serif;
|
|
--heading-spacing: 1px;
|
|
--heading-transform: uppercase;
|
|
--cell-bg: #1c1428;
|
|
--board-bg: linear-gradient(135deg, rgba(243, 104, 224, 0.14), rgba(255, 45, 45, 0.12));
|
|
--input-bg: rgba(0, 0, 0, 0.35);
|
|
--title-gradient: linear-gradient(45deg, #f368e0, #b06bff, #ff2d2d, #ff6a00, #f368e0);
|
|
}
|
|
|
|
/* ---------- Camp accents (per player) ---------- */
|
|
.camp-sparkle { --pc: var(--sparkle-color); --pg: var(--sparkle-glow); --pt: var(--sparkle-tint); }
|
|
.camp-shred { --pc: var(--shred-color); --pg: var(--shred-glow); --pt: var(--shred-tint); }
|
|
|
|
/* ---------- Base layout (full-screen, never scroll) ----------
|
|
The viewport is locked to the screen; the active card is scaled by JS
|
|
(--fit) to always fit. It's tic-tac-toe — everything stays in view. */
|
|
html { height: 100%; overflow: hidden; }
|
|
body {
|
|
font-family: var(--body-font);
|
|
background: var(--bg);
|
|
background-size: var(--bg-size);
|
|
animation: gradientShift 15s ease infinite;
|
|
height: 100dvh;
|
|
min-height: 100dvh;
|
|
display: grid;
|
|
place-items: center;
|
|
overflow: hidden;
|
|
position: relative;
|
|
padding: 8px;
|
|
color: var(--text);
|
|
transition: color 0.4s ease;
|
|
}
|
|
|
|
@keyframes gradientShift {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
|
|
.container {
|
|
margin: auto;
|
|
width: 100%;
|
|
max-width: var(--maxw);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* ---------- Animated backdrops ---------- */
|
|
.backdrop { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
|
|
|
|
.stars { position: absolute; inset: 0; opacity: 0; transition: opacity 0.6s ease; }
|
|
[data-stage="sparkle"] .stars { opacity: 1; }
|
|
[data-stage="mixed"] .stars { opacity: 0.5; }
|
|
|
|
.stars::before,
|
|
.stars::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 2px;
|
|
height: 2px;
|
|
background: white;
|
|
box-shadow:
|
|
100px 200px white, 300px 100px white, 500px 300px white,
|
|
700px 150px white, 200px 400px white, 600px 450px white,
|
|
150px 50px white, 450px 250px white, 350px 500px white,
|
|
800px 400px white, 50px 300px white, 550px 50px white,
|
|
900px 250px white, 1100px 400px white, 1300px 150px white,
|
|
250px 150px rgba(255,255,255,0.6), 650px 350px rgba(255,255,255,0.6),
|
|
400px 100px rgba(255,255,255,0.6), 100px 450px rgba(255,255,255,0.6),
|
|
1000px 500px rgba(255,255,255,0.6), 1200px 300px rgba(255,255,255,0.6);
|
|
border-radius: 50%;
|
|
animation: twinkle 3s infinite;
|
|
}
|
|
.stars::after { animation-delay: 1.5s; left: 40px; top: 30px; }
|
|
|
|
@keyframes twinkle {
|
|
0%, 100% { opacity: 0.3; transform: scale(1); }
|
|
50% { opacity: 1; transform: scale(1.3); }
|
|
}
|
|
|
|
.embers { position: absolute; inset: 0; opacity: 0; transition: opacity 0.6s ease; }
|
|
[data-stage="shred"] .embers { opacity: 1; }
|
|
[data-stage="mixed"] .embers { opacity: 0.7; }
|
|
|
|
.embers::before,
|
|
.embers::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 3px;
|
|
height: 3px;
|
|
border-radius: 50%;
|
|
background: #ff6a00;
|
|
box-shadow:
|
|
10vw 90vh 0 0 #ff3000, 25vw 95vh 0 1px #ff6a00, 40vw 88vh 0 0 #ff1a1a,
|
|
55vw 96vh 0 1px #ff8c00, 70vw 92vh 0 0 #ff3000, 85vw 97vh 0 1px #ff6a00,
|
|
18vw 93vh 0 0 #ffae00, 33vw 91vh 0 0 #ff1a1a, 48vw 98vh 0 1px #ff6a00,
|
|
63vw 94vh 0 0 #ff3000, 78vw 90vh 0 1px #ff8c00, 92vw 95vh 0 0 #ff1a1a,
|
|
5vw 97vh 0 0 #ffae00, 30vw 99vh 0 0 #ff3000, 60vw 89vh 0 1px #ff6a00;
|
|
animation: emberRise 6s linear infinite;
|
|
}
|
|
.embers::after { animation-delay: 3s; animation-duration: 8s; }
|
|
|
|
@keyframes emberRise {
|
|
0% { transform: translateY(0) scale(1); opacity: 0; }
|
|
10% { opacity: 1; }
|
|
90% { opacity: 0.8; }
|
|
100% { transform: translateY(-100vh) scale(0.3); opacity: 0; }
|
|
}
|
|
|
|
.lightning { position: absolute; inset: 0; background: #fff; opacity: 0; }
|
|
[data-stage="shred"] .lightning,
|
|
[data-stage="mixed"] .lightning { animation: lightningFlash 9s infinite; }
|
|
|
|
@keyframes lightningFlash {
|
|
0%, 92%, 100% { opacity: 0; }
|
|
93% { opacity: 0.16; }
|
|
94% { opacity: 0; }
|
|
95% { opacity: 0.26; }
|
|
96% { opacity: 0; }
|
|
}
|
|
|
|
/* ---------- Cards & screens ---------- */
|
|
/* Opacity-only fade: a transform on .screen would become the containing block
|
|
for the fixed-positioned card and break its viewport centering mid-animation. */
|
|
.screen { display: none; animation: fadeIn 0.4s ease; }
|
|
.screen.active { display: block; }
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--card-shadow);
|
|
backdrop-filter: blur(10px);
|
|
border: 3px solid var(--surface-border);
|
|
transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
|
/* Fixed + viewport-centered so a card taller than the screen still centers
|
|
(grid centering pins an over-tall item to the top). fitActiveScreen() in
|
|
script.js folds the fit scale into the transform:
|
|
translate(-50%, -50%) scale(fit)
|
|
so the whole card always fits with no scrolling, at any window size. */
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: min(var(--maxw), calc(100vw - 16px));
|
|
transform: translate(-50%, -50%);
|
|
transform-origin: center center;
|
|
will-change: transform;
|
|
z-index: 2;
|
|
}
|
|
|
|
.name-card { padding: 30px 30px 34px; }
|
|
.game-card { padding: 36px; }
|
|
/* No position override here — inherit .card's position:fixed so the card stays
|
|
viewport-centered. fixed is still a positioning context for .celebration. */
|
|
.winner-card { padding: 56px 40px; text-align: center; overflow: hidden; }
|
|
|
|
/* ---------- Headings ---------- */
|
|
.title, .game-title, .winner-title {
|
|
text-align: center;
|
|
font-family: var(--heading-font);
|
|
font-weight: 700;
|
|
letter-spacing: var(--heading-spacing);
|
|
text-transform: var(--heading-transform);
|
|
background: var(--title-gradient);
|
|
background-size: 200% 200%;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
animation: gradientText 3s ease infinite;
|
|
line-height: 1.05;
|
|
}
|
|
.title { font-size: 2.4em; margin-bottom: 14px; }
|
|
.game-title { font-size: 2.6em; margin-bottom: 22px; }
|
|
.winner-title { font-size: 3.2em; margin-bottom: 22px; }
|
|
|
|
@keyframes gradientText {
|
|
0%, 100% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
}
|
|
|
|
.sparkle-divider {
|
|
height: 3px;
|
|
background: linear-gradient(90deg, transparent, var(--accent1), var(--accent2), var(--accent1), transparent);
|
|
margin: 14px 0 20px;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
.sparkle-divider::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.9), transparent);
|
|
animation: sparkleMove 2s linear infinite;
|
|
}
|
|
@keyframes sparkleMove {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(200%); }
|
|
}
|
|
|
|
/* ---------- Stage selector ---------- */
|
|
.stage-selector { margin-bottom: 16px; }
|
|
.stage-label, .avatar-label, .setup-hint {
|
|
display: block;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-weight: 700;
|
|
letter-spacing: var(--heading-spacing);
|
|
}
|
|
.stage-label { margin-bottom: 10px; font-size: 0.95em; }
|
|
.setup-hint { margin-bottom: 18px; font-size: 0.92em; font-weight: 500; }
|
|
.setup-hint strong { color: var(--text-strong); }
|
|
|
|
.stage-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
.stage-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 14px 8px;
|
|
border-radius: var(--radius-md);
|
|
border: 3px solid transparent;
|
|
background: var(--surface-2);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
transition: transform var(--transition), border-color 0.25s, box-shadow 0.25s, background 0.25s;
|
|
font-family: var(--body-font);
|
|
}
|
|
.stage-emoji { font-size: 1.8em; line-height: 1; }
|
|
.stage-name { font-size: 1.15em; font-weight: 700; color: var(--text-strong); letter-spacing: var(--heading-spacing); }
|
|
.stage-desc { font-size: 0.78em; color: var(--text-muted); text-align: center; }
|
|
.stage-option:hover { transform: translateY(-3px); border-color: var(--accent2); }
|
|
.stage-option[aria-checked="true"] {
|
|
border-color: var(--accent1);
|
|
background: linear-gradient(135deg, color-mix(in srgb, var(--accent1) 24%, transparent), color-mix(in srgb, var(--accent2) 18%, transparent));
|
|
box-shadow: 0 0 22px color-mix(in srgb, var(--accent1) 45%, transparent);
|
|
}
|
|
|
|
/* ---------- Player setup ---------- */
|
|
.players-setup { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
|
|
|
.name-input-group { margin-bottom: 16px; }
|
|
.name-input-group label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
font-size: 1.15em;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
letter-spacing: var(--heading-spacing);
|
|
}
|
|
.player-icon { font-size: 1.4em; animation: bounce 1.4s infinite; }
|
|
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
|
|
|
|
.name-input-group input {
|
|
width: 100%;
|
|
padding: 13px 16px;
|
|
font-size: 1.05em;
|
|
font-family: var(--body-font);
|
|
border: 3px solid var(--accent1);
|
|
border-radius: var(--radius-sm);
|
|
outline: none;
|
|
transition: all 0.3s ease;
|
|
background: var(--input-bg);
|
|
color: var(--text);
|
|
}
|
|
.name-input-group input::placeholder { color: var(--text-muted); }
|
|
.name-input-group input:focus {
|
|
border-color: var(--accent2);
|
|
box-shadow: 0 0 18px color-mix(in srgb, var(--accent2) 45%, transparent);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.avatar-label { margin-bottom: 8px; font-size: 0.95em; }
|
|
.avatar-options {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 8px;
|
|
}
|
|
.avatar-option {
|
|
aspect-ratio: 1;
|
|
font-size: 1.55em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 2px solid var(--surface-border);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: transform var(--transition), border-color 0.25s, box-shadow 0.25s, background 0.25s;
|
|
background: var(--cell-bg);
|
|
color: var(--text);
|
|
position: relative;
|
|
}
|
|
/* Camp tint so the two "teams" of avatars read at a glance */
|
|
.avatar-option.camp-sparkle { background: linear-gradient(135deg, var(--sparkle-tint), var(--cell-bg)); }
|
|
.avatar-option.camp-shred { background: linear-gradient(135deg, var(--shred-tint), var(--cell-bg)); }
|
|
.avatar-option:hover {
|
|
transform: scale(1.15);
|
|
border-color: var(--pc, var(--accent2));
|
|
box-shadow: 0 0 14px var(--pg, var(--sparkle-glow));
|
|
}
|
|
.avatar-option.selected {
|
|
border-color: var(--pc);
|
|
box-shadow: 0 0 20px var(--pg);
|
|
transform: scale(1.1);
|
|
}
|
|
.avatar-option.selected::after {
|
|
content: '✓';
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -6px;
|
|
width: 18px;
|
|
height: 18px;
|
|
font-size: 11px;
|
|
line-height: 18px;
|
|
text-align: center;
|
|
border-radius: 50%;
|
|
background: var(--pc);
|
|
color: #fff;
|
|
font-weight: 700;
|
|
}
|
|
.avatar-option.selected:hover { transform: scale(1.18); }
|
|
|
|
/* ---------- Buttons ---------- */
|
|
.btn {
|
|
font-family: var(--heading-font);
|
|
font-weight: 700;
|
|
letter-spacing: var(--heading-spacing);
|
|
border: none;
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
transition: transform var(--transition), box-shadow 0.25s, background 0.25s, color 0.25s;
|
|
}
|
|
.btn-primary {
|
|
width: 100%;
|
|
padding: 15px;
|
|
font-size: 1.35em;
|
|
background: linear-gradient(135deg, var(--accent1), var(--accent-hot));
|
|
color: #fff;
|
|
box-shadow: 0 10px 30px color-mix(in srgb, var(--accent1) 40%, transparent);
|
|
margin-top: 22px;
|
|
text-transform: uppercase;
|
|
}
|
|
.btn-primary:hover { transform: translateY(-4px) scale(1.03); box-shadow: 0 16px 40px color-mix(in srgb, var(--accent1) 55%, transparent); }
|
|
.btn-primary:active { transform: translateY(-1px) scale(1.01); }
|
|
|
|
.btn-ghost {
|
|
flex: 1;
|
|
padding: 13px;
|
|
font-size: 1.05em;
|
|
border: 3px solid var(--accent1);
|
|
background: var(--surface-2);
|
|
color: var(--text-strong);
|
|
}
|
|
.btn-ghost:hover {
|
|
background: linear-gradient(135deg, var(--accent1), var(--accent-hot));
|
|
color: #fff;
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 10px 20px color-mix(in srgb, var(--accent1) 40%, transparent);
|
|
}
|
|
.btn-tiny {
|
|
padding: 8px 12px;
|
|
font-size: 0.85em;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
border: 2px solid var(--surface-border);
|
|
margin-top: 4px;
|
|
}
|
|
.btn-tiny:hover { color: var(--text-strong); border-color: var(--accent1); }
|
|
|
|
.icon-btn {
|
|
position: fixed;
|
|
top: 16px;
|
|
right: 16px;
|
|
z-index: 50;
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--surface-border);
|
|
background: var(--surface);
|
|
cursor: pointer;
|
|
font-size: 1.3em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
|
|
transition: transform var(--transition), border-color 0.25s;
|
|
}
|
|
.icon-btn:hover { transform: scale(1.1); border-color: var(--accent1); }
|
|
.icon-btn .icon-off { display: none; }
|
|
.icon-btn[aria-pressed="true"] .icon-on { display: none; }
|
|
.icon-btn[aria-pressed="true"] .icon-off { display: inline; }
|
|
|
|
/* ---------- Game layout ---------- */
|
|
.game-layout { display: flex; gap: 28px; align-items: flex-start; }
|
|
.game-area { flex: 1; min-width: 0; }
|
|
|
|
.player-info {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
margin-bottom: 18px;
|
|
gap: 16px;
|
|
}
|
|
.player-display {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 14px 18px;
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-2);
|
|
border: 3px solid transparent;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.player-display.active {
|
|
border-color: var(--pc, var(--accent1));
|
|
box-shadow: 0 0 26px var(--pg, var(--sparkle-glow));
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); } }
|
|
|
|
.player-glow {
|
|
position: absolute;
|
|
top: -50%; left: -50%;
|
|
width: 200%; height: 200%;
|
|
background: radial-gradient(circle, var(--pg, var(--sparkle-glow)), transparent 70%);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
.player-display.active .player-glow { opacity: 1; animation: rotate 4s linear infinite; }
|
|
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
|
.player-symbol { font-size: 2em; margin-bottom: 4px; position: relative; z-index: 1; filter: drop-shadow(0 0 6px var(--pc, transparent)); }
|
|
.player-name {
|
|
font-size: 1.05em; font-weight: 700; color: var(--text-strong);
|
|
position: relative; z-index: 1; letter-spacing: var(--heading-spacing);
|
|
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.vs {
|
|
font-size: 1.4em; font-weight: 700; color: var(--text-strong);
|
|
font-family: var(--heading-font); letter-spacing: var(--heading-spacing);
|
|
}
|
|
|
|
/* ---------- Status ---------- */
|
|
.game-status {
|
|
text-align: center;
|
|
font-size: 1.4em;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
min-height: 38px;
|
|
margin: 8px 0 16px;
|
|
letter-spacing: var(--heading-spacing);
|
|
}
|
|
.game-status.draw { color: var(--accent-hot); animation: pulse 0.8s ease 3; }
|
|
|
|
/* ---------- Board ---------- */
|
|
.game-board {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 14px;
|
|
padding: 18px;
|
|
background: var(--board-bg);
|
|
border-radius: var(--radius-md);
|
|
aspect-ratio: 1;
|
|
max-width: 460px;
|
|
margin: 0 auto;
|
|
}
|
|
.cell {
|
|
aspect-ratio: 1;
|
|
background: var(--cell-bg);
|
|
border: 4px solid color-mix(in srgb, var(--accent1) 60%, transparent);
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: clamp(2rem, 9vw, 3.4rem);
|
|
cursor: pointer;
|
|
transition: transform var(--transition), border-color 0.25s, box-shadow 0.25s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
font-family: var(--body-font);
|
|
color: var(--text);
|
|
padding: 0;
|
|
}
|
|
.cell::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(45deg, transparent, color-mix(in srgb, var(--accent1) 30%, transparent), transparent);
|
|
transform: translateX(-100%);
|
|
transition: transform 0.6s ease;
|
|
}
|
|
.cell:hover:not(.filled)::before { transform: translateX(100%); }
|
|
.cell:hover:not(.filled) {
|
|
transform: scale(1.08);
|
|
border-color: var(--accent2);
|
|
box-shadow: 0 0 18px color-mix(in srgb, var(--accent2) 45%, transparent);
|
|
}
|
|
.cell:focus-visible { outline: 3px solid var(--accent2); outline-offset: 3px; }
|
|
.cell.filled { cursor: not-allowed; animation: cellAppear 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
/* Filled cell takes the placing player's camp color */
|
|
.cell.filled.camp-sparkle { color: var(--sparkle-color); border-color: var(--sparkle-color); box-shadow: 0 0 16px var(--sparkle-glow); }
|
|
.cell.filled.camp-shred { color: var(--shred-color); border-color: var(--shred-color); box-shadow: 0 0 16px var(--shred-glow); }
|
|
|
|
@keyframes cellAppear {
|
|
from { transform: scale(0) rotate(180deg); opacity: 0; }
|
|
to { transform: scale(1) rotate(0deg); opacity: 1; }
|
|
}
|
|
|
|
.cell.winning {
|
|
animation: winningCell 0.6s ease infinite;
|
|
color: #fff;
|
|
border-color: #fff;
|
|
}
|
|
.cell.winning.camp-sparkle { background: linear-gradient(135deg, var(--sparkle-color), #4facfe); }
|
|
.cell.winning.camp-shred { background: linear-gradient(135deg, var(--shred-color), #ff6a00); }
|
|
@keyframes winningCell { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.12); } }
|
|
|
|
/* ---------- Buttons row ---------- */
|
|
.button-group { display: flex; gap: 14px; margin-top: 20px; }
|
|
|
|
/* ---------- Scoreboard ---------- */
|
|
.score-sidebar {
|
|
width: 210px;
|
|
flex-shrink: 0;
|
|
background: var(--board-bg);
|
|
border-radius: var(--radius-md);
|
|
padding: 22px 18px;
|
|
border: 2px solid var(--surface-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.score-title {
|
|
text-align: center;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
font-family: var(--heading-font);
|
|
letter-spacing: var(--heading-spacing);
|
|
text-transform: var(--heading-transform);
|
|
}
|
|
.score-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 14px;
|
|
background: var(--surface-2);
|
|
border-radius: var(--radius-sm);
|
|
border: 2px solid transparent;
|
|
}
|
|
.score-item.camp-sparkle { border-color: color-mix(in srgb, var(--sparkle-color) 40%, transparent); }
|
|
.score-item.camp-shred { border-color: color-mix(in srgb, var(--shred-color) 40%, transparent); }
|
|
.score-avatar { font-size: 2.6em; animation: bounce 2.4s infinite; filter: drop-shadow(0 0 6px var(--pc, transparent)); }
|
|
.score-name {
|
|
font-size: 0.95em; font-weight: 700; color: var(--text-strong);
|
|
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
letter-spacing: var(--heading-spacing);
|
|
}
|
|
.score-value {
|
|
font-size: 2.2em;
|
|
font-weight: 700;
|
|
color: var(--pc, var(--accent-hot));
|
|
padding: 6px 18px;
|
|
background: var(--cell-bg);
|
|
border-radius: var(--radius-sm);
|
|
border: 3px solid var(--pc, var(--accent1));
|
|
min-width: 64px;
|
|
text-align: center;
|
|
font-family: var(--heading-font);
|
|
}
|
|
.score-divider { font-size: 2.2em; text-align: center; animation: rotate 4s linear infinite; }
|
|
.score-value.score-update { animation: scorePop 0.5s ease; }
|
|
@keyframes scorePop {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(1.5); box-shadow: 0 0 30px currentColor; }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
/* ---------- Winner screen ---------- */
|
|
.winner-title { animation: gradientText 3s ease infinite, popIn 0.6s ease; }
|
|
@keyframes popIn { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
|
|
|
.trophy { font-size: 7em; margin: 10px 0; }
|
|
/* Winner card carries the winner's camp; trophy reacts accordingly */
|
|
.winner-card.camp-sparkle .trophy { animation: trophyBounce 1s ease-in-out infinite; }
|
|
.winner-card.camp-shred .trophy { animation: headbang 0.5s ease-in-out infinite; filter: drop-shadow(0 0 16px var(--shred-color)); }
|
|
.winner-card .trophy { animation: trophyBounce 1s ease-in-out infinite; }
|
|
@keyframes trophyBounce {
|
|
0%, 100% { transform: translateY(0) rotate(-5deg); }
|
|
50% { transform: translateY(-18px) rotate(5deg); }
|
|
}
|
|
@keyframes headbang {
|
|
0%, 100% { transform: translateY(0) rotate(-8deg) scale(1); }
|
|
50% { transform: translateY(8px) rotate(8deg) scale(1.08); }
|
|
}
|
|
|
|
.winner-name {
|
|
font-size: 2.4em;
|
|
font-weight: 700;
|
|
color: var(--text-strong);
|
|
margin: 18px 0 6px;
|
|
font-family: var(--heading-font);
|
|
letter-spacing: var(--heading-spacing);
|
|
text-transform: var(--heading-transform);
|
|
text-shadow: 0 0 20px var(--pg, var(--sparkle-glow));
|
|
}
|
|
.winner-sub { font-size: 1.1em; color: var(--text-muted); margin-bottom: 26px; }
|
|
.winner-buttons { display: flex; flex-direction: column; gap: 12px; max-width: 320px; margin: 0 auto; }
|
|
.winner-buttons .btn-primary { margin-top: 0; }
|
|
.winner-buttons .btn-ghost { flex: none; }
|
|
|
|
.celebration { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
|
|
.confetti-piece {
|
|
position: absolute;
|
|
top: -10%;
|
|
font-size: 1.8em;
|
|
animation: confettiFall linear infinite;
|
|
}
|
|
@keyframes confettiFall {
|
|
0% { transform: translateY(0) rotate(0deg); opacity: 1; }
|
|
100% { transform: translateY(120vh) rotate(720deg); opacity: 0.3; }
|
|
}
|
|
|
|
/* ---------- Particle burst (move feedback) ---------- */
|
|
.particle {
|
|
position: fixed;
|
|
pointer-events: none;
|
|
font-size: 1.5em;
|
|
z-index: 9999;
|
|
animation: particleBurst 0.8s ease-out forwards;
|
|
}
|
|
@keyframes particleBurst {
|
|
0% { transform: translate(0, 0) scale(1) rotate(0deg); opacity: 1; }
|
|
100% { transform: translate(var(--tx), var(--ty)) scale(0) rotate(360deg); opacity: 0; }
|
|
}
|
|
|
|
/* ---------- Screen shake (shred move) ---------- */
|
|
.shake { animation: shake 0.25s ease; }
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-4px); }
|
|
75% { transform: translateX(4px); }
|
|
}
|
|
|
|
/* ---------- Responsive ---------- */
|
|
@media (max-width: 780px) {
|
|
.game-layout { flex-direction: column; }
|
|
.score-sidebar {
|
|
width: 100%;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
padding: 16px;
|
|
}
|
|
.score-title { width: 100%; }
|
|
.score-item { flex: 1; min-width: 120px; }
|
|
.score-divider { display: flex; align-items: center; }
|
|
#resetScoresBtn { width: 100%; order: 5; }
|
|
}
|
|
|
|
@media (max-width: 620px) {
|
|
.players-setup { grid-template-columns: 1fr; gap: 18px; }
|
|
.name-card, .game-card { padding: 24px 18px; }
|
|
.winner-card { padding: 40px 22px; }
|
|
.title { font-size: 2em; }
|
|
.game-title { font-size: 2.1em; }
|
|
.winner-title { font-size: 2.4em; }
|
|
.trophy { font-size: 5.5em; }
|
|
.button-group { flex-direction: column; }
|
|
.player-info { gap: 10px; }
|
|
.vs { display: none; }
|
|
.avatar-option { font-size: 1.4em; }
|
|
.stage-desc { display: none; }
|
|
}
|
|
|
|
@media (max-width: 380px) {
|
|
.stage-options { gap: 8px; }
|
|
.stage-option { padding: 10px 4px; }
|
|
}
|
|
|
|
/* ---------- Reduced motion ---------- */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|