Files
Pete/internal/web/static/css/input.css
prosolis 39ed293f4f games: the word you owe the table, and the hand you were already holding
Three things, and the first one was a bug.

Your own hand didn't move until the lap ended. bump() keeps the bots'
fans honest and has always refused seat zero, and nothing else touched
yours — so a +4 landing on you at the top of a lap put four backs into
your hand and then nothing, and the cards themselves turned up seconds
later when the script finished and paint() finally ran. You spent the
whole lap looking at a hand you no longer held. The engine now stamps
your hand onto every event that changes it (Event.Hand, seat zero only,
which is the one hand the browser is already entitled to see) and the
table redraws as the cards land. Measured in the running app: 2 -> 3
cards at 414ms into a 1791ms lap.

You couldn't call UNO, and not because the button was missing: going
down to one card *was* the call. discard() fired the uno event by
itself, which made it a thing that happened to you rather than a thing
you did, and a rule nobody can fail is not a rule. So now you say it or
you don't (Move.Uno), and if you don't, every bot still in the game gets
one look at you before any of them plays — because a bot that has moved
on is a bot that has stopped watching your hand. It runs the other way
too, and that half is the fun one: a bot forgets often enough to be
worth watching for, and when it does it says *nothing*. No event, no
badge, no tell on the felt except the count beside its fan reading
"1 card". Catch it and it takes two; call a seat that had nothing to
hide and you take two yourself, which is what stops the catch button
from being a thing you simply mash.

Which cards owe the call is the engine's answer, not a count of your
hand: No Mercy's "discard all" takes every card of its colour with it,
so a six-card hand can land on one, and a browser subtracting one from
six walks you into a catch it never warned you about.

And the room was silent. Every sound in here is *made* — an oscillator,
a burst of filtered noise, an envelope — the same bargain the weather
engine takes with its clouds. A card is a slap of noise through a
bandpass, a chip is two detuned sines with a knock on the front, a win
is four notes going up. No asset files, no round trips, and a sound can
be pitched and detuned per call instead of being the same wav three
hundred times. Hooked into the FX layer rather than into the games, so
every table that throws a chip or turns a card got it at once.

The multiples moved, and the test that exists to catch that caught it.
The naive strategy now calls UNO, because calling is a button and not a
strategy — what these tiers price is bad card play, not a player who
ignores the felt shouting at them — and on that footing the normal
tables come back to where they were (40.1 / 28.5 / 23.1). No Mercy Full
House did not: it was paying a *negative* house edge, which is the house
paying you to sit down. Re-priced 3.8 -> 3.5.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
2026-07-14 13:15:11 -07:00

