Files
super-auto-pets-board-game/web/src/styles.css
T

1616 lines
28 KiB
CSS

/* ============================================================
Super Auto Pets: The Board Game — cozy tabletop theme.
Deep felt table, cream cards with cocoa borders, chunky type.
============================================================ */
:root {
--felt-900: #12351f;
--felt-800: #1a4a2b;
--felt-700: #226038;
--wood: #5b3a1e;
--wood-light: #7a5230;
--cream: #fdf3dc;
--cream-dark: #f3e3bd;
--cocoa: #4a2c14;
--ink: #33230f;
--coral: #ff8a3d;
--coral-dark: #e06a1b;
--gold: #ffcf5c;
--red: #d94a38;
--teal: #2f9c8a;
--font-display: 'Lilita One', system-ui, sans-serif;
--font-body: 'Nunito', system-ui, sans-serif;
--card-radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#root {
min-height: 100vh;
}
body {
font-family: var(--font-body);
color: var(--cream);
background-color: var(--felt-900);
background-image:
radial-gradient(ellipse at 50% -20%, rgba(255, 255, 255, 0.09), transparent 60%),
radial-gradient(ellipse at 50% 120%, rgba(0, 0, 0, 0.45), transparent 60%),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"),
linear-gradient(160deg, var(--felt-800), var(--felt-900) 70%);
}
h1,
h2,
h3 {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.02em;
}
.muted {
color: rgba(253, 243, 220, 0.55);
}
.centered {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
text-align: center;
}
.hint {
font-size: 0.9rem;
color: rgba(253, 243, 220, 0.65);
text-align: center;
}
.warn-text {
color: var(--gold);
font-weight: 700;
}
/* ---------- buttons ---------- */
.btn {
font-family: var(--font-display);
font-size: 1rem;
letter-spacing: 0.03em;
color: var(--cream);
background: var(--wood);
border: 3px solid rgba(0, 0, 0, 0.25);
border-radius: 12px;
padding: 10px 18px;
cursor: pointer;
box-shadow: 0 4px 0 rgba(0, 0, 0, 0.35);
transition: transform 80ms ease, box-shadow 80ms ease, filter 120ms ease;
}
.btn:hover:not(:disabled) {
filter: brightness(1.1);
}
.btn:active:not(:disabled) {
transform: translateY(3px);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(180deg, var(--coral), var(--coral-dark));
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.btn-secondary {
background: linear-gradient(180deg, var(--teal), #227465);
}
.btn-ghost {
background: transparent;
border-color: rgba(253, 243, 220, 0.3);
box-shadow: none;
}
.btn-big {
font-size: 1.25rem;
padding: 14px 28px;
}
.btn-sm {
font-size: 0.8rem;
padding: 4px 10px;
border-width: 2px;
}
/* ---------- home ---------- */
.home {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 24px;
}
.home-pets {
font-size: 2.6rem;
letter-spacing: 0.3em;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.home-title {
font-size: clamp(2.2rem, 6vw, 3.6rem);
text-align: center;
color: var(--gold);
text-shadow: 0 4px 0 rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
line-height: 1.05;
}
.home-subtitle {
font-size: 0.42em;
color: var(--cream);
letter-spacing: 0.25em;
text-transform: uppercase;
}
.home-card {
background: rgba(0, 0, 0, 0.25);
border: 2px solid rgba(253, 243, 220, 0.15);
border-radius: 20px;
padding: 28px;
display: flex;
flex-direction: column;
gap: 16px;
width: min(380px, 92vw);
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
font-weight: 700;
font-size: 0.9rem;
}
.field input,
.code-input {
font-family: var(--font-body);
font-size: 1.1rem;
font-weight: 700;
color: var(--ink);
background: var(--cream);
border: 3px solid var(--cocoa);
border-radius: 10px;
padding: 10px 12px;
outline: none;
width: 100%;
}
.code-input {
font-family: var(--font-display);
letter-spacing: 0.35em;
text-transform: uppercase;
text-align: center;
}
.home-divider {
text-align: center;
font-size: 0.85rem;
color: rgba(253, 243, 220, 0.5);
display: flex;
align-items: center;
gap: 10px;
}
.home-divider::before,
.home-divider::after {
content: '';
flex: 1;
height: 1px;
background: rgba(253, 243, 220, 0.2);
}
.home-join {
display: flex;
gap: 10px;
}
.home-error {
color: #ffb3a7;
font-weight: 700;
text-align: center;
}
/* ---------- topbar ---------- */
.table {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.topbar {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
background: linear-gradient(180deg, var(--wood-light), var(--wood));
border-bottom: 4px solid rgba(0, 0, 0, 0.35);
flex-wrap: wrap;
}
.topbar-brand {
font-family: var(--font-display);
font-size: 1.2rem;
color: var(--gold);
}
.topbar-round {
font-size: 0.95rem;
}
.topbar-players {
display: flex;
gap: 14px;
flex: 1;
flex-wrap: wrap;
}
.topbar-player {
display: flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.2);
border-radius: 999px;
padding: 4px 12px;
font-size: 0.9rem;
}
.topbar-player.is-you {
outline: 2px solid var(--gold);
}
.topbar-name {
font-weight: 900;
}
.chip {
font-size: 0.85rem;
}
.conn-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.conn-dot.on {
background: #6fe08b;
}
.conn-dot.off {
background: var(--red);
}
.topbar-code {
font-family: var(--font-display);
letter-spacing: 0.25em;
background: rgba(0, 0, 0, 0.25);
border: 2px dashed rgba(253, 243, 220, 0.4);
border-radius: 8px;
padding: 4px 10px;
}
.table-body {
flex: 1;
display: flex;
align-items: flex-start;
min-height: 0;
}
.table-main {
flex: 1;
padding: 20px 16px 40px;
max-width: 1100px;
width: 100%;
margin: 0 auto;
}
/* ---------- event log ---------- */
.event-log {
width: 300px;
flex-shrink: 0;
align-self: stretch;
position: sticky;
top: 0;
max-height: calc(100vh - 58px);
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.28);
border-left: 3px solid rgba(0, 0, 0, 0.35);
}
.event-log.is-collapsed {
width: 42px;
}
.event-log-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 8px 10px;
font-family: var(--font-display);
font-size: 0.9rem;
color: var(--gold);
background: rgba(0, 0, 0, 0.25);
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
white-space: nowrap;
}
.event-log.is-collapsed .event-log-head {
writing-mode: vertical-rl;
justify-content: flex-start;
gap: 10px;
padding: 10px 8px;
height: 100%;
}
.event-log.is-collapsed .event-log-head button {
writing-mode: horizontal-tb;
}
.event-log-body {
flex: 1;
overflow-y: auto;
padding: 8px 10px 12px;
display: flex;
flex-direction: column;
gap: 5px;
font-size: 0.82rem;
line-height: 1.25;
}
.log-empty {
padding: 8px 2px;
font-style: italic;
}
.log-line {
display: flex;
gap: 6px;
align-items: baseline;
padding: 3px 6px;
border-radius: 6px;
border-left: 3px solid transparent;
background: rgba(255, 255, 255, 0.03);
}
.log-icon {
flex-shrink: 0;
}
.log-text {
min-width: 0;
word-break: break-word;
}
.log-line.log-you {
border-left-color: var(--gold);
}
.log-line.log-opp {
border-left-color: var(--red);
}
.log-line.log-system {
background: none;
color: var(--cream-dark);
justify-content: center;
font-family: var(--font-display);
font-size: 0.8rem;
opacity: 0.85;
margin-top: 4px;
}
.log-line.log-divider {
border-top: 1px dashed rgba(253, 243, 220, 0.25);
padding-top: 8px;
color: var(--gold);
}
.log-line.log-current {
background: rgba(255, 207, 92, 0.18);
outline: 1px solid rgba(255, 207, 92, 0.4);
}
@media (max-width: 860px) {
.table-body {
flex-direction: column;
}
.event-log {
width: 100%;
position: static;
max-height: 220px;
border-left: none;
border-top: 3px solid rgba(0, 0, 0, 0.35);
}
.event-log.is-collapsed {
width: 100%;
}
.event-log.is-collapsed .event-log-head {
writing-mode: horizontal-tb;
height: auto;
}
}
/* ---------- lobby ---------- */
.lobby {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
text-align: center;
}
.lobby-bounce {
font-size: 3rem;
animation: float 2s ease-in-out infinite;
}
.lobby-code {
font-family: var(--font-display);
font-size: 3rem;
letter-spacing: 0.35em;
color: var(--gold);
background: rgba(0, 0, 0, 0.25);
border: 3px dashed rgba(255, 207, 92, 0.5);
border-radius: 16px;
padding: 12px 28px 12px 40px;
}
/* ---------- cards ---------- */
.card {
position: relative;
width: 108px;
height: 148px;
background: linear-gradient(180deg, var(--cream), var(--cream-dark));
border: 3px solid var(--cocoa);
border-radius: var(--card-radius);
color: var(--ink);
display: flex;
flex-direction: column;
align-items: center;
padding: 6px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.35);
flex-shrink: 0;
transition: transform 120ms ease, box-shadow 120ms ease;
user-select: none;
}
.card-lg {
width: 128px;
height: 176px;
}
.card-sm {
width: 84px;
height: 116px;
}
.card.is-clickable {
cursor: pointer;
}
.card.is-clickable:hover {
transform: translateY(-6px) rotate(-1deg);
box-shadow: 0 10px 16px rgba(0, 0, 0, 0.4);
}
.card.is-selected {
outline: 4px solid var(--gold);
transform: translateY(-6px);
}
.card.is-disabled {
filter: saturate(0.6) brightness(0.85);
}
.card.is-dead {
filter: grayscale(1);
}
.card-top {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.72rem;
}
.card-tier {
font-weight: 900;
color: rgba(51, 35, 15, 0.55);
}
.card-suit {
font-size: 0.95rem;
}
.card-art {
font-size: 1.8rem;
line-height: 1;
flex: 0 0 auto;
filter: drop-shadow(0 3px 2px rgba(0, 0, 0, 0.25));
}
.card-lg .card-art {
font-size: 2.5rem;
}
.card-sm .card-art {
font-size: 1.8rem;
}
.card-name {
font-family: var(--font-display);
font-size: 0.8rem;
line-height: 1.1;
margin-top: 1px;
flex: 0 0 auto;
}
.card-effect {
font-size: 0.53rem;
line-height: 1.15;
font-weight: 700;
color: rgba(51, 35, 15, 0.75);
text-align: center;
padding: 1px 2px 0;
/* Grow into the space between the name and the power badge and center the
text there, so long effects use the whole middle band instead of being
clipped just under the name. */
flex: 1 1 auto;
display: flex;
/* Center short text in the band, but fall back to top-aligned when the text
is taller than the band, so the leading words are never the ones clipped. */
align-items: safe center;
justify-content: center;
overflow: hidden;
}
.card-lg .card-effect {
font-size: 0.62rem;
}
.card-sm .card-effect {
display: none;
}
.card-bottom {
display: flex;
gap: 6px;
align-items: center;
margin-top: auto;
}
.suit-dot {
display: inline-block;
width: 13px;
height: 13px;
border-radius: 50%;
border: 2px solid rgba(0, 0, 0, 0.35);
vertical-align: middle;
}
.suit-red {
background: #e2483d;
}
.suit-blue {
background: #3d7de2;
}
.suit-yellow {
background: #f4c430;
}
.card-power {
font-family: var(--font-display);
background: radial-gradient(circle at 35% 30%, #ffb347, var(--coral-dark));
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
border: 2px solid rgba(0, 0, 0, 0.25);
border-radius: 50%;
width: 30px;
height: 30px;
display: grid;
place-items: center;
font-size: 0.95rem;
}
.card-power.is-buffed {
background: radial-gradient(circle at 35% 30%, #7be495, #1f9e55);
}
.card-damage {
font-family: var(--font-display);
color: #fff;
background: var(--red);
border: 2px solid rgba(0, 0, 0, 0.25);
border-radius: 8px;
padding: 1px 6px;
font-size: 0.8rem;
}
.card-food {
background: linear-gradient(180deg, #ffe9e0, #ffd4c2);
}
.card-food .card-effect {
color: #7a2e1e;
}
.card-check {
position: absolute;
top: -10px;
right: -10px;
background: var(--gold);
color: var(--ink);
font-weight: 900;
border: 2px solid var(--cocoa);
border-radius: 50%;
width: 26px;
height: 26px;
display: grid;
place-items: center;
}
.card-slot-empty {
width: 128px;
height: 176px;
border: 3px dashed rgba(253, 243, 220, 0.25);
border-radius: var(--card-radius);
flex-shrink: 0;
}
/* ---------- shop ---------- */
.shop {
display: flex;
flex-direction: column;
gap: 22px;
}
.shop-status {
text-align: center;
font-size: 1.15rem;
font-weight: 900;
min-height: 1.6em;
}
.status-hot {
color: var(--gold);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.65;
}
}
.section-label {
font-family: var(--font-display);
font-size: 1rem;
margin-bottom: 10px;
color: rgba(253, 243, 220, 0.9);
}
.shop-row-wrap,
.deck-wrap {
background: rgba(0, 0, 0, 0.18);
border: 2px solid rgba(253, 243, 220, 0.1);
border-radius: 18px;
padding: 14px 16px;
}
.shop-row,
.deck-row {
display: flex;
gap: 14px;
flex-wrap: wrap;
justify-content: center;
}
.deck-empty {
text-align: center;
padding: 20px 0;
}
.actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
/* ---------- modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: grid;
place-items: center;
z-index: 50;
}
.modal {
background: linear-gradient(180deg, var(--felt-700), var(--felt-800));
border: 3px solid rgba(253, 243, 220, 0.25);
border-radius: 20px;
padding: 24px;
text-align: center;
display: flex;
flex-direction: column;
gap: 18px;
max-width: 92vw;
}
.modal-cards {
display: flex;
gap: 18px;
justify-content: center;
}
/* ---------- arrange ---------- */
.arrange {
display: flex;
flex-direction: column;
gap: 18px;
}
.arrange-row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
background: rgba(0, 0, 0, 0.18);
border: 2px solid rgba(253, 243, 220, 0.1);
border-radius: 18px;
padding: 18px 16px;
min-height: 220px;
}
.arrange-marker {
font-family: var(--font-display);
color: var(--gold);
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.9rem;
opacity: 0.8;
}
.arrange-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: grab;
}
.arrange-card:active {
cursor: grabbing;
}
.arrange-arrows {
display: flex;
gap: 4px;
}
/* ---------- battle ---------- */
.battle {
display: flex;
flex-direction: column;
gap: 16px;
}
.battle-header {
display: flex;
justify-content: center;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.battle-controls {
display: flex;
align-items: center;
gap: 6px;
}
.battle-step {
font-family: var(--font-display);
font-size: 0.85rem;
color: rgba(253, 243, 220, 0.7);
min-width: 3.5em;
text-align: center;
}
.battle-names {
display: flex;
justify-content: center;
gap: 18px;
font-weight: 900;
}
.battle-vs {
font-family: var(--font-display);
color: var(--gold);
}
.battlefield {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background:
radial-gradient(ellipse at center, rgba(255, 207, 92, 0.07), transparent 65%),
rgba(0, 0, 0, 0.2);
border: 2px solid rgba(253, 243, 220, 0.1);
border-radius: 18px;
padding: 26px 12px;
min-height: 240px;
overflow-x: auto;
}
.battle-center {
position: relative;
font-size: 1.6rem;
flex-shrink: 0;
display: grid;
place-items: center;
min-width: 2rem;
}
.battle-center-bolt {
opacity: 0.45;
}
.battle-side {
display: flex;
gap: 12px;
align-items: center;
flex: 1;
min-width: 0;
justify-content: center;
}
/* --- deck stacks --- */
.stackpile {
position: relative;
flex-shrink: 0;
}
.card-back {
width: 84px;
height: 116px;
border-radius: var(--card-radius);
border: 3px solid var(--cocoa);
background:
repeating-linear-gradient(
45deg,
#b3552b 0 10px,
#a34a22 10px 20px
);
box-shadow:
2px 2px 0 rgba(0, 0, 0, 0.25),
4px 4px 0 rgba(0, 0, 0, 0.15);
display: grid;
place-items: center;
}
.card-back-count {
font-family: var(--font-display);
font-size: 1.3rem;
color: var(--cream);
background: rgba(0, 0, 0, 0.35);
border-radius: 50%;
width: 38px;
height: 38px;
display: grid;
place-items: center;
}
.stack-empty {
width: 84px;
height: 116px;
}
@keyframes summon-pop {
0% {
opacity: 0;
transform: translateY(18px) scale(0.6);
}
30% {
opacity: 1;
transform: translateY(-14px) scale(1.05);
}
80% {
opacity: 1;
transform: translateY(-10px) scale(1);
}
100% {
opacity: 0;
transform: translateY(0) scale(0.7);
}
}
.summon-pop {
position: absolute;
top: -30px;
left: 50%;
margin-left: -42px;
z-index: 6;
animation: summon-pop 1000ms ease forwards;
pointer-events: none;
}
/* Spawn arcs out from the pet (inner edge) onto the deck. The pet sits to the
right of the deck on the left side and to the left on the right side, so
each side pulls its spawn in from the opposite direction. */
@keyframes summon-from-right {
0% {
opacity: 0;
transform: translate(120px, 8px) scale(0.5) rotate(10deg);
}
25% {
opacity: 1;
transform: translate(64px, -22px) scale(1.05) rotate(5deg);
}
70% {
opacity: 1;
transform: translate(0, -12px) scale(1) rotate(0);
}
100% {
opacity: 0;
transform: translate(0, 2px) scale(0.72);
}
}
@keyframes summon-from-left {
0% {
opacity: 0;
transform: translate(-120px, 8px) scale(0.5) rotate(-10deg);
}
25% {
opacity: 1;
transform: translate(-64px, -22px) scale(1.05) rotate(-5deg);
}
70% {
opacity: 1;
transform: translate(0, -12px) scale(1) rotate(0);
}
100% {
opacity: 0;
transform: translate(0, 2px) scale(0.72);
}
}
.summon-pop.summon-from-right {
animation-name: summon-from-right;
}
.summon-pop.summon-from-left {
animation-name: summon-from-left;
}
/* --- foods waiting for a pet --- */
.pending-foods {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 1.3rem;
min-width: 1.5rem;
}
.unit-foods {
display: flex;
justify-content: center;
gap: 2px;
font-size: 1rem;
margin-top: 4px;
}
/* --- the pet in play --- */
.battle-unit-zone {
min-width: 100px;
min-height: 150px;
display: grid;
place-items: center;
}
.battle-unit {
position: relative;
flex-shrink: 0;
}
.battle-unit .card {
width: 96px;
height: 132px;
}
@keyframes unit-reveal {
from {
opacity: 0;
transform: rotateY(90deg) scale(0.8);
}
to {
opacity: 1;
transform: rotateY(0) scale(1);
}
}
.battle-unit.unit-reveal {
animation: unit-reveal 500ms ease;
}
@keyframes rock-fly-right {
0% {
opacity: 0;
transform: translateX(-70px) translateY(-10px) rotate(0);
}
20% {
opacity: 1;
}
100% {
opacity: 1;
transform: translateX(70px) translateY(4px) rotate(360deg);
}
}
@keyframes rock-fly-left {
0% {
opacity: 0;
transform: translateX(70px) translateY(-10px) rotate(0);
}
20% {
opacity: 1;
}
100% {
opacity: 1;
transform: translateX(-70px) translateY(4px) rotate(-360deg);
}
}
.rock-fly {
position: absolute;
font-size: 1.4rem;
z-index: 7;
pointer-events: none;
}
.rock-fly-right {
animation: rock-fly-right 700ms ease-in forwards;
}
.rock-fly-left {
animation: rock-fly-left 700ms ease-in forwards;
}
/* --- 3D rock dice --- */
.dice-tray {
position: absolute;
z-index: 8;
display: flex;
gap: 10px;
perspective: 420px;
pointer-events: none;
}
.die {
--size: 34px;
--half: 17px;
position: relative;
width: var(--size);
height: var(--size);
/* A filter here would flatten the cube, so it only carries the shadow. */
filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.45));
}
.die-cube {
position: absolute;
inset: 0;
transform-style: preserve-3d;
/* From a heavily-spun start, settle onto this die's rolled face. */
animation: die-roll 720ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
animation-delay: var(--delay, 0ms);
}
@keyframes die-roll {
0% {
transform: rotateX(calc(var(--rx) - 900deg)) rotateY(calc(var(--ry) - 1080deg));
}
100% {
transform: rotateX(var(--rx)) rotateY(var(--ry));
}
}
.die-face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 6px;
background: linear-gradient(145deg, var(--cream), var(--cream-dark));
border: 2px solid var(--cocoa);
box-sizing: border-box;
}
.die-front {
transform: translateZ(var(--half));
}
.die-back {
transform: rotateY(180deg) translateZ(var(--half));
}
.die-right {
transform: rotateY(90deg) translateZ(var(--half));
}
.die-left {
transform: rotateY(-90deg) translateZ(var(--half));
}
.die-top {
transform: rotateX(90deg) translateZ(var(--half));
}
.die-bottom {
transform: rotateX(-90deg) translateZ(var(--half));
}
.die-pips {
display: grid;
gap: 4px;
padding: 5px;
}
.die-pips-1 {
grid-template-columns: 1fr;
}
.die-pips-2 {
grid-template-columns: repeat(2, 1fr);
}
.die-pip {
width: 8px;
height: 8px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #6b7075, #2c2f33);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
@keyframes fx-pop {
0% {
opacity: 0;
transform: translate(-50%, 6px) scale(0.6);
}
30% {
opacity: 1;
transform: translate(-50%, -14px) scale(1.2);
}
100% {
opacity: 0;
transform: translate(-50%, -34px) scale(1);
}
}
.fx-pop {
position: absolute;
top: 0;
left: 50%;
font-family: var(--font-display);
font-size: 1rem;
color: #7be495;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
animation: fx-pop 1000ms ease forwards;
pointer-events: none;
z-index: 5;
white-space: nowrap;
}
.mill-pop .card {
filter: grayscale(1);
}
@keyframes clash-left {
0% {
transform: translateX(0);
}
35% {
transform: translateX(26px) rotate(4deg);
}
60% {
transform: translateX(-6px);
}
100% {
transform: translateX(0);
}
}
@keyframes clash-right {
0% {
transform: translateX(0);
}
35% {
transform: translateX(-26px) rotate(-4deg);
}
60% {
transform: translateX(6px);
}
100% {
transform: translateX(0);
}
}
.battle-unit.clash-left {
animation: clash-left 500ms ease;
}
.battle-unit.clash-right {
animation: clash-right 500ms ease;
}
@keyframes dying {
to {
opacity: 0;
transform: translateY(30px) rotate(12deg) scale(0.85);
}
}
.battle-unit.unit-dying {
animation: dying 700ms ease 500ms forwards;
}
@keyframes damage-pop {
0% {
opacity: 0;
transform: translate(-50%, 0) scale(0.6);
}
25% {
opacity: 1;
transform: translate(-50%, -16px) scale(1.15);
}
100% {
opacity: 0;
transform: translate(-50%, -44px) scale(1);
}
}
.damage-pop {
position: absolute;
top: 0;
left: 50%;
font-family: var(--font-display);
font-size: 1.4rem;
color: #ff6b52;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
animation: damage-pop 1100ms ease 250ms forwards;
opacity: 0;
pointer-events: none;
z-index: 5;
}
.battle-result {
text-align: center;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
animation: result-in 400ms ease;
}
@keyframes result-in {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.battle-result-title {
font-family: var(--font-display);
font-size: 2.4rem;
}
.battle-result.is-win .battle-result-title {
color: var(--gold);
}
.battle-result.is-loss .battle-result-title {
color: #ff8f7a;
}
.battle-result.is-draw .battle-result-title {
color: var(--teal);
}
.battle-result-sub {
font-size: 1.1rem;
font-weight: 700;
}
/* ---------- game over ---------- */
.gameover {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
text-align: center;
}
.gameover-emoji {
font-size: 4rem;
}
.gameover-title {
font-size: 3rem;
color: var(--gold);
text-shadow: 0 4px 0 rgba(0, 0, 0, 0.35);
}
.gameover-scores {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(0, 0, 0, 0.22);
border-radius: 16px;
padding: 18px 26px;
min-width: min(360px, 90vw);
}
.score-line {
display: flex;
justify-content: space-between;
gap: 24px;
font-size: 1.1rem;
}
.score-line.is-you .score-name {
color: var(--gold);
font-weight: 900;
}
/* ---------- toasts & banners ---------- */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--red);
color: #fff;
font-weight: 700;
border-radius: 12px;
padding: 10px 20px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4);
animation: result-in 200ms ease;
z-index: 100;
}
.banner {
position: fixed;
top: 64px;
left: 50%;
transform: translateX(-50%);
border-radius: 10px;
padding: 6px 16px;
font-size: 0.9rem;
font-weight: 700;
z-index: 90;
}
.banner-warn {
background: rgba(217, 74, 56, 0.9);
}
@media (max-width: 600px) {
.card {
width: 92px;
height: 128px;
}
.card-lg {
width: 104px;
height: 146px;
}
.battle-unit .card {
width: 78px;
height: 110px;
}
}
/* --- debug panel (server DEBUG mode) --- */
.debug-panel {
position: fixed;
top: 64px;
right: 0;
z-index: 120;
display: flex;
align-items: flex-start;
}
.debug-toggle {
background: #7a1f1f;
color: var(--cream);
border: 2px solid var(--cocoa);
border-right: none;
border-radius: 8px 0 0 8px;
font-weight: 800;
font-size: 0.8rem;
padding: 8px 10px;
cursor: pointer;
white-space: nowrap;
writing-mode: vertical-rl;
transform: rotate(180deg);
}
.debug-panel.is-open .debug-toggle {
writing-mode: horizontal-tb;
transform: none;
border-radius: 8px 0 0 0;
}
.debug-body {
width: min(340px, 80vw);
max-height: calc(100vh - 80px);
overflow-y: auto;
background: rgba(18, 53, 31, 0.97);
border: 2px solid var(--cocoa);
border-radius: 8px 0 0 8px;
padding: 12px;
box-shadow: -6px 6px 18px rgba(0, 0, 0, 0.45);
}
.debug-head {
color: var(--gold);
font-weight: 800;
margin-bottom: 8px;
}
.debug-tier-label {
color: var(--cream);
font-size: 0.75rem;
font-weight: 700;
opacity: 0.8;
margin: 8px 0 4px;
}
.debug-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
/* --- hover magnifier --- */
.card-magnify {
position: fixed;
z-index: 200;
pointer-events: none;
transform: scale(1.5);
transform-origin: top left;
filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.55));
animation: magnify-in 90ms ease-out;
}
/* The magnifier exists to be read, so let the preview card grow to fit its
full effect text instead of clipping like a fixed-height card. */
.card-magnify .card {
height: auto;
min-height: 176px;
padding-bottom: 10px;
}
.card-magnify .card-effect {
flex: 0 0 auto;
overflow: visible;
align-items: center;
margin-bottom: 6px;
}
@keyframes magnify-in {
from {
opacity: 0;
transform: scale(1.1);
}
to {
opacity: 1;
transform: scale(1.5);
}
}
/* --- deck peek (battle) --- */
.stackpile.peekable {
cursor: help;
}
/* Portaled to <body> and anchored to the hovered stack's viewport rect (see
BattlePhase) so it floats over the whole window instead of being clipped by
the battlefield's overflow. left/right/bottom come from inline styles. */
.deck-peek {
position: fixed;
z-index: 60;
width: max-content;
max-width: 60vw;
background: rgba(18, 53, 31, 0.98);
border: 2px solid var(--cocoa);
border-radius: 10px;
padding: 8px 10px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
pointer-events: none;
}
.deck-peek-label {
color: var(--gold);
font-size: 0.72rem;
font-weight: 800;
margin-bottom: 6px;
}
.deck-peek-cards {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-width: min(60vw, 560px);
}