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

2623 lines
56 KiB
CSS

/* ============================================================
Super Auto Pets: The Board Game — cozy tabletop, elevated.
The whole app is one lamp-lit game table: a warm spotlight over
deep felt, walnut rails, and thick printed cards that cast real
shadows. Panels read as recessed felt trays, not floating boxes.
Palette stays warm end-to-end — pine, walnut, parchment, honey,
coral — so nothing feels generic or cold.
============================================================ */
:root {
/* Felt — a lamp-lit pine table, warm in the middle, dark at the rim. */
--felt-900: #0f2c20;
--felt-800: #163a2a;
--felt-700: #1d4c35;
--felt-600: #266145;
--felt-lit: #2c6f4d;
/* Walnut rails and trays. */
--wood: #5a3a20;
--wood-light: #815129;
--wood-dark: #3d2614;
/* Printed parchment cards. */
--cream: #fbf1d7;
--cream-dark: #efdcac;
--cocoa: #4a2c14;
--cocoa-soft: #6b4426;
--ink: #35240f;
--ink-soft: rgba(53, 36, 15, 0.66);
/* Warm accents. */
--coral: #f4884a;
--coral-dark: #d9611a;
--gold: #f6c94e;
--gold-deep: #e0a52f;
--honey: #f0b64a;
--sage: #4f9d78;
--sage-dark: #327a58;
--red: #d94a38;
--red-soft: #ffb3a7;
--font-display: 'Lilita One', system-ui, sans-serif;
--font-body: 'Nunito', system-ui, sans-serif;
--card-radius: 14px;
/* Reusable warm shadows so depth stays consistent everywhere. */
--shadow-card: 0 10px 20px -6px rgba(20, 12, 4, 0.55), 0 2px 4px rgba(20, 12, 4, 0.4);
--tray-inset: inset 0 2px 5px rgba(0, 0, 0, 0.45), inset 0 -1px 0 rgba(255, 255, 255, 0.05);
}
* {
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);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
/* A warm overhead lamp, a soft felt weave, fine grain, and a dark rim. */
background-image:
radial-gradient(ellipse 90% 60% at 50% -8%, rgba(255, 214, 138, 0.16), transparent 60%),
radial-gradient(ellipse 70% 55% at 50% 32%, rgba(64, 150, 104, 0.22), transparent 70%),
radial-gradient(ellipse at 50% 116%, rgba(0, 0, 0, 0.55), transparent 62%),
repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.012) 0 2px, transparent 2px 4px),
repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.03) 0 2px, transparent 2px 4px),
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.045'/%3E%3C/svg%3E"),
linear-gradient(165deg, var(--felt-700), var(--felt-900) 78%);
background-attachment: fixed;
}
h1,
h2,
h3 {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.01em;
}
.muted {
color: rgba(253, 243, 220, 0.52);
}
.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.62);
text-align: center;
}
.warn-text {
color: var(--gold);
font-weight: 800;
}
/* ---------- buttons ---------- */
/* Tactile printed tokens: a soft top sheen, a pressed edge underneath, and a
satisfying travel on click — like pushing a wooden meeple down. */
.btn {
position: relative;
font-family: var(--font-display);
font-size: 1rem;
letter-spacing: 0.03em;
color: var(--cream);
background: linear-gradient(180deg, var(--wood-light), var(--wood));
border: none;
border-radius: 12px;
padding: 11px 20px;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
inset 0 -2px 3px rgba(0, 0, 0, 0.28),
0 4px 0 var(--wood-dark),
0 6px 10px rgba(0, 0, 0, 0.35);
transition: transform 90ms ease, box-shadow 90ms ease, filter 140ms ease;
}
.btn:hover:not(:disabled) {
filter: brightness(1.07) saturate(1.05);
}
.btn:active:not(:disabled) {
transform: translateY(3px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -1px 2px rgba(0, 0, 0, 0.3),
0 1px 0 var(--wood-dark),
0 2px 4px rgba(0, 0, 0, 0.35);
}
.btn:disabled {
opacity: 0.42;
cursor: not-allowed;
filter: saturate(0.6);
}
.btn-primary {
background: linear-gradient(180deg, #ffa257, var(--coral-dark));
color: #fff;
text-shadow: 0 1px 2px rgba(120, 45, 5, 0.55);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
inset 0 -2px 3px rgba(150, 55, 0, 0.4),
0 4px 0 #ab4a13,
0 7px 12px rgba(120, 45, 5, 0.4);
}
.btn-primary:active:not(:disabled) {
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 2px rgba(150, 55, 0, 0.4),
0 1px 0 #ab4a13,
0 2px 5px rgba(120, 45, 5, 0.4);
}
.btn-secondary {
background: linear-gradient(180deg, #5cb68c, var(--sage-dark));
color: #f4fff8;
text-shadow: 0 1px 2px rgba(15, 60, 40, 0.5);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -2px 3px rgba(15, 60, 40, 0.35),
0 4px 0 #245840,
0 7px 12px rgba(15, 60, 40, 0.35);
}
.btn-secondary:active:not(:disabled) {
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 2px rgba(15, 60, 40, 0.35),
0 1px 0 #245840,
0 2px 5px rgba(15, 60, 40, 0.35);
}
.btn-ghost {
background: rgba(253, 243, 220, 0.06);
border: 2px solid rgba(253, 243, 220, 0.22);
color: var(--cream);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.btn-ghost:hover:not(:disabled) {
background: rgba(253, 243, 220, 0.12);
filter: none;
}
.btn-ghost:active:not(:disabled) {
transform: translateY(2px);
box-shadow: none;
}
.btn-big {
font-size: 1.3rem;
padding: 15px 32px;
letter-spacing: 0.02em;
}
.btn-sm {
font-size: 0.8rem;
padding: 6px 11px;
border-radius: 9px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 3px 0 var(--wood-dark),
0 4px 6px rgba(0, 0, 0, 0.3);
}
.btn-ghost.btn-sm {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
/* ---------- home ---------- */
.home {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 22px;
padding: 24px;
}
.home-pets {
font-size: 2.8rem;
letter-spacing: 0.28em;
padding-left: 0.28em;
filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.45));
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.home-title {
font-size: clamp(2.4rem, 6.5vw, 3.9rem);
text-align: center;
color: var(--gold);
text-shadow:
0 2px 0 var(--gold-deep),
0 5px 0 rgba(90, 58, 32, 0.5),
0 8px 14px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
line-height: 1.02;
}
.home-subtitle {
font-size: 0.4em;
color: var(--cream);
letter-spacing: 0.34em;
padding-left: 0.34em;
text-transform: uppercase;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
margin-top: 0.15em;
}
/* The home card is a little parchment sign-in slip pinned to the felt. */
.home-card {
position: relative;
background:
linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.22));
border: 1px solid rgba(253, 243, 220, 0.14);
border-radius: 20px;
padding: 30px;
display: flex;
flex-direction: column;
gap: 16px;
width: min(384px, 92vw);
box-shadow:
var(--tray-inset),
0 18px 40px -12px rgba(0, 0, 0, 0.6);
}
.field {
display: flex;
flex-direction: column;
gap: 7px;
font-weight: 800;
font-size: 0.82rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(253, 243, 220, 0.7);
}
.field input,
.code-input {
font-family: var(--font-body);
font-size: 1.1rem;
font-weight: 700;
color: var(--ink);
background: linear-gradient(180deg, #fff8e8, var(--cream));
border: 2px solid var(--cocoa-soft);
border-radius: 11px;
padding: 11px 13px;
outline: none;
width: 100%;
box-shadow: inset 0 2px 4px rgba(74, 44, 20, 0.18);
transition: border-color 120ms ease, box-shadow 120ms ease;
}
.field input::placeholder {
color: rgba(53, 36, 15, 0.38);
font-weight: 600;
}
.field input:focus,
.code-input:focus {
border-color: var(--coral);
box-shadow:
inset 0 2px 4px rgba(74, 44, 20, 0.18),
0 0 0 3px rgba(244, 136, 74, 0.28);
}
.code-input {
font-family: var(--font-display);
letter-spacing: 0.35em;
padding-left: calc(13px + 0.35em);
text-transform: uppercase;
text-align: center;
}
.home-divider {
text-align: center;
font-size: 0.82rem;
color: rgba(253, 243, 220, 0.5);
display: flex;
align-items: center;
gap: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.home-divider::before,
.home-divider::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(253, 243, 220, 0.28), transparent);
}
.home-join {
display: flex;
gap: 10px;
}
.home-hint {
text-align: center;
font-size: 0.9rem;
margin: 0;
}
.home-error {
color: var(--red-soft);
font-weight: 700;
text-align: center;
}
/* ---------- topbar ---------- */
.table {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* A solid walnut rail across the top of the table, with grain and a worn
highlight along its top edge. */
.topbar {
display: flex;
align-items: center;
gap: 16px;
padding: 11px 18px;
background:
repeating-linear-gradient(92deg, rgba(0, 0, 0, 0.05) 0 3px, transparent 3px 7px),
linear-gradient(180deg, var(--wood-light), var(--wood) 65%, var(--wood-dark));
border-bottom: 3px solid var(--wood-dark);
box-shadow:
inset 0 1px 0 rgba(255, 220, 170, 0.25),
0 4px 14px rgba(0, 0, 0, 0.4);
flex-wrap: wrap;
position: relative;
z-index: 10;
}
.topbar-brand {
display: flex;
align-items: center;
gap: 7px;
font-family: var(--font-display);
font-size: 1.3rem;
color: var(--gold);
text-shadow: 0 2px 2px rgba(0, 0, 0, 0.45);
letter-spacing: 0.04em;
}
.topbar-round {
font-size: 0.95rem;
font-weight: 700;
color: rgba(255, 245, 225, 0.9);
}
.topbar-round strong {
color: var(--gold);
}
.topbar-players {
display: flex;
gap: 12px;
flex: 1;
flex-wrap: wrap;
}
/* Player tags read like enamel pins clipped to the rail. */
.topbar-player {
display: flex;
align-items: center;
gap: 7px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.3));
border-radius: 999px;
padding: 5px 13px;
font-size: 0.9rem;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 1px 2px rgba(0, 0, 0, 0.3);
}
.topbar-player.is-you {
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 0 0 2px var(--gold),
0 1px 3px rgba(0, 0, 0, 0.35);
}
.topbar-name {
font-weight: 800;
}
.chip {
font-size: 0.85rem;
font-weight: 700;
}
/* A chip that's actually a button (e.g. peek at the opponent's deck). */
.chip-btn {
font-family: var(--font-body);
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.08);
color: inherit;
border-radius: 999px;
padding: 1px 8px;
transition: background 120ms ease, border-color 120ms ease;
}
.chip-btn:hover {
background: rgba(255, 255, 255, 0.18);
}
.chip-btn.is-active {
background: var(--gold);
color: var(--cocoa);
border-color: var(--gold);
}
.conn-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);
}
.conn-dot.on {
background: #6fe08b;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25), 0 0 6px rgba(111, 224, 139, 0.7);
}
.conn-dot.off {
background: var(--red);
}
/* Bots swap the connection dot for a little robot face. */
.bot-dot {
font-size: 0.85rem;
line-height: 1;
}
/* The room code, stamped like a label on a card tray. */
.topbar-code {
font-family: var(--font-display);
letter-spacing: 0.28em;
padding: 5px 10px 5px calc(10px + 0.28em);
color: var(--gold);
background: rgba(0, 0, 0, 0.28);
border: 2px dashed rgba(246, 201, 78, 0.5);
border-radius: 8px;
}
.table-body {
flex: 1;
display: flex;
align-items: flex-start;
min-height: 0;
}
.table-main {
flex: 1;
padding: 26px 20px 48px;
max-width: 1100px;
width: 100%;
margin: 0 auto;
}
/* ---------- event log ---------- */
/* A pinned-up ledger of what happened, paper-warm against the felt. */
.event-log {
width: 300px;
flex-shrink: 0;
align-self: stretch;
position: sticky;
top: 0;
max-height: calc(100vh - 60px);
display: flex;
flex-direction: column;
background:
linear-gradient(180deg, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.34));
border-left: 1px solid rgba(0, 0, 0, 0.4);
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.04);
}
.event-log.is-collapsed {
width: 42px;
}
.event-log-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 10px 12px;
font-family: var(--font-display);
font-size: 0.92rem;
color: var(--gold);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.15));
border-bottom: 1px solid rgba(0, 0, 0, 0.35);
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.05);
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: 10px 10px 14px;
display: flex;
flex-direction: column;
gap: 5px;
font-size: 0.82rem;
line-height: 1.3;
}
.log-empty {
padding: 8px 2px;
font-style: italic;
}
.log-line {
display: flex;
gap: 7px;
align-items: baseline;
padding: 4px 8px;
border-radius: 7px;
border-left: 3px solid transparent;
background: rgba(255, 255, 255, 0.035);
}
.log-icon {
flex-shrink: 0;
}
.log-text {
min-width: 0;
word-break: break-word;
}
/* Card names embedded in a log line: a subtle dotted underline hints they can
be hovered to preview the card. */
.log-card-ref {
text-decoration: underline dotted rgba(253, 243, 220, 0.45);
text-underline-offset: 2px;
cursor: help;
font-weight: 600;
color: var(--cream);
}
.log-card-ref:hover {
text-decoration-color: var(--gold);
color: var(--gold);
}
.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;
letter-spacing: 0.06em;
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(246, 201, 78, 0.18);
box-shadow: inset 0 0 0 1px rgba(246, 201, 78, 0.45);
}
@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 {
max-width: 620px;
margin: 0 auto;
padding: 8px 16px 32px;
display: flex;
flex-direction: column;
gap: 22px;
}
.lobby-heading {
text-align: center;
color: var(--gold);
}
.lobby-bounce {
font-size: 3.2rem;
filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.45));
animation: float 2s ease-in-out infinite;
}
.lobby-removed {
gap: 14px;
text-align: center;
}
.lobby-code-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.lobby-code {
font-family: var(--font-display);
font-size: 1.8rem;
letter-spacing: 0.3em;
padding: 6px 16px 6px calc(16px + 0.3em);
color: var(--gold);
background: rgba(0, 0, 0, 0.25);
border: 3px dashed rgba(246, 201, 78, 0.5);
border-radius: 14px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
box-shadow: var(--tray-inset);
}
.lobby-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.lobby-section-title {
font-family: var(--font-display);
font-size: 1.15rem;
color: var(--cream);
}
.lobby-note,
.lobby-waiting {
text-align: center;
}
.lobby-waiting {
font-size: 1.05rem;
}
/* ---------- pack picker ---------- */
.pack-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.pack-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 16px 10px 12px;
border-radius: var(--card-radius);
border: 3px solid transparent;
background: rgba(0, 0, 0, 0.22);
color: var(--cream);
cursor: pointer;
transition: transform 0.12s ease, border-color 0.12s ease;
}
.pack-card:not(:disabled):hover {
transform: translateY(-2px);
}
.pack-card.is-selected {
border-color: var(--gold);
background: rgba(246, 201, 78, 0.14);
}
.pack-card.is-locked {
opacity: 0.45;
cursor: not-allowed;
filter: grayscale(0.7);
}
.pack-card:disabled {
cursor: not-allowed;
}
.pack-emoji {
font-size: 2.4rem;
line-height: 1;
}
.pack-name {
font-family: var(--font-display);
font-size: 0.95rem;
}
.pack-tag {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--gold);
}
.pack-card.is-locked .pack-tag {
color: var(--cream);
}
/* ---------- seat list ---------- */
.seat-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.seat-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.22);
box-shadow: var(--tray-inset);
}
.seat-open {
border: 2px dashed rgba(251, 241, 215, 0.25);
background: rgba(0, 0, 0, 0.12);
box-shadow: none;
}
.seat-avatar {
font-size: 1.5rem;
line-height: 1;
}
.seat-name {
font-weight: 800;
flex: 1;
}
.seat-you {
color: var(--gold);
font-weight: 700;
}
.seat-badges {
display: flex;
align-items: center;
gap: 8px;
}
.seat-remove {
border: none;
background: rgba(217, 74, 56, 0.2);
color: var(--red-soft);
font-weight: 900;
width: 26px;
height: 26px;
border-radius: 50%;
cursor: pointer;
line-height: 1;
}
.seat-remove:hover {
background: var(--red);
color: #fff;
}
.seat-open-host {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.seat-bot-picker {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.seat-bot-picker .btn {
flex: 1;
white-space: nowrap;
}
/* ---------- cards ---------- */
/* Thick printed stock: a warm parchment face, a soft ink border with an inner
keyline, a physical shadow, and a lit sheen along the top. */
.card {
position: relative;
width: 108px;
height: 148px;
background: linear-gradient(168deg, #fdf6e3, var(--cream) 45%, var(--cream-dark));
border: 2px solid var(--cocoa);
border-radius: var(--card-radius);
color: var(--ink);
display: flex;
flex-direction: column;
align-items: center;
padding: 7px 6px;
box-shadow:
inset 0 0 0 1.5px rgba(255, 255, 255, 0.55),
inset 0 -8px 14px rgba(180, 140, 80, 0.22),
var(--shadow-card);
flex-shrink: 0;
transition: transform 130ms cubic-bezier(0.34, 1.4, 0.5, 1), box-shadow 130ms ease;
user-select: none;
}
/* A faint printed sheen sweeping across the top of every card. */
.card::before {
content: '';
position: absolute;
inset: 1.5px 1.5px auto 1.5px;
height: 42%;
border-radius: 11px 11px 40% 40%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent);
pointer-events: none;
}
.card-lg {
width: 132px;
height: 180px;
}
.card-sm {
width: 84px;
height: 116px;
}
.card.is-clickable {
cursor: pointer;
}
.card.is-clickable:hover {
transform: translateY(-8px) rotate(-1.2deg);
box-shadow:
inset 0 0 0 1.5px rgba(255, 255, 255, 0.6),
inset 0 -8px 14px rgba(180, 140, 80, 0.22),
0 18px 30px -8px rgba(20, 12, 4, 0.6),
0 4px 8px rgba(20, 12, 4, 0.4);
}
.card.is-selected {
outline: 3px solid var(--gold);
outline-offset: 1px;
transform: translateY(-8px);
box-shadow:
inset 0 0 0 1.5px rgba(255, 255, 255, 0.6),
0 0 20px rgba(246, 201, 78, 0.45),
var(--shadow-card);
}
.card.is-disabled {
filter: saturate(0.55) brightness(0.86);
}
.card.is-dead {
filter: grayscale(1);
}
.card-top {
width: 100%;
display: flex;
/* Tier sits at the top-left, suit dot at the top-right. */
justify-content: space-between;
align-items: center;
gap: 5px;
font-size: 0.72rem;
z-index: 1;
min-height: 14px;
}
/* Combat stats (power badge + battle damage marker) get their own centered
row just under the tier/suit line, so they never overlap the art or text. */
.card-combat {
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin: 2px 0 1px;
}
.card-tier {
font-family: var(--font-display);
font-size: 0.68rem;
color: rgba(74, 44, 20, 0.5);
}
.card-suit {
font-size: 0.95rem;
}
/* The art sits in a soft printed halo so it reads as the card's hero. */
.card-art {
position: relative;
font-size: 2rem;
line-height: 1;
flex: 0 0 auto;
margin-top: 1px;
z-index: 1;
filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.28));
}
.card-art::before {
content: '';
position: absolute;
left: 50%;
top: 52%;
width: 2.5em;
height: 2.5em;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(circle, rgba(246, 201, 78, 0.32), transparent 68%);
z-index: -1;
}
.card-lg .card-art {
font-size: 2.7rem;
}
.card-sm .card-art {
font-size: 1.9rem;
}
.card-name {
font-family: var(--font-display);
font-size: 0.84rem;
line-height: 1.05;
margin-top: 3px;
flex: 0 0 auto;
z-index: 1;
color: var(--cocoa);
/* Always center the name — including when a long name wraps to two lines, so
the second line stays centered instead of drifting off to one side. */
width: 100%;
text-align: center;
overflow-wrap: break-word;
}
.card-lg .card-name {
font-size: 0.94rem;
}
.card-effect {
font-size: 0.55rem;
line-height: 1.2;
font-weight: 700;
color: var(--ink-soft);
text-align: center;
padding: 2px 3px 0;
z-index: 1;
/* 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;
/* Stack multiple abilities (e.g. Squirrel's Buy and Sell) vertically rather
than side by side. */
flex-direction: column;
gap: 2px;
/* 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;
}
/* Each ability on its own line within the effect band. */
.card-effect-line {
display: block;
}
.card-lg .card-effect {
font-size: 0.63rem;
}
.card-sm .card-effect {
display: none;
}
/* Suit markers as glossy enamel dots. */
.suit-dot {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
vertical-align: middle;
/* Inset ring instead of a real border — see .card-power. */
box-shadow:
inset 0 0 0 1.5px rgba(0, 0, 0, 0.3),
inset 0 1.5px 1px rgba(255, 255, 255, 0.6),
inset 0 -2px 2px rgba(0, 0, 0, 0.25);
}
.suit-red {
background: radial-gradient(circle at 35% 28%, #ff6b5f, #d23124);
}
.suit-blue {
background: radial-gradient(circle at 35% 28%, #6ba6f5, #2f6bd0);
}
.suit-yellow {
background: radial-gradient(circle at 35% 28%, #ffdd6b, #e6ac1f);
}
/* The power badge is a fiery starburst — like a little explosion — carrying the
pet's attack. The spikes are cut with clip-path, so the drop shadow lives on
`filter` (a box-shadow would be clipped away with the corners). */
.card-power {
font-family: var(--font-display);
background: radial-gradient(circle at 38% 30%, #ffcf6b 4%, var(--coral) 42%, var(--coral-dark));
color: #fff;
text-shadow: 0 1px 2px rgba(120, 45, 5, 0.7);
width: 32px;
height: 32px;
display: grid;
place-items: center;
font-size: 0.9rem;
clip-path: polygon(
50% 0%, 61% 18%, 82% 12%, 78% 34%, 100% 43%, 83% 58%,
93% 79%, 70% 76%, 62% 98%, 50% 82%, 38% 98%, 30% 76%,
7% 79%, 17% 58%, 0% 43%, 22% 34%, 18% 12%, 39% 18%
);
filter: drop-shadow(0 2px 2px rgba(20, 12, 4, 0.5));
}
.card-power.is-buffed {
background: radial-gradient(circle at 38% 30%, #c9ffd4 4%, #4fd07f 42%, #1f9e55);
text-shadow: 0 1px 2px rgba(15, 60, 30, 0.7);
filter: drop-shadow(0 0 5px rgba(80, 220, 120, 0.75));
}
.card-lg .card-power {
width: 37px;
height: 37px;
font-size: 1.02rem;
}
.card-sm .card-power {
width: 27px;
height: 27px;
font-size: 0.76rem;
}
.card-damage {
font-family: var(--font-display);
color: #fff;
background: linear-gradient(180deg, #e85c49, var(--red));
border: 1.5px solid rgba(120, 20, 10, 0.4);
border-radius: 8px;
padding: 2px 7px;
font-size: 0.8rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* Food cards get a rosy stock so they read apart from pets at a glance. */
.card-food {
background: linear-gradient(168deg, #fff0ea, #ffe0d1 55%, #ffceba);
}
.card-food::before {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.6), transparent);
}
.card-food .card-effect {
color: #8a3320;
}
.card-food .card-name {
color: #8a3320;
}
.card-food .card-art::before {
background: radial-gradient(circle, rgba(255, 140, 100, 0.28), transparent 68%);
}
.card-check {
position: absolute;
top: -10px;
right: -10px;
background: radial-gradient(circle at 35% 28%, #ffe08a, var(--gold-deep));
color: var(--cocoa);
font-weight: 900;
border: 2px solid var(--cocoa);
border-radius: 50%;
width: 27px;
height: 27px;
display: grid;
place-items: center;
z-index: 2;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.card-slot-empty {
width: 132px;
height: 180px;
border: 2px dashed rgba(253, 243, 220, 0.22);
border-radius: var(--card-radius);
flex-shrink: 0;
background: rgba(0, 0, 0, 0.12);
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.25);
}
/* ---------- shop ---------- */
.shop {
display: flex;
flex-direction: column;
gap: 24px;
}
.shop-status {
text-align: center;
font-size: 1.2rem;
font-weight: 900;
min-height: 1.6em;
}
.status-hot {
color: var(--gold);
text-shadow: 0 1px 6px rgba(246, 201, 78, 0.4);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.62;
}
}
.section-label {
font-family: var(--font-display);
font-size: 1.02rem;
margin-bottom: 12px;
color: var(--gold);
letter-spacing: 0.02em;
display: flex;
align-items: baseline;
gap: 4px;
}
.section-label .muted {
font-family: var(--font-body);
font-size: 0.82rem;
font-weight: 700;
}
/* Recessed felt trays: a darker inset well with a stitched edge, so cards
look like they're laid into the mat rather than floating on it. */
.shop-row-wrap,
.deck-wrap {
background:
radial-gradient(ellipse 80% 120% at 50% 0%, rgba(255, 255, 255, 0.04), transparent 70%),
rgba(0, 0, 0, 0.24);
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 18px;
padding: 16px 18px 18px;
box-shadow:
var(--tray-inset),
inset 0 0 0 1px rgba(246, 201, 78, 0.06);
}
/* Trumpets banked for the next battle, shown as a pill under the deck. */
.trumpet-indicator {
margin: 12px auto 0;
width: fit-content;
font-family: var(--font-display);
font-size: 1.05rem;
padding: 5px 14px;
border-radius: 999px;
border: 1px solid rgba(246, 201, 78, 0.3);
background: rgba(0, 0, 0, 0.28);
color: var(--gold);
}
/* Coins shown as big golden discs above the buy row. */
.shop-coins {
display: flex;
gap: 6px;
align-items: center;
justify-content: center;
min-height: 40px;
margin-bottom: 4px;
}
.coin-disc {
font-size: 1.9rem;
line-height: 1;
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.4));
animation: coin-pop 0.25s ease;
transition: filter 200ms ease, opacity 200ms ease, transform 200ms ease;
}
/* A spent coin stays in place but greys out and fades. */
.coin-disc.is-spent {
filter: grayscale(1) brightness(0.7);
opacity: 0.4;
transform: scale(0.88);
animation: none;
}
@keyframes coin-pop {
from {
transform: scale(0.4);
opacity: 0;
}
}
.coin-empty {
font-style: italic;
font-size: 0.9rem;
}
/* "Your turn" flourish across the shop. */
.turn-banner {
position: fixed;
inset: 0;
display: grid;
place-items: center;
pointer-events: none;
z-index: 50;
}
/* Light text on a golden rounded-rect plate, so the flourish reads as a solid
badge rather than bare text floating over the board. */
.turn-banner-pill {
font-family: var(--font-display);
font-size: clamp(2rem, 7vw, 4.5rem);
color: #fff8e6;
background: linear-gradient(180deg, var(--gold), var(--gold-deep));
padding: 0.28em 0.8em;
border-radius: 22px;
border: 3px solid rgba(255, 255, 255, 0.55);
text-shadow: 0 2px 5px rgba(120, 78, 8, 0.55);
box-shadow:
inset 0 2px 0 rgba(255, 255, 255, 0.55),
inset 0 -6px 14px rgba(150, 100, 20, 0.35),
0 14px 34px rgba(0, 0, 0, 0.5);
animation: turn-flash 1.4s ease forwards;
}
@keyframes turn-flash {
0% {
opacity: 0;
transform: scale(0.6);
}
20% {
opacity: 1;
transform: scale(1);
}
75% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.15);
}
}
/* Pass button nudge when there's nothing left to buy. */
.btn-pass-hint {
border-color: var(--gold);
color: var(--gold);
box-shadow: 0 0 0 1px var(--gold), 0 0 14px rgba(246, 201, 78, 0.5);
animation: pass-pulse 1.4s ease-in-out infinite;
}
@keyframes pass-pulse {
50% {
box-shadow: 0 0 0 1px var(--gold), 0 0 22px rgba(246, 201, 78, 0.8);
}
}
/* Trigger explainer under the hover magnifier. */
.trigger-gloss {
margin-top: 8px;
max-width: 200px;
background: rgba(0, 0, 0, 0.82);
border: 1px solid rgba(246, 201, 78, 0.35);
border-radius: 10px;
padding: 8px 10px;
font-size: 0.78rem;
line-height: 1.35;
color: var(--parchment, #eee);
}
.trigger-gloss-line + .trigger-gloss-line {
margin-top: 4px;
}
.trigger-gloss strong {
color: var(--gold);
}
/* Set-aside Avocado tokens (Golden pack): a slim tray with a toggle pill. */
.avocado-zone {
background: rgba(0, 0, 0, 0.24);
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 18px;
padding: 12px 18px 14px;
box-shadow:
var(--tray-inset),
inset 0 0 0 1px rgba(246, 201, 78, 0.06);
}
.avocado-token {
font-family: var(--font-display);
font-size: 1.05rem;
padding: 6px 14px;
border-radius: 999px;
border: 1px solid rgba(246, 201, 78, 0.3);
background: rgba(0, 0, 0, 0.28);
color: var(--gold);
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.avocado-token:hover:not(:disabled) {
transform: translateY(-1px);
}
.avocado-token.is-active {
border-color: var(--gold);
box-shadow: 0 0 10px rgba(246, 201, 78, 0.45);
}
.avocado-token:disabled {
opacity: 0.6;
cursor: default;
}
.shop-row,
.deck-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
/* Positioning context for the exit "ghost" cards, which are pinned to where
the departing card sat while they shrink away (see .card-ghost). */
position: relative;
}
/* A thin wrapper around each animated card. The FLIP animator moves and flips
this element (not the .card inside), so the card's own hover/selected lift
transforms stay independent. backface-visibility hides the card mid-flip so
a dealt-in card reads as turning face-up rather than showing mirrored text. */
.card-cell {
display: flex;
flex: 0 0 auto;
backface-visibility: hidden;
}
/* An exiting card: pinned (inline styles) where it last sat, shrinking away as
its former neighbors glide into the gap. */
.card-ghost {
z-index: 3;
animation: card-ghost-out 280ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
}
@keyframes card-ghost-out {
to {
opacity: 0;
transform: scale(0.62) translateY(-6px);
}
}
.deck-empty {
text-align: center;
padding: 22px 0;
}
.actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
/* ---------- modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(10, 20, 14, 0.72);
backdrop-filter: blur(3px);
display: grid;
place-items: center;
z-index: 50;
animation: result-in 200ms ease;
}
.modal {
background:
radial-gradient(ellipse 80% 60% at 50% 0%, rgba(255, 214, 138, 0.1), transparent 70%),
linear-gradient(180deg, var(--felt-600), var(--felt-800));
border: 1px solid rgba(253, 243, 220, 0.2);
border-radius: 20px;
padding: 28px;
text-align: center;
display: flex;
flex-direction: column;
gap: 20px;
max-width: 92vw;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 24px 60px -12px rgba(0, 0, 0, 0.7);
}
.modal-cards {
display: flex;
gap: 20px;
justify-content: center;
}
/* ---------- arrange ---------- */
.arrange {
display: flex;
flex-direction: column;
gap: 20px;
}
.arrange-row {
position: relative;
display: flex;
/* Battle line reads right-to-left, wrapping downward: card 0 (fights first)
sits top-right and later pets flow left, then onto rows below. The wide
row-gap leaves a band for the wrap-around connector arrows. */
direction: rtl;
column-gap: 14px;
row-gap: 56px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
background:
radial-gradient(ellipse 80% 120% at 50% 0%, rgba(255, 255, 255, 0.04), transparent 70%),
rgba(0, 0, 0, 0.24);
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 18px;
/* Extra right padding reserves the strip the "first" marker sits in, so its
width doesn't shove the top row left of the wrapped rows below it. */
padding: 20px 52px 20px 18px;
min-height: 224px;
box-shadow: var(--tray-inset);
}
/* Pinned out of flow to the reserved strip beside the first (rightmost) card,
vertically centred on the top row, so it never affects card alignment. */
.arrange-marker {
position: absolute;
top: 20px;
right: 16px;
height: 148px;
display: flex;
align-items: center;
z-index: 2;
direction: ltr;
font-family: var(--font-display);
color: var(--gold);
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.95rem;
letter-spacing: 0.04em;
opacity: 0.85;
}
/* SVG overlay for the wrap-around connector arrows. Absolute so it sits out of
the flex flow; pointer-events off so it never blocks dragging. overflow is
visible because the curve control points can bow just past the tray edge. */
.arrange-connectors {
position: absolute;
inset: 0;
z-index: 3;
overflow: visible;
pointer-events: none;
}
.arrange-connector {
fill: none;
stroke: rgba(190, 194, 200, 0.55);
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.arrange-arrowhead-shape {
fill: rgba(190, 194, 200, 0.55);
}
.arrange-card {
direction: ltr;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: grab;
}
.arrange-card:active {
cursor: grabbing;
}
.arrange-arrows {
display: flex;
gap: 5px;
}
/* ---------- battle ---------- */
.battle {
display: flex;
flex-direction: column;
gap: 18px;
}
.battle-header {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.battle-header h2 {
font-size: 1.5rem;
color: var(--gold);
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
}
.battle-controls {
display: flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.22);
border-radius: 12px;
padding: 5px 8px;
box-shadow: var(--tray-inset);
}
.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: 800;
font-size: 1.05rem;
}
.battle-vs {
font-family: var(--font-display);
color: var(--gold);
}
/* The battlefield is the lit heart of the table — a spotlit arena inset into
the felt, with a faint centre line where the two sides meet. */
.battlefield {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background:
radial-gradient(ellipse 55% 80% at center, rgba(246, 201, 78, 0.12), transparent 66%),
linear-gradient(180deg, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0.18));
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 20px;
/* Roomy top/bottom padding: pets keep their central row while set-aside
cards fan above and attached foods fan below. The bottom needs plenty of
room since a pet can carry a big stack of apples (Manatee, Fire Ant, …). */
padding: 132px 14px 268px;
min-height: 150px;
overflow-x: auto;
box-shadow:
var(--tray-inset),
inset 0 0 60px rgba(0, 0, 0, 0.25);
}
.battle-center {
position: relative;
font-size: 1.7rem;
flex-shrink: 0;
display: grid;
place-items: center;
min-width: 2.4rem;
align-self: stretch;
}
/* A soft seam down the middle of the arena. */
.battle-center::before {
content: '';
position: absolute;
top: 8%;
bottom: 8%;
width: 2px;
background: linear-gradient(180deg, transparent, rgba(246, 201, 78, 0.25), transparent);
}
.battle-center-bolt {
opacity: 0.5;
filter: drop-shadow(0 0 8px rgba(246, 201, 78, 0.5));
}
.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;
}
/* The face-down deck: a walnut-backed card with a woven diamond pattern and a
little stacked-depth shadow beneath it. */
.card-back {
width: 84px;
height: 116px;
border-radius: var(--card-radius);
border: 2px solid var(--cocoa);
background:
repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.06) 0 6px, transparent 6px 12px),
repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.12) 0 6px, transparent 6px 12px),
linear-gradient(160deg, #c25f31, #a2451f);
box-shadow:
inset 0 0 0 3px rgba(255, 230, 200, 0.18),
inset 0 2px 4px rgba(255, 255, 255, 0.15),
3px 3px 0 rgba(0, 0, 0, 0.22),
6px 6px 0 rgba(0, 0, 0, 0.12),
0 8px 12px rgba(0, 0, 0, 0.35);
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.4);
border-radius: 50%;
width: 40px;
height: 40px;
display: grid;
place-items: center;
box-shadow: inset 0 0 0 2px rgba(255, 230, 200, 0.25);
}
.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;
}
/* Apple/bee spawned in the shop, flying out from the creature that made it. */
@keyframes spawn-fly {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.4) rotate(-10deg);
}
20% {
opacity: 1;
transform: translate(-50%, -85%) scale(1.35) rotate(4deg);
}
100% {
opacity: 0;
transform: translate(-50%, -165%) scale(1) rotate(0);
}
}
.spawn-flyer {
position: fixed;
z-index: 60;
font-size: 2rem;
pointer-events: none;
animation: spawn-fly 1000ms ease-out forwards;
filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.5));
}
/* A bought card in flight from the shop slot to its landing spot in your hand.
The element is positioned over its destination slot (fixed) and inverted back
to where it was clicked (--dx/--dy/--s), then plays that offset out — a FLIP
from shop to hand. Its deck slot stays empty (.is-arriving) until it lands. */
@keyframes buy-fly {
from {
transform: translate(var(--dx), var(--dy)) scale(var(--s));
}
to {
transform: translate(0, 0) scale(1);
}
}
.buy-flyer {
position: fixed;
z-index: 60;
transform-origin: top left;
animation: buy-fly 480ms cubic-bezier(0.5, 0.02, 0.3, 1) forwards;
pointer-events: none;
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.5));
}
.card-cell.is-arriving {
opacity: 0;
}
/* --- the pet in play --- */
.battle-unit-zone {
position: relative;
min-width: 100px;
min-height: 150px;
display: grid;
place-items: center;
}
/* Set-aside pets (Blowfish, Badger, …) laid out in a row above the active
pet, kept until their pending effect resolves. */
.setaside-row {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
z-index: 5;
}
/* Attached (or pending) foods fanned below the pet: a vertical stack that
overlaps ~¾ so only each card's top edge shows, last card fully on top. */
.food-fan {
position: absolute;
top: calc(100% - 6px);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
z-index: 4;
}
.food-fan-card {
position: relative;
}
.food-fan-card:not(:first-child) {
/* Slightly tighter overlap so a tall apple stack fits the play area. */
margin-top: -92px;
}
/* Released cards (a spent set-aside pet, or a used-up food perk) shrink and
fade out over their release step rather than vanishing instantly. The step
lasts 500ms (EVENT_MS.release), so the 400ms exit finishes before unmount. */
@keyframes card-leave {
to {
opacity: 0;
transform: scale(0.55) translateY(-8px);
}
}
.setaside-card.is-leaving,
.food-fan-card.is-leaving {
animation: card-leave 400ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
pointer-events: none;
}
/* A freshly dealt food (apple) sails in from the deck — up and toward the outer
edge — and settles onto the fan, rather than blinking into place. Direction
follows the side the deck sits on (see food-in-left / food-in-right). */
@keyframes food-in-left {
from {
opacity: 0;
transform: translate(-64px, -52px) rotate(-14deg) scale(0.6);
}
to {
opacity: 1;
transform: translate(0, 0) rotate(0) scale(1);
}
}
@keyframes food-in-right {
from {
opacity: 0;
transform: translate(64px, -52px) rotate(14deg) scale(0.6);
}
to {
opacity: 1;
transform: translate(0, 0) rotate(0) scale(1);
}
}
.food-fan-card.food-in-left {
animation: food-in-left 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
.food-fan-card.food-in-right {
animation: food-in-right 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
/* A pet that faints with a pending effect rises up out of the arena into the
set-aside row, shrinking from its arena size to the smaller set-aside size. */
@keyframes setaside-in {
from {
opacity: 0;
transform: translateY(150px) scale(1.2);
}
55% {
opacity: 1;
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.setaside-card.setaside-in {
animation: setaside-in 600ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
.battle-unit {
position: relative;
flex-shrink: 0;
}
.battle-unit .card {
width: 96px;
/* Taller than a shop card so the full ability text fits without shrinking to
an unreadable size during the battle. */
height: 162px;
}
/* Battle cards are short, so give the ability text more of the card by
shrinking the decorative art; useFitText then keeps the text at a larger
size before it has to shrink to fit. */
.battle-unit .card-art {
font-size: 1.5rem;
}
/* A pet flips face-up off its deck: it starts over the stack (outer edge) still
turned edge-on, then slides in toward its arena slot while turning to face us.
The deck sits to the outer side of each unit, so each side pulls the reveal in
from its own edge. */
@keyframes unit-reveal-left {
from {
opacity: 0;
transform: translateX(-108px) perspective(760px) rotateY(-92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateX(0) perspective(760px) rotateY(0deg) scale(1);
}
}
@keyframes unit-reveal-right {
from {
opacity: 0;
transform: translateX(108px) perspective(760px) rotateY(92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateX(0) perspective(760px) rotateY(0deg) scale(1);
}
}
.battle-unit.unit-reveal-left {
transform-origin: left center;
animation: unit-reveal-left 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
.battle-unit.unit-reveal-right {
transform-origin: right center;
animation: unit-reveal-right 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
/* --- rock dice --- */
/* A little tray of dice, drawn just below the throwing side's deck (see
.stack-dice). Fixed slots: the dice scramble in place, then settle showing
the rolled rock faces. */
.stack-dice {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
z-index: 8;
pointer-events: none;
}
.dice-roll {
display: flex;
gap: 7px;
}
.droll {
position: relative;
width: 34px;
height: 34px;
border-radius: 7px;
box-sizing: border-box;
border: 2px solid var(--cocoa);
background: linear-gradient(145deg, var(--cream), var(--cream-dark));
box-shadow:
0 3px 5px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.droll.is-rolling {
animation: droll-shake 170ms linear infinite;
animation-delay: calc(var(--i) * 45ms);
}
.droll.is-settled {
animation: droll-settle 280ms cubic-bezier(0.2, 0.85, 0.3, 1) both;
animation-delay: calc(var(--i) * 55ms);
}
@keyframes droll-shake {
0% {
transform: rotate(-9deg) translateY(0) scale(1.06);
}
25% {
transform: rotate(7deg) translateY(-2px) scale(1.09);
}
50% {
transform: rotate(-6deg) translateY(1px) scale(1.05);
}
75% {
transform: rotate(8deg) translateY(-1px) scale(1.08);
}
100% {
transform: rotate(-9deg) translateY(0) scale(1.06);
}
}
@keyframes droll-settle {
0% {
transform: scale(1.22) rotate(-5deg);
}
60% {
transform: scale(0.95) rotate(1deg);
}
100% {
transform: scale(1) rotate(0);
}
}
/* A rock icon: a lumpy gray boulder. */
.droll-rock {
position: absolute;
width: 14px;
height: 14px;
background: radial-gradient(circle at 35% 28%, #838a90, #34373c);
clip-path: polygon(22% 6%, 60% 0%, 90% 24%, 100% 58%, 82% 96%, 42% 100%, 8% 82%, 0% 40%);
}
.droll-n-1 .droll-rock {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.droll-n-2 .droll-rock {
width: 12px;
height: 12px;
}
.droll-n-2 .droll-rock:nth-child(1) {
top: 30%;
left: 30%;
transform: translate(-50%, -50%);
}
.droll-n-2 .droll-rock:nth-child(2) {
top: 70%;
left: 70%;
transform: translate(-50%, -50%);
}
@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: 12px;
align-items: center;
padding: 8px 0;
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.6rem;
text-shadow: 0 3px 0 rgba(0, 0, 0, 0.3), 0 6px 14px rgba(0, 0, 0, 0.4);
}
.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: #6fd6c2;
}
.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: 20px;
text-align: center;
}
.gameover-emoji {
font-size: 4.4rem;
filter: drop-shadow(0 8px 10px rgba(0, 0, 0, 0.45));
animation: float 3s ease-in-out infinite;
}
.gameover-title {
font-size: 3.2rem;
color: var(--gold);
text-shadow:
0 2px 0 var(--gold-deep),
0 5px 0 rgba(90, 58, 32, 0.5),
0 8px 16px rgba(0, 0, 0, 0.4);
}
.gameover-scores {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(0, 0, 0, 0.24);
border-radius: 16px;
padding: 20px 28px;
min-width: min(360px, 90vw);
box-shadow: var(--tray-inset);
}
.score-line {
display: flex;
justify-content: space-between;
gap: 24px;
font-size: 1.1rem;
font-weight: 700;
padding: 4px 0;
}
.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: linear-gradient(180deg, #e85c49, var(--red));
color: #fff;
font-weight: 700;
border-radius: 12px;
padding: 11px 22px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
0 8px 18px rgba(0, 0, 0, 0.45);
animation: result-in 200ms ease;
z-index: 100;
}
.banner {
position: fixed;
top: 66px;
left: 50%;
transform: translateX(-50%);
border-radius: 10px;
padding: 7px 18px;
font-size: 0.9rem;
font-weight: 700;
z-index: 90;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4);
}
.banner-warn {
background: rgba(217, 74, 56, 0.92);
}
@media (max-width: 600px) {
.card {
width: 92px;
height: 128px;
}
.arrange-marker {
height: 128px;
}
.card-lg {
width: 104px;
height: 146px;
}
.battle-unit .card {
width: 78px;
height: 134px;
}
}
/* --- debug panel (server DEBUG mode) --- */
.debug-panel {
position: fixed;
top: 66px;
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;
/* The button is flipped 180° (below) to read text upward, which also flips
these corners — so round the right pair here to land rounded corners on
the inner (screen-facing) edge after the rotation. */
border-radius: 0 8px 8px 0;
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;
}
/* Right-anchored variant: scale outward from the top-right so the preview
grows leftward and stays on-screen when hovered near the right edge. */
.card-magnify-flip {
transform-origin: top right;
}
/* 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: 180px;
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: 1px solid var(--cocoa);
border-radius: 12px;
padding: 10px 12px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 10px 24px rgba(0, 0, 0, 0.55);
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);
/* Read right-to-left like the arrangement screen: the first (top) card sits
on the right and later pets flow left. */
direction: rtl;
}
/* Keep each card's own contents left-to-right despite the rtl row. */
.deck-peek-cards > * {
direction: ltr;
}
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
below the popover (z 60) but above the board. */
.deck-peek-backdrop {
position: fixed;
inset: 0;
z-index: 55;
}