From 39ed293f4f637c92a8d21c6694ea20f2707f790e Mon Sep 17 00:00:00 2001
From: prosolis <5590409+prosolis@users.noreply.github.com>
Date: Tue, 14 Jul 2026 13:15:11 -0700
Subject: [PATCH] games: the word you owe the table, and the hand you were
already holding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
internal/games/uno/call.go | 201 ++++++++++++++
internal/games/uno/call_test.go | 323 +++++++++++++++++++++++
internal/games/uno/nomercy.go | 8 +-
internal/games/uno/nomercy_test.go | 18 +-
internal/games/uno/uno.go | 135 ++++++++--
internal/web/games_uno.go | 54 +++-
internal/web/static/css/input.css | 83 ++++++
internal/web/static/css/output.css | 2 +-
internal/web/static/js/blackjack.js | 10 +
internal/web/static/js/casino-cards.js | 6 +
internal/web/static/js/casino-fx.js | 29 +-
internal/web/static/js/casino-sfx.js | 265 +++++++++++++++++++
internal/web/static/js/games.js | 19 ++
internal/web/static/js/hangman.js | 7 +
internal/web/static/js/holdem.js | 11 +-
internal/web/static/js/prefs.js | 2 +-
internal/web/static/js/solitaire.js | 34 +++
internal/web/static/js/trivia.js | 9 +
internal/web/static/js/uno.js | 253 ++++++++++++++++--
internal/web/templates/_chipbar.html | 19 ++
internal/web/templates/games_layout.html | 5 +
internal/web/templates/uno.html | 16 ++
22 files changed, 1450 insertions(+), 59 deletions(-)
create mode 100644 internal/games/uno/call.go
create mode 100644 internal/games/uno/call_test.go
create mode 100644 internal/web/static/js/casino-sfx.js
diff --git a/internal/games/uno/call.go b/internal/games/uno/call.go
new file mode 100644
index 0000000..ea72701
--- /dev/null
+++ b/internal/games/uno/call.go
@@ -0,0 +1,201 @@
+package uno
+
+import "math/rand/v2"
+
+// Calling UNO, and catching the seat that didn't.
+//
+// This was the one rule on the box the table wasn't playing. A hand going down to
+// one card used to emit the "uno" event by itself, which made the call a thing
+// that *happened to you* rather than a thing you did — and a rule nobody can fail
+// is not a rule, it's an announcement.
+//
+// So now: play your second-to-last card and you owe the table a word. Say it
+// (Move.Uno) and you're safe. Stay quiet and the bots get one look at you, right
+// then, before any of them plays — because a bot that has moved on is a bot that
+// has stopped watching your hand. Miss it and you take two.
+//
+// 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* — there is
+// no event, no badge, no tell on the felt except the count beside its fan reading
+// "1 card" with no UNO on it. Catch it (MoveCatch) 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 every turn.
+//
+// The whole rule turns on one asymmetry, and it's deliberate: the bots get a
+// *roll* to notice you, and you get to actually look. Attention is the edge here,
+// and it's the player's to take.
+
+const (
+ // CatchPenalty is what silence costs, both ways. Two, as printed on the box.
+ CatchPenalty = 2
+
+ // botForget is how often a bot goes down to one card without saying so. High
+ // enough that watching the counts pays — at a full table you'll get a catch to
+ // make every few games — and low enough that a quiet seat is still a thing you
+ // have to notice rather than assume.
+ botForget = 0.30
+
+ // botAlert is the chance a *single* bot notices that you didn't call. They each
+ // get a look, so forgetting is punished about 75% of the time heads-up and 98%
+ // at a full table: the more seats there are watching you, the less you get away
+ // with, which is the right shape for it.
+ botAlert = 0.75
+)
+
+// botForgets rolls for whether a bot muffs its call.
+func botForgets(rng *rand.Rand) bool { return rng.Float64() < botForget }
+
+// declare records whether a seat that is now on one card said so, and — if it did
+// — announces it. A seat on any other number of cards owes nothing and this does
+// nothing, which is what makes it safe to call after every play.
+func (s *State) declare(seat int, called bool, evs *[]Event) {
+ if s.Phase == PhaseDone || len(s.Hands[seat]) != 1 {
+ return
+ }
+ s.ensureCalled()
+ s.Called[seat] = called
+ if called {
+ *evs = append(*evs, Event{Kind: EvUno, Seat: seat, Left: 1})
+ }
+}
+
+// botsCatch is the window. You have just played, you are holding one card, and
+// you didn't say the word — so every bot still in the game gets one look, in seat
+// order, and the first to see it takes you for two.
+//
+// It runs before runBots on purpose. The catch has to land while the table is
+// still looking at the card you played, not three turns later.
+func (s *State) botsCatch(evs *[]Event, rng *rand.Rand) {
+ if s.Phase == PhaseDone || len(s.Hands[You]) != 1 || s.called(You) {
+ return
+ }
+ for _, seat := range s.alive() {
+ if seat == You {
+ continue
+ }
+ if rng.Float64() >= botAlert {
+ continue // this one wasn't looking
+ }
+ s.penalise(You, seat, EvCaught, evs, rng)
+ return // caught once is caught. They don't queue up to take turns at you
+ }
+}
+
+// playerCatches calls out a seat you think is holding one card in silence.
+//
+// It is not a turn: right or wrong, the turn stays where it was, which is with
+// you. What it costs is the risk — a seat that did call, or isn't on one card at
+// all, is a seat you have just accused of nothing, and that is two cards to you.
+func (s *State) playerCatches(m Move, rng *rand.Rand) ([]Event, error) {
+ seat := m.Seat
+ if seat == You || seat < 0 || seat >= len(s.Hands) || !s.live(seat) {
+ return nil, ErrNoCatch
+ }
+ var evs []Event
+ if len(s.Hands[seat]) == 1 && !s.called(seat) {
+ s.penalise(seat, You, EvCaught, &evs, rng) // got them
+ } else {
+ s.penalise(You, seat, EvMiscall, &evs, rng) // they were clean, and you weren't looking
+ }
+ return evs, nil
+}
+
+// penalise makes a seat take the price of the call: cards off the deck, quietly —
+// no draw event, because what the table is being told about is the catch, and a
+// draw event alongside it would animate the same two cards twice.
+//
+// In No Mercy those two cards can be the two that bury them, which is a fine way
+// to go: caught on one card, dead on twenty-five.
+func (s *State) penalise(victim, by int, kind string, evs *[]Event, rng *rand.Rand) {
+ got := s.drawCards(victim, CatchPenalty, evs, rng)
+ if len(got) == 0 {
+ return // the table has nothing left to punish anybody with
+ }
+ s.ensureCalled()
+ s.Called[victim] = false
+ *evs = append(*evs, s.mine(Event{
+ Kind: kind, Seat: victim, By: by, N: len(got), Left: len(s.Hands[victim]),
+ }))
+ s.mercy(victim, evs, rng)
+}
+
+// UnoAt is which cards in your hand would leave you holding exactly one, if you
+// played them. It is the table's cue to ask you for the call, and it comes from
+// here rather than from the browser counting your cards because No Mercy's
+// "discard all" doesn't take one card out of your hand — it takes every card of
+// its colour, and the browser guessing at that is the browser getting it wrong.
+//
+// It answers for every card, legal or not. The table only ever asks about a card
+// you were allowed to play anyway.
+func (s State) UnoAt() []int {
+ if s.Phase == PhaseDone || s.Turn != You {
+ return nil
+ }
+ hand := s.Hands[You]
+ var out []int
+ for i, c := range hand {
+ left := len(hand) - 1
+ if c.Value == DiscardAll {
+ for j, other := range hand {
+ if j != i && other.Color == c.Color && !other.IsWild() {
+ left--
+ }
+ }
+ }
+ if left == 1 {
+ out = append(out, i)
+ }
+ }
+ return out
+}
+
+// Catchable is which seats are, right now, sitting on one card they never
+// announced. It is what the browser puts a button on.
+//
+// This leaks nothing. The card counts are already on the felt and the UNO badge
+// already isn't — this is the same two facts, subtracted, and doing that
+// subtraction on the server is only so that the rule for what counts as catchable
+// lives in one place instead of two.
+func (s State) Catchable() []int {
+ if s.Phase == PhaseDone || s.Turn != You {
+ return nil // you can only catch a seat on your own turn
+ }
+ var out []int
+ for _, seat := range s.alive() {
+ if seat != You && len(s.Hands[seat]) == 1 && !s.called(seat) {
+ out = append(out, seat)
+ }
+ }
+ return out
+}
+
+// called reports whether a seat holding one card announced it. Callers outside
+// the package read the Called slice, which is on the state they already hold.
+func (s State) called(seat int) bool {
+ return seat < len(s.Called) && s.Called[seat]
+}
+
+// ensureCalled grows the slice to fit the table. A game dealt before this rule
+// existed has no Called at all, and the seats in it are all — correctly —
+// uncalled: nobody in that game ever said the word, because there was no way to.
+func (s *State) ensureCalled() {
+ for len(s.Called) < len(s.Hands) {
+ s.Called = append(s.Called, false)
+ }
+}
+
+// tidyCalls forgets the calls that no longer mean anything. A seat's call is only
+// ever about the one card it is holding — draw into two and the word you said is
+// spent, and saying it again is a new thing you have to do.
+//
+// Without this, a seat that called on one card, was made to draw, and worked its
+// way back down to one would still be wearing the old call, and could never be
+// caught again for the rest of the game.
+func (s *State) tidyCalls() {
+ s.ensureCalled()
+ for seat := range s.Called {
+ if len(s.Hands[seat]) != 1 {
+ s.Called[seat] = false
+ }
+ }
+}
diff --git a/internal/games/uno/call_test.go b/internal/games/uno/call_test.go
new file mode 100644
index 0000000..2d6995e
--- /dev/null
+++ b/internal/games/uno/call_test.go
@@ -0,0 +1,323 @@
+package uno
+
+import "testing"
+
+// The UNO call, and the catch on the other side of it. See call.go.
+
+// oneCardAway sets you up holding two cards, both of which go on the pile, so
+// playing either one takes you to UNO.
+//
+// The bot is given a hand that can't touch a red pile and a deck that can't help
+// it, so whatever it does on its turn, it does not make you draw. That matters:
+// a hand that grows spends the call (see tidyCalls), which is correct and would
+// otherwise make these tests flap on the seeds where the bot happens to turn up
+// a +2.
+func oneCardAway(t *testing.T, seed uint64) State {
+ t.Helper()
+ s := deal(t, duel(), 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Red, Two}}
+ s.Hands[1] = []Card{{Blue, Three}, {Green, Four}, {Yellow, Six}}
+ s.Deck = make([]Card, 24)
+ for i := range s.Deck {
+ s.Deck[i] = Card{Blue, Nine} // nothing here plays on a red one, and nothing bites
+ }
+ s.Turn = You
+ s.Phase = PhasePlay
+ return s
+}
+
+// TestCallingUnoKeepsYouSafe — say the word and the table has nothing on you.
+// Across a spread of seeds, not one bot ever gets a catch.
+func TestCallingUnoKeepsYouSafe(t *testing.T) {
+ for seed := uint64(0); seed < 200; seed++ {
+ s := oneCardAway(t, seed)
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Uno: true})
+ if err != nil {
+ t.Fatalf("seed %d: play: %v", seed, err)
+ }
+ if hasKind(evs, EvCaught) {
+ t.Fatalf("seed %d: caught after calling UNO", seed)
+ }
+ if !hasKind(evs, EvUno) {
+ t.Fatalf("seed %d: called UNO and the table never said so", seed)
+ }
+ if !next.Called[You] {
+ t.Fatalf("seed %d: the call wasn't recorded", seed)
+ }
+ }
+}
+
+// TestForgettingUnoGetsYouCaught — stay quiet on one card and the bot takes you
+// for two. It gets one look, so it misses sometimes; over 400 games it should
+// land near botAlert, and the two cards should actually arrive.
+func TestForgettingUnoGetsYouCaught(t *testing.T) {
+ caught, games := 0, 400
+ for seed := uint64(0); seed < uint64(games); seed++ {
+ s := oneCardAway(t, seed)
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("seed %d: play: %v", seed, err)
+ }
+ if !hasKind(evs, EvCaught) {
+ continue
+ }
+ caught++
+ // One card left after the play, plus the two the catch cost.
+ if n := len(next.Hands[You]); n != 3 {
+ t.Fatalf("seed %d: caught and holding %d, want 3", seed, n)
+ }
+ if hasKind(evs, EvUno) {
+ t.Fatalf("seed %d: an UNO was announced by a seat that never called", seed)
+ }
+ }
+ rate := float64(caught) / float64(games)
+ if rate < botAlert-0.08 || rate > botAlert+0.08 {
+ t.Errorf("one bot caught you %.0f%% of the time, want about %.0f%% (botAlert)",
+ rate*100, botAlert*100)
+ }
+}
+
+// TestMoreBotsMeansLessGettingAwayWithIt — every seat gets its own look, so
+// forgetting at a full table is very nearly always punished.
+func TestMoreBotsMeansLessGettingAwayWithIt(t *testing.T) {
+ away := func(tier Tier, seed uint64) bool {
+ s := deal(t, tier, 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Red, Two}}
+ s.Turn = You
+ s.Phase = PhasePlay
+ _, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("play: %v", err)
+ }
+ return !hasKind(evs, EvCaught)
+ }
+ var got [2]float64
+ for i, tier := range []Tier{duel(), table()} {
+ escapes := 0
+ for seed := uint64(0); seed < 400; seed++ {
+ if away(tier, seed) {
+ escapes++
+ }
+ }
+ got[i] = float64(escapes) / 400
+ }
+ if got[1] >= got[0] {
+ t.Errorf("you got away with it %.0f%% of the time against one bot and %.0f%% against two; "+
+ "two pairs of eyes should catch you more often, not less", got[0]*100, got[1]*100)
+ }
+}
+
+// quietBot puts a bot on one card it never called, with the turn back on you.
+func quietBot(t *testing.T, called bool) State {
+ t.Helper()
+ s := deal(t, duel(), 100, 21)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Blue, Two}, {Green, Three}}
+ s.Hands[1] = []Card{{Yellow, Nine}}
+ s.Called = []bool{false, called}
+ s.Turn = You
+ s.Phase = PhasePlay
+ return s
+}
+
+// TestCatchingAQuietBot — it's on one card and it never said so. Two cards to it,
+// and the turn is still yours: catching is not a move you spend a turn on.
+func TestCatchingAQuietBot(t *testing.T) {
+ s := quietBot(t, false)
+ before := total(census(s))
+
+ next, evs, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: 1})
+ if err != nil {
+ t.Fatalf("catch: %v", err)
+ }
+ if !hasKind(evs, EvCaught) {
+ t.Fatal("no catch event")
+ }
+ if n := len(next.Hands[1]); n != 3 {
+ t.Errorf("the bot holds %d, want 3: one card, plus the two it just took", n)
+ }
+ if n := len(next.Hands[You]); n != 3 {
+ t.Errorf("your hand is %d, want 3: a catch costs you nothing", n)
+ }
+ if next.Turn != You {
+ t.Errorf("the turn went to seat %d: a catch is not a turn", next.Turn)
+ }
+ if total(census(next)) != before {
+ t.Error("the catch lost a card")
+ }
+}
+
+// TestCatchingACleanBotCostsYou — it called, or it isn't on one card at all.
+// Either way you've accused it of nothing, and that is two cards to you.
+func TestCatchingACleanBotCostsYou(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ state State
+ }{
+ {"it called", quietBot(t, true)},
+ {"it isn't even close", func() State {
+ s := quietBot(t, false)
+ s.Hands[1] = []Card{{Yellow, Nine}, {Yellow, Eight}}
+ return s
+ }()},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ next, evs, err := ApplyMove(tc.state, Move{Kind: MoveCatch, Seat: 1})
+ if err != nil {
+ t.Fatalf("catch: %v", err)
+ }
+ if !hasKind(evs, EvMiscall) {
+ t.Fatal("no miscall event")
+ }
+ if n := len(next.Hands[You]); n != 5 {
+ t.Errorf("your hand is %d, want 5: three, plus the two a bad call cost", n)
+ }
+ if next.Turn != You {
+ t.Errorf("the turn went to seat %d: even a bad catch isn't a turn", next.Turn)
+ }
+ })
+ }
+}
+
+// TestYouCannotCatchYourself, or a seat that isn't at the table.
+func TestYouCannotCatchYourself(t *testing.T) {
+ s := quietBot(t, false)
+ for _, seat := range []int{You, -1, 9} {
+ if _, _, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: seat}); err != ErrNoCatch {
+ t.Errorf("catching seat %d: got %v, want ErrNoCatch", seat, err)
+ }
+ }
+}
+
+// TestACallIsSpentWhenTheHandGrows. Call on one card, get made to draw, and work
+// your way back down to one: that is a new call you owe, not the old one still
+// standing. Without this a seat could be caught out once and never again.
+func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
+ s := deal(t, duel(), 100, 5)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}}
+ s.Called = []bool{true, false}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ // Draw, and the hand is two: the word you said was about a card you no longer
+ // hold on its own.
+ next, _, err := ApplyMove(s, Move{Kind: MoveDraw})
+ if err != nil {
+ t.Fatalf("draw: %v", err)
+ }
+ if next.Called[You] {
+ t.Error("the call survived the hand growing; it should be spent")
+ }
+}
+
+// TestCatchableIsWhatTheTableCanSee — a quiet bot on one card, and nobody else.
+func TestCatchable(t *testing.T) {
+ s := quietBot(t, false)
+ if got := s.Catchable(); len(got) != 1 || got[0] != 1 {
+ t.Errorf("Catchable() = %v, want [1]", got)
+ }
+ clean := quietBot(t, true)
+ if got := clean.Catchable(); len(got) != 0 {
+ t.Errorf("Catchable() = %v on a bot that called, want none", got)
+ }
+ // And not on somebody else's turn: you can only call it out when it's on you.
+ off := quietBot(t, false)
+ off.Turn = 1
+ if got := off.Catchable(); len(got) != 0 {
+ t.Errorf("Catchable() = %v off-turn, want none", got)
+ }
+}
+
+// TestUnoAtSeesThroughDiscardAll — the whole reason the table asks the engine
+// which cards take you to one, rather than counting your hand itself. "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 gets a player caught.
+func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
+ s := deal(t, nmDuel(), 100, 3)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, DiscardAll}, {Red, One}, {Red, Nine}, {Red, Seven}, {Blue, Two}}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ // Index 0 dumps itself and the three other reds: five cards become one.
+ // Index 4 is an ordinary play: five become four.
+ got := s.UnoAt()
+ if len(got) != 1 || got[0] != 0 {
+ t.Errorf("UnoAt() = %v, want [0]: only the discard-all lands you on one card", got)
+ }
+}
+
+// TestUnoAtIsTheOrdinaryCaseToo — two cards in hand, and either of them is a call.
+func TestUnoAtIsTheOrdinaryCaseToo(t *testing.T) {
+ s := oneCardAway(t, 1)
+ got := s.UnoAt()
+ if len(got) != 2 {
+ t.Errorf("UnoAt() = %v, want both cards: either one leaves you holding one", got)
+ }
+}
+
+// TestGoingOutNeedsNoCall — your last card is not one card, it's none. Nobody
+// owes the table a word for winning.
+func TestGoingOutNeedsNoCall(t *testing.T) {
+ s := deal(t, duel(), 100, 9)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("play the last card: %v", err)
+ }
+ if !next.Outcome.Won() {
+ t.Fatalf("outcome %q, want a win", next.Outcome)
+ }
+ if hasKind(evs, EvCaught) {
+ t.Error("caught for not calling UNO on the card that won the game")
+ }
+}
+
+// TestABotThatForgetsSaysNothing — the tell is the absence of the badge, and the
+// count beside the fan. If a quiet bot emitted anything at all there'd be nothing
+// to spot.
+func TestABotThatForgetsSaysNothing(t *testing.T) {
+ quiet := 0
+ for seed := uint64(0); seed < 300 && quiet < 1; seed++ {
+ s := deal(t, duel(), 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Blue, Two}, {Blue, Three}, {Blue, Four}}
+ s.Hands[1] = []Card{{Red, One}, {Red, Nine}}
+ s.Turn = 1
+ s.Phase = PhasePlay
+ s.Turn = You // the bot plays on the back of your move
+
+ // Draw, handing the turn over: the bot plays a red and lands on one card.
+ next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
+ if err != nil {
+ t.Fatalf("draw: %v", err)
+ }
+ if len(next.Hands[1]) != 1 || next.Called[1] {
+ continue // it either didn't get down to one, or it remembered
+ }
+ quiet++
+ if hasKind(evs, EvUno) {
+ t.Error("a bot that forgot to call still announced it")
+ }
+ if len(next.Catchable()) != 1 {
+ t.Error("a quiet bot on one card isn't catchable")
+ }
+ }
+ if quiet == 0 {
+ t.Skip("no bot forgot in 300 games; botForget may have been turned down")
+ }
+}
diff --git a/internal/games/uno/nomercy.go b/internal/games/uno/nomercy.go
index f42ee70..6b66a00 100644
--- a/internal/games/uno/nomercy.go
+++ b/internal/games/uno/nomercy.go
@@ -152,7 +152,7 @@ func (s *State) roulette(victim int, color Color, evs *[]Event, rng *rand.Rand)
}
if got > 0 {
e := Event{Kind: EvRoulette, Seat: victim, N: got, Color: color, Left: len(s.Hands[victim])}
- *evs = append(*evs, e)
+ *evs = append(*evs, s.mine(e))
}
if s.mercy(victim, evs, rng) {
return
@@ -181,8 +181,8 @@ func (s *State) discardAll(seat int, color Color, evs *[]Event) int {
top := s.Discard[len(s.Discard)-1]
s.Discard = append(s.Discard[:len(s.Discard)-1], dumped...)
s.Discard = append(s.Discard, top)
- *evs = append(*evs, Event{Kind: EvDiscardAll, Seat: seat, N: len(dumped),
- Color: color, Left: len(kept)})
+ *evs = append(*evs, s.mine(Event{Kind: EvDiscardAll, Seat: seat, N: len(dumped),
+ Color: color, Left: len(kept)}))
}
return len(dumped)
}
@@ -205,7 +205,7 @@ func (s *State) mercy(seat int, evs *[]Event, rng *rand.Rand) bool {
s.Hands[seat] = nil
s.Out[seat] = true
s.Pending = 0 // a dead seat pays no bill, and leaves none behind
- *evs = append(*evs, Event{Kind: EvMercy, Seat: seat, N: n, Left: 0})
+ *evs = append(*evs, s.mine(Event{Kind: EvMercy, Seat: seat, N: n, Left: 0}))
if seat == You {
s.lose(evs)
diff --git a/internal/games/uno/nomercy_test.go b/internal/games/uno/nomercy_test.go
index a9087d2..a7b4b03 100644
--- a/internal/games/uno/nomercy_test.go
+++ b/internal/games/uno/nomercy_test.go
@@ -120,6 +120,19 @@ func playMove(s State, idx int, rng *rand.Rand) Move {
if s.Hands[You][idx].IsWild() {
m.Color = Red + Color(rng.IntN(4))
}
+ // It calls UNO. That is not a strategy, it is a button — the table puts it in
+ // front of you and waits — and what these multiples price is *bad card play*,
+ // not a player who ignores the felt shouting at them.
+ //
+ // A player who genuinely never calls loses far more than this one does, and
+ // nothing here is priced for them. That is the rule having teeth, which is the
+ // point of it. Naive is the floor for someone playing the game badly, not for
+ // someone not playing it.
+ for _, at := range s.UnoAt() {
+ if at == idx {
+ m.Uno = true
+ }
+ }
return m
}
@@ -323,7 +336,10 @@ func TestDiscardAllTakesTheColourWithIt(t *testing.T) {
s.Phase = PhasePlay
before := total(census(s))
- next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ // With the call, because this play takes three reds out of the hand at once and
+ // lands on exactly one card — which is precisely the case a browser counting
+ // "hand length minus one" would miss, and the reason UnoAt is the engine's job.
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Uno: true})
if err != nil {
t.Fatalf("play discard-all: %v", err)
}
diff --git a/internal/games/uno/uno.go b/internal/games/uno/uno.go
index 6e1d913..8a45cd7 100644
--- a/internal/games/uno/uno.go
+++ b/internal/games/uno/uno.go
@@ -39,6 +39,7 @@ var (
ErrMustPlayNow = errors.New("uno: play the card you drew, or pass")
ErrMustStack = errors.New("uno: answer the stack with a draw card, or take it")
ErrNoStack = errors.New("uno: there's no stack to take")
+ ErrNoCatch = errors.New("uno: there's nobody to catch there")
ErrUnknownMove = errors.New("uno: unknown move")
ErrBadBet = errors.New("uno: bet must be positive")
ErrUnknownTier = errors.New("uno: no such tier")
@@ -233,6 +234,13 @@ func (t Tier) Deck() []Card {
// instead of the 8% they were meant to. The numbers below are the honest ones.
//
// This is exactly the drift TestTheMultiplesAreStillPriced now exists to stop.
+//
+// Re-measured again when the UNO call went in (call.go): 40.1% / 28.5% / 23.1%,
+// which is the same game. That is the point — the naive player *calls*, because
+// calling is a button and not a strategy, so the rule costs them nothing and
+// these multiples stand. What the rule adds is upside for a player who watches
+// the counts and catches a quiet bot, which is the good play these tiers are
+// meant to leave room for.
var Tiers = []Tier{
{Slug: "duel", Name: "Duel", Bots: 1, Base: 2.4,
Blurb: "One bot, head to head. A reverse is a skip with two at the table."},
@@ -246,7 +254,7 @@ var Tiers = []Tier{
//
// The multiples are measured, not guessed, and they are *not* the normal ones —
// the naive strategy (play the first legal card; take a stack you can't answer)
-// wins 46.7% / 31.2% / 25.3% here, against 40.3 / 29.2 / 23.3 on the normal deck.
+// wins 45.6% / 31.8% / 27.4% here, against 40.1 / 28.5 / 23.1 on the normal deck.
//
// Which is to say: **No Mercy is easier than UNO**, at every table size, and so
// it pays less. That reads backwards until you see why. The mercy rule kills
@@ -264,7 +272,11 @@ var NoMercyTiers = []Tier{
Blurb: "One bot, 168 cards. Stack the draws or eat them."},
{Slug: "nm-table", Name: "No Mercy Table", Bots: 2, Base: 3.1, NoMercy: true,
Blurb: "Two bots. A +10 answered twice is somebody's whole hand."},
- {Slug: "nm-full", Name: "No Mercy Full House", Bots: 3, Base: 3.8, NoMercy: true,
+ // Re-priced 2026-07-14 with the call rule in: 3.8 was paying a *negative* house
+ // edge here (-0.2%), which is the house paying you to sit down. The naive win
+ // rate at this table is 27.4%, not the 25.3% it was priced off — three bots
+ // burying each other is even better for you than the last measurement caught.
+ {Slug: "nm-full", Name: "No Mercy Full House", Bots: 3, Base: 3.5, NoMercy: true,
Blurb: "Three bots. Twenty-five cards and you're out of the game."},
}
@@ -325,6 +337,11 @@ type State struct {
Out []bool `json:"out,omitempty"`
Pending int `json:"pending,omitempty"`
+ // Called is who, holding one card, said so. It is only ever meaningful for a
+ // seat on exactly one card — every other seat's entry is false and means
+ // nothing — and it is what a catch is tested against. See call.go.
+ Called []bool `json:"called,omitempty"`
+
Seed1 uint64 `json:"seed1"`
Seed2 uint64 `json:"seed2"`
Step uint64 `json:"step"` // how many moves have been applied; the rng's other half
@@ -346,7 +363,26 @@ type Event struct {
Color Color `json:"color,omitempty"` // the colour now in play, on a wild
N int `json:"n,omitempty"` // how many cards were drawn
Left int `json:"left"` // cards left in that seat's hand afterwards
+ By int `json:"by"` // who caught them, on a catch. Seat zero is a real answer here, so never omitempty
Text string `json:"text,omitempty"`
+
+ // Hand is *your* hand as it stands after this event, and it is only ever set
+ // on an event that changed it. The table plays a lap of events back over
+ // several seconds, and for all of them the hand on screen has to be the hand
+ // you actually hold — a +4 that lands on you at the top of the lap must show
+ // up in your fan as it lands, not when the lap ends. There is no leak here:
+ // this is the one hand the browser is already entitled to see.
+ Hand []Card `json:"hand,omitempty"`
+}
+
+// mine stamps the player's hand onto an event that just changed it. Events about
+// a bot's hand carry nothing — the browser never learns those, and stamping is
+// scoped to seat zero precisely so it can't start.
+func (s *State) mine(e Event) Event {
+ if e.Seat == You {
+ e.Hand = append([]Card(nil), s.Hands[You]...)
+ }
+ return e
}
// The kinds an Event comes in.
@@ -359,7 +395,9 @@ type Event struct {
// pass the turn moves on with nothing played
// skip a seat loses its turn
// reverse the direction flips
-// uno a hand is down to one card
+// uno a hand is down to one card, and its owner said so
+// caught a seat went down to one card *quietly*, and somebody noticed: +2
+// miscall you called a seat that had nothing to hide, and paid for it: +2
// reshuffle the discard goes back under
// settle it's over
//
@@ -379,6 +417,8 @@ const (
EvSkip = "skip"
EvReverse = "reverse"
EvUno = "uno"
+ EvCaught = "caught"
+ EvMiscall = "miscall"
EvReshuffle = "reshuffle"
EvSettle = "settle"
@@ -392,19 +432,26 @@ const (
// Move is what the player sends: play this card, take one off the deck, or —
// having taken one you can play — decline to play it.
type Move struct {
- Kind string `json:"kind"` // "play" | "draw" | "pass"
+ Kind string `json:"kind"` // "play" | "draw" | "pass" | "take" | "catch"
Index int `json:"index"` // which card of your hand, for a play
Color Color `json:"color"` // the colour you name, for a wild
+ Uno bool `json:"uno"` // "…and UNO!", for a play that leaves you on one card
+ Seat int `json:"seat"` // whose silence you're calling, for a catch
}
// Move kinds. Take is No Mercy's: it is how you give in to a stack you can't
// answer, and it is a *decision*, so it gets a name of its own rather than being
// bolted onto draw.
+//
+// Catch is the other half of the UNO call — see call.go. It is a move you make
+// out of turn order (it costs you nothing but the risk of being wrong), so it is
+// a kind rather than a flag on the moves that do cost a turn.
const (
- MovePlay = "play"
- MoveDraw = "draw"
- MovePass = "pass"
- MoveTake = "take"
+ MovePlay = "play"
+ MoveDraw = "draw"
+ MovePass = "pass"
+ MoveTake = "take"
+ MoveCatch = "catch"
)
// New deals a game: a shuffled deck, seven each, and a card turned over.
@@ -434,6 +481,7 @@ func New(bet int64, t Tier, rakePct float64, seed1, seed2 uint64) (State, []Even
seats := t.Bots + 1
s.Out = make([]bool, seats)
+ s.Called = make([]bool, seats)
s.Hands = make([][]Card, seats)
for i := range s.Hands {
s.Hands[i] = make([]Card, 0, HandSize)
@@ -460,7 +508,7 @@ func New(bet int64, t Tier, rakePct float64, seed1, seed2 uint64) (State, []Even
break
}
- return s, []Event{{Kind: EvDeal, Card: s.topPtr(), Color: s.Color}}, nil
+ return s, []Event{s.mine(Event{Kind: EvDeal, Card: s.topPtr(), Color: s.Color})}, nil
}
// ApplyMove is the engine. Your move goes in; your move, and every bot turn it
@@ -479,6 +527,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
next := s.clone()
next.Step++
+ next.ensureCalled()
rng := stepRNG(next.Seed1, next.Seed2, next.Step)
var evs []Event
@@ -492,6 +541,8 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
evs, err = next.playerPasses()
case MoveTake:
evs, err = next.playerTakes(rng)
+ case MoveCatch:
+ evs, err = next.playerCatches(m, rng)
default:
return s, nil, ErrUnknownMove
}
@@ -499,8 +550,17 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
return s, nil, err // the caller's state, untouched
}
+ // Before anybody moves: did you go down to one card without saying so? This is
+ // the only window there is. The bots are about to take their turns, and a bot
+ // that has played on is a bot that has stopped looking at your hand.
+ next.botsCatch(&evs, rng)
+
// The bots take their turns on the back of yours, and the whole run comes
// back as one script. This is the reason solo UNO needs no socket.
+ //
+ // A catch is not a turn — it leaves the turn where it was, which is with you —
+ // so this is a no-op after one, and that is the whole mechanism by which
+ // catching a bot costs you nothing but the risk of being wrong.
next.runBots(&evs, rng)
// And if that left a table nobody can move at, it ends here rather than
@@ -508,6 +568,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
if next.Phase != PhaseDone && next.stalled() {
next.stuck(&evs)
}
+ next.tidyCalls()
return next, evs, nil
}
@@ -543,6 +604,11 @@ func (s *State) playerPlays(m Move, rng *rand.Rand) ([]Event, error) {
var evs []Event
s.discard(You, card, m.Color, &evs)
s.after(You, card, &evs, rng)
+ // Whether you called is checked *after* the card has finished resolving, not
+ // after it left your hand. No Mercy's "discard all" takes every card of a
+ // colour with it, so it can drop you from six cards to one in a single play —
+ // and the seat that gets there that way owes the call just the same.
+ s.declare(You, m.Uno, &evs)
return evs, nil
}
@@ -693,6 +759,11 @@ func (s *State) botPlays(seat int, card Card, idx int, evs *[]Event, rng *rand.R
}
s.discard(seat, card, color, evs)
s.after(seat, card, evs, rng)
+ // A bot that has just gone down to one card mostly remembers to say so. When it
+ // doesn't, it says nothing at all — no event, no badge — and the only thing on
+ // the table that gives it away is the count beside its fan reading "1 card".
+ // Spotting that is the player's to do.
+ s.declare(seat, !botForgets(rng), evs)
}
// stalled reports whether the table is dead: nothing left to draw anywhere, and
@@ -737,10 +808,11 @@ func (s *State) discard(seat int, card Card, color Color, evs *[]Event) {
}
s.Discard = append(s.Discard, card)
e := Event{Kind: EvPlay, Seat: seat, Card: &card, Color: s.Color, Left: len(s.Hands[seat])}
- *evs = append(*evs, e)
- if len(s.Hands[seat]) == 1 {
- *evs = append(*evs, Event{Kind: EvUno, Seat: seat})
- }
+ *evs = append(*evs, s.mine(e))
+ // No UNO event here any more. Going down to one card used to *be* the call,
+ // which meant nobody at this table could ever fail to make it. Now the call is
+ // a thing a seat does or doesn't do, and it is announced — or conspicuously not
+ // — by the two functions that know whether it was made. See call.go.
}
// after resolves what the card just played does, and moves the turn on. It is
@@ -834,18 +906,7 @@ func (s *State) punish(victim, n int, evs *[]Event, rng *rand.Rand) {
// A bot's cards go face down: the event carries the count, never the card. The
// only hand whose faces cross the wire is yours.
func (s *State) deal(seat, n int, forced bool, evs *[]Event, rng *rand.Rand) []Card {
- got := make([]Card, 0, n)
- for i := 0; i < n; i++ {
- if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
- break
- }
- c, ok := s.pop()
- if !ok {
- break
- }
- s.Hands[seat] = append(s.Hands[seat], c)
- got = append(got, c)
- }
+ got := s.drawCards(seat, n, evs, rng)
if len(got) == 0 {
return got
}
@@ -858,7 +919,28 @@ func (s *State) deal(seat, n int, forced bool, evs *[]Event, rng *rand.Rand) []C
c := got[0]
e.Card = &c // your own card, and only yours, comes face up
}
- *evs = append(*evs, e)
+ *evs = append(*evs, s.mine(e))
+ return got
+}
+
+// drawCards is deal without the announcement: cards come off the deck (with a
+// reshuffle under them if it runs dry) and go into a hand, and nothing is said
+// about it. deal wraps it to say the usual thing; a catch wraps it to say a
+// different thing. It hands back what was actually drawn, which can be fewer
+// cards than asked for when the table has nothing left to give.
+func (s *State) drawCards(seat, n int, evs *[]Event, rng *rand.Rand) []Card {
+ got := make([]Card, 0, n)
+ for i := 0; i < n; i++ {
+ if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
+ break
+ }
+ c, ok := s.pop()
+ if !ok {
+ break
+ }
+ s.Hands[seat] = append(s.Hands[seat], c)
+ got = append(got, c)
+ }
return got
}
@@ -1105,6 +1187,7 @@ func (s State) clone() State {
s.Discard = append([]Card(nil), s.Discard...)
s.Bots = append([]string(nil), s.Bots...)
s.Out = append([]bool(nil), s.Out...)
+ s.Called = append([]bool(nil), s.Called...)
return s
}
diff --git a/internal/web/games_uno.go b/internal/web/games_uno.go
index 1b1074e..3323a00 100644
--- a/internal/web/games_uno.go
+++ b/internal/web/games_uno.go
@@ -45,11 +45,12 @@ func viewUnoCard(c uno.Card) unoCardView {
// unoSeatView is one seat at the table: a name, and a number of cards. A bot's
// cards are a *count*. There is no field here for what they are.
type unoSeatView struct {
- Name string `json:"name"`
- Cards int `json:"cards"`
- You bool `json:"you"`
- Uno bool `json:"uno"` // down to one card
- Out bool `json:"out"` // No Mercy: buried at twenty-five, and not coming back
+ Name string `json:"name"`
+ Cards int `json:"cards"`
+ You bool `json:"you"`
+ Uno bool `json:"uno"` // down to one card
+ Called bool `json:"called"` // …and said so. A seat where Uno is true and this isn't is a seat you can catch
+ Out bool `json:"out"` // No Mercy: buried at twenty-five, and not coming back
}
// unoView is a game as its player may see it.
@@ -63,6 +64,14 @@ type unoView struct {
Color string `json:"color"` // the colour in play, which a wild renames
Deck int `json:"deck"` // cards left to draw
+ // The UNO call. UnoAt is which of your cards would leave you holding exactly
+ // one if you played it — the table asks for the call on those, and it asks the
+ // engine which they are because No Mercy's "discard all" makes counting the
+ // hand the wrong answer. Catchable is which seats are sitting on one card they
+ // never announced.
+ UnoAt []int `json:"uno_at"`
+ Catchable []int `json:"catchable"`
+
Turn int `json:"turn"`
Dir int `json:"dir"`
@@ -104,7 +113,10 @@ func viewUno(g uno.State) unoView {
// alone, which is why a buried seat is asked about rather than inferred.
for i, n := range g.Counts() {
live := g.Live(i)
- seat := unoSeatView{Cards: n, You: i == uno.You, Uno: live && n == 1, Out: !live}
+ seat := unoSeatView{
+ Cards: n, You: i == uno.You, Uno: live && n == 1, Out: !live,
+ Called: i < len(g.Called) && g.Called[i],
+ }
if i == uno.You {
seat.Name = "You"
} else if i-1 < len(g.Bots) {
@@ -127,6 +139,16 @@ func viewUno(g uno.State) unoView {
if v.Playable == nil {
v.Playable = []int{}
}
+ // Empty arrays, never null: the table indexes into these, and `null.indexOf`
+ // is a broken game rather than a quiet no-op.
+ v.UnoAt = g.UnoAt()
+ if v.UnoAt == nil {
+ v.UnoAt = []int{}
+ }
+ v.Catchable = g.Catchable()
+ if v.Catchable == nil {
+ v.Catchable = []int{}
+ }
return v
}
@@ -141,16 +163,32 @@ type unoEventView struct {
Color string `json:"color,omitempty"`
N int `json:"n,omitempty"`
Left int `json:"left"` // never omitempty: a seat that goes out leaves zero, and zero is the number the table has to draw
+ By int `json:"by"` // who did the catching. Seat zero is you, and "you" is a real answer, so never omitempty either
Text string `json:"text,omitempty"`
+
+ // Hand is your hand as it stands after this event, on the events that changed
+ // it. The table redraws your fan from this as the lap plays back — see the
+ // engine's Event.Hand for why. It is your own hand, so there is nothing here
+ // the browser wasn't already holding.
+ Hand []unoCardView `json:"hand,omitempty"`
}
func viewUnoEvents(evs []uno.Event) []unoEventView {
out := make([]unoEventView, 0, len(evs))
for _, e := range evs {
- v := unoEventView{Kind: e.Kind, Seat: e.Seat, N: e.N, Left: e.Left, Text: e.Text}
+ v := unoEventView{Kind: e.Kind, Seat: e.Seat, N: e.N, Left: e.Left, By: e.By, Text: e.Text}
if e.Color != uno.Wild {
v.Color = e.Color.String()
}
+ // The engine only stamps a hand on an event about seat zero. This is the belt
+ // to that brace: whatever the engine thinks it's doing, no hand but yours
+ // leaves this building.
+ if e.Seat == uno.You && e.Hand != nil {
+ v.Hand = make([]unoCardView, 0, len(e.Hand))
+ for _, c := range e.Hand {
+ v.Hand = append(v.Hand, viewUnoCard(c))
+ }
+ }
if e.Card != nil {
// The engine only ever attaches a card to an event the seat is entitled
// to see it in — a card played face up, or one *you* drew. This check is
@@ -268,6 +306,8 @@ func (s *Server) handleUnoMove(w http.ResponseWriter, r *http.Request) {
msg = "answer the stack with a draw card, or take it"
case errors.Is(err, uno.ErrNoStack):
msg = "there's nothing pointed at you to take"
+ case errors.Is(err, uno.ErrNoCatch):
+ msg = "there's nobody in that seat to catch"
}
writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
return
diff --git a/internal/web/static/css/input.css b/internal/web/static/css/input.css
index 10a77a9..f06962e 100644
--- a/internal/web/static/css/input.css
+++ b/internal/web/static/css/input.css
@@ -1563,6 +1563,10 @@ html[data-phase="night"] {
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);
@@ -1750,6 +1754,85 @@ html[data-phase="night"] {
.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
diff --git a/internal/web/static/css/output.css b/internal/web/static/css/output.css
index 6325cb6..941377f 100644
--- a/internal/web/static/css/output.css
+++ b/internal/web/static/css/output.css
@@ -1 +1 @@
-*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.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),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.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)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.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:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.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,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.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,.05)}.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}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow: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,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.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:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.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{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.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;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.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}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.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}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(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,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.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)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.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,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.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)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.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 .08s ease,background .15s ease,opacity .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(.96)}.pete-key:disabled{cursor:default}.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:.4}.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)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.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);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 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(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.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)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.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)}.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))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.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}}.pete-rack[data-at=rail]{position:static;justify-content:center}.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:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.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}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.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,.1)}.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:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.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(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.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,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{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:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(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}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .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%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.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,.3);transition:transform .12s ease,filter .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}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:1.05rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.5rem;line-height:1.1;letter-spacing:.02em;text-align:center;padding:0 .15rem}.pete-uno-card[data-glow="1"]{position:relative;z-index:0}.pete-uno-card[data-glow="1"]:before{content:"";position:absolute;inset:-.35rem;z-index:-1;border-radius:.9rem;pointer-events:none;background:conic-gradient(from 0deg,#d64545,#e0b02c,#46a86b,#3d7fd6,#d64545);filter:blur(7px);opacity:.75;animation:pete-uno-glow 3.2s linear infinite}.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%,hsla(0,0%,100%,.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}to{background-position:-40% 0}}.pete-uno-pending{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.18rem .7rem;border-radius:999px;color:#fff;background:#cc3d4a;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-bill 1.1s ease-in-out infinite}@keyframes pete-uno-bill{0%,to{transform:scale(1)}50%{transform:scale(1.06)}}.pete-uno-seat[data-out="1"]{opacity:.4;filter:grayscale(1)}.pete-uno-seat[data-out="1"] .pete-uno-count{color:#ffb3ba}.pete-seg{display:inline-flex;padding:.2rem;border-radius:999px;background:color-mix(in srgb,var(--ink) 6%,transparent)}.pete-seg-btn{padding:.3rem .9rem;border-radius:999px;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:color-mix(in srgb,var(--ink) 55%,transparent);transition:background .15s ease,color .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,.15)}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.42rem}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:.9rem}.pete-uno-discard{--uno-h:5.2rem;--uno-w:3.5rem;height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}.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}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[\#cc3d4a\]{--tw-bg-opacity:1;background-color:rgb(204 61 74/var(--tw-bg-opacity,1))}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.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:#039}.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:#039}.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:#039}.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}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.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,.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:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}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;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),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}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.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}.pete-poker{--seat-w:7.5rem}.pete-poker-seats{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:.75rem}.pete-seat{position:relative;display:flex;flex-direction:column;align-items:center;gap:.4rem;width:var(--seat-w);transition:opacity .3s ease,filter .3s ease}.pete-seat[data-state=folded]{opacity:.38;filter:grayscale(.7)}.pete-seat[data-state=out]{opacity:.2}.pete-seat[data-turn="1"] .pete-seat-plate{box-shadow:0 0 0 2px var(--accent),0 0 1.6rem -.2rem var(--accent);animation:pete-seat-wait 1.6s ease-in-out infinite}@keyframes pete-seat-wait{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}.pete-seat-cards{display:flex;justify-content:center;gap:.15rem;min-height:4.4rem;--card-h:4.4rem;--card-w:3.15rem}.pete-seat-cards[data-mucked="1"]{opacity:0;transform:translateY(-.6rem) scale(.9);transition:all .35s ease}.pete-seat-plate{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;padding:.3rem .4rem;border-radius:.85rem;background:rgba(0,0,0,.32);border:1px solid hsla(0,0%,100%,.12);transition:box-shadow .25s ease}.pete-seat-name{font-family:var(--font-display,inherit);font-weight:700;font-size:.8rem;line-height:1.1;color:hsla(0,0%,100%,.92);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pete-seat-stack{font-weight:700;font-size:.78rem;font-variant-numeric:tabular-nums;color:hsla(0,0%,100%,.58)}.pete-seat[data-state=allin] .pete-seat-stack:after{content:" all in";color:var(--accent)}.pete-seat-pos{position:absolute;top:-.5rem;right:-.5rem;display:grid;place-items:center;min-width:1.45rem;height:1.45rem;padding:0 .3rem;border-radius:999px;font-size:.58rem;font-weight:800;letter-spacing:.02em;background:hsla(0,0%,100%,.9);color:#2b2118;box-shadow:0 2px 0 rgba(0,0,0,.3)}.pete-seat-pos[data-pos=BTN]{background:#f5d76e}.pete-seat-spot{height:3.6rem;width:3.6rem;margin-bottom:.5rem}.pete-seat-spot .pete-disc{height:1.6rem;width:1.6rem;margin:-.8rem 0 0 -.8rem}.pete-seat-spot .pete-spot-total{font-size:.66rem;padding:.05rem .45rem}.pete-poker-middle{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:.5rem 0}.pete-poker-pot{margin-top:.9rem}.pete-poker-board{display:flex;gap:.35rem;min-height:6rem;--card-h:6rem;--card-w:4.3rem}.pete-poker-pot{display:flex;flex-direction:column;align-items:center;gap:.1rem}.pete-poker-pot-pile{position:relative;width:7rem;height:3rem}.pete-poker-pot-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:hsla(0,0%,100%,.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,.28)}.pete-poker-side{font-size:.68rem;font-weight:700;color:hsla(0,0%,100%,.5)}.pete-poker-you .pete-seat{width:auto}.pete-poker-you .pete-seat-cards{--card-h:6.8rem;--card-w:4.85rem;gap:.3rem;min-height:6.8rem}.pete-poker-you .pete-seat-plate{padding:.4rem 1.1rem}.pete-poker-you .pete-seat-name,.pete-poker-you .pete-seat-stack{font-size:.95rem}.pete-poker-verdict{border-radius:999px;background:hsla(0,0%,100%,.95);padding:.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,.18)}.pete-poker-verdict[data-tone=win]{background:#4caf7d;color:#fff}.pete-poker-verdict[data-tone=lose]{background:hsla(0,0%,100%,.9);color:#7a5c50}.pete-raise{display:flex;align-items:center;gap:.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:.75rem;line-height:1.5;color:hsla(0,0%,100%,.55)}.pete-poker-log b{color:hsla(0,0%,100%,.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:.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}}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}}
\ No newline at end of file
+*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.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),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.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)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.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:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.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,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.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,.05)}.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}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow: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,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.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:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.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{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.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;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.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}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.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}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(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,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.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)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.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,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.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)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.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 .08s ease,background .15s ease,opacity .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(.96)}.pete-key:disabled{cursor:default}.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:.4}.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)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.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);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 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(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.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)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.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)}.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))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.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}}.pete-rack[data-at=rail]{position:static;justify-content:center}.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:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.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}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.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,.1)}.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:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-settled="1"]{animation:none}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.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(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.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,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{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:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(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}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .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%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.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,.3);transition:transform .12s ease,filter .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}.pete-uno-call{margin-top:.85rem;padding:.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:.02em;box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.55);animation:pete-uno-call .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,.3)}@keyframes pete-uno-call{0%,to{transform:scale(1);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.5)}50%{transform:scale(1.05);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 .9rem rgba(224,176,44,0)}}.pete-uno-timer{margin-top:.8rem;height:.35rem;border-radius:999px;background:hsla(0,0%,100%,.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{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.pete-uno-catch{position:absolute;top:-.55rem;left:50%;transform:translateX(-50%);z-index:6;padding:.22rem .7rem;border-radius:999px;background:#cc3d4a;color:#fff;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.25);animation:pete-uno-catch 1.1s ease-in-out infinite;transition:filter .12s ease}.pete-uno-catch:hover{filter:brightness(1.12)}.pete-uno-catch:disabled{opacity:.45;animation:none}@keyframes pete-uno-catch{0%,to{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}}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:1.05rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.5rem;line-height:1.1;letter-spacing:.02em;text-align:center;padding:0 .15rem}.pete-uno-card[data-glow="1"]{position:relative;z-index:0}.pete-uno-card[data-glow="1"]:before{content:"";position:absolute;inset:-.35rem;z-index:-1;border-radius:.9rem;pointer-events:none;background:conic-gradient(from 0deg,#d64545,#e0b02c,#46a86b,#3d7fd6,#d64545);filter:blur(7px);opacity:.75;animation:pete-uno-glow 3.2s linear infinite}.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%,hsla(0,0%,100%,.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}to{background-position:-40% 0}}.pete-uno-pending{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.18rem .7rem;border-radius:999px;color:#fff;background:#cc3d4a;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-bill 1.1s ease-in-out infinite}@keyframes pete-uno-bill{0%,to{transform:scale(1)}50%{transform:scale(1.06)}}.pete-uno-seat[data-out="1"]{opacity:.4;filter:grayscale(1)}.pete-uno-seat[data-out="1"] .pete-uno-count{color:#ffb3ba}.pete-seg{display:inline-flex;padding:.2rem;border-radius:999px;background:color-mix(in srgb,var(--ink) 6%,transparent)}.pete-seg-btn{padding:.3rem .9rem;border-radius:999px;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:color-mix(in srgb,var(--ink) 55%,transparent);transition:background .15s ease,color .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,.15)}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.42rem}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:.9rem}.pete-uno-discard{--uno-h:5.2rem;--uno-w:3.5rem;height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}.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}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[\#cc3d4a\]{--tw-bg-opacity:1;background-color:rgb(204 61 74/var(--tw-bg-opacity,1))}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.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:#039}.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:#039}.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:#039}.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}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.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,.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:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}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;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),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}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.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}.pete-poker{--seat-w:7.5rem}.pete-poker-seats{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:.75rem}.pete-seat{position:relative;display:flex;flex-direction:column;align-items:center;gap:.4rem;width:var(--seat-w);transition:opacity .3s ease,filter .3s ease}.pete-seat[data-state=folded]{opacity:.38;filter:grayscale(.7)}.pete-seat[data-state=out]{opacity:.2}.pete-seat[data-turn="1"] .pete-seat-plate{box-shadow:0 0 0 2px var(--accent),0 0 1.6rem -.2rem var(--accent);animation:pete-seat-wait 1.6s ease-in-out infinite}@keyframes pete-seat-wait{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}.pete-seat-cards{display:flex;justify-content:center;gap:.15rem;min-height:4.4rem;--card-h:4.4rem;--card-w:3.15rem}.pete-seat-cards[data-mucked="1"]{opacity:0;transform:translateY(-.6rem) scale(.9);transition:all .35s ease}.pete-seat-plate{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;padding:.3rem .4rem;border-radius:.85rem;background:rgba(0,0,0,.32);border:1px solid hsla(0,0%,100%,.12);transition:box-shadow .25s ease}.pete-seat-name{font-family:var(--font-display,inherit);font-weight:700;font-size:.8rem;line-height:1.1;color:hsla(0,0%,100%,.92);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pete-seat-stack{font-weight:700;font-size:.78rem;font-variant-numeric:tabular-nums;color:hsla(0,0%,100%,.58)}.pete-seat[data-state=allin] .pete-seat-stack:after{content:" all in";color:var(--accent)}.pete-seat-pos{position:absolute;top:-.5rem;right:-.5rem;display:grid;place-items:center;min-width:1.45rem;height:1.45rem;padding:0 .3rem;border-radius:999px;font-size:.58rem;font-weight:800;letter-spacing:.02em;background:hsla(0,0%,100%,.9);color:#2b2118;box-shadow:0 2px 0 rgba(0,0,0,.3)}.pete-seat-pos[data-pos=BTN]{background:#f5d76e}.pete-seat-spot{height:3.6rem;width:3.6rem;margin-bottom:.5rem}.pete-seat-spot .pete-disc{height:1.6rem;width:1.6rem;margin:-.8rem 0 0 -.8rem}.pete-seat-spot .pete-spot-total{font-size:.66rem;padding:.05rem .45rem}.pete-poker-middle{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:.5rem 0}.pete-poker-pot{margin-top:.9rem}.pete-poker-board{display:flex;gap:.35rem;min-height:6rem;--card-h:6rem;--card-w:4.3rem}.pete-poker-pot{display:flex;flex-direction:column;align-items:center;gap:.1rem}.pete-poker-pot-pile{position:relative;width:7rem;height:3rem}.pete-poker-pot-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:hsla(0,0%,100%,.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,.28)}.pete-poker-side{font-size:.68rem;font-weight:700;color:hsla(0,0%,100%,.5)}.pete-poker-you .pete-seat{width:auto}.pete-poker-you .pete-seat-cards{--card-h:6.8rem;--card-w:4.85rem;gap:.3rem;min-height:6.8rem}.pete-poker-you .pete-seat-plate{padding:.4rem 1.1rem}.pete-poker-you .pete-seat-name,.pete-poker-you .pete-seat-stack{font-size:.95rem}.pete-poker-verdict{border-radius:999px;background:hsla(0,0%,100%,.95);padding:.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,.18)}.pete-poker-verdict[data-tone=win]{background:#4caf7d;color:#fff}.pete-poker-verdict[data-tone=lose]{background:hsla(0,0%,100%,.9);color:#7a5c50}.pete-raise{display:flex;align-items:center;gap:.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:.75rem;line-height:1.5;color:hsla(0,0%,100%,.55)}.pete-poker-log b{color:hsla(0,0%,100%,.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:.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}}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}}
\ No newline at end of file
diff --git a/internal/web/static/js/blackjack.js b/internal/web/static/js/blackjack.js
index ee0ada0..0fc5645 100644
--- a/internal/web/static/js/blackjack.js
+++ b/internal/web/static/js/blackjack.js
@@ -164,7 +164,13 @@
// The one thing in this room that gets confetti. A natural is rare, it pays
// 3:2, and if everything celebrated then nothing would.
+ //
+ // The *sound* is not so precious: a win is a win and you should hear it. So
+ // the fanfare rides on the money, not on the confetti.
if (v.outcome === "blackjack") FX.burst(verdictEl, { count: 34 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
+ else FX.sfx("push");
}
// setPhase swaps the controls: bet between hands, act during one.
@@ -221,10 +227,12 @@
playerEl.innerHTML = "";
playerEl.dataset.won = "0";
verdictEl.classList.add("hidden");
+ FX.sfx("shuffle");
return;
case "player_card":
playerEl.appendChild(cardEl(e.card));
+ FX.sfx("deal");
return wait(DEAL_MS);
case "dealer_card":
@@ -235,12 +243,14 @@
drew = true;
return beat.then(function () {
dealerEl.appendChild(cardEl(e.card));
+ FX.sfx("deal", { v: 1 });
return wait(DEAL_MS);
});
case "dealer_hole":
hole = cardEl(null);
dealerEl.appendChild(hole);
+ FX.sfx("deal", { v: 2 });
return wait(DEAL_MS);
case "reveal":
diff --git a/internal/web/static/js/casino-cards.js b/internal/web/static/js/casino-cards.js
index c8d663a..aa65c90 100644
--- a/internal/web/static/js/casino-cards.js
+++ b/internal/web/static/js/casino-cards.js
@@ -155,11 +155,17 @@
}
// turnOver flips a face-down card up, now that we've been told what it is.
+ //
+ // Unlike el(), this is only ever a *gesture* — a card that was down and is now
+ // up, because something happened. It never runs on a repaint, which is why the
+ // flip sound can live here and be right for every table at once, and why the
+ // deal sound cannot: el() runs every time a board is redrawn.
function turnOver(wrap, face) {
if (!wrap) return;
paint(wrap.querySelector(".pete-card-front"), face);
wrap.dataset.face = "up";
wrap.dataset.key = face.label;
+ if (window.PeteSFX) window.PeteSFX.play("flip");
}
window.PeteCards = {
diff --git a/internal/web/static/js/casino-fx.js b/internal/web/static/js/casino-fx.js
index 6c978d5..0a4fc30 100644
--- a/internal/web/static/js/casino-fx.js
+++ b/internal/web/static/js/casino-fx.js
@@ -18,6 +18,14 @@
var reduced =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ // The room's noise, if it's loaded. Everything that flies through this file
+ // makes a sound when it lands, which is how every table got sound at once
+ // without any of them being told about it: a chip is a chip wherever it's
+ // thrown from. A game that wants something more specific says so per call.
+ function sfx(name, opts) {
+ if (window.PeteSFX) window.PeteSFX.play(name, opts);
+ }
+
var layer = null;
function stage() {
if (!layer) {
@@ -107,13 +115,23 @@
// than averaging itself back down to nothing.
var sMid = Math.max(s0, s1) * 1.12;
+ var wait = reduced ? 0 : opts.delay || 0;
+
+ // It makes a noise when it lands, and the noise is scheduled on the audio
+ // clock for the moment it lands — not fired by a timer that goes off when the
+ // animation ends. A timer that drifts thirty milliseconds is a card you hear
+ // before you see, and that is worse than silence.
+ if (opts.sound) {
+ sfx(opts.sound, { delay: (wait + dur) / 1000, v: opts.index || 0 });
+ }
+
var anim = node.animate(
[
{ transform: t(a.x, a.y, s0, opts.fromSpin || 0), opacity: 1, offset: 0 },
{ transform: t(midX, midY, sMid, spin * 0.6), opacity: 1, offset: 0.5 },
{ transform: t(b.x, b.y, s1, spin), opacity: opts.fade ? 0 : 1, offset: 1 },
],
- { duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: reduced ? 0 : opts.delay || 0, fill: "both" }
+ { duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: wait, fill: "both" }
);
return anim.finished
@@ -127,6 +145,9 @@
// was — UNO wanted the same throw with a card in it, so the throw moved out.
function fly(from, to, opts) {
opts = opts || {};
+ // A chip clinks. Every table in the room bets, pays and sweeps through this
+ // one function, so saying it once here is every table having chip sounds.
+ if (opts.sound === undefined) opts = Object.assign({ sound: "chip" }, opts);
return flyNode(disc(opts.denom || 25), from, to, opts);
}
@@ -225,6 +246,7 @@
var n = amount == null ? api.amount : amount;
var left = api.amount - n;
if (n <= 0) return Promise.resolve();
+ sfx("sweep"); // the pile leaving the felt, under the chips landing
var chain = flyMany(els.spot, to, chipsFor(n, 8), Object.assign({ gap: 40, lift: 0.8 }, opts || {}));
api.render(left > 0 ? left : 0);
return chain;
@@ -233,8 +255,10 @@
return api;
}
- // burst: confetti out of a point. Saved for the things worth celebrating.
+ // burst: confetti out of a point. Saved for the things worth celebrating — so
+ // it is also, everywhere in the room, the sound of winning.
function burst(target, opts) {
+ sfx("win"); // before the reduced-motion bail: no confetti still deserves the fanfare
if (reduced) return Promise.resolve();
opts = opts || {};
var c = centre(target);
@@ -326,6 +350,7 @@
window.PeteFX = {
reduced: reduced,
+ sfx: sfx, // so a table can make a noise without reaching past this file
chipsFor: chipsFor,
disc: disc,
jitter: jitter,
diff --git a/internal/web/static/js/casino-sfx.js b/internal/web/static/js/casino-sfx.js
new file mode 100644
index 0000000..6a1ed09
--- /dev/null
+++ b/internal/web/static/js/casino-sfx.js
@@ -0,0 +1,265 @@
+// The noise the room makes.
+//
+// There are no audio files here and there is nothing to download. Every sound in
+// this casino 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 short
+// slap of noise through a bandpass; a chip is two detuned sines with a click on
+// the front; a win is four notes going up. It costs about six kilobytes and no
+// round trips, and it means a sound can be pitched, stretched and detuned per
+// call instead of being the same wav 300 times.
+//
+// Two rules hold the whole file up.
+//
+// **Nothing is built until it's asked for.** A browser will not let a page make a
+// noise before the user has touched it, and a page that builds an AudioContext on
+// load gets one in the "suspended" state and a warning in the console for its
+// trouble. So the context is made on the first sound anybody actually asks for —
+// which, in a casino, is a click on a chip.
+//
+// **Muted means silent, not quiet.** When the room is muted nothing is
+// constructed, nothing is scheduled and no context is opened. Mute is the
+// default-off switch for the entire file, checked before anything else happens.
+//
+// Exposed as window.PeteSFX. Nothing in here knows what blackjack is.
+(function () {
+ "use strict";
+
+ var KEY = "pete.sfx.off";
+
+ var ctx = null;
+ var master = null;
+ var noiseBuf = null;
+ var muted = false;
+ var listeners = [];
+
+ try { muted = localStorage.getItem(KEY) === "1"; } catch (e) {}
+
+ // ---- the instrument --------------------------------------------------------
+
+ function boot() {
+ if (ctx) return ctx;
+ var AC = window.AudioContext || window.webkitAudioContext;
+ if (!AC) return null;
+ try {
+ ctx = new AC();
+ } catch (e) {
+ return null;
+ }
+ master = ctx.createGain();
+ master.gain.value = 0.45; // the room is furniture, not a nightclub
+ master.connect(ctx.destination);
+ return ctx;
+ }
+
+ // A second of white noise, made once and played at different speeds and through
+ // different filters for the rest of the session. Card flicks, riffles, the
+ // transient on a chip: all of them are this buffer wearing a different coat.
+ function noise() {
+ if (noiseBuf) return noiseBuf;
+ var n = ctx.sampleRate;
+ noiseBuf = ctx.createBuffer(1, n, n);
+ var d = noiseBuf.getChannelData(0);
+ for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
+ return noiseBuf;
+ }
+
+ // env is the shape of every sound in here: up fast, down slow, and never to
+ // zero — an exponential ramp to actual zero is undefined, and a browser that
+ // hits one either throws or clicks.
+ function env(g, t0, attack, decay, peak) {
+ g.gain.setValueAtTime(0.0001, t0);
+ g.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + attack);
+ g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay);
+ }
+
+ // tone is one note.
+ function tone(t0, freq, o) {
+ o = o || {};
+ var osc = ctx.createOscillator();
+ var g = ctx.createGain();
+ osc.type = o.type || "sine";
+ osc.frequency.setValueAtTime(freq, t0);
+ if (o.to) osc.frequency.exponentialRampToValueAtTime(o.to, t0 + (o.glide || o.decay || 0.1));
+ env(g, t0, o.attack || 0.004, o.decay || 0.12, o.gain == null ? 0.3 : o.gain);
+ osc.connect(g).connect(master);
+ osc.start(t0);
+ osc.stop(t0 + (o.attack || 0.004) + (o.decay || 0.12) + 0.02);
+ }
+
+ // hiss is a burst of the noise buffer through a filter, which is what a card,
+ // a riffle and the knock on a chip all are.
+ function hiss(t0, o) {
+ o = o || {};
+ var src = ctx.createBufferSource();
+ src.buffer = noise();
+ src.playbackRate.value = o.rate || 1;
+
+ var f = ctx.createBiquadFilter();
+ f.type = o.filter || "bandpass";
+ f.frequency.setValueAtTime(o.freq || 2000, t0);
+ if (o.sweepTo) f.frequency.exponentialRampToValueAtTime(o.sweepTo, t0 + (o.decay || 0.1));
+ f.Q.value = o.q == null ? 1.1 : o.q;
+
+ var g = ctx.createGain();
+ env(g, t0, o.attack || 0.003, o.decay || 0.09, o.gain == null ? 0.3 : o.gain);
+
+ src.connect(f).connect(g).connect(master);
+ src.start(t0);
+ src.stop(t0 + (o.attack || 0.003) + (o.decay || 0.09) + 0.02);
+ }
+
+ // ---- the sounds ------------------------------------------------------------
+ //
+ // Each one takes the time it starts at, and a `v` — a small per-call variation,
+ // usually the index of the card or chip in a run. Four cards dealt with the
+ // identical sample four times is a machine gun; four cards each a semitone off
+ // is a hand dealing.
+
+ var SOUNDS = {
+ // A card landing: mostly air, with a slap on the front of it.
+ card: function (t, v) {
+ hiss(t, { freq: 1800 + v * 140, q: 0.9, decay: 0.075, rate: 1 + v * 0.05, gain: 0.3 });
+ hiss(t, { filter: "highpass", freq: 4200, decay: 0.02, gain: 0.16 });
+ },
+
+ // Dealing is the same card, lower and softer: it's being placed, not thrown.
+ deal: function (t, v) {
+ hiss(t, { freq: 1250 + v * 110, q: 1.1, decay: 0.085, rate: 0.9, gain: 0.24 });
+ },
+
+ // A card turning over — shorter, brighter, and it comes in two halves, which
+ // is what a flip actually sounds like.
+ flip: function (t) {
+ hiss(t, { freq: 2600, q: 1.4, decay: 0.035, gain: 0.22 });
+ hiss(t + 0.045, { freq: 1500, q: 1.0, decay: 0.06, gain: 0.24 });
+ },
+
+ // The riffle. One long sweep of noise with the filter climbing through it,
+ // and a rattle laid over the top so it isn't just a shhh.
+ shuffle: function (t) {
+ hiss(t, { freq: 700, sweepTo: 3400, q: 0.7, attack: 0.02, decay: 0.34, gain: 0.2 });
+ for (var i = 0; i < 9; i++) {
+ hiss(t + 0.03 + i * 0.032, { freq: 2400 + i * 130, q: 2.2, decay: 0.02, gain: 0.09 });
+ }
+ },
+
+ // A chip landing on a chip. Two detuned partials well above the staff, with a
+ // knock underneath — the knock is most of what makes it read as *clay* rather
+ // than as a bell.
+ chip: function (t, v) {
+ var f = 1750 + v * 55;
+ tone(t, f, { type: "sine", decay: 0.07, gain: 0.16 });
+ tone(t, f * 1.34, { type: "sine", decay: 0.05, gain: 0.1 });
+ hiss(t, { filter: "lowpass", freq: 900, decay: 0.03, gain: 0.3, q: 0.5 });
+ },
+
+ // Chips sliding away across felt.
+ sweep: function (t) {
+ hiss(t, { freq: 1600, sweepTo: 500, q: 0.6, attack: 0.015, decay: 0.26, gain: 0.16 });
+ },
+
+ // Four notes up. This is the only sound in the room allowed to be pleased.
+ win: function (t) {
+ [523.25, 659.25, 783.99, 1046.5].forEach(function (f, i) {
+ tone(t + i * 0.085, f, { type: "triangle", decay: 0.26, gain: 0.24 });
+ });
+ },
+
+ // Two notes down, and the second one is flat. Nobody needs telling twice.
+ lose: function (t) {
+ tone(t, 311.13, { type: "triangle", decay: 0.24, gain: 0.22 });
+ tone(t + 0.15, 233.08, { type: "triangle", decay: 0.42, gain: 0.22 });
+ },
+
+ // Nothing happened and nothing was lost.
+ push: function (t) {
+ tone(t, 392, { type: "sine", decay: 0.2, gain: 0.16 });
+ },
+
+ // A button. Short enough that you feel it rather than hear it.
+ blip: function (t) {
+ tone(t, 880, { type: "sine", decay: 0.055, gain: 0.12 });
+ },
+
+ // UNO, called. A bright stab plus a zip upwards: it is a shout, and it is the
+ // one thing on that table you want to feel good about pressing.
+ uno: function (t) {
+ [659.25, 830.61, 987.77].forEach(function (f) {
+ tone(t, f, { type: "triangle", decay: 0.3, gain: 0.2 });
+ });
+ tone(t, 500, { type: "sawtooth", to: 1600, glide: 0.16, decay: 0.18, gain: 0.09 });
+ },
+
+ // Got them. A rising snap with a thump on the end — the sound of a finger
+ // being pointed.
+ catch: function (t) {
+ tone(t, 300, { type: "square", to: 900, glide: 0.11, decay: 0.12, gain: 0.13 });
+ hiss(t + 0.1, { filter: "lowpass", freq: 1100, decay: 0.1, gain: 0.34, q: 0.7 });
+ tone(t + 0.1, 174.61, { type: "triangle", decay: 0.22, gain: 0.2 });
+ },
+
+ // Something bad landed on you: a stack, a +4, a catch you walked into.
+ bad: function (t) {
+ tone(t, 155.56, { type: "square", to: 98, glide: 0.18, decay: 0.22, gain: 0.14 });
+ hiss(t, { filter: "lowpass", freq: 700, decay: 0.14, gain: 0.3, q: 0.6 });
+ },
+
+ // A clock you can hear. Used sparingly, and never in a run of more than a few.
+ tick: function (t) {
+ hiss(t, { freq: 3200, q: 3, decay: 0.02, gain: 0.14 });
+ },
+ };
+
+ // ---- the door --------------------------------------------------------------
+
+ // A browser will not make a noise until the user has touched the page, and a
+ // context built before that arrives suspended. This wakes it on the first real
+ // gesture — after which play() can schedule freely.
+ function wake() {
+ if (ctx && ctx.state === "suspended") ctx.resume().catch(function () {});
+ }
+ ["pointerdown", "keydown", "touchstart"].forEach(function (ev) {
+ window.addEventListener(ev, wake, { passive: true });
+ });
+
+ // play makes a sound, or — if the room is muted — makes nothing at all, opens no
+ // context and builds no graph.
+ //
+ // `v` is the variation: pass the index of the card or chip in a run and the
+ // sound moves a little each time, which is the difference between a hand and a
+ // machine. `delay` schedules it ahead in seconds, so a caller that knows a card
+ // lands in 400ms can say so rather than sleeping.
+ function play(name, opts) {
+ if (muted) return;
+ var s = SOUNDS[name];
+ if (!s) return;
+ if (!boot()) return;
+ wake();
+ if (ctx.state !== "running") return; // not yet touched: no sound, and no error
+ try {
+ s(ctx.currentTime + ((opts && opts.delay) || 0), ((opts && opts.v) || 0));
+ } catch (e) {
+ /* a sound is never worth throwing over */
+ }
+ }
+
+ function setMuted(on) {
+ muted = !!on;
+ try {
+ if (muted) localStorage.setItem(KEY, "1");
+ else localStorage.removeItem(KEY);
+ } catch (e) {}
+ if (window.PetePrefs) window.PetePrefs.push();
+ listeners.forEach(function (fn) { fn(muted); });
+ if (!muted) play("blip"); // so you know what you just turned back on
+ }
+
+ window.PeteSFX = {
+ play: play,
+ muted: function () { return muted; },
+ toggle: function () { setMuted(!muted); return muted; },
+ set: setMuted,
+ // onChange lets the mute button re-label itself without owning the state.
+ onChange: function (fn) { listeners.push(fn); fn(muted); },
+ };
+})();
diff --git a/internal/web/static/js/games.js b/internal/web/static/js/games.js
index 7aefbb0..fc3c713 100644
--- a/internal/web/static/js/games.js
+++ b/internal/web/static/js/games.js
@@ -154,6 +154,25 @@
});
}
+ // The room's volume. The button lives in the chip bar, which is the one thing
+ // every table has — the sound belongs to the room, not to any one game — and
+ // the state itself belongs to PeteSFX, which persists it. This only draws it.
+ var sfxBtn = document.querySelector("[data-sfx-toggle]");
+ if (sfxBtn && window.PeteSFX) {
+ var onIcon = sfxBtn.querySelector("[data-sfx-on]");
+ var offIcon = sfxBtn.querySelector("[data-sfx-off]");
+ var sfxLbl = sfxBtn.querySelector("[data-sfx-label]");
+
+ window.PeteSFX.onChange(function (muted) {
+ sfxBtn.setAttribute("aria-pressed", muted ? "true" : "false");
+ sfxBtn.title = muted ? "Sound is off" : "Sound is on";
+ if (onIcon) onIcon.classList.toggle("hidden", muted);
+ if (offIcon) offIcon.classList.toggle("hidden", !muted);
+ if (sfxLbl) sfxLbl.textContent = muted ? "Turn the sound on" : "Mute the room";
+ });
+ sfxBtn.addEventListener("click", function () { window.PeteSFX.toggle(); });
+ }
+
window.PeteGames = {
// onUpdate registers a listener called on every fresh view of the money.
onUpdate: function (fn) { listeners.push(fn); if (view) fn(view); },
diff --git a/internal/web/static/js/hangman.js b/internal/web/static/js/hangman.js
index 0ee4846..ba8953e 100644
--- a/internal/web/static/js/hangman.js
+++ b/internal/web/static/js/hangman.js
@@ -117,6 +117,9 @@
at.forEach(function (i, n) {
var t = boardEl.querySelector('.pete-tile[data-at="' + i + '"]');
if (!t) return;
+ // One note per tile, climbing: a letter that turns up three times should
+ // sound like it's worth three times as much, because it is.
+ FX.sfx("tick", { delay: pace(n * 90) / 1000, v: n });
setTimeout(function () {
// Left as it comes: the tile is uppercased in CSS, and doing it here too
// would mean the resume path (which paints the phrase's own casing) and
@@ -126,6 +129,7 @@
t.classList.add("pete-tile-hit");
}, pace(n * 90));
});
+ FX.sfx("flip");
return wait(FLIP_MS + (at.length - 1) * 90);
}
@@ -292,6 +296,8 @@
// Confetti for a phrase guessed outright — the one call you make on your own
// rather than by grinding the alphabet.
if (v.outcome === "solved" && v.net > 0) FX.burst(verdictEl, { count: 30 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
}
function setPhase(v) {
@@ -362,6 +368,7 @@
// they are one event: this is what a wrong guess costs, all of it.
drawGallows(countMisses(events, e), true);
shake();
+ FX.sfx("bad");
if (final) knock(final);
return wait(MISS_MS);
diff --git a/internal/web/static/js/holdem.js b/internal/web/static/js/holdem.js
index 5ba6d87..01606ba 100644
--- a/internal/web/static/js/holdem.js
+++ b/internal/web/static/js/holdem.js
@@ -301,6 +301,7 @@
pot.render(0);
potTotal.textContent = "0";
sideEl.classList.add("hidden");
+ FX.sfx("shuffle");
return wait(140);
case "rebuy":
@@ -377,6 +378,7 @@
var face = (i === 0 && s.cards) ? s.cards[round] : null;
var card = Cards.el(face, { deal: true, tilt: i !== 0 });
built.cards.appendChild(card);
+ FX.sfx("deal", { delay: 0.07 * i, v: i });
beats.push(wait(70 * i));
});
return Promise.all(beats).then(function () { return wait(180); });
@@ -394,10 +396,12 @@
if (e.text === "fold") {
s.root.dataset.state = "folded";
s.cards.dataset.mucked = "1";
+ FX.sfx("card", { v: 2 }); // cards going into the muck
return wait(320);
}
if (e.text === "check") {
flash(s.root);
+ FX.sfx("blip"); // a knuckle on the table
return wait(320);
}
// call, raise, allin: chips leave their stack for their spot.
@@ -437,9 +441,10 @@
// The board turns one card at a time, even the flop. Three cards appearing
// at once is a screenshot; three cards appearing in a row is a flop.
var chain = Promise.resolve();
- (e.cards || []).forEach(function (c) {
+ (e.cards || []).forEach(function (c, i) {
chain = chain.then(function () {
boardEl.appendChild(Cards.el(c, { deal: true, tilt: false }));
+ FX.sfx("card", { v: i });
return wait(240);
});
});
@@ -478,12 +483,15 @@
});
if (busted) {
show("You're out of chips. Sit down again when you're ready.", "lose");
+ FX.sfx("lose");
return;
}
if (!events.some(function (e) { return e.kind === "end"; })) return;
var me = final.seats[0];
if (won > 0) {
+ // The pot coming your way already burst (and so already cheered) back in
+ // the "win" event. This is only the words.
show(showed
? "You win " + money(won) + " with " + article(handName(events)) + "."
: "They folded. You take " + money(won) + ".", "win");
@@ -491,6 +499,7 @@
show("Folded.", "lose");
} else {
show("No good this time.", "lose");
+ FX.sfx("lose");
}
function show(text, tone) {
diff --git a/internal/web/static/js/prefs.js b/internal/web/static/js/prefs.js
index e4e1d9c..4723a6c 100644
--- a/internal/web/static/js/prefs.js
+++ b/internal/web/static/js/prefs.js
@@ -10,7 +10,7 @@
(function () {
// The localStorage keys we sync. The weather *cache* is deliberately excluded:
// it's transient and per-device.
- var SYNCED = ["pete.disabledSources.v1", "pete.weather.loc.v1", "pete-weather-off"];
+ var SYNCED = ["pete.disabledSources.v1", "pete.weather.loc.v1", "pete-weather-off", "pete.sfx.off"];
var user = window.PETE_USER || null;
var serverPrefs = window.PETE_PREFS || null;
diff --git a/internal/web/static/js/solitaire.js b/internal/web/static/js/solitaire.js
index d447130..ba1f52a 100644
--- a/internal/web/static/js/solitaire.js
+++ b/internal/web/static/js/solitaire.js
@@ -396,6 +396,36 @@
});
}
+ // noises is the board's soundtrack, walked off the *same* STEP_MS ladder the
+ // animation is walked off. That matters more than which sound goes where: a
+ // card you hear land a step before it lands is worse than a card that lands in
+ // silence, so this counts its way through the script exactly as planOf and
+ // flashHome do, and any change to their timing has to be made here too.
+ function noises(events) {
+ var at = 0;
+ (events || []).forEach(function (e) {
+ switch (e.kind) {
+ case "draw":
+ FX.sfx("card", { delay: at / 1000 });
+ break;
+ case "recycle":
+ FX.sfx("shuffle", { delay: at / 1000 });
+ break;
+ case "move":
+ FX.sfx("deal", { delay: (at + MOVE_MS) / 1000 });
+ at += STEP_MS;
+ break;
+ case "home":
+ // A card reaching a foundation is the only move that pays, so it is the
+ // only one that gets a note on the end of it.
+ FX.sfx("deal", { delay: (at + MOVE_MS) / 1000 });
+ FX.sfx("blip", { delay: (at + MOVE_MS + 60) / 1000 });
+ at += STEP_MS;
+ break;
+ }
+ });
+ }
+
// ---- the money -------------------------------------------------------------
// bank moves the spot to what the server says the board is worth. Up is chips
@@ -548,6 +578,9 @@
// Clearing 52 cards out of a Vegas deal is the rarest thing that happens in
// this room, so it's the one that gets the confetti.
if (v.outcome === "cleared") FX.burst(verdictEl, { count: 40 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
+ else FX.sfx("push");
}
// play walks a server response onto the felt: the board is re-rendered, the
@@ -570,6 +603,7 @@
render(v);
var plan = planOf(events);
flashHome(events);
+ noises(events);
return animate(before, plan);
})
.then(function () {
diff --git a/internal/web/static/js/trivia.js b/internal/web/static/js/trivia.js
index 1ccdb40..7e0c454 100644
--- a/internal/web/static/js/trivia.js
+++ b/internal/web/static/js/trivia.js
@@ -372,10 +372,16 @@
renderQuestion(final);
renderLadder(final);
if (final && final.phase === "playing") startClock(final.left, final.limit);
+ FX.sfx("blip");
return;
case "right":
reveal(e.choice, e.correct);
+ // Three notes up, one per rung climbed — the ladder, in sound.
+ [0, 0.09, 0.18].forEach(function (d, i) {
+ FX.sfx("tick", { delay: d, v: i });
+ });
+ FX.sfx("win", { delay: 0.1 });
if (final) {
// The rung lighting and the multiple climbing are one event,
// because they are one event: this is what the answer was worth.
@@ -387,10 +393,13 @@
case "wrong":
reveal(e.choice, e.correct);
+ FX.sfx("bad");
return wait(1100);
case "timeout":
+ // The clock beat you to it, which is a different kind of bad.
reveal(-1, e.correct);
+ FX.sfx("lose");
return wait(1100);
case "settle":
diff --git a/internal/web/static/js/uno.js b/internal/web/static/js/uno.js
index 63430eb..0427d0e 100644
--- a/internal/web/static/js/uno.js
+++ b/internal/web/static/js/uno.js
@@ -39,6 +39,9 @@
var tableEl = root.querySelector("[data-table-name]");
var wildEl = root.querySelector("[data-wild]");
+ var gateEl = root.querySelector("[data-unogate]");
+ var callBtn = root.querySelector("[data-uno-call]");
+ var timerEl = root.querySelector("[data-uno-timer]");
// Not [data-pending]: that is the chip bar's, it lives inside this root, and it
// comes first in the document. See the note in uno.html.
var billEl = root.querySelector("[data-bill]");
@@ -72,6 +75,14 @@
var played = -1; // the card you just played, so the script lifts that one out
// of the hand and not merely the first one that lit up
+ // The UNO call. CALL_MS is how long you get to say it once the card is on its
+ // way down — long enough that nobody playing the game misses it, short enough
+ // that it is still a thing you have to *do*. The engine has the other half of
+ // this rule (see call.go); this is only the window.
+ var CALL_MS = 2600;
+ var gate = null; // the play waiting on a call: {index, color}
+ var gateTimer = 0;
+
var reduced = FX.reduced;
function pace(ms) { return reduced ? 0 : ms; }
function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); }
@@ -207,6 +218,9 @@
function renderSeats(v) {
seatsEl.innerHTML = "";
if (!v) return;
+ var catchable = {};
+ (v.catchable || []).forEach(function (i) { catchable[i] = true; });
+
v.seats.forEach(function (s, i) {
if (s.you) return; // you are the hand at the bottom, not a seat up here
var el = document.createElement("div");
@@ -216,6 +230,24 @@
if (s.uno) el.dataset.uno = "1";
if (s.out) el.dataset.out = "1"; // No Mercy buried them; they are not coming back
+ // One card, and it never said so. This button is the only thing on the felt
+ // that gives it away — a bot that forgets to call is silent by definition,
+ // and the tell was always meant to be the count beside its fan. The button
+ // is the table doing you the courtesy of putting a target on it.
+ if (catchable[i]) {
+ var got = document.createElement("button");
+ got.type = "button";
+ got.className = "pete-uno-catch";
+ got.dataset.catch = String(i);
+ got.textContent = "Catch!";
+ got.setAttribute("aria-label", "Catch " + s.name + " — one card, never called");
+ got.addEventListener("click", function (e) {
+ e.stopPropagation();
+ catchSeat(i);
+ });
+ el.appendChild(got);
+ }
+
var fan = document.createElement("div");
fan.className = "pete-uno-fan";
var show = Math.min(s.cards, FAN);
@@ -252,23 +284,65 @@
return el ? el.querySelector(".pete-uno-fan") : deckEl;
}
- function renderHand(v) {
- handEl.innerHTML = "";
- if (!v || !v.hand) return;
- var playable = {};
- (v.playable || []).forEach(function (i) { playable[i] = true; });
- var yours = v.turn === 0 && v.phase !== "done";
+ // shownHand is the faces currently on screen. paintHand diffs against it so that
+ // only cards that are actually new fan in — without it, every redraw re-deals
+ // the whole hand in front of you, and a hand gets redrawn several times a lap.
+ var shownHand = [];
- v.hand.forEach(function (c, i) {
+ function key(c) { return c.color + "/" + c.value; }
+
+ // paintHand draws your fan. `live` is whether the cards are yours to click,
+ // which mid-script they are not: the game on screen is one the server has
+ // already moved past, and a card clicked during the lap would be a move against
+ // a board that no longer exists.
+ function paintHand(cards, playable, live) {
+ cards = cards || [];
+ // What was already on the table stays put. Matched by face and consumed, so a
+ // hand holding two red fives doesn't count one of them twice.
+ var pool = shownHand.map(key);
+ handEl.innerHTML = "";
+ var fresh = 0;
+
+ cards.forEach(function (c, i) {
var el = card(c, { button: true });
- el.style.setProperty("--i", i);
el.dataset.at = String(i);
- var ok = yours && playable[i];
+ var ok = !!live && !!playable[i];
el.dataset.on = ok ? "1" : "0";
el.disabled = !ok || busy;
if (ok) el.addEventListener("click", function () { pick(i, c); });
+ var was = pool.indexOf(key(c));
+ if (was >= 0) {
+ pool.splice(was, 1);
+ el.dataset.settled = "1"; // it was already in your hand: don't deal it again
+ } else {
+ el.style.setProperty("--i", fresh++);
+ }
handEl.appendChild(el);
});
+ shownHand = cards.slice();
+ }
+
+ function renderHand(v) {
+ if (!v || !v.hand) { handEl.innerHTML = ""; shownHand = []; return; }
+ var playable = {};
+ (v.playable || []).forEach(function (i) { playable[i] = true; });
+ paintHand(v.hand, playable, v.turn === 0 && v.phase !== "done");
+ }
+
+ // showHand redraws your fan from the hand the server stamped on the event that
+ // just changed it — see Event.Hand in the engine.
+ //
+ // This is the fix for a hand that used to sit there stale for the whole lap.
+ // bump() keeps the *bots'* fans honest and has always refused seat zero, so
+ // when a +4 landed on you at the top of a lap you watched four backs fly into
+ // your hand and then nothing: the cards themselves only turned up seconds
+ // later, when the script ended and paint() finally ran. You were looking at a
+ // hand you no longer held.
+ function showHand(cards) {
+ if (!cards) return;
+ paintHand(cards, {}, false);
+ // And the line above the fan counts what's in it, so it moves too.
+ countEl.textContent = cards.length + (cards.length === 1 ? " card — UNO!" : " cards");
}
function renderPiles(v) {
@@ -357,7 +431,10 @@
else if (v.net < 0) text += " " + v.net.toLocaleString();
verdictEl.textContent = text;
verdictEl.classList.remove("hidden");
+ // burst() is the room's win sound as well as its confetti; anything else is
+ // a night that didn't go your way.
if (v.outcome === "won") FX.burst(verdictEl, { count: 34 });
+ else FX.sfx("lose");
}
// controls is the one place that decides what you can click: whose turn the
@@ -373,6 +450,11 @@
handEl.querySelectorAll(".pete-uno-card").forEach(function (b) {
b.disabled = busy || !yours || b.dataset.on !== "1";
});
+ // Catching is free, but it is still a move: it can't go up while one of yours
+ // is in flight, and it can't go up on somebody else's turn.
+ seatsEl.querySelectorAll("[data-catch]").forEach(function (b) {
+ b.disabled = busy || !yours;
+ });
// Under a stack you cannot draw. The deck is not a way out of a bill: the two
// moves that exist are answering it with a draw card or taking the lot.
if (drawBtn) drawBtn.disabled = busy || !yours || drawn || stack;
@@ -398,6 +480,7 @@
betting.classList.toggle("hidden", live);
playing.classList.toggle("hidden", !live);
hideWild();
+ hideGate();
if (drawBtn) drawBtn.classList.toggle("hidden", drawn || stack);
if (takeBtn) takeBtn.classList.toggle("hidden", !stack);
@@ -406,7 +489,13 @@
// this is the table not offering a button that cannot work.
var canPass = drawn && !(v && v.tier && v.tier.no_mercy);
if (passBtn) passBtn.classList.toggle("hidden", !canPass);
- if (hintEl && live) hintEl.textContent = HINTS[v.phase] || HINTS.play;
+ if (hintEl && live) {
+ // A seat sitting on one card it never called is the most valuable thing on
+ // the table, and it is worth more than whatever the phase was going to say.
+ hintEl.textContent = (v.catchable || []).length
+ ? "Somebody's down to one card and never said so. Catch them before you play."
+ : (HINTS[v.phase] || HINTS.play);
+ }
if (!live && v) rulesFor(v); // it's over: leave the panel where the game was
controls();
if (!v || !v.outcome) verdictEl.classList.add("hidden");
@@ -426,7 +515,9 @@
// ---- the script ------------------------------------------------------------
- // throwCard sends a card from one place to another across the felt.
+ // throwCard sends a card from one place to another across the felt. It lands
+ // with a slap — "card" for one going down on the pile, "deal" (softer, lower)
+ // for one coming off the deck into a hand, which is a card being *placed*.
function throwCard(node, from, to, opts) {
opts = opts || {};
return FX.flyNode(node, from, to, {
@@ -435,12 +526,17 @@
spin: opts.spin == null ? FX.jitter((opts.index || 0) + 3, 14) : opts.spin,
fromScale: opts.fromScale == null ? 0.9 : opts.fromScale,
delay: opts.delay || 0,
+ index: opts.index || 0,
+ sound: opts.sound === undefined ? "card" : opts.sound,
});
}
// bump keeps a bot's fan honest during the script: the server's count is the
// truth, but between events the fan has to grow and shrink as cards move, or
// the flight lands on a pile that hasn't changed.
+ //
+ // Seat zero is not a bot and is not bumped — a fan of backs is no use to you,
+ // you want the faces. showHand does yours, from the hand the event carries.
function bump(seat, left) {
if (seat === 0 || left == null) return;
var el = seatEl(seat);
@@ -499,6 +595,10 @@
// The deal isn't animated card by card: seven cards to each of four
// seats is 28 flights and a player who wants to play. The hand fans in
// on its own (CSS), which reads as being dealt without taking as long.
+ // The sound does the same job: a riffle, then cards landing, rather
+ // than twenty-eight separate slaps.
+ FX.sfx("shuffle");
+ for (var c = 0; c < 5; c++) FX.sfx("deal", { delay: 0.28 + c * 0.06, v: c });
paint(final);
return wait(320);
@@ -525,6 +625,7 @@
colourEl.dataset.c = e.color;
feltEl.dataset.c = e.color;
}
+ showHand(e.hand); // the card has landed; close the gap it left
return wait(e.seat === 0 ? 120 : 300);
});
}
@@ -535,14 +636,16 @@
var to = seatAnchor(e.seat);
var backs = [];
for (var i = 0; i < Math.min(e.n, 4); i++) {
- backs.push(throwCard(back(), deckEl, to, { index: i, delay: i * 90 }));
+ backs.push(throwCard(back(), deckEl, to, { index: i, delay: i * 90, sound: "deal" }));
}
var punished = e.kind === "forced";
+ if (punished) FX.sfx("bad");
// A forced draw is also how a stack ends: somebody stopped answering
// and paid the bill, so the bill is gone.
if (punished) { pending(0); badge(e.seat, "+" + e.n, "bad"); }
return Promise.all(backs).then(function () {
bump(e.seat, e.left);
+ showHand(e.hand); // your cards, face up, as they land
deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
// Your own drawn card comes face up — it's yours, and the server
// sent its face for exactly this.
@@ -551,12 +654,58 @@
});
}
+ // ---- the call, and the catch ---------------------------------------
+ //
+ // Somebody went down to one card without saying so, and somebody else
+ // noticed. Two cards off the deck, and the seat that took them wears the
+ // name of whoever spotted it — because "caught" with nobody attached is
+ // just a mystery.
+ case "caught": {
+ spotlight(e.seat);
+ var mine = e.seat === 0;
+ var caughtBy = (game && game.seats && game.seats[e.by]) || {};
+ var whom = mine
+ ? "Caught! " + (caughtBy.name || "They") + " saw it"
+ : "Caught! +" + e.n;
+ // Catching a bot is a pointed finger; being caught is a thud.
+ FX.sfx(mine ? "bad" : "catch");
+ var lands = [];
+ var at = seatAnchor(e.seat);
+ for (var k = 0; k < e.n; k++) {
+ lands.push(throwCard(back(), deckEl, at, { index: k, delay: k * 100, sound: "deal" }));
+ }
+ badge(e.seat, whom, "bad");
+ return Promise.all(lands).then(function () {
+ bump(e.seat, e.left);
+ showHand(e.hand);
+ deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
+ return wait(520);
+ });
+ }
+
+ // You called a seat that had nothing to hide. Two cards, and they're
+ // yours: the price of the catch button not being a thing you just mash.
+ case "miscall": {
+ FX.sfx("bad");
+ var wrong = [];
+ for (var w = 0; w < e.n; w++) {
+ wrong.push(throwCard(back(), deckEl, handEl, { index: w, delay: w * 100, sound: "deal" }));
+ }
+ badge(0, "They called it. +" + e.n, "bad");
+ return Promise.all(wrong).then(function () {
+ showHand(e.hand);
+ deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
+ return wait(520);
+ });
+ }
+
case "stack":
// The bill has moved on to somebody, and N is what it stands at now —
// not what was just added to it. A +2 answered with a +10 is a seat
// looking at twelve cards.
pending(e.n);
spotlight(e.seat);
+ FX.sfx("bad");
return badge(e.seat, "+" + e.n, "bad").then(function () { return wait(140); });
case "skipall":
@@ -580,7 +729,10 @@
dumped.push(throwCard(back(), dumpFrom, discardEl, { index: d, delay: d * 70 }));
}
badge(e.seat, "−" + e.n + " " + (e.color || ""));
- return Promise.all(dumped).then(function () { return wait(220); });
+ return Promise.all(dumped).then(function () {
+ showHand(e.hand);
+ return wait(220);
+ });
}
case "roulette": {
@@ -595,6 +747,7 @@
badge(e.seat, "Roulette +" + e.n, "bad");
return Promise.all(spun).then(function () {
bump(e.seat, e.left);
+ showHand(e.hand);
deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
return wait(400);
});
@@ -606,7 +759,9 @@
var grave = seatEl(e.seat);
if (grave) grave.dataset.out = "1";
bump(e.seat, 0);
+ showHand(e.hand); // if it was you, the hand goes with you
pending(0); // a dead seat pays no bill and leaves none behind
+ FX.sfx("bad");
return badge(e.seat, "Buried on " + e.n, "bad").then(function () { return wait(460); });
}
@@ -618,6 +773,7 @@
return badge(e.seat, "Reverse").then(function () { return wait(60); });
case "uno":
+ FX.sfx("uno");
return badge(e.seat, "UNO!", "uno");
case "reshuffle":
@@ -626,6 +782,7 @@
// and counting down from a deck that still reads empty gets nowhere.
deckCntEl.textContent = String(e.n);
deckEl.classList.add("pete-uno-shuffle");
+ FX.sfx("shuffle");
return wait(420).then(function () {
deckEl.classList.remove("pete-uno-shuffle");
});
@@ -699,8 +856,72 @@
function pick(i, c) {
if (busy || !game || game.phase === "done" || game.turn !== 0) return;
- if (c.wild) { askColour(i); return; }
- move({ kind: "play", index: i });
+ if (c.wild) { askColour(i); return; } // the colour first; the call comes after
+ offer(i, 0);
+ }
+
+ // offer is the last thing between a card and the pile. If playing it leaves you
+ // holding one card, the table wants a word out of you first.
+ //
+ // Which cards those are is the server'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 would let you
+ // walk into a catch it never warned you about.
+ function offer(i, color) {
+ if (game && (game.uno_at || []).indexOf(i) >= 0) {
+ openGate(i, color);
+ return;
+ }
+ move({ kind: "play", index: i, color: color });
+ }
+
+ function openGate(i, color) {
+ gate = { index: i, color: color };
+ gateEl.classList.remove("hidden");
+ if (timerEl) {
+ // Restart the drain. The bar is a CSS animation and this element gets
+ // reused, so without kicking the browser between the two assignments it
+ // stays where the last one finished — a full bar that never moves, which
+ // is worse than no bar at all.
+ timerEl.style.animation = "none";
+ void timerEl.offsetWidth;
+ timerEl.style.animation = "";
+ timerEl.style.animationDuration = CALL_MS + "ms";
+ }
+ if (callBtn) callBtn.focus();
+ gateTimer = setTimeout(function () { shut(false); }, CALL_MS);
+ }
+
+ // shut plays the card the gate was holding, with or without the word. Letting
+ // the clock run out is a real answer — it is the answer that gets you caught.
+ function shut(called) {
+ if (!gate) return;
+ var g = gate;
+ gate = null;
+ clearTimeout(gateTimer);
+ gateTimer = 0;
+ gateEl.classList.add("hidden");
+ move({ kind: "play", index: g.index, color: g.color, uno: called });
+ }
+
+ // hideGate drops the gate without playing anything. Only for the paths that tear
+ // the board down under it (an error, a resume) — a gate that outlived its game
+ // would play a card into the next one.
+ function hideGate() {
+ if (gateTimer) clearTimeout(gateTimer);
+ gateTimer = 0;
+ gate = null;
+ if (gateEl) gateEl.classList.add("hidden");
+ }
+
+ if (callBtn) callBtn.addEventListener("click", function () { shut(true); });
+
+ // Catching a seat that went quiet. It is not a turn — right or wrong, the turn
+ // stays with you — but it is a move, so it goes through the same door as one.
+ function catchSeat(seat) {
+ if (busy || !game || game.phase === "done" || game.turn !== 0) return;
+ if ((game.catchable || []).indexOf(seat) < 0) return;
+ move({ kind: "catch", seat: seat });
}
function askColour(i) {
@@ -724,7 +945,7 @@
var i = pendingWild;
var c = COLOURS[b.dataset.colourPick];
hideWild();
- move({ kind: "play", index: i, color: c });
+ offer(i, c); // named the colour; now, is it your last but one?
});
});
diff --git a/internal/web/templates/_chipbar.html b/internal/web/templates/_chipbar.html
index 54774ba..62eb655 100644
--- a/internal/web/templates/_chipbar.html
+++ b/internal/web/templates/_chipbar.html
@@ -37,6 +37,25 @@
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
Cash out
+
+
+
diff --git a/internal/web/templates/games_layout.html b/internal/web/templates/games_layout.html
index 0be30f0..097cfaf 100644
--- a/internal/web/templates/games_layout.html
+++ b/internal/web/templates/games_layout.html
@@ -73,6 +73,11 @@
Play for what you can lose. Cash out whenever you like.
+
+
{{block "scripts" .}}{{end}}