Files
Pete/internal/web/static/css/input.css
prosolis 5ca056bf20 games: you buy the deck, and win it back a card at a time
Solitaire, Vegas rules — the only shape solitaire has ever had as a
gambling game. You don't win or lose the deal: the stake buys the deck
outright, and every card you get home to a foundation pays a fifty-second
of the tier's multiple back. Cash the board whenever you like and keep
what you've banked, so a board that has gone dead is a decision rather
than a wall. No undo: the stake is spent the moment the deck is bought,
and an undo would be a way to walk a losing board backwards until it wins.

Three deals, and the two dials are the whole difficulty of Klondike.
Patient draws one with unlimited passes and pays 1.4x, so it takes 38
cards home to get square. Vegas draws three, three times round, 2.2x,
square at 24. Cutthroat draws three and gives you one pass, 3.4x, square
at 16 — most of those boards never clear, and you're ahead long before
they would.

internal/games/klondike is the same pure reducer as the other two, and
Pays() is one function for the same reason hangman's is. Two fuzzers hold
the deck together: no sequence of moves can lose or duplicate a card, and
the board stays well-formed. They earned their keep immediately — the
first thing they caught was a recycle that reversed the waste. It flips as
a block, so the card drawn first comes out first, and reversing it would
have dealt a different game on every pass and quietly broken the seed in
the audit log.

The browser never sees the stock or a face-down card, which here is most
of the deck rather than blackjack's one hole card: a column sends how many
cards are under it, never which.

The table re-renders and animates the difference. Blackjack plays back a
script because a hand only ever grows at one end; solitaire moves runs
from anywhere to anywhere and an auto-finish moves eleven cards at once,
so a script of "append this card there" would be a second engine over here
and it would be the one that's wrong. Instead the board on screen is
always exactly the board the server says exists, and each card is played
from where it just was to where it now is. The events supply only what a
diff can't: where a newly-revealed card came from, and what the board is
worth.

The rules are mirrored in JS on purpose, and only to light up the columns
a held card can go to. Being shown where a card goes is the game teaching
you; being told no after you commit is the game scolding you. The server
still decides, and a disagreement snaps the board back to what it says.

Two things came out into the open rather than being copied, which is the
rule this room runs on: casino-cards.js (the deck — faces, pips, the flip)
and PeteFX.spot() (the pile of chips and the number under it, which now
owns the rule that the number is a readout of the pile). Blackjack uses
both.

Not yet driven in a browser.
2026-07-14 01:40:14 -07:00

1413 lines
49 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; }
/* 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);
}
/* ---- 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;
}
@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 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; }