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}} {{end}} diff --git a/internal/web/templates/uno.html b/internal/web/templates/uno.html index 0d8acb8..53632fd 100644 --- a/internal/web/templates/uno.html +++ b/internal/web/templates/uno.html @@ -104,6 +104,22 @@ + + +