2362 lines
82 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ----------------------------------------------------------------------------
Day/night palette. JS sets data-phase={dawn|day|dusk|night} on <html>.
Each phase drives a small set of CSS vars; the page transitions smoothly
thanks to the `transition-colors duration-1000` on <body>.
---------------------------------------------------------------------------- */
:root,
html[data-phase="day"] {
--bg: #fff7e4; /* warm cream */
--bg-grad: #ffeec2; /* soft sun wash */
--card: #ffffff;
--ink: #3a2e1f;
--accent: #f2a541; /* sunshine yellow */
}
html[data-phase="dawn"] {
--bg: #ffe7d6;
--bg-grad: #ffc9c9;
--card: #fff4ea;
--ink: #4a2e2a;
--accent: #ff8a65;
}
html[data-phase="dusk"] {
--bg: #ffd6a8;
--bg-grad: #f7a07e;
--card: #fff1de;
--ink: #3d2417;
--accent: #e6553a;
}
html[data-phase="night"] {
--bg: #1a1f3a;
--bg-grad: #2a3358;
--card: #2d365a;
--ink: #f1ecd8; /* moonlight */
--accent: #f9d976; /* lantern */
}
@layer base {
body { font-family: "Nunito", system-ui, sans-serif; }
html { scroll-behavior: smooth; }
/* Belt-and-braces: never let stray wide content push horizontal scroll on
the whole page (the channel nav scrolls inside itself instead). */
html, body { overflow-x: hidden; }
}
@layer components {
/* Channel pill row may exceed viewport on mobile once enough channels are
configured. Let it scroll inside its own rounded container and hide the
scrollbar chrome so it still reads as a clean pill. */
.pete-channel-nav {
scrollbar-width: none;
-ms-overflow-style: none;
}
.pete-channel-nav::-webkit-scrollbar { display: none; }
}
@layer utilities {
.font-display { font-family: "Fredoka", "Nunito", system-ui, sans-serif; letter-spacing: -0.01em; }
/* Soft signage shadow — Animal Crossing wooden plank vibe. */
.shadow-pete { box-shadow: 0 4px 0 rgba(60,40,20,0.10), 0 8px 24px rgba(60,40,20,0.08); }
.shadow-pete-lg { box-shadow: 0 6px 0 rgba(60,40,20,0.12), 0 16px 32px rgba(60,40,20,0.12); }
/* Per-channel theme colors. Kept as plain utilities so Tailwind doesn't
need to know to safelist them. */
.bg-theme-gaming { background-color: #4caf7d; }
.bg-theme-tech { background-color: #5aa9e6; }
.bg-theme-politics { background-color: #e07a5f; }
.bg-theme-eu { background-color: #003399; }
.bg-theme-music { background-color: #b079d6; }
.bg-theme-anime { background-color: #ec5e8a; }
.bg-theme-foss { background-color: #d97706; }
.bg-theme-kids { background-color: #14b8a6; }
.bg-theme-finance { background-color: #10b981; }
.bg-theme-lego { background-color: #d01012; }
.bg-theme-adventure { background-color: #6d4bd8; }
.text-theme-gaming { color: #2d8a5a; }
.text-theme-tech { color: #2f7fb8; }
.text-theme-politics { color: #b8523a; }
.text-theme-eu { color: #003399; }
.text-theme-music { color: #8a4fb8; }
.text-theme-anime { color: #c33a6a; }
.text-theme-foss { color: #b8530a; }
.text-theme-kids { color: #0f766e; }
.text-theme-finance { color: #059669; }
.text-theme-lego { color: #b00d0e; }
.text-theme-adventure { color: #5836b8; }
.decoration-theme-gaming { text-decoration-color: #4caf7d; }
.decoration-theme-tech { text-decoration-color: #5aa9e6; }
.decoration-theme-politics { text-decoration-color: #e07a5f; }
.decoration-theme-eu { text-decoration-color: #003399; }
.decoration-theme-music { text-decoration-color: #b079d6; }
.decoration-theme-anime { text-decoration-color: #ec5e8a; }
.decoration-theme-foss { text-decoration-color: #d97706; }
.decoration-theme-kids { text-decoration-color: #14b8a6; }
.decoration-theme-finance { text-decoration-color: #10b981; }
.decoration-theme-lego { text-decoration-color: #d01012; }
.decoration-theme-adventure { text-decoration-color: #6d4bd8; }
.border-theme-gaming { border-color: #4caf7d; }
.border-theme-tech { border-color: #5aa9e6; }
.border-theme-politics { border-color: #e07a5f; }
.border-theme-eu { border-color: #003399; }
.border-theme-music { border-color: #b079d6; }
.border-theme-anime { border-color: #ec5e8a; }
.border-theme-foss { border-color: #d97706; }
.border-theme-kids { border-color: #14b8a6; }
.border-theme-finance { border-color: #10b981; }
.border-theme-lego { border-color: #d01012; }
.border-theme-adventure { border-color: #6d4bd8; }
/* "Posted to Matrix" glow — soft pulsing aura in the channel's theme color. */
.glow-theme-gaming { box-shadow: 0 0 0 2px rgba(76,175,125,0.35), 0 0 24px 4px rgba(76,175,125,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-tech { box-shadow: 0 0 0 2px rgba(90,169,230,0.35), 0 0 24px 4px rgba(90,169,230,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-politics { box-shadow: 0 0 0 2px rgba(224,122,95,0.35), 0 0 24px 4px rgba(224,122,95,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-eu { box-shadow: 0 0 0 2px rgba(0,51,153,0.35), 0 0 24px 4px rgba(0,51,153,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-music { box-shadow: 0 0 0 2px rgba(176,121,214,0.35), 0 0 24px 4px rgba(176,121,214,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-anime { box-shadow: 0 0 0 2px rgba(236,94,138,0.35), 0 0 24px 4px rgba(236,94,138,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-foss { box-shadow: 0 0 0 2px rgba(217,119,6,0.35), 0 0 24px 4px rgba(217,119,6,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-kids { box-shadow: 0 0 0 2px rgba(20,184,166,0.35), 0 0 24px 4px rgba(20,184,166,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-finance { box-shadow: 0 0 0 2px rgba(16,185,129,0.35), 0 0 24px 4px rgba(16,185,129,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-lego { box-shadow: 0 0 0 2px rgba(208,16,18,0.35), 0 0 24px 4px rgba(208,16,18,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
.glow-theme-adventure { box-shadow: 0 0 0 2px rgba(109,75,216,0.35), 0 0 24px 4px rgba(109,75,216,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
@keyframes pete-glow-pulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.08); }
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* "PAYWALLED" diagonal rubber stamp overlay — slapped across the whole
card when the source is gated and no archive snapshot worked. The card
stays clickable (link still goes to the original); the stamp is just
visual warning. pointer-events:none keeps it from eating clicks. */
.paywall-stamp {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
background: rgba(255, 250, 240, 0.18);
}
.paywall-stamp::before {
content: "PAYWALLED";
font-family: var(--font-display, ui-sans-serif), system-ui, sans-serif;
font-weight: 900;
font-size: clamp(1rem, 3.2vw, 1.75rem);
letter-spacing: 0.12em;
color: rgba(180, 30, 30, 0.85);
border: 0.28rem double rgba(180, 30, 30, 0.85);
padding: 0.3rem 0.9rem;
transform: rotate(-15deg);
text-shadow: 0 1px 0 rgba(255,255,255,0.4);
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.15);
background: rgba(255, 240, 230, 0.55);
border-radius: 0.25rem;
/* slight roughness to mimic a real ink stamp */
filter: contrast(1.05);
}
}
/* ----------------------------------------------------------------------------
Feed / reader mode. A focused, one-story-at-a-time overlay driven by
reader.js. Left/right arrows page through the stories currently on screen,
marking each read as it's shown. Styling leans on the phase palette vars so
it recolours with day/night like everything else.
---------------------------------------------------------------------------- */
@layer components {
.pete-reader-backdrop {
position: absolute;
inset: 0;
background: rgba(20, 14, 6, 0.55);
backdrop-filter: blur(6px);
}
html[data-phase="night"] .pete-reader-backdrop { background: rgba(6, 8, 20, 0.65); }
.pete-reader-shell {
position: relative;
height: 100%;
width: 100%;
max-width: 44rem;
margin: 0 auto;
display: flex;
flex-direction: column;
padding: max(env(safe-area-inset-top), 0.75rem) 1rem 0.75rem;
gap: 0.75rem;
}
.pete-reader-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
color: #fff;
}
.pete-reader-progress {
font-family: "Fredoka", "Nunito", system-ui, sans-serif;
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.02em;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
.pete-reader-nav { display: flex; align-items: center; gap: 0.4rem; }
.pete-reader-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding: 0 0.7rem;
border-radius: 9999px;
background: rgba(255, 255, 255, 0.14);
color: #fff;
font-weight: 700;
font-size: 0.95rem;
border: 2px solid rgba(255, 255, 255, 0.18);
cursor: pointer;
transition: background 0.15s ease, transform 0.15s ease, opacity 0.15s ease;
}
.pete-reader-btn:hover { background: rgba(255, 255, 255, 0.28); transform: translateY(-1px); }
.pete-reader-btn:disabled { opacity: 0.35; cursor: default; transform: none; }
.pete-reader-btn-open { background: var(--accent); border-color: transparent; color: #1c1305; text-decoration: none; }
.pete-reader-btn-open:hover { background: var(--accent); filter: brightness(1.08); }
/* Active/pressed toolbar toggle (Listen while speaking, Aa while menu open). */
.pete-reader-btn[aria-pressed="true"],
.pete-reader-btn[aria-expanded="true"] {
background: var(--accent);
border-color: transparent;
color: #1c1305;
}
/* Text-options popover, anchored under the Aa button in the reader bar. */
.pete-reader-type { position: relative; display: inline-flex; }
.pete-reader-typemenu[hidden] { display: none; }
.pete-reader-typemenu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
z-index: 10;
width: 15rem;
padding: 0.75rem;
border-radius: 1rem;
background: var(--card);
color: var(--ink);
border: 2px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(20, 14, 6, 0.28);
display: flex;
flex-direction: column;
gap: 0.6rem;
}
html[data-phase="night"] .pete-reader-typemenu { border-color: rgba(255, 255, 255, 0.12); }
.pete-reader-typerow { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.pete-reader-typelabel { font-size: 0.8rem; font-weight: 700; opacity: 0.7; }
.pete-reader-typebtns { display: inline-flex; gap: 0.25rem; }
.pete-reader-typebtns button {
min-width: 2rem;
height: 2rem;
padding: 0 0.55rem;
border-radius: 0.6rem;
background: rgba(20, 14, 6, 0.06);
color: var(--ink);
font-weight: 700;
font-size: 0.82rem;
border: 2px solid transparent;
cursor: pointer;
transition: background 0.15s ease;
}
html[data-phase="night"] .pete-reader-typebtns button { background: rgba(255, 255, 255, 0.08); }
.pete-reader-typebtns button:hover { background: rgba(20, 14, 6, 0.12); }
.pete-reader-typebtns button.is-active {
background: var(--accent);
color: #1c1305;
border-color: transparent;
}
.pete-reader-voicerow[hidden] { display: none; }
.pete-reader-voice {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.4rem;
border-radius: 0.55rem;
border: 1px solid rgba(20, 14, 6, 0.18);
background: rgba(20, 14, 6, 0.06);
color: inherit;
max-width: 11rem;
cursor: pointer;
}
html[data-phase="night"] .pete-reader-voice {
border-color: rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.08);
}
.pete-reader-scroll {
flex: 1 1 auto;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
border-radius: 1.75rem;
}
.pete-reader-scroll:focus { outline: none; }
.pete-reader-article {
background: var(--card);
color: var(--ink);
border-radius: 1.75rem;
border: 2px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 6px 0 rgba(60, 40, 20, 0.12), 0 16px 32px rgba(60, 40, 20, 0.18);
padding: clamp(1.25rem, 4vw, 2.5rem);
}
html[data-phase="night"] .pete-reader-article { border-color: rgba(255, 255, 255, 0.08); }
.pete-reader-eyebrow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.75rem;
margin-bottom: 0.85rem;
}
.pete-reader-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 9999px;
padding: 0.15rem 0.65rem;
color: #fff;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.pete-reader-source { font-weight: 700; opacity: 0.75; }
.pete-reader-time { opacity: 0.55; }
.pete-reader-title {
font-family: "Fredoka", "Nunito", system-ui, sans-serif;
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.01em;
font-size: clamp(1.6rem, 4.5vw, 2.4rem);
margin-bottom: 1rem;
}
.pete-reader-hero {
width: 100%;
border-radius: 1.1rem;
margin-bottom: 1.25rem;
background: rgba(0, 0, 0, 0.05);
}
/* Reader typography, driven by the Aa popover and persisted per-device. The
size lives as a CSS var on the scroll container; font + paper are classes. */
.pete-reader-scroll { --reader-fs: 1.08rem; }
.pete-reader-body { font-size: var(--reader-fs); line-height: 1.75; }
.pete-reader-body p { margin-bottom: 1.05em; }
.pete-reader-body p:last-child { margin-bottom: 0; }
.pete-reader-scroll.is-size-s { --reader-fs: 0.98rem; }
.pete-reader-scroll.is-size-m { --reader-fs: 1.08rem; }
.pete-reader-scroll.is-size-l { --reader-fs: 1.22rem; }
.pete-reader-scroll.is-size-xl { --reader-fs: 1.4rem; }
.pete-reader-scroll.is-serif .pete-reader-body {
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
}
/* Sepia paper: warm background + ink regardless of day/night phase. */
.pete-reader-scroll.is-sepia .pete-reader-article {
background: #f6ecd6;
color: #4a3a28;
border-color: rgba(74, 58, 40, 0.16);
}
.pete-reader-scroll.is-sepia .pete-reader-note { border-top-color: rgba(74, 58, 40, 0.18); opacity: 0.8; }
.pete-reader-scroll.is-sepia .pete-reader-hero { background: rgba(74, 58, 40, 0.08); }
/* Currently-spoken paragraph highlight while reading aloud. */
.pete-reader-body p.is-speaking {
background: color-mix(in srgb, var(--accent) 32%, transparent);
border-radius: 0.4rem;
box-shadow: 0 0 0 0.35rem color-mix(in srgb, var(--accent) 32%, transparent);
}
.pete-reader-note {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
font-size: 0.85rem;
opacity: 0.7;
}
html[data-phase="night"] .pete-reader-note { border-color: rgba(255, 255, 255, 0.12); }
.pete-reader-note a { color: var(--accent); font-weight: 700; }
.pete-reader-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 0.75rem;
padding: clamp(2rem, 8vw, 4rem) 1rem;
color: var(--ink);
}
.pete-reader-empty-emoji { font-size: 2.5rem; }
/* Transient confirmation toast (e.g. "Link copied") shown over the reader. */
.pete-reader-toast {
position: absolute;
left: 50%;
bottom: 4.5rem;
transform: translate(-50%, 0.5rem);
z-index: 20;
padding: 0.5rem 0.9rem;
border-radius: 9999px;
background: rgba(20, 14, 6, 0.9);
color: #fff;
font-weight: 700;
font-size: 0.85rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.pete-reader-toast.is-shown { opacity: 1; transform: translate(-50%, 0); }
.pete-reader-hint {
text-align: center;
color: rgba(255, 255, 255, 0.85);
font-size: 0.75rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
.pete-reader-hint kbd {
font-family: ui-monospace, monospace;
background: rgba(255, 255, 255, 0.16);
border-radius: 0.3rem;
padding: 0.05rem 0.3rem;
}
@media (max-width: 640px) {
.pete-reader-hint { display: none; }
/* With Listen/Share/Aa added, let the toolbar wrap instead of overflowing. */
.pete-reader-bar { flex-wrap: wrap; }
.pete-reader-nav { flex-wrap: wrap; justify-content: flex-end; row-gap: 0.4rem; }
}
/* "You might also like" rail, shown under the article in feed mode. Lives
inside the reader's scroll area, below the article card. */
.pete-reader-related { width: 100%; margin: 0.85rem auto 0; }
.pete-reader-related-title {
font-family: "Fredoka", "Nunito", system-ui, sans-serif;
font-weight: 700;
font-size: 0.95rem;
color: #fff;
margin: 0 0 0.6rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
.pete-reader-related-grid { display: grid; gap: 0.6rem; }
.pete-reader-related-card {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--card);
color: var(--ink);
border-radius: 1rem;
border: 2px solid rgba(0, 0, 0, 0.06);
padding: 0.6rem;
text-decoration: none;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.pete-reader-related-card:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(60, 40, 20, 0.16);
}
html[data-phase="night"] .pete-reader-related-card { border-color: rgba(255, 255, 255, 0.08); }
.pete-reader-related-thumb {
width: 4.5rem;
height: 3.25rem;
flex-shrink: 0;
object-fit: cover;
border-radius: 0.6rem;
background: rgba(0, 0, 0, 0.06);
}
.pete-reader-related-meta { min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.pete-reader-related-eyebrow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
font-size: 0.7rem;
}
.pete-reader-related-source { font-weight: 700; opacity: 0.7; }
.pete-reader-related-headline {
font-weight: 700;
line-height: 1.25;
font-size: 0.92rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Grid treatment for stories already read in feed mode: dimmed, with a small
corner check. Hovering restores full opacity so nothing feels lost. */
[data-story-card][data-read="1"] { opacity: 0.5; transition: opacity 0.2s ease; }
[data-story-card][data-read="1"]:hover { opacity: 1; }
[data-story-card][data-read="1"]::after {
content: "✓";
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 6;
display: grid;
place-items: center;
height: 1.5rem;
width: 1.5rem;
border-radius: 9999px;
background: rgba(20, 14, 6, 0.72);
color: #fff;
font-size: 0.8rem;
font-weight: 700;
}
/* ---- the casino ---------------------------------------------------------
Cards are dealt, not swapped in: each one starts at the shoe in the corner,
flies to its place, and turns over. The whole point of the table is that you
watch it happen, so this is written as one animation per card with a delay,
and the JS just says which cards exist and in what order. */
.pete-felt {
background:
radial-gradient(120% 90% at 50% -10%, rgba(255,255,255,0.10), transparent 60%),
linear-gradient(160deg, #2f7d5b 0%, #24614a 55%, #1c4d3c 100%);
}
/* The shoe: where every card comes from. */
.pete-shoe {
position: absolute;
top: 1.25rem;
right: 1.25rem;
height: 4.2rem;
width: 3rem;
border-radius: 0.55rem;
background: linear-gradient(150deg, #b4553f, #8d3f2f);
border: 2px solid rgba(0,0,0,0.18);
box-shadow: 0 4px 0 rgba(0,0,0,0.18), inset 0 0 0 3px rgba(255,255,255,0.12);
}
.pete-shoe::after {
content: "";
position: absolute;
inset: 0.45rem 0.4rem auto 0.4rem;
height: 0.5rem;
border-radius: 0.2rem;
background: rgba(0,0,0,0.22);
}
.pete-hand {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
min-height: 8.6rem;
}
/* One card. The wrapper does the flight, the inner face does the flip, so the
two never fight over the same transform. */
/* The size is a variable because solitaire has seven columns to fit and
blackjack has two hands. Everything below is in terms of it, so a table can
shrink its cards without a second set of rules. */
.pete-card {
perspective: 700px;
height: var(--card-h, 8.4rem);
width: var(--card-w, 6rem);
animation: pete-deal 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.pete-card-inner {
position: relative;
height: 100%;
width: 100%;
transform-style: preserve-3d;
transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Face-down is the resting state of a card that hasn't been turned over: the
hole card sits like this until the dealer plays. */
.pete-card[data-face="down"] .pete-card-inner { transform: rotateY(180deg); }
.pete-card-front,
.pete-card-back {
position: absolute;
inset: 0;
display: grid;
border-radius: 0.55rem;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
box-shadow: 0 3px 0 rgba(0,0,0,0.18), 0 6px 14px rgba(0,0,0,0.22);
}
/* The face is a single SVG on a 100×140 field (see blackjack.js) — suits as
vector shapes, pips at printed-deck coordinates. The card element only has
to hold it and colour it: `fill: currentColor` means the red/black switch
below repaints every pip, index and court letter at once. */
.pete-card-front {
place-items: stretch;
background: #fdfaf2;
border: 2px solid rgba(30,20,10,0.12);
color: #2b2118;
font-family: "Fredoka", "Nunito", system-ui, sans-serif;
line-height: 1;
overflow: hidden;
}
.pete-card-front[data-red="1"] { color: #cc3d4a; }
/* The back. Turned away from you until the card is: this is what the dealer's
hole card shows while you're still acting. */
.pete-card-back {
transform: rotateY(180deg);
background:
repeating-linear-gradient(45deg, rgba(255,255,255,0.10) 0 6px, transparent 6px 12px),
linear-gradient(150deg, #b4553f, #8d3f2f);
border: 2px solid rgba(0,0,0,0.15);
}
.pete-card-svg {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
}
/* The corner index. Tabular-ish and tight, so a 10 doesn't shove the suit. */
.pete-card-idx {
font-size: 26px;
font-weight: 700;
text-anchor: middle;
fill: currentColor;
}
/* The court panel: a frame, not a portrait. */
.pete-card-panel {
fill: currentColor;
fill-opacity: 0.06;
stroke: currentColor;
stroke-opacity: 0.35;
stroke-width: 1.5;
}
.pete-card-court {
font-size: 34px;
font-weight: 700;
text-anchor: middle;
fill: currentColor;
}
/* The flight itself: out of the shoe (up and to the right), scaled down and
spinning slightly, into place — and then a beat of settling, because a card
that stops dead on the felt looks like it was pasted there. It overshoots a
little and rocks back onto --tilt, the small resting angle the JS gives each
card so a hand looks handled rather than typeset. */
@keyframes pete-deal {
0% {
opacity: 0;
transform: translate(var(--deal-x, 14rem), var(--deal-y, -7rem)) scale(0.72) rotate(9deg);
}
62% {
opacity: 1;
transform: translate(0, 0.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg));
}
100% {
opacity: 1;
transform: translate(0, 0) scale(1) rotate(var(--tilt, 0deg));
}
}
/* And the weight: the felt takes the hit. The shadow under a landing card
spreads and darkens for the instant it arrives. */
.pete-card::after {
content: "";
position: absolute;
inset: auto 6% -0.35rem 6%;
height: 0.6rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.35);
filter: blur(5px);
animation: pete-thud 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes pete-thud {
0%, 55% { opacity: 0; transform: scale(0.4); }
72% { opacity: 0.9; transform: scale(1.15); }
100% { opacity: 0.45; transform: scale(1); }
}
/* A settled hand: the winning side gets a little lift, so a win reads at a
glance rather than only in the text. */
.pete-hand[data-won="1"] .pete-card { animation: pete-deal 0.42s cubic-bezier(0.22,1,0.36,1) backwards, pete-win 0.6s ease 0.1s; }
@keyframes pete-win {
0%, 100% { transform: rotate(var(--tilt, 0deg)) translateY(0); }
40% { transform: rotate(var(--tilt, 0deg)) translateY(-0.6rem); }
}
/* A losing hand doesn't just sit there being wrong: it greys off. */
.pete-hand[data-won="-1"] .pete-card {
filter: saturate(0.45) brightness(0.78);
transition: filter 0.5s ease 0.2s;
}
/* ---- chips ---------------------------------------------------------------
One disc, three jobs: the buttons you bet with, the stack sitting in the bet
spot, and the chip in mid-air between them. Same face on all three, because
they are meant to read as the *same chip* being moved around. */
.pete-disc {
position: relative;
border-radius: 999px;
background:
radial-gradient(circle at 50% 30%, rgba(255,255,255,0.30), transparent 62%),
var(--chip, #e07a5f);
border: 2px solid rgba(0,0,0,0.22);
box-shadow: 0 3px 0 rgba(0,0,0,0.28), 0 6px 14px rgba(0,0,0,0.20);
}
/* The edge spots every casino chip has, cut as a dashed ring inside the rim. */
.pete-disc::before {
content: "";
position: absolute;
inset: 11%;
border-radius: 999px;
border: 2px dashed rgba(255,255,255,0.55);
}
.pete-disc[data-chip="5"] { --chip: #5aa9e6; }
.pete-disc[data-chip="25"] { --chip: #4caf7d; }
.pete-disc[data-chip="100"] { --chip: #2b2118; }
.pete-disc[data-chip="500"] { --chip: #b079d6; }
.pete-chip { /* the buttons */
transition: transform 0.12s ease;
}
.pete-chip > span {
position: relative; /* over the dashed ring */
}
.pete-chip:active { transform: translateY(1px) scale(0.94); }
/* The bet spot: where your stake actually sits while the hand is played. */
.pete-spot {
position: relative;
display: grid;
place-items: center;
height: 7rem;
width: 7rem;
flex: none;
border-radius: 999px;
border: 2px dashed rgba(255,255,255,0.35);
background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.10), transparent 70%);
box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08);
transition: box-shadow 0.3s ease, border-color 0.3s ease;
}
.pete-spot[data-live="1"] {
border-color: rgba(var(--glow, 242,181,61), 0.85);
box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08), 0 0 22px rgba(var(--glow, 242,181,61), 0.35);
}
.pete-spot-label {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255,255,255,0.35);
}
.pete-spot[data-live="1"] .pete-spot-label { opacity: 0; }
/* The stack in the spot. Each chip is offset up the pile by its index, so ten
chips look like ten chips rather than one chip with a number on it. */
.pete-stack {
position: absolute;
inset: 0;
pointer-events: none;
}
.pete-stack .pete-disc {
position: absolute;
left: 50%;
top: 50%;
height: 2.6rem;
width: 2.6rem;
margin: -1.3rem 0 0 -1.3rem;
/* Each chip sits proud of the one below it by more than its own rim, so a
stack of three reads as three. Less than this and they hide inside each
other and a 150 bet looks the same as a 25 one. */
transform: translateY(calc(-0.5rem * var(--i, 0))) rotate(var(--spin, 0deg));
box-shadow: 0 2px 0 rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.25);
animation: pete-chip-land 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}
@keyframes pete-chip-land {
from { transform: translateY(calc(-0.5rem * var(--i, 0) - 1.1rem)) rotate(var(--spin, 0deg)) scale(1.18); }
}
.pete-spot-total {
position: absolute;
bottom: -0.6rem;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
border-radius: 999px;
background: rgba(0,0,0,0.45);
padding: 0.1rem 0.6rem;
font-size: 0.72rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #fff;
}
/* A chip in mid-air. Lives in a fixed overlay so it can cross from the betting
buttons, over the page, and onto the felt without any container clipping it. */
.pete-fly-layer {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
overflow: hidden;
}
.pete-fly {
position: absolute;
height: 2.6rem;
width: 2.6rem;
margin: -1.3rem 0 0 -1.3rem;
will-change: transform;
}
/* The house's own chips, racked on the felt. This is where a payout comes
from, and where a losing stake goes — so the money has somewhere to be.
It sits alongside the shoe rather than over on the left, because the left of
the felt is where the dealer's cards land. */
.pete-rack {
position: absolute;
top: 1.25rem;
right: 5.75rem;
display: flex;
align-items: flex-end;
gap: 0.4rem;
padding: 0.5rem 0.6rem 0.45rem;
border-radius: 0.75rem;
background: rgba(0,0,0,0.20);
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.06);
}
/* A column in the rack is a *stack*, so it's striped: one band per chip, with
the shadow of the chip above it cut into each. A plain coloured lozenge here
read as a blob, which is the one thing a pile of money shouldn't. */
.pete-rack span {
display: block;
width: 1.7rem;
border-radius: 0.35rem;
background:
linear-gradient(90deg, rgba(0,0,0,0.28), transparent 35%, transparent 65%, rgba(0,0,0,0.28)),
repeating-linear-gradient(180deg,
rgba(255,255,255,0.18) 0 0.06rem,
var(--chip, #e07a5f) 0.06rem 0.3rem,
rgba(0,0,0,0.35) 0.3rem 0.36rem);
border: 1px solid rgba(0,0,0,0.35);
height: calc(0.36rem * var(--stack, 3));
}
.pete-rack span[data-chip="5"] { --chip: #5aa9e6; }
.pete-rack span[data-chip="25"] { --chip: #4caf7d; }
.pete-rack span[data-chip="100"] { --chip: #2b2118; }
.pete-rack span[data-chip="500"] { --chip: #b079d6; }
/* On a phone the rack has to get out of the way. At its desk size it is a
147px block sitting 92px in from the edge — a corner on a wide felt, but the
middle of the table on a 390px one, where it sat on top of trivia's
multiplier. So on a small screen it gets smaller, and where there's nothing
in the corner it pulls into the corner.
Where the rack sits is the one thing that differs per table, which is what
data-at says. Unmarked, it's alone in the corner. "shoe" means blackjack,
where the 5.75rem inset is not a margin but the width of the shoe it sits
beside — pull that one to the edge and it slides under the deck. "rail" means
solitaire, where the rack isn't on the felt at all (position: static). */
@media (max-width: 639px) {
.pete-rack:not([data-at="rail"]) {
gap: 0.25rem;
padding: 0.35rem 0.4rem 0.3rem;
}
.pete-rack:not([data-at="rail"]) span { width: 1.15rem; }
.pete-rack:not([data-at]) { right: 1rem; }
}
/* The burst. A natural gets confetti; nothing else in the room does, which is
what keeps it worth something. */
.pete-spark {
position: absolute;
height: 0.75rem;
width: 0.45rem;
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0,0,0,0.35);
will-change: transform, opacity;
}
/* The dealer's beat before they draw out. */
.pete-dealer-think {
animation: pete-think 0.9s ease-in-out infinite;
}
@keyframes pete-think {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; transform: translateY(-1px); }
}
/* ---- hangman -------------------------------------------------------------
The gallows is the meter: it counts down your lives and your winnings at the
same time. So a miss has to *land* — the limb draws itself in along its own
length, the whole thing flinches, and the multiple falls. Three parts of one
event, and the game is about watching it happen to you. */
.pete-gallows {
overflow: visible;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.pete-gallows-frame path {
stroke: rgba(0, 0, 0, 0.35);
stroke-width: 6;
}
/* The rope. Same post, thinner, so it reads as something that would hold. */
.pete-gallows-frame path:last-child {
stroke: rgba(0, 0, 0, 0.3);
stroke-width: 3.5;
}
.pete-gallows-body > * {
stroke: #fff;
stroke-width: 5;
opacity: 0;
filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.25));
}
.pete-gallows-body > [data-on="1"] { opacity: 1; }
/* Drawn, not faded in: the stroke runs on along its own path. 260 is longer
than any limb here, which is all a dash offset needs to be. */
.pete-part-draw {
stroke-dasharray: 260;
animation: pete-part 0.45s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes pete-part {
from { stroke-dashoffset: 260; }
to { stroke-dashoffset: 0; }
}
.pete-shake { animation: pete-shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes pete-shake {
10%, 90% { transform: translateX(-1.5px); }
20%, 80% { transform: translateX(3px); }
30%, 50%, 70% { transform: translateX(-5px); }
40%, 60% { transform: translateX(5px); }
}
/* The phrase. Tiles wrap between words, never inside one. */
.pete-board {
display: flex;
flex-wrap: wrap;
gap: 0.35rem 1.1rem;
min-height: 5rem;
align-items: center;
}
.pete-word {
display: flex;
gap: 0.3rem;
}
.pete-tile {
display: grid;
place-items: center;
height: 2.9rem;
width: 2.2rem;
border-radius: 0.5rem;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 1.4rem;
font-weight: 700;
color: #fff;
background: rgba(0, 0, 0, 0.22);
border-bottom: 3px solid rgba(0, 0, 0, 0.28);
text-transform: uppercase;
}
/* A letter you've earned sits proud of the ones you haven't. */
.pete-tile[data-up="1"] {
background: rgba(255, 255, 255, 0.95);
color: #2b2118;
border-bottom-color: rgba(0, 0, 0, 0.35);
}
/* Punctuation is scaffolding: it was never yours to guess, so it gets no tile
to guess it into. */
.pete-tile[data-punct="1"] {
background: none;
border: 0;
width: auto;
min-width: 0.7rem;
color: rgba(255, 255, 255, 0.55);
}
.pete-tile-hit { animation: pete-tile-hit 0.34s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes pete-tile-hit {
0% { transform: rotateX(90deg) scale(1.1); }
100% { transform: rotateX(0) scale(1); }
}
.pete-missed {
display: grid;
place-items: center;
height: 1.5rem;
min-width: 1.5rem;
padding: 0 0.3rem;
border-radius: 0.375rem;
background: rgba(0, 0, 0, 0.25);
font-size: 0.75rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
text-decoration: line-through;
text-transform: uppercase;
}
/* What a win is worth, right now. */
.pete-meter {
display: flex;
align-items: baseline;
gap: 0.5rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.28);
padding: 0.4rem 0.9rem;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1);
transition: box-shadow 0.3s ease;
}
.pete-meter-label {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.45);
}
.pete-meter-value {
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 1.5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--accent);
}
/* Down at the floor: a win now hands back the stake and nothing else. */
.pete-meter[data-cold="1"] .pete-meter-value { color: rgba(255, 255, 255, 0.55); }
.pete-meter[data-hit="1"] {
box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.9);
animation: pete-meter-hit 0.4s ease;
}
@keyframes pete-meter-hit {
0% { transform: scale(1); }
35% { transform: scale(0.94); }
100% { transform: scale(1); }
}
/* The keyboard. */
.pete-keys { display: grid; gap: 0.35rem; }
.pete-key-row {
display: flex;
justify-content: center;
gap: 0.35rem;
}
.pete-key-row[data-digits="1"] { margin-top: 0.25rem; opacity: 0.75; }
.pete-key-row[data-digits="1"] .pete-key {
height: 2rem;
min-width: 1.8rem;
font-size: 0.8rem;
}
.pete-key {
height: 2.75rem;
min-width: 2.2rem;
flex: 0 1 2.4rem;
border-radius: 0.6rem;
background: color-mix(in srgb, var(--ink) 6%, transparent);
border: 2px solid color-mix(in srgb, var(--ink) 10%, transparent);
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-weight: 700;
color: var(--ink);
transition: transform 0.08s ease, background 0.15s ease, opacity 0.15s ease;
}
.pete-key:hover:not(:disabled) { background: color-mix(in srgb, var(--ink) 12%, transparent); }
.pete-key:active:not(:disabled) { transform: translateY(1px) scale(0.96); }
.pete-key:disabled { cursor: default; }
/* A key that's been spent looks spent — otherwise you spend it twice. */
.pete-key[data-state="hit"] {
background: #4caf7d;
border-color: #3d9367;
color: #fff;
opacity: 1;
}
.pete-key[data-state="miss"] {
background: color-mix(in srgb, var(--ink) 4%, transparent);
color: color-mix(in srgb, var(--ink) 30%, transparent);
text-decoration: line-through;
}
.pete-key:disabled[data-state=""] { opacity: 0.4; }
/* Picking a length. The one you're on is lit. */
.pete-tier {
background: color-mix(in srgb, var(--ink) 4%, transparent);
border-color: color-mix(in srgb, var(--ink) 10%, transparent);
}
.pete-tier:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.pete-tier[data-on="1"] {
background: color-mix(in srgb, var(--accent) 18%, transparent);
border-color: var(--accent);
}
/* ---- trivia --------------------------------------------------------------
The clock is the game, so the clock is the biggest thing on the felt: a bar
that drains across the top of the question, going hot as it runs out. It is
*decoration* — the server timed the answer the moment it arrived — but it
has to be honest decoration, so it's driven from the seconds the server said
were left rather than from when the browser happened to paint. */
.pete-clock {
position: relative;
height: 0.6rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.3);
overflow: hidden;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.08);
}
.pete-clock-fill {
height: 100%;
width: 100%;
border-radius: 999px;
background: var(--accent);
transform-origin: left center;
/* Driven by a transform the browser can run on its own: a width animated on
every frame from JS is a layout on every frame. */
transform: scaleX(1);
}
/* The last few seconds. The bar stops being a progress meter and starts being
a warning. */
.pete-clock[data-hot="1"] .pete-clock-fill { background: #cc3d4a; }
.pete-clock[data-hot="1"] { animation: pete-clock-pulse 0.7s ease-in-out infinite; }
@keyframes pete-clock-pulse {
0%, 100% { box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.35); }
50% { box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.95); }
}
/* The four answers. Big targets, because the clock is already the hard part —
a question you lose to a mis-tap is a question about pointing. */
.pete-answer {
position: relative;
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
border-radius: 1rem;
padding: 0.85rem 1rem;
text-align: left;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.26);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1);
transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.2s ease;
}
.pete-answer:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.36);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.28);
}
.pete-answer:active:not(:disabled) { transform: translateY(1px); }
.pete-answer:disabled { cursor: default; }
/* The letter down the side, so an answer can be picked with the keyboard and
the key you press is printed on the thing you're pressing it for. */
.pete-answer-key {
display: grid;
place-items: center;
height: 1.75rem;
width: 1.75rem;
flex: none;
border-radius: 0.6rem;
background: rgba(255, 255, 255, 0.12);
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.8rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.7);
}
/* How it went. Only ever set once the server has decided — the browser has no
idea which of these is right until it's told. */
.pete-answer[data-state="right"] {
background: rgba(46, 160, 103, 0.9);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.5);
}
.pete-answer[data-state="wrong"] {
background: rgba(204, 61, 74, 0.9);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35);
animation: pete-answer-no 0.45s ease;
}
/* The one they *should* have picked, shown alongside the one they did. */
.pete-answer[data-state="missed"] {
box-shadow: inset 0 0 0 2px rgba(46, 160, 103, 0.95);
}
.pete-answer[data-state="dim"] { opacity: 0.4; }
@keyframes pete-answer-no {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
45% { transform: translateX(5px); }
70% { transform: translateX(-3px); }
}
/* The ladder: one pip per rung, filling as they're climbed. It is the only
picture of how far there is left to fall. */
.pete-ladder {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.pete-rung {
height: 0.5rem;
width: 1.1rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
transition: background 0.3s ease, transform 0.3s ease;
}
.pete-rung[data-on="1"] {
background: var(--accent);
transform: scaleY(1.35);
}
/* ---- solitaire -----------------------------------------------------------
Seven columns, four foundations, a stock and a waste, all of which have to
fit across the felt on a phone as well as a desk. So the card size is one
variable derived from the width available, and every gap and fan below is
derived from *that* — the board scales as one thing rather than as nine
things that each stop fitting at a different width.
The cards themselves don't move by CSS here. A move in solitaire can take a
card from anywhere to anywhere, so the table re-renders the board and then
animates each card from where it just was (see the FLIP note in
solitaire.js). CSS's job is only to say where things sit. */
.pete-solitaire {
--card-w: clamp(2.5rem, 10.6vw, 5.2rem);
--card-h: calc(var(--card-w) * 1.4);
--fan-up: calc(var(--card-h) * 0.29); /* how much of a face-up card shows under the next */
--fan-down: calc(var(--card-h) * 0.14); /* a face-down one shows less: there's nothing to read */
}
/* An empty place a card could be: the stock when it's spent, a foundation
waiting for its ace, a column waiting for a king. Same footprint as a card,
so nothing on the board reflows when one empties. */
.pete-slot {
position: relative;
display: grid;
place-items: center;
height: var(--card-h);
width: var(--card-w);
flex: none;
border-radius: 0.55rem;
border: 2px dashed rgba(255, 255, 255, 0.25);
background: rgba(0, 0, 0, 0.12);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.18);
transition: border-color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}
.pete-slot-glyph {
font-size: calc(var(--card-w) * 0.42);
line-height: 1;
color: rgba(255, 255, 255, 0.3);
}
.pete-slot-glyph[data-red="1"] { color: rgba(255, 170, 170, 0.35); }
/* A card sitting *in* a slot — the top of a foundation. The slot keeps its
footprint whether or not there's a card on it, so an ace going home doesn't
reflow the row, and a drop target has somewhere to be while it's empty. */
.pete-slot > .pete-card {
position: absolute;
inset: 0;
height: 100%;
width: 100%;
}
/* The stock is a button, and it looks like the back of a card because that is
what it is: a pile you can turn over. */
.pete-stock {
border-style: solid;
border-color: rgba(0, 0, 0, 0.15);
background:
repeating-linear-gradient(45deg, rgba(255,255,255,0.10) 0 6px, transparent 6px 12px),
linear-gradient(150deg, #b4553f, #8d3f2f);
cursor: pointer;
}
.pete-stock:hover { filter: brightness(1.08); }
.pete-stock:active { transform: translateY(1px) scale(0.98); }
/* Spent, but there's a pass left: it stops being a pile of cards and starts
being the gesture of turning the waste back over. */
.pete-stock[data-empty="1"] {
background: rgba(0, 0, 0, 0.12);
border-style: dashed;
border-color: rgba(255, 255, 255, 0.25);
}
/* Spent, and no passes left. Not a button any more — and it says so by looking
like the dead thing it is rather than by silently refusing clicks. */
.pete-stock[data-dead="1"] {
cursor: default;
opacity: 0.4;
}
.pete-stock[data-dead="1"]:hover { filter: none; }
.pete-stock[data-dead="1"]:active { transform: none; }
.pete-slot-count {
position: absolute;
bottom: -0.5rem;
left: 50%;
transform: translateX(-50%);
border-radius: 999px;
background: rgba(0, 0, 0, 0.5);
padding: 0.05rem 0.5rem;
font-size: 0.7rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #fff;
}
.pete-slot-recycle { color: rgba(255, 255, 255, 0.45); }
.pete-slot-recycle svg { height: 1.4rem; width: 1.4rem; }
/* The waste. Three cards fanned sideways, the top one on the right — you can
see what's under it, and only the top one is yours to take. */
.pete-waste {
position: relative;
display: flex;
height: var(--card-h);
min-width: var(--card-w);
}
.pete-waste .pete-card + .pete-card {
margin-left: calc(var(--card-w) * -0.72);
}
.pete-waste .pete-card { position: relative; }
/* The seven columns. They share the width evenly and never scroll: a board you
have to scroll sideways is a board you can't plan on. */
.pete-tableau {
display: grid;
grid-template-columns: repeat(7, var(--card-w));
justify-content: space-between;
gap: 0.5rem;
align-items: start;
min-height: calc(var(--card-h) * 2.6);
}
.pete-col {
display: flex;
flex-direction: column;
align-items: center;
min-height: var(--card-h);
}
/* Cards overlap up the column, and how much of one shows depends on whether
there's anything to see. The rule keys off the card *above*, which is why
it's an adjacent-sibling selector and not something the JS has to compute. */
.pete-col .pete-card { position: relative; }
.pete-col .pete-card[data-face="down"] + .pete-card {
margin-top: calc(var(--fan-down) - var(--card-h));
}
.pete-col .pete-card[data-face="up"] + .pete-card {
margin-top: calc(var(--fan-up) - var(--card-h));
}
/* A card you can pick up says so. */
.pete-card[data-live="1"] { cursor: pointer; }
.pete-card[data-live="1"]:hover .pete-card-front {
filter: brightness(1.06);
}
/* The card (or run) in your hand: lifted off the felt, and glowing, so it's
obvious what a click on a column is about to move. */
.pete-card[data-held="1"] {
z-index: 5;
transform: translateY(-0.55rem);
transition: transform 0.14s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.pete-card[data-held="1"] .pete-card-front {
box-shadow:
0 3px 0 rgba(0,0,0,0.18),
0 10px 22px rgba(0,0,0,0.32),
0 0 0 3px rgba(242, 181, 61, 0.9);
}
/* A place the held card would actually go. Lit *before* you click it, because
the alternative is learning the rules by being told no. */
.pete-slot[data-drop="1"],
.pete-col[data-drop="1"] .pete-slot,
.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front {
border-color: rgba(242, 181, 61, 0.9);
box-shadow: 0 0 0 3px rgba(242, 181, 61, 0.45);
}
.pete-slot[data-drop="1"],
.pete-col[data-drop="1"] .pete-slot {
background: rgba(242, 181, 61, 0.12);
}
/* A move that won't go. Said in the one language a board can speak. */
.pete-nope { animation: pete-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
/* A card arriving on a foundation lands with a flash: it is the only move in
the game that pays you, so it is the only one that gets a noise. */
.pete-home-flash { animation: pete-home 0.5s ease-out; }
@keyframes pete-home {
0% { box-shadow: 0 0 0 0 rgba(242, 181, 61, 0.9); }
100% { box-shadow: 0 0 0 1.4rem rgba(242, 181, 61, 0); }
}
/* The rail: the house's rack, what you've banked, the meter. On a wide screen
it's a column down the right of the felt; on a narrow one it lies down and
sits under the board. */
.pete-rail {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 1rem 1.5rem;
}
@media (min-width: 1024px) {
.pete-rail {
flex-direction: column;
justify-content: flex-start;
width: 9.5rem;
gap: 1.5rem;
}
}
/* In the rail the rack is just another thing in a column, so it drops the
absolute positioning it uses when it's pinned to the corner of a felt. */
.pete-rack[data-at="rail"] {
position: static;
justify-content: center;
}
/* ---- uno ------------------------------------------------------------------
The card is the whole look: a coloured rectangle with a white oval laid
across it at an angle. Drop the oval and it reads as a button, so the oval
is not decoration — it is what makes the thing a card.
The bots are drawn from a *count*. That is all the server sends, and the fan
of backs up there is generated from a number rather than from cards, because
there are no cards to draw. */
/* The felt takes a tint of the colour in play. After a wild, the card on the
pile is not the colour you have to follow — this is the table saying so. */
.pete-uno[data-c="red"] { --play: #d64545; }
.pete-uno[data-c="blue"] { --play: #3d7fd6; }
.pete-uno[data-c="yellow"] { --play: #e0b02c; }
.pete-uno[data-c="green"] { --play: #46a86b; }
.pete-uno::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(80% 55% at 50% 45%, var(--play, transparent), transparent 70%);
opacity: 0.16;
transition: opacity 0.5s ease, background 0.5s ease;
}
.pete-uno-card {
position: relative;
height: var(--uno-h, 6.4rem);
width: var(--uno-w, 4.3rem);
border-radius: 0.55rem;
padding: 0.28rem;
background: #fff;
box-shadow: 0 3px 0 rgba(0,0,0,0.22), 0 6px 14px rgba(0,0,0,0.18);
flex: none;
}
.pete-uno-face {
position: relative;
display: grid;
place-items: center;
height: 100%;
width: 100%;
border-radius: 0.38rem;
overflow: hidden;
background: var(--uno, #2b2118);
color: #fff;
}
.pete-uno-card[data-c="red"] { --uno: #d64545; }
.pete-uno-card[data-c="blue"] { --uno: #3d7fd6; }
.pete-uno-card[data-c="yellow"] { --uno: #e0b02c; }
.pete-uno-card[data-c="green"] { --uno: #46a86b; }
.pete-uno-card[data-c="wild"] { --uno: #2b2118; }
/* A wild that has been played wears the colour it was called as — the pile has
to show what you must follow, not what was printed. */
.pete-uno-card[data-named="red"] { box-shadow: 0 0 0 3px #d64545, 0 3px 0 rgba(0,0,0,0.22); }
.pete-uno-card[data-named="blue"] { box-shadow: 0 0 0 3px #3d7fd6, 0 3px 0 rgba(0,0,0,0.22); }
.pete-uno-card[data-named="yellow"] { box-shadow: 0 0 0 3px #e0b02c, 0 3px 0 rgba(0,0,0,0.22); }
.pete-uno-card[data-named="green"] { box-shadow: 0 0 0 3px #46a86b, 0 3px 0 rgba(0,0,0,0.22); }
/* The oval. Tilted, because it is tilted on the card in your hand. */
.pete-uno-oval {
position: relative;
z-index: 1;
display: grid;
place-items: center;
height: 78%;
width: 62%;
border-radius: 50%;
transform: rotate(-24deg);
background: #fff;
color: var(--uno, #2b2118);
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 1.45rem;
font-weight: 700;
line-height: 1;
text-shadow: 0 1px 0 rgba(0,0,0,0.10);
}
.pete-uno-card[data-c="wild"] .pete-uno-oval { color: #2b2118; font-size: 1.15rem; }
.pete-uno-corner {
position: absolute;
z-index: 1;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.62rem;
font-weight: 700;
line-height: 1;
color: #fff;
text-shadow: 0 1px 1px rgba(0,0,0,0.35);
}
.pete-uno-corner[data-at="tl"] { top: 0.22rem; left: 0.28rem; }
.pete-uno-corner[data-at="br"] { bottom: 0.22rem; right: 0.28rem; transform: rotate(180deg); }
/* The four quadrants of a wild, under the oval. */
.pete-uno-wheel {
position: absolute;
inset: 0;
background:
conic-gradient(#d64545 0 25%, #e0b02c 0 50%, #46a86b 0 75%, #3d7fd6 0);
opacity: 0.92;
}
/* The back. Every card you are not entitled to see is one of these. */
.pete-uno-card-back { padding: 0.28rem; }
.pete-uno-back {
display: grid;
place-items: center;
height: 100%;
width: 100%;
border-radius: 0.38rem;
background:
radial-gradient(120% 80% at 50% 0%, rgba(255,255,255,0.16), transparent 60%),
#2b2118;
}
.pete-uno-back::after {
content: "UNO";
display: block;
transform: rotate(-24deg);
padding: 0.1rem 0.35rem;
border-radius: 999px;
background: #e0b02c;
color: #2b2118;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.02em;
}
/* Your hand. It fans, it wraps, and a card you can play stands up out of it —
being shown what goes is the game teaching you, and the server still decides. */
.pete-uno-hand {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.4rem;
min-height: 6.8rem;
padding-top: 0.6rem;
}
.pete-uno-hand .pete-uno-card {
animation: pete-uno-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards;
animation-delay: calc(var(--i, 0) * 45ms);
transition: transform 0.16s ease, filter 0.16s ease;
}
/* A card that was already in your hand before this redraw doesn't get dealt to
you again. The hand is redrawn several times a lap now (every event that
changes it), and without this every one of them re-fans the whole thing. */
.pete-uno-hand .pete-uno-card[data-settled="1"] { animation: none; }
.pete-uno-hand .pete-uno-card[data-on="1"] {
cursor: pointer;
transform: translateY(-0.5rem);
}
.pete-uno-hand .pete-uno-card[data-on="1"]:hover { transform: translateY(-1.1rem) scale(1.03); }
.pete-uno-hand .pete-uno-card[data-on="0"] { filter: brightness(0.62) saturate(0.7); }
@keyframes pete-uno-in {
from { transform: translateY(2rem) rotate(6deg); opacity: 0; }
to { transform: translateY(-0.5rem); opacity: 1; }
}
/* A seat: a fan of backs, a name, a count. The fan overlaps, so eight cards
read as a hand rather than a row. */
.pete-uno-seat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
position: relative;
padding: 0.5rem 0.7rem;
border-radius: 1rem;
transition: background 0.25s ease, transform 0.25s ease;
}
.pete-uno-seat[data-turn="1"] {
background: rgba(255,255,255,0.12);
transform: translateY(-2px);
}
.pete-uno-seat[data-uno="1"] .pete-uno-count { color: #ffd76e; }
.pete-uno-fan {
display: flex;
--uno-h: 3.2rem;
--uno-w: 2.15rem;
}
.pete-uno-fan .pete-uno-card {
margin-left: -1.35rem;
transform: rotate(calc((var(--i, 0) - (var(--n, 1) - 1) / 2) * 4deg));
transform-origin: bottom center;
box-shadow: 0 2px 0 rgba(0,0,0,0.22);
}
.pete-uno-fan .pete-uno-card:first-child { margin-left: 0; }
.pete-uno-fan .pete-uno-back::after { font-size: 0.4rem; padding: 0.06rem 0.22rem; }
.pete-uno-name {
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.9rem;
font-weight: 700;
color: #fff;
line-height: 1;
}
.pete-uno-count {
font-size: 0.7rem;
font-weight: 700;
color: rgba(255,255,255,0.55);
line-height: 1;
}
/* The deck you draw from, and the pile you play onto. */
.pete-uno-deck {
position: relative;
--uno-h: 6.4rem;
--uno-w: 4.3rem;
height: var(--uno-h);
width: var(--uno-w);
border-radius: 0.55rem;
padding: 0.28rem;
background: #fff;
box-shadow:
0 3px 0 rgba(0,0,0,0.22),
0.28rem 0.28rem 0 -0.06rem rgba(255,255,255,0.35),
0.5rem 0.5rem 0 -0.12rem rgba(255,255,255,0.18);
transition: transform 0.15s ease;
}
.pete-uno-deck:not(:disabled):hover { transform: translateY(-3px); }
.pete-uno-deck:disabled { opacity: 0.75; }
.pete-uno-deck-count {
position: absolute;
bottom: -0.55rem;
left: 50%;
transform: translateX(-50%);
padding: 0.1rem 0.5rem;
border-radius: 999px;
background: rgba(0,0,0,0.55);
color: #fff;
font-size: 0.65rem;
font-weight: 700;
line-height: 1.4;
}
.pete-uno-shuffle { animation: pete-uno-shuffle 0.42s ease; }
@keyframes pete-uno-shuffle {
0%, 100% { transform: none; }
30% { transform: translateY(-4px) rotate(-3deg); }
65% { transform: translateY(2px) rotate(2deg); }
}
.pete-uno-discard {
display: grid;
place-items: center;
height: 6.4rem;
width: 4.3rem;
border-radius: 0.55rem;
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.14);
}
.pete-uno-land { animation: pete-uno-land 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes pete-uno-land {
from { transform: scale(1.14) rotate(-4deg); }
to { transform: none; }
}
.pete-uno-colour {
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.15rem 0.6rem;
border-radius: 999px;
color: #fff;
background: rgba(0,0,0,0.3);
}
.pete-uno-colour[data-c="red"] { background: #d64545; }
.pete-uno-colour[data-c="blue"] { background: #3d7fd6; }
.pete-uno-colour[data-c="yellow"] { background: #e0b02c; color: #2b2118; }
.pete-uno-colour[data-c="green"] { background: #46a86b; }
/* A word thrown at a seat: SKIPPED, UNO!, +4. */
.pete-uno-badge {
position: absolute;
top: -0.4rem;
left: 50%;
z-index: 5;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: #fff;
color: #2b2118;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 700;
white-space: nowrap;
box-shadow: 0 3px 0 rgba(0,0,0,0.2);
animation: pete-uno-badge 0.9s ease forwards;
}
.pete-uno-badge[data-tone="bad"] { background: #cc3d4a; color: #fff; }
.pete-uno-badge[data-tone="uno"] { background: #e0b02c; }
@keyframes pete-uno-badge {
0% { transform: translate(-50%, 0.4rem) scale(0.7); opacity: 0; }
25% { transform: translate(-50%, -0.5rem) scale(1.06); opacity: 1; }
75% { transform: translate(-50%, -0.9rem) scale(1); opacity: 1; }
100% { transform: translate(-50%, -1.6rem) scale(0.95); opacity: 0; }
}
/* Naming a colour. It covers the felt because until it's answered there is no
legal move on the table underneath it. */
.pete-uno-wild {
position: absolute;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: rgba(10, 20, 15, 0.55);
backdrop-filter: blur(2px);
}
.pete-uno-wild-box {
padding: 1.25rem 1.5rem;
border-radius: 1.25rem;
background: rgba(20, 40, 30, 0.92);
box-shadow: 0 12px 30px rgba(0,0,0,0.35);
text-align: center;
}
.pete-uno-swatch {
padding: 0.7rem 1.4rem;
border-radius: 0.75rem;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
background: var(--sw, #666);
box-shadow: 0 3px 0 rgba(0,0,0,0.3);
transition: transform 0.12s ease, filter 0.12s ease;
}
.pete-uno-swatch:hover { transform: translateY(-2px); filter: brightness(1.08); }
.pete-uno-swatch:active { transform: translateY(1px); }
.pete-uno-swatch[data-c="red"] { --sw: #d64545; }
.pete-uno-swatch[data-c="blue"] { --sw: #3d7fd6; }
.pete-uno-swatch[data-c="yellow"] { --sw: #e0b02c; color: #2b2118; }
.pete-uno-swatch[data-c="green"] { --sw: #46a86b; }
/* ---- calling it, and catching the seat that didn't -------------------------
The call is a button with a clock on it. It wants to be the loudest thing on
the felt for the two-and-a-bit seconds it exists, because the whole rule is
that you can miss it. */
.pete-uno-call {
margin-top: 0.85rem;
padding: 0.9rem 2.6rem;
border-radius: 999px;
background: #e0b02c;
color: #2b2118;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 1.7rem;
font-weight: 700;
letter-spacing: 0.02em;
box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0 rgba(224,176,44,0.55);
animation: pete-uno-call 0.9s ease-in-out infinite;
}
.pete-uno-call:hover { filter: brightness(1.07); }
.pete-uno-call:active { transform: translateY(2px); box-shadow: 0 3px 0 rgba(0,0,0,0.3); }
@keyframes pete-uno-call {
0%, 100% { transform: scale(1); box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0 rgba(224,176,44,0.5); }
50% { transform: scale(1.05); box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0.9rem rgba(224,176,44,0); }
}
/* How long you've got, draining left to right. The duration is set on the
element: the window lives in uno.js, and a bar that disagrees with the clock
it's drawing is worse than no bar. */
.pete-uno-timer {
margin-top: 0.8rem;
height: 0.35rem;
border-radius: 999px;
background: rgba(255,255,255,0.16);
overflow: hidden;
}
.pete-uno-timer > span {
display: block;
height: 100%;
width: 100%;
background: #e0b02c;
transform-origin: left center;
animation: pete-uno-drain linear forwards;
}
@keyframes pete-uno-drain {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
/* A seat on one card that never said so. It gets a button, and the button is
the only thing on the felt that says it — there's no badge, because not
announcing is exactly what it did. */
.pete-uno-catch {
position: absolute;
top: -0.55rem;
left: 50%;
transform: translateX(-50%);
z-index: 6;
padding: 0.22rem 0.7rem;
border-radius: 999px;
background: #cc3d4a;
color: #fff;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.72rem;
font-weight: 700;
white-space: nowrap;
box-shadow: 0 3px 0 rgba(0,0,0,0.25);
animation: pete-uno-catch 1.1s ease-in-out infinite;
transition: filter 0.12s ease;
}
.pete-uno-catch:hover { filter: brightness(1.12); }
.pete-uno-catch:disabled { opacity: 0.45; animation: none; }
@keyframes pete-uno-catch {
0%, 100% { transform: translateX(-50%) scale(1); }
50% { transform: translateX(-50%) scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
.pete-uno-call, .pete-uno-catch { animation: none; }
}
/* ---- No Mercy -------------------------------------------------------------
A rules dial, not a fourth table, and the felt says so: the same table, the
same cards, plus six faces the normal box doesn't print and one thing that
can be pointed at you — a stack of draw cards with a bill on it.
Sizing note, and it is the rule this table was taught the hard way: a card is
sized by --uno-h/--uno-w and nothing else. These faces set a *font*, never a
box, so a "DISCARD ALL" is the same card as a "7" at every width. */
/* The long faces. "DISCARD ALL" does not fit across an oval at card size, so it
wraps — and the oval is tilted, so it wraps on the tilt, which is what the
printed card does too. Specificity has to clear the wild rule above (a
reverse-draw-four is a wild), hence the .pete-uno-card in front. */
.pete-uno-card .pete-uno-oval[data-size="mid"] { font-size: 1.05rem; }
.pete-uno-card .pete-uno-oval[data-size="words"] {
font-size: 0.5rem;
line-height: 1.1;
letter-spacing: 0.02em;
text-align: center;
padding: 0 0.15rem;
}
/* The wild draws glow.
These are the cards that decide a game, and No Mercy prints a *coloured* +4
right next to the wild one, so at a glance in a hand of twenty they have to be
tellable apart. The aura is a sibling behind the card rather than a shadow on
it: a box-shadow here would fight the one that says which colour a played wild
was named as (data-named, above), and lose.
It sits under the card (z-index -1) and does not take clicks, so the card is
still the button. */
.pete-uno-card[data-glow="1"] { position: relative; z-index: 0; }
.pete-uno-card[data-glow="1"]::before {
content: "";
position: absolute;
inset: -0.35rem;
z-index: -1;
border-radius: 0.9rem;
pointer-events: none;
background: conic-gradient(from 0deg,
#d64545, #e0b02c, #46a86b, #3d7fd6, #d64545);
filter: blur(7px);
opacity: 0.75;
animation: pete-uno-glow 3.2s linear infinite;
}
/* The shimmer across the face itself: a slow sweep of light, the way a foil card
catches the room when you tilt it. */
.pete-uno-card[data-glow="1"] .pete-uno-face::after {
content: "";
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
background: linear-gradient(115deg,
transparent 35%, rgba(255,255,255,0.55) 50%, transparent 65%);
background-size: 260% 100%;
animation: pete-uno-shine 2.6s ease-in-out infinite;
}
@keyframes pete-uno-glow {
to { transform: rotate(1turn); }
}
@keyframes pete-uno-shine {
0% { background-position: 140% 0; }
55% { background-position: -40% 0; }
100% { background-position: -40% 0; }
}
/* The bill. While a stack stands it is the only thing on the table that matters,
so it is loud, and it is red. */
.pete-uno-pending {
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.18rem 0.7rem;
border-radius: 999px;
color: #fff;
background: #cc3d4a;
box-shadow: 0 3px 0 rgba(0,0,0,0.2);
animation: pete-uno-bill 1.1s ease-in-out infinite;
}
@keyframes pete-uno-bill {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
/* A seat the mercy rule buried. It stays at the table — the felt would jump if
it didn't, and you want to see who's gone — but it is plainly out. */
.pete-uno-seat[data-out="1"] {
opacity: 0.4;
filter: grayscale(1);
}
.pete-uno-seat[data-out="1"] .pete-uno-count { color: #ffb3ba; }
/* The rules switch: two dials, and this is the one that isn't the table size. */
.pete-seg {
display: inline-flex;
padding: 0.2rem;
border-radius: 999px;
background: color-mix(in srgb, var(--ink) 6%, transparent);
}
.pete-seg-btn {
padding: 0.3rem 0.9rem;
border-radius: 999px;
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
font-size: 0.8rem;
font-weight: 700;
color: color-mix(in srgb, var(--ink) 55%, transparent);
transition: background 0.15s ease, color 0.15s ease;
}
.pete-seg-btn:hover { color: var(--ink); }
.pete-seg-btn[data-on="1"] {
background: var(--accent);
color: #fff;
box-shadow: 0 2px 0 rgba(0,0,0,0.15);
}
/* A phone. The cards get smaller so a hand of ten still fits without the felt
scrolling sideways, which is the thing to check at 390px with a game on it.
No Mercy deals bigger hands than that — it is a deck built to bury you — so
this is the table where a twenty-card hand has to wrap and not overflow. */
@media (max-width: 639px) {
.pete-uno-hand { --uno-h: 5.2rem; --uno-w: 3.5rem; gap: 0.3rem; }
.pete-uno-deck { --uno-h: 5.2rem; --uno-w: 3.5rem; }
.pete-uno-card .pete-uno-oval[data-size="words"] { font-size: 0.42rem; }
.pete-uno-card .pete-uno-oval[data-size="mid"] { font-size: 0.9rem; }
/* The vars, not just the box: the card in the discard is a .pete-uno-card and
takes its size from them, so sizing the box alone left a full-size card
hanging out of a small hole, on top of the colour in play. */
.pete-uno-discard { --uno-h: 5.2rem; --uno-w: 3.5rem; height: 5.2rem; width: 3.5rem; }
.pete-uno-hand .pete-uno-card { padding: 0.22rem; }
.pete-uno-oval { font-size: 1.15rem; }
.pete-uno-seat { padding: 0.35rem 0.45rem; }
}
@media (prefers-reduced-motion: reduce) {
.pete-card,
.pete-card::after,
.pete-stack .pete-disc,
.pete-dealer-think { animation: none; }
.pete-card-inner { transition: none; }
.pete-hand[data-won="1"] .pete-card { animation: none; }
.pete-part-draw,
.pete-shake,
.pete-tile-hit,
.pete-meter[data-hit="1"] { animation: none; }
.pete-nope,
.pete-home-flash { animation: none; }
.pete-card[data-held="1"] { transition: none; }
/* The clock still drains — it is information, not decoration — but it stops
pulsing at you, and a wrong answer stops shaking. */
.pete-clock[data-hot="1"],
.pete-answer[data-state="wrong"] { animation: none; }
.pete-rung { transition: none; }
/* The wild draws keep their glow — it is what tells the card apart from the
coloured +4 beside it, so it is information — but it stops moving. */
.pete-uno-card[data-glow="1"]::before { animation: none; }
.pete-uno-card[data-glow="1"] .pete-uno-face::after { display: none; }
.pete-uno-pending { animation: none; }
}
}
/* ----------------------------------------------------------------------------
The rooms.
The casino borrows the news app's design language wholesale — the same four
palette vars, the same Fredoka/Nunito, the same fat rounded cards and dropped
shadows — and none of its furniture.
It borrows the clock too, but tells a different joke with it. News shifts
dawn→day→dusk→night. The casino has two rooms and swaps between them on the
hour, name and all:
casinopolis by day — honey and green felt, lamps low
casino-night by dark — neon blue, pinball purple, lit like a machine
Both are dark rooms of the same shape; only the light and the sign change.
Adding a third is a palette block, a felt, and a name — nothing else.
Written outside @layer on purpose: these must beat the base .shadow-pete.
---------------------------------------------------------------------------- */
html[data-room="casinopolis"] {
--bg: #16211c; /* the room, lights low */
--card: #23342b; /* a table in it */
--ink: #f6ecd8; /* warm cream, not white */
--accent: #f2b53d; /* honey */
--felt-a: #2f7d5b;
--felt-b: #24614a;
--felt-c: #1c4d3c;
--glow: 242,181,61; /* what the lamps throw, as rgb parts */
}
html[data-room="casino-night"] {
--bg: #140f2e; /* the machine's cabinet */
--card: #241a4d;
--ink: #f2ecff;
--accent: #ffcc2f; /* the jackpot bulb */
--felt-a: #4a2fa8; /* the table is a pinball table now */
--felt-b: #351f80;
--felt-c: #241659;
--glow: 126,86,255;
}
/* A lamp over the table and a darker floor at the edges, so the page has a
middle to sit in rather than being flat dark. */
html[data-room] .cs-room {
background:
radial-gradient(80% 55% at 50% -8%, rgba(var(--glow), 0.18), transparent 62%),
radial-gradient(120% 90% at 50% 120%, rgba(0,0,0,0.45), transparent 55%);
}
/* Casino Night is lit by bulbs, so it gets a few of them: a slow chase across
the top of the room. Cheap (one gradient, one transform) and it makes the
place feel plugged in rather than painted. */
html[data-room="casino-night"] .cs-room::before {
content: "";
position: absolute;
inset: 0 -50% auto -50%;
height: 0.5rem;
background: repeating-linear-gradient(90deg,
rgba(255,204,47,0.55) 0 0.5rem, transparent 0.5rem 2.25rem);
filter: blur(1px);
animation: cs-bulbs 2.4s linear infinite;
}
@keyframes cs-bulbs {
from { transform: translateX(0); }
to { transform: translateX(2.75rem); }
}
/* The house shadow, restruck for a dark room: the news app's soft brown lift is
invisible down here, so the same shape is cut in black instead. */
html[data-room] .shadow-pete { box-shadow: 0 4px 0 rgba(0,0,0,0.30), 0 10px 26px rgba(0,0,0,0.30); }
html[data-room] .shadow-pete-lg { box-shadow: 0 6px 0 rgba(0,0,0,0.34), 0 20px 40px rgba(0,0,0,0.38); }
html[data-room] .cs-comb svg { filter: drop-shadow(0 3px 0 rgba(0,0,0,0.35)); }
/* The felt takes its colours from the room, so switching rooms reupholsters the
table too. */
html[data-room] .pete-felt {
background:
radial-gradient(120% 90% at 50% -10%, rgba(255,255,255,0.10), transparent 60%),
linear-gradient(160deg, var(--felt-a) 0%, var(--felt-b) 55%, var(--felt-c) 100%);
}
@media (prefers-reduced-motion: reduce) {
html[data-room="casino-night"] .cs-room::before { animation: none; }
}
/* Three stacks of chips on the lobby's welcome card, in the same denomination
colours you actually bet with. Height comes from --stack, so the row reads as
a real pile someone left on the table rather than an icon of one. */
.cs-stack {
display: block;
width: 2.75rem;
height: calc(0.45rem * var(--stack, 1) + 0.55rem);
border-radius: 999px;
background:
radial-gradient(circle at 50% 30%, rgba(255,255,255,0.30), transparent 60%),
var(--chip, #e07a5f);
border: 2px solid rgba(0,0,0,0.25);
box-shadow:
0 3px 0 rgba(0,0,0,0.30),
inset 0 -0.4rem 0 rgba(0,0,0,0.16),
inset 0 0.35rem 0 rgba(255,255,255,0.14);
}
.cs-stack[data-chip="5"] { --chip: #5aa9e6; }
.cs-stack[data-chip="25"] { --chip: #4caf7d; }
.cs-stack[data-chip="100"] { --chip: #2b2118; }
.cs-stack[data-chip="500"] { --chip: #b079d6; }
/* ── Hold'em ────────────────────────────────────────────────────────────────
The one table with other people at it. Everything else in the casino has a
single bet spot, because there was only ever one player; here every seat has
its own, chips travel between them and the pot rather than between you and the
house, and the pot in the middle is the thing they all move toward.
Seats are a wrapping row rather than an ellipse. An oval of six seats is what a
poker table looks like, and it is also what stops fitting the moment a phone is
held up — this keeps the geometry honest at 390px and still reads as a table
because the board and the pot sit in the middle of it, which is where the eye
goes anyway. */
.pete-poker { --seat-w: 7.5rem; }
.pete-poker-seats {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 0.75rem;
}
.pete-seat {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
width: var(--seat-w);
transition: opacity 0.3s ease, filter 0.3s ease;
}
/* A folded seat is still there — you can see they're in the game and out of the
hand, which is information you're entitled to. It just stops competing. */
.pete-seat[data-state="folded"] { opacity: 0.38; filter: grayscale(0.7); }
.pete-seat[data-state="out"] { opacity: 0.2; }
/* Whose turn it is. The ring is the only thing on the felt that moves on its own,
because it is the only thing you are waiting for. */
.pete-seat[data-turn="1"] .pete-seat-plate {
box-shadow: 0 0 0 2px var(--accent), 0 0 1.6rem -0.2rem var(--accent);
animation: pete-seat-wait 1.6s ease-in-out infinite;
}
@keyframes pete-seat-wait {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
.pete-seat-cards {
display: flex;
justify-content: center;
gap: 0.15rem;
min-height: 4.4rem;
--card-h: 4.4rem;
--card-w: 3.15rem;
}
/* A seat that folded throws its cards in. */
.pete-seat-cards[data-mucked="1"] { opacity: 0; transform: translateY(-0.6rem) scale(0.9); transition: all 0.35s ease; }
.pete-seat-plate {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 0.3rem 0.4rem;
border-radius: 0.85rem;
background: rgba(0,0,0,0.32);
border: 1px solid rgba(255,255,255,0.12);
transition: box-shadow 0.25s ease;
}
.pete-seat-name {
font-family: var(--font-display, inherit);
font-weight: 700;
font-size: 0.8rem;
line-height: 1.1;
color: rgba(255,255,255,0.92);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pete-seat-stack {
font-weight: 700;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
color: rgba(255,255,255,0.58);
}
.pete-seat[data-state="allin"] .pete-seat-stack::after {
content: " all in";
color: var(--accent);
}
/* The dealer button, and the blinds. Small, and worth having: position is most of
what makes one hand different from the next, and a player who cannot see where
the button is cannot see the game. */
.pete-seat-pos {
position: absolute;
top: -0.5rem;
right: -0.5rem;
display: grid;
place-items: center;
min-width: 1.45rem;
height: 1.45rem;
padding: 0 0.3rem;
border-radius: 999px;
font-size: 0.58rem;
font-weight: 800;
letter-spacing: 0.02em;
background: rgba(255,255,255,0.9);
color: #2b2118;
box-shadow: 0 2px 0 rgba(0,0,0,0.3);
}
.pete-seat-pos[data-pos="BTN"] { background: #f5d76e; }
/* Every seat's bet, sitting between them and the pot.
A .pete-spot is 7rem across, because blackjack has exactly one of them. Six of
those is most of a felt, so a seat's is less than half the size — and so are the
chips in it, which are otherwise bigger than the ring they land in. */
.pete-seat-spot {
height: 3.6rem;
width: 3.6rem;
margin-bottom: 0.5rem; /* the bet total hangs below the ring; leave it somewhere to hang */
}
.pete-seat-spot .pete-disc {
height: 1.6rem;
width: 1.6rem;
margin: -0.8rem 0 0 -0.8rem;
}
.pete-seat-spot .pete-spot-total {
font-size: 0.66rem;
padding: 0.05rem 0.45rem;
}
/* The middle of the table: the board, and the pot under it. */
.pete-poker-middle {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0;
}
/* The pot's chips stack upwards, so they need room under the board or they climb
into the river card. */
.pete-poker-pot { margin-top: 0.9rem; }
/* The board keeps its height when it is empty, so the felt does not jump a hundred
pixels the moment the flop lands. */
.pete-poker-board {
display: flex;
gap: 0.35rem;
min-height: 6rem;
--card-h: 6rem;
--card-w: 4.3rem;
}
.pete-poker-pot {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
}
/* The pot's chips need a box to themselves. .pete-stack is position:absolute with
inset:0, so a pile that shares a box with the number under it lands on top of
the number — which is exactly what it did: the pot showed a chip and no total. */
.pete-poker-pot-pile {
position: relative;
width: 7rem;
height: 3rem;
}
.pete-poker-pot-label {
font-size: 0.62rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255,255,255,0.42);
}
.pete-poker-pot-total {
font-family: var(--font-display, inherit);
font-size: 1.5rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #fff;
text-shadow: 0 2px 0 rgba(0,0,0,0.28);
}
.pete-poker-side {
font-size: 0.68rem;
font-weight: 700;
color: rgba(255,255,255,0.5);
}
/* Your seat. Bigger cards, because they are the two you are actually reading. */
.pete-poker-you .pete-seat { width: auto; }
.pete-poker-you .pete-seat-cards {
--card-h: 6.8rem;
--card-w: 4.85rem;
gap: 0.3rem;
min-height: 6.8rem;
}
.pete-poker-you .pete-seat-plate { padding: 0.4rem 1.1rem; }
.pete-poker-you .pete-seat-name { font-size: 0.95rem; }
.pete-poker-you .pete-seat-stack { font-size: 0.95rem; }
/* What the hand did to you, said once, in the middle of the felt. */
.pete-poker-verdict {
border-radius: 999px;
background: rgba(255,255,255,0.95);
padding: 0.5rem 1.25rem;
font-family: var(--font-display, inherit);
font-size: 1.05rem;
font-weight: 700;
color: #2b2118;
box-shadow: 0 4px 0 rgba(0,0,0,0.18);
}
.pete-poker-verdict[data-tone="win"] { background: #4caf7d; color: #fff; }
.pete-poker-verdict[data-tone="lose"] { background: rgba(255,255,255,0.9); color: #7a5c50; }
/* The action bar. Raise is a slider, because a raise is a *size* and a text box
makes you type a number you have not thought about. */
.pete-raise {
display: flex;
align-items: center;
gap: 0.6rem;
flex: 1 1 14rem;
min-width: 0;
}
.pete-raise input[type="range"] {
flex: 1;
min-width: 0;
accent-color: var(--accent);
}
.pete-raise-to {
font-family: var(--font-display, inherit);
font-size: 1.15rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
min-width: 3.5rem;
text-align: right;
}
.pete-poker-log {
max-height: 7rem;
overflow-y: auto;
font-size: 0.75rem;
line-height: 1.5;
color: rgba(255,255,255,0.55);
}
.pete-poker-log b { color: rgba(255,255,255,0.85); font-weight: 700; }
@media (max-width: 640px) {
.pete-poker { --seat-w: 5.6rem; }
.pete-seat-cards { --card-h: 3.5rem; --card-w: 2.5rem; min-height: 3.5rem; }
.pete-poker-board { --card-h: 5rem; --card-w: 3.55rem; min-height: 5rem; gap: 0.25rem; }
.pete-poker-you .pete-seat-cards { --card-h: 6rem; --card-w: 4.3rem; min-height: 6rem; }
.pete-poker-pot-total { font-size: 1.25rem; }
}