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

2847 lines
60 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;
}
/* A centred portrait column — the app is laid out for a tall, narrow play
field, so it stays compact even on a wide desktop (the event log docks
alongside it). */
.table-main {
flex: 1;
padding: 22px 16px 48px;
max-width: 620px;
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);
}
/* On wide screens the dock wrapper is transparent to layout, so the log docks
as a right column exactly as its own rules describe. */
.event-log-dock {
display: contents;
}
/* The floating log button and drawer backdrop only exist on narrow screens. */
.log-fab,
.log-backdrop {
display: none;
}
/* Narrow screens: the event log becomes a drawer that slides in from the right
over the play field, opened by a floating button. */
@media (max-width: 1000px) {
.event-log-dock {
display: block;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: min(340px, 88vw);
z-index: 80;
transform: translateX(100%);
transition: transform 240ms ease;
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.5);
}
.event-log-dock.is-open {
transform: translateX(0);
}
.event-log-dock .event-log {
width: 100%;
height: 100%;
max-height: none;
position: static;
border-left: 1px solid rgba(0, 0, 0, 0.4);
}
.event-log-dock .event-log.is-collapsed {
width: 100%;
}
.event-log-dock .event-log.is-collapsed .event-log-head {
writing-mode: horizontal-tb;
height: auto;
}
.log-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 75;
background: rgba(10, 20, 14, 0.5);
backdrop-filter: blur(1.5px);
animation: result-in 200ms ease;
}
.log-fab {
display: grid;
place-items: center;
position: fixed;
right: 14px;
bottom: 14px;
width: 52px;
height: 52px;
padding: 0;
border-radius: 50%;
z-index: 70;
font-size: 1.4rem;
background: linear-gradient(180deg, var(--wood-light), var(--wood));
color: var(--cream);
border: none;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 4px 0 var(--wood-dark),
0 6px 14px rgba(0, 0, 0, 0.45);
}
.log-fab:active {
transform: translateY(3px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 1px 0 var(--wood-dark),
0 2px 6px rgba(0, 0, 0, 0.4);
}
}
/* ---------- 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. */
/* Cards echo the printed board-game cards: a chunky golden frame around a
landscape illustration up top and a green-framed rule panel below. */
.card {
position: relative;
width: 114px;
height: 154px;
background: linear-gradient(165deg, #f8c451, #f0a828 55%, #e2941c);
border-radius: var(--card-radius);
color: var(--ink);
display: flex;
flex-direction: column;
padding: 5px;
gap: 4px;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.5),
inset 0 0 0 2.5px rgba(150, 92, 18, 0.55),
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;
}
.card-lg {
width: 140px;
height: 190px;
}
.card-sm {
width: 88px;
height: 120px;
}
.card.is-clickable {
cursor: pointer;
}
.card.is-clickable:hover {
transform: translateY(-8px) rotate(-1.2deg);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.5),
inset 0 0 0 2.5px rgba(150, 92, 18, 0.55),
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 2.5px rgba(150, 92, 18, 0.55),
0 0 20px rgba(246, 201, 78, 0.5),
var(--shadow-card);
}
.card.is-disabled {
filter: saturate(0.55) brightness(0.86);
}
.card.is-dead {
filter: grayscale(1);
}
/* --- Upper illustration: a stylised landscape --- */
.card-scene {
position: relative;
/* Fixed share of the card so the panel below (and the ability text inside it)
stays height-bounded — that's what lets useFitText shrink long abilities. */
flex: 0 0 58%;
min-height: 0;
border-radius: 7px;
overflow: hidden;
box-shadow: inset 0 0 0 1.5px rgba(90, 55, 15, 0.35);
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(120% 70% at 50% -8%, rgba(255, 255, 255, 0.4), transparent 55%),
linear-gradient(
180deg,
#c2e8f8 0%,
#a7dcf0 24%,
#74bf68 44%,
#57a44b 58%,
#e0a751 66%,
#d5942f 86%,
#57a44b 90%,
#4c9640 100%
);
}
/* Distant mountain silhouette in the hills band. */
.card-scene::before {
content: '';
position: absolute;
left: -2%;
right: -2%;
top: 24%;
height: 22%;
background: linear-gradient(180deg, #6ea0c6, #5388b2);
clip-path: polygon(
0 100%, 10% 45%, 20% 78%, 34% 22%, 47% 66%, 60% 30%, 73% 72%, 85% 40%, 100% 82%, 100% 100%
);
opacity: 0.9;
}
/* A row of rounded bush tops where the greenery begins. */
.card-scene::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 40%;
height: 12%;
background: radial-gradient(circle at 50% 100%, #3f8f3a 0 45%, transparent 47%) 0 0 / 21% 100% repeat-x;
opacity: 0.92;
}
/* The pet reads as a die-cut sticker: a white outline hugging the art. */
.card-art {
position: relative;
z-index: 2;
font-size: 2.5rem;
line-height: 1;
transform: translateY(8%);
filter:
drop-shadow(0 0 1.5px #fff) drop-shadow(0 0 1.5px #fff) drop-shadow(0 0 1.5px #fff)
drop-shadow(0 3px 3px rgba(0, 0, 0, 0.35));
}
.card-lg .card-art {
font-size: 3.1rem;
}
.card-sm .card-art {
font-size: 2rem;
}
/* --- Lower rule panel --- */
.card-panel {
flex: 1 1 auto;
min-height: 0;
background: #fbf7ee;
border: 2px solid #4e9a3f;
border-radius: 8px;
padding: 3px 4px 4px;
display: flex;
flex-direction: column;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
}
/* Small cards (no ability text) get a compact name plate and a taller scene. */
.card-sm .card-scene {
flex: 1 1 auto;
}
.card-sm .card-panel {
flex: 0 0 auto;
}
.card-panel-head {
display: grid;
/* minmax(0, 1fr) lets the name column shrink below its min-content so a long
name wraps instead of shoving the tier die out past the card edge. */
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 4px;
border-bottom: 1.5px solid rgba(78, 154, 63, 0.4);
padding-bottom: 2px;
}
.card-hat {
width: 20px;
height: 13px;
display: block;
}
.card-die {
width: 15px;
height: 15px;
display: block;
justify-self: end;
}
.card-panel-slot {
display: block;
width: 15px;
}
.card-lg .card-hat {
width: 23px;
height: 15px;
}
.card-lg .card-die,
.card-lg .card-panel-slot {
width: 17px;
height: 17px;
}
.card-sm .card-hat {
width: 16px;
height: 10px;
}
.card-sm .card-die,
.card-sm .card-panel-slot {
width: 12px;
height: 12px;
}
.card-name {
font-family: var(--font-display);
font-size: 0.78rem;
line-height: 1.05;
color: var(--cocoa);
text-align: center;
min-width: 0;
overflow-wrap: break-word;
/* Cap the name at ~two lines; useFitText shrinks longer names to fit. */
max-height: 2.1em;
overflow: hidden;
}
.card-lg .card-name {
font-size: 0.9rem;
}
.card-sm .card-name {
font-size: 0.66rem;
}
.card-effect {
font-size: 0.52rem;
line-height: 1.18;
font-weight: 700;
color: var(--ink-soft);
text-align: center;
padding: 2px 1px 0;
/* Grow to fill the panel and center short abilities; long ones top-align so
the leading words are never the clipped ones. */
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 2px;
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.6rem;
}
.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 red starburst floating at the top of the illustration,
like a little explosion carrying the pet's attack. The spikes are cut with
clip-path, so the drop shadow lives on `filter`. */
.card-power {
position: absolute;
top: 4%;
left: 50%;
transform: translateX(-50%);
z-index: 3;
font-family: var(--font-display);
background: radial-gradient(circle at 40% 30%, #ff8a6b 2%, #e23b28 44%, #bd2716);
color: #fff;
-webkit-text-stroke: 0.5px rgba(120, 20, 5, 0.85);
text-shadow: 0 1px 1px rgba(120, 20, 5, 0.8);
width: 30px;
height: 30px;
display: grid;
place-items: center;
font-size: 0.92rem;
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 1.5px 2px rgba(20, 12, 4, 0.55));
}
.card-power.is-buffed {
background: radial-gradient(circle at 40% 30%, #c9ffd4 4%, #4fd07f 42%, #1f9e55);
-webkit-text-stroke: 0.5px rgba(15, 60, 30, 0.85);
text-shadow: 0 1px 1px rgba(15, 60, 30, 0.7);
filter: drop-shadow(0 0 5px rgba(80, 220, 120, 0.75));
}
.card-lg .card-power {
width: 35px;
height: 35px;
font-size: 1.02rem;
}
.card-sm .card-power {
width: 24px;
height: 24px;
font-size: 0.74rem;
}
.card-damage {
position: absolute;
top: 5%;
right: 5%;
z-index: 3;
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: 1px 6px;
font-size: 0.72rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* Food cards swap the golden frame for a warm terracotta one, ailments a cold
violet one, so the three kinds read apart at a glance. */
.card-food {
background: linear-gradient(165deg, #f3a67a, #e07f4f 55%, #cf6a3a);
}
.card-food .card-panel {
border-color: #c96a3a;
}
.card-food .card-effect,
.card-food .card-name {
color: #8a3320;
}
.card-ailment .card-panel {
border-color: #6f57b0;
}
.card-ailment .card-effect,
.card-ailment .card-name {
color: #45308a;
}
.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;
}
/* The "it's your turn" prompt as a solid golden badge with light text, so it
reads as a call to action rather than bare pulsing text. */
.shop-status-pill {
display: inline-block;
font-family: var(--font-display);
font-size: 0.92em;
color: #fff8e6;
background: linear-gradient(180deg, var(--gold), var(--gold-deep));
padding: 0.22em 0.85em;
border-radius: 999px;
border: 2px solid rgba(255, 255, 255, 0.5);
text-shadow: 0 1px 3px rgba(120, 78, 8, 0.5);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -4px 10px rgba(150, 100, 20, 0.3),
0 4px 14px rgba(0, 0, 0, 0.35);
}
@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);
}
/* Bigfoot's shop-deck peek (Unicorn pack): a button and the revealed card. */
.peek-indicator {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin: 12px auto 0;
width: fit-content;
}
/* Unicorn ailment cards: a cold violet frame (panel + text colours are set
with the other card-kind overrides above). */
.card-ailment {
background: linear-gradient(165deg, #b9a6e6, #9a83d9 55%, #7d64c6);
}
/* Ailment badges pinned to a battle unit. */
.ailment-badges {
position: absolute;
top: -8px;
left: -8px;
display: flex;
gap: 3px;
z-index: 5;
}
.ailment-badge {
font-size: 0.8rem;
font-weight: 800;
line-height: 1;
padding: 3px 5px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.35);
background: rgba(20, 16, 40, 0.82);
color: #fff;
}
.ailment-spooked {
box-shadow: 0 0 6px rgba(150, 130, 255, 0.7);
}
.ailment-exposed {
box-shadow: 0 0 6px rgba(255, 120, 90, 0.7);
}
/* 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;
}
/* A vertical battle line laid into a felt tray: card 0 (fights first) sits at
the top and later pets flow downward. */
.arrange-col {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
background:
radial-gradient(ellipse 120% 80% 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 20px;
min-height: 224px;
box-shadow: var(--tray-inset);
}
/* The "first to fight" label sits at the top of the column, above the first
card. It's a static first child (no data-flip-key), so the FLIP animator
ignores it. */
.arrange-marker {
font-family: var(--font-display);
color: var(--gold);
font-size: 0.95rem;
letter-spacing: 0.04em;
opacity: 0.85;
padding-bottom: 2px;
}
/* Each row: reorder arrows on the left, the card on the right. */
.arrange-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
cursor: grab;
}
.arrange-card:active {
cursor: grabbing;
}
.arrange-arrows {
display: flex;
flex-direction: column;
gap: 6px;
}
/* ---------- 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;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 6px;
background:
radial-gradient(ellipse 80% 55% 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;
/* Side padding leaves room for set-aside pets (left) and attached foods
(right) to fan out beside each pet; a big apple stack (Manatee, Fire Ant)
scrolls horizontally rather than clipping. */
padding: 18px 14px;
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-height: 2.4rem;
}
/* A soft seam across the middle of the arena, where the two sides clash. */
.battle-center::before {
content: '';
position: absolute;
left: 8%;
right: 8%;
height: 2px;
background: linear-gradient(90deg, 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));
}
/* One player's half. The opponent (top) stacks deck-over-pet; you (bottom)
stack pet-over-deck, so both decks sit on the outer edge and the two pets
meet at the centre seam. */
.battle-side {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
min-width: 0;
}
/* Centres the deck stack under (you) / over (opponent) the pet. */
.battle-deck {
display: flex;
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 onto the deck. The pet sits toward the centre
seam — above your deck (bottom half) and below the opponent's (top half) — so
each spawn is pulled in from its own pet's side. */
@keyframes summon-to-bottom {
0% {
opacity: 0;
transform: translateY(-70px) scale(0.5);
}
25% {
opacity: 1;
transform: translateY(-30px) scale(1.05);
}
70% {
opacity: 1;
transform: translateY(4px) scale(1);
}
100% {
opacity: 0;
transform: translateY(14px) scale(0.72);
}
}
@keyframes summon-to-top {
0% {
opacity: 0;
transform: translateY(70px) scale(0.5);
}
25% {
opacity: 1;
transform: translateY(30px) scale(1.05);
}
70% {
opacity: 1;
transform: translateY(-4px) scale(1);
}
100% {
opacity: 0;
transform: translateY(-14px) scale(0.72);
}
}
/* You throw from the bottom (deck below the pet); the opponent from the top. */
.summon-pop.summon-to-bottom {
top: auto;
bottom: calc(100% - 24px);
animation-name: summon-to-bottom;
}
.summon-pop.summon-to-top {
top: calc(100% - 24px);
animation-name: summon-to-top;
}
/* 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 to the left of the
active pet, kept until their pending effect resolves. */
.setaside-row {
position: absolute;
right: calc(100% + 6px);
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 6px;
z-index: 5;
}
/* Attached (or pending) foods fanned to the right of the pet: a horizontal row
that overlaps ~¾ so only each card's left edge shows, last card fully on top. */
.food-fan {
position: absolute;
left: calc(100% - 6px);
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: row;
align-items: center;
z-index: 4;
}
.food-fan-card {
position: relative;
}
.food-fan-card:not(:first-child) {
/* Overlap so a tall apple stack stays compact beside the pet. */
margin-left: -64px;
}
/* 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 and settles onto the fan,
rather than blinking into place. The deck sits below the pet on your side and
above it on the opponent's, so the apple flies up (bottom) or down (top). */
@keyframes food-in-bottom {
from {
opacity: 0;
transform: translate(-30px, 70px) rotate(-12deg) scale(0.6);
}
to {
opacity: 1;
transform: translate(0, 0) rotate(0) scale(1);
}
}
@keyframes food-in-top {
from {
opacity: 0;
transform: translate(-30px, -70px) rotate(12deg) scale(0.6);
}
to {
opacity: 1;
transform: translate(0, 0) rotate(0) scale(1);
}
}
.food-fan-card.food-in-bottom {
animation: food-in-bottom 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
.food-fan-card.food-in-top {
animation: food-in-top 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
/* A pet that faints with a pending effect slides out of the arena into the
set-aside row on its left, shrinking to the smaller set-aside size. */
@keyframes setaside-in {
from {
opacity: 0;
transform: translateX(120px) scale(1.2);
}
55% {
opacity: 1;
}
to {
opacity: 1;
transform: translateX(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.95rem;
}
/* A pet flips face-up off its deck: it starts over the stack (outer edge) still
turned edge-on, then rises in toward its arena slot while turning to face us.
The deck sits below your pet and above the opponent's, so each side pulls the
reveal in from its own edge. */
@keyframes unit-reveal-bottom {
from {
opacity: 0;
transform: translateY(108px) perspective(760px) rotateX(-92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateY(0) perspective(760px) rotateX(0deg) scale(1);
}
}
@keyframes unit-reveal-top {
from {
opacity: 0;
transform: translateY(-108px) perspective(760px) rotateX(92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateY(0) perspective(760px) rotateX(0deg) scale(1);
}
}
.battle-unit.unit-reveal-bottom {
transform-origin: center bottom;
animation: unit-reveal-bottom 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
.battle-unit.unit-reveal-top {
transform-origin: center top;
animation: unit-reveal-top 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
/* --- rock dice --- */
/* A little tray of dice, drawn beside the throwing side's deck on the side that
faces the centre seam — above your deck, below the opponent's. Fixed slots:
the dice scramble in place, then settle showing the rolled rock faces. */
.stack-dice {
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 8;
pointer-events: none;
}
.battle-deck-bottom .stack-dice {
bottom: calc(100% + 8px);
}
.battle-deck-top .stack-dice {
top: calc(100% + 8px);
}
.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);
}
/* You (bottom) lunge up into the seam; the opponent (top) lunges down. */
@keyframes clash-bottom {
0% {
transform: translateY(0);
}
35% {
transform: translateY(-26px) rotate(2deg);
}
60% {
transform: translateY(6px);
}
100% {
transform: translateY(0);
}
}
@keyframes clash-top {
0% {
transform: translateY(0);
}
35% {
transform: translateY(26px) rotate(-2deg);
}
60% {
transform: translateY(-6px);
}
100% {
transform: translateY(0);
}
}
.battle-unit.clash-bottom {
animation: clash-bottom 500ms ease;
}
.battle-unit.clash-top {
animation: clash-top 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;
}
.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;
}
/* Keep the illustration a healthy size while the panel grows to fit the full,
unclipped ability text. */
.card-magnify .card-scene {
flex: 0 0 auto;
height: 104px;
}
.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;
align-items: center;
gap: 6px;
max-width: min(60vw, 560px);
}
/* first-left: the lineup runs first-to-last, left to right, with the "⚔️ first"
marker as the first DOM child so it lands next to the first card. */
.deck-peek-cards.first-left {
direction: ltr;
}
/* Compact echo of the arrangement phase's "⚔️ first" indicator. */
.deck-peek-first {
display: flex;
align-items: center;
align-self: stretch;
font-family: var(--font-display);
font-size: 0.68rem;
color: var(--gold);
white-space: nowrap;
padding: 0 2px;
}
/* Deck peek shown as a centered modal (shop-phase rival deck): cap the width so
a wide lineup wraps instead of running off-screen. */
.deck-peek-modal {
padding: 20px;
gap: 12px;
max-width: min(92vw, 720px);
}
.deck-peek-modal .deck-peek-cards {
max-width: 100%;
justify-content: flex-start;
}
.deck-peek-modal .deck-peek-label {
font-size: 0.85rem;
}