tictactoe/style.css
Parley Hatch 450d46c55d Actually fix no-scroll: fixed-centered card scaled to fit + cache busting
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>
2026-05-31 19:00:32 -06:00

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;
}
}