diff --git a/internal/web/devcasino_test.go b/internal/web/devcasino_test.go new file mode 100644 index 0000000..9cfe638 --- /dev/null +++ b/internal/web/devcasino_test.go @@ -0,0 +1,71 @@ +package web + +import ( + "encoding/json" + "fmt" + "io/fs" + "net" + "net/http" + "os" + "testing" + "time" +) + +// TestDevCasino is not a test. It is the casino, running, on a port, with one +// signed-in player who has chips — so the table can be driven in a real browser, +// which is the only honest way to review an animation. +// +// Skipped unless you ask for it: +// +// PETE_DEV_CASINO=:7788 go test ./internal/web -run TestDevCasino -timeout 0 +// +// It prints the session cookie to plant. The routes are wired here rather than +// taken from New(), because New() decides whether the casino exists at the +// moment it builds the mux, and the test rig only signs the player in afterwards. +func TestDevCasino(t *testing.T) { + addr := os.Getenv("PETE_DEV_CASINO") + if addr == "" { + t.Skip("set PETE_DEV_CASINO=:port to run the casino for a browser") + } + + s := newCasino(t) + fund(t, 5000) + + payload, _ := json.Marshal(SessionUser{ + Sub: "sub-1", Username: "reala", Name: "Reala", + Exp: time.Now().Add(24 * time.Hour).Unix(), + }) + cookie := s.auth.sign(payload) + + staticSub, err := fs.Sub(staticFS, "static") + if err != nil { + t.Fatal(err) + } + + mux := http.NewServeMux() + mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub)))) + mux.HandleFunc("GET /games", s.handleLobby) + mux.HandleFunc("GET /games/blackjack", s.handleBlackjack) + mux.HandleFunc("GET /api/games/table", s.handleTable) + mux.HandleFunc("POST /api/games/buyin", s.handleBuyIn) + mux.HandleFunc("POST /api/games/cashout", s.handleCashOut) + mux.HandleFunc("POST /api/games/blackjack/deal", s.handleDeal) + mux.HandleFunc("POST /api/games/blackjack/move", s.handleMove) + + ln, err := net.Listen("tcp", addr) + if err != nil { + t.Fatal(err) + } + // Written to a file, not printed: `go test` buffers stdout, and the browser + // driver needs the cookie while the server is still running. + if out := os.Getenv("PETE_DEV_COOKIE_FILE"); out != "" { + if err := os.WriteFile(out, []byte(cookie), 0o600); err != nil { + t.Fatal(err) + } + } + fmt.Printf("\nCASINO http://localhost%s/games/blackjack\nCOOKIE %s=%s\n\n", addr, sessionCookie, cookie) + + srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} + t.Cleanup(func() { _ = srv.Close() }) + _ = srv.Serve(ln) +} diff --git a/internal/web/static/css/input.css b/internal/web/static/css/input.css index 3a2351e..27dff44 100644 --- a/internal/web/static/css/input.css +++ b/internal/web/static/css/input.css @@ -653,40 +653,235 @@ html[data-phase="night"] { } /* The flight itself: out of the shoe (up and to the right), scaled down and - spinning slightly, into place. */ + spinning slightly, into place — and then a beat of settling, because a card + that stops dead on the felt looks like it was pasted there. It overshoots a + little and rocks back onto --tilt, the small resting angle the JS gives each + card so a hand looks handled rather than typeset. */ @keyframes pete-deal { - from { + 0% { opacity: 0; transform: translate(var(--deal-x, 14rem), var(--deal-y, -7rem)) scale(0.72) rotate(9deg); } - to { + 62% { opacity: 1; - transform: translate(0, 0) scale(1) rotate(0); + transform: translate(0, 0.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg)); } + 100% { + opacity: 1; + transform: translate(0, 0) scale(1) rotate(var(--tilt, 0deg)); + } + } + /* And the weight: the felt takes the hit. The shadow under a landing card + spreads and darkens for the instant it arrives. */ + .pete-card::after { + content: ""; + position: absolute; + inset: auto 6% -0.35rem 6%; + height: 0.6rem; + border-radius: 999px; + background: rgba(0, 0, 0, 0.35); + filter: blur(5px); + animation: pete-thud 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards; + } + @keyframes pete-thud { + 0%, 55% { opacity: 0; transform: scale(0.4); } + 72% { opacity: 0.9; transform: scale(1.15); } + 100% { opacity: 0.45; transform: scale(1); } } /* A settled hand: the winning side gets a little lift, so a win reads at a glance rather than only in the text. */ .pete-hand[data-won="1"] .pete-card { animation: pete-deal 0.42s cubic-bezier(0.22,1,0.36,1) backwards, pete-win 0.6s ease 0.1s; } @keyframes pete-win { - 0%, 100% { transform: translateY(0); } - 40% { transform: translateY(-0.6rem); } + 0%, 100% { transform: rotate(var(--tilt, 0deg)) translateY(0); } + 40% { transform: rotate(var(--tilt, 0deg)) translateY(-0.6rem); } + } + /* A losing hand doesn't just sit there being wrong: it greys off. */ + .pete-hand[data-won="-1"] .pete-card { + filter: saturate(0.45) brightness(0.78); + transition: filter 0.5s ease 0.2s; } - /* Chips: the denominations you bet with. */ - .pete-chip { - background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.28), transparent 60%), var(--chip, #e07a5f); - border: 3px dashed rgba(255,255,255,0.55); - box-shadow: 0 3px 0 rgba(60,40,20,0.22), 0 6px 14px rgba(60,40,20,0.18); + /* ---- chips --------------------------------------------------------------- + One disc, three jobs: the buttons you bet with, the stack sitting in the bet + spot, and the chip in mid-air between them. Same face on all three, because + they are meant to read as the *same chip* being moved around. */ + + .pete-disc { + position: relative; + border-radius: 999px; + background: + radial-gradient(circle at 50% 30%, rgba(255,255,255,0.30), transparent 62%), + var(--chip, #e07a5f); + border: 2px solid rgba(0,0,0,0.22); + box-shadow: 0 3px 0 rgba(0,0,0,0.28), 0 6px 14px rgba(0,0,0,0.20); + } + /* The edge spots every casino chip has, cut as a dashed ring inside the rim. */ + .pete-disc::before { + content: ""; + position: absolute; + inset: 11%; + border-radius: 999px; + border: 2px dashed rgba(255,255,255,0.55); + } + .pete-disc[data-chip="5"] { --chip: #5aa9e6; } + .pete-disc[data-chip="25"] { --chip: #4caf7d; } + .pete-disc[data-chip="100"] { --chip: #2b2118; } + .pete-disc[data-chip="500"] { --chip: #b079d6; } + + .pete-chip { /* the buttons */ + transition: transform 0.12s ease; + } + .pete-chip > span { + position: relative; /* over the dashed ring */ + } + .pete-chip:active { transform: translateY(1px) scale(0.94); } + + /* The bet spot: where your stake actually sits while the hand is played. */ + .pete-spot { + position: relative; + display: grid; + place-items: center; + height: 7rem; + width: 7rem; + flex: none; + border-radius: 999px; + border: 2px dashed rgba(255,255,255,0.35); + background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.10), transparent 70%); + box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08); + transition: box-shadow 0.3s ease, border-color 0.3s ease; + } + .pete-spot[data-live="1"] { + border-color: rgba(var(--glow, 242,181,61), 0.85); + box-shadow: inset 0 0 0 6px rgba(0,0,0,0.08), 0 0 22px rgba(var(--glow, 242,181,61), 0.35); + } + .pete-spot-label { + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(255,255,255,0.35); + } + .pete-spot[data-live="1"] .pete-spot-label { opacity: 0; } + + /* The stack in the spot. Each chip is offset up the pile by its index, so ten + chips look like ten chips rather than one chip with a number on it. */ + .pete-stack { + position: absolute; + inset: 0; + pointer-events: none; + } + .pete-stack .pete-disc { + position: absolute; + left: 50%; + top: 50%; + height: 2.6rem; + width: 2.6rem; + margin: -1.3rem 0 0 -1.3rem; + /* Each chip sits proud of the one below it by more than its own rim, so a + stack of three reads as three. Less than this and they hide inside each + other and a 150 bet looks the same as a 25 one. */ + transform: translateY(calc(-0.5rem * var(--i, 0))) rotate(var(--spin, 0deg)); + box-shadow: 0 2px 0 rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.25); + animation: pete-chip-land 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; + } + @keyframes pete-chip-land { + from { transform: translateY(calc(-0.5rem * var(--i, 0) - 1.1rem)) rotate(var(--spin, 0deg)) scale(1.18); } + } + .pete-spot-total { + position: absolute; + bottom: -0.6rem; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + border-radius: 999px; + background: rgba(0,0,0,0.45); + padding: 0.1rem 0.6rem; + font-size: 0.72rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: #fff; + } + + /* A chip in mid-air. Lives in a fixed overlay so it can cross from the betting + buttons, over the page, and onto the felt without any container clipping it. */ + .pete-fly-layer { + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; + overflow: hidden; + } + .pete-fly { + position: absolute; + height: 2.6rem; + width: 2.6rem; + margin: -1.3rem 0 0 -1.3rem; + will-change: transform; + } + + /* The house's own chips, racked on the felt. This is where a payout comes + from, and where a losing stake goes — so the money has somewhere to be. + It sits alongside the shoe rather than over on the left, because the left of + the felt is where the dealer's cards land. */ + .pete-rack { + position: absolute; + top: 1.25rem; + right: 5.75rem; + display: flex; + align-items: flex-end; + gap: 0.4rem; + padding: 0.5rem 0.6rem 0.45rem; + border-radius: 0.75rem; + background: rgba(0,0,0,0.20); + box-shadow: inset 0 0 0 2px rgba(255,255,255,0.06); + } + /* A column in the rack is a *stack*, so it's striped: one band per chip, with + the shadow of the chip above it cut into each. A plain coloured lozenge here + read as a blob, which is the one thing a pile of money shouldn't. */ + .pete-rack span { + display: block; + width: 1.7rem; + border-radius: 0.35rem; + background: + linear-gradient(90deg, rgba(0,0,0,0.28), transparent 35%, transparent 65%, rgba(0,0,0,0.28)), + repeating-linear-gradient(180deg, + rgba(255,255,255,0.18) 0 0.06rem, + var(--chip, #e07a5f) 0.06rem 0.3rem, + rgba(0,0,0,0.35) 0.3rem 0.36rem); + border: 1px solid rgba(0,0,0,0.35); + height: calc(0.36rem * var(--stack, 3)); + } + .pete-rack span[data-chip="5"] { --chip: #5aa9e6; } + .pete-rack span[data-chip="25"] { --chip: #4caf7d; } + .pete-rack span[data-chip="100"] { --chip: #2b2118; } + .pete-rack span[data-chip="500"] { --chip: #b079d6; } + + /* The burst. A natural gets confetti; nothing else in the room does, which is + what keeps it worth something. */ + .pete-spark { + position: absolute; + height: 0.75rem; + width: 0.45rem; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0,0,0,0.35); + will-change: transform, opacity; + } + + /* The dealer's beat before they draw out. */ + .pete-dealer-think { + animation: pete-think 0.9s ease-in-out infinite; + } + @keyframes pete-think { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; transform: translateY(-1px); } } - .pete-chip[data-chip="5"] { --chip: #5aa9e6; } - .pete-chip[data-chip="25"] { --chip: #4caf7d; } - .pete-chip[data-chip="100"] { --chip: #2b2118; } - .pete-chip[data-chip="500"] { --chip: #b079d6; } - .pete-chip[aria-pressed="true"] { outline: 3px solid var(--accent); outline-offset: 2px; } @media (prefers-reduced-motion: reduce) { - .pete-card { animation: none; } + .pete-card, + .pete-card::after, + .pete-stack .pete-disc, + .pete-dealer-think { animation: none; } .pete-card-inner { transition: none; } .pete-hand[data-won="1"] .pete-card { animation: none; } } diff --git a/internal/web/static/css/output.css b/internal/web/static/css/output.css index 75d8ed5..6f98a20 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:8.4rem;width: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)}to{opacity:1;transform:translate(0) scale(1) rotate(0)}}.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:translateY(0)}40%{transform:translateY(-.6rem)}}.pete-chip{background:radial-gradient(circle at 50% 35%,hsla(0,0%,100%,.28),transparent 60%),var(--chip,#e07a5f);border:3px dashed hsla(0,0%,100%,.55);box-shadow:0 3px 0 rgba(60,40,20,.22),0 6px 14px rgba(60,40,20,.18)}.pete-chip[data-chip="5"]{--chip:#5aa9e6}.pete-chip[data-chip="25"]{--chip:#4caf7d}.pete-chip[data-chip="100"]{--chip:#2b2118}.pete-chip[data-chip="500"]{--chip:#b079d6}.pete-chip[aria-pressed=true]{outline:3px solid var(--accent);outline-offset:2px}@media (prefers-reduced-motion:reduce){.pete-card{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card{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-1{margin-top:.25rem}.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-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-5{height:1.25rem}.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-\[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-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{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-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-4{row-gap:1rem}.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-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))}.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}.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-dashed{border-style:dashed}.border-transparent{border-color:transparent}.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-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-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}.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-\[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\/60{color:hsla(0,0%,100%,.6)}.text-white\/85{color:hsla(0,0%,100%,.85)}.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}.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)}.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}.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\: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-0:active{--tw-translate-y:0px}.active\:translate-y-0:active,.active\:translate-y-px:active{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))}.active\:translate-y-px:active{--tw-translate-y:1px}.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\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.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))}}@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:8.4rem;width: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}.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)}}@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{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-1{margin-top:.25rem}.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-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-5{height:1.25rem}.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-\[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-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{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-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-4{row-gap:1rem}.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-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))}.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}.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-dashed{border-style:dashed}.border-transparent{border-color:transparent}.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-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-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}.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\(--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\/60{color:hsla(0,0%,100%,.6)}.text-white\/85{color:hsla(0,0%,100%,.85)}.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}.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\: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\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.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))}}@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 6c55ee5..305514e 100644 --- a/internal/web/static/js/blackjack.js +++ b/internal/web/static/js/blackjack.js @@ -9,17 +9,26 @@ // Which is also why the hole card works the way it does: the server sends a // "dealer_hole" event with no card attached, because while you are still acting // it hasn't told anyone what that card is. It arrives with the reveal. +// +// The money is animated on the same principle. Every number the server sends is +// also a movement: a stake is chips leaving your pile and landing on the spot, a +// payout is chips coming out of the house's rack, a loss is your stack being +// taken away. Nothing about the money changes on this table without something +// crossing the felt to make it change — including the count in the chip bar, +// which is deliberately not updated until the chips that justify it have landed. (function () { "use strict"; var root = document.querySelector("[data-blackjack]"); if (!root) return; + var FX = window.PeteFX; + var dealerEl = root.querySelector("[data-dealer]"); var playerEl = root.querySelector("[data-player]"); var dTotalEl = root.querySelector("[data-dealer-total]"); var pTotalEl = root.querySelector("[data-player-total]"); - var betChip = root.querySelector("[data-bet]"); + var dLabelEl = root.querySelector("[data-dealer-label]"); var verdictEl = root.querySelector("[data-verdict]"); var betting = root.querySelector("[data-betting]"); var actions = root.querySelector("[data-actions]"); @@ -27,14 +36,27 @@ var dealBtn = root.querySelector("[data-deal]"); var msgEl = root.querySelector("[data-table-msg]"); - var bet = 25; - var busy = false; // a request is in flight, or cards are still landing - var hand = null; // the hand as the server last described it + // The three places a chip can be: your pile (in the bar above), the spot in + // front of you, and the house's rack on the felt. + var purseEl = document.querySelector("[data-chips]"); + var spotEl = root.querySelector("[data-spot]"); + var stackEl = root.querySelector("[data-stack]"); + var spotTotalEl = root.querySelector("[data-spot-total]"); + var houseEl = root.querySelector("[data-house]"); + + // Nothing is bet until a chip is on the felt. The number in the panel is a + // readout of the pile, so it starts where the pile does — at nothing — rather + // than at a default stake nobody put down. + var bet = 0; // what you're building between hands + var staked = 0; // what is actually sitting on the spot right now + var busy = false; // a request is in flight, or cards are still landing + var hand = null; // the hand as the server last described it var DEAL_MS = 380; // one card's flight, and the gap before the next var FLIP_MS = 450; + var BEAT_MS = 600; // the dealer thinking before they draw out - var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + var reduced = FX.reduced; function pace(ms) { return reduced ? 0 : ms; } function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); } @@ -47,6 +69,8 @@ // ---- drawing -------------------------------------------------------------- + var dealt = 0; // how many cards this table has put down, ever — the tilt seed + // cardEl builds one card. face === null means face-down: the card is dealt, // but this browser has not been told what it is. function cardEl(face) { @@ -57,6 +81,10 @@ // The offset is per-card, so a card landing further left flies further. wrap.style.setProperty("--deal-x", "14rem"); wrap.style.setProperty("--deal-y", "-6rem"); + // Where it comes to rest. A degree or two either way is the whole difference + // between cards that were dealt onto a table and cards that were laid out in + // a grid, and it costs one custom property. + wrap.style.setProperty("--tilt", FX.jitter(dealt++, 2.4).toFixed(2) + "deg"); var inner = document.createElement("div"); inner.className = "pete-card-inner"; @@ -180,6 +208,87 @@ wrap.dataset.face = "up"; } + // ---- the money on the felt ------------------------------------------------- + // + // `staked` is what the spot is holding. Every path that changes it also moves + // chips to say so, so the two can't come apart: renderStack draws the pile, + // and the fly* calls are what put it there. + + function renderStack(amount) { + staked = amount || 0; + stackEl.innerHTML = ""; + spotEl.dataset.live = staked > 0 ? "1" : "0"; + + if (!staked) { + spotTotalEl.classList.add("hidden"); + return; + } + FX.chipsFor(staked).forEach(function (d, i) { + var c = FX.disc(d); + c.style.setProperty("--i", i); + c.style.setProperty("--spin", FX.jitter(i, 12).toFixed(1) + "deg"); + c.style.animationDelay = pace(i * 40) + "ms"; + stackEl.appendChild(c); + }); + spotTotalEl.textContent = staked.toLocaleString(); + spotTotalEl.classList.remove("hidden"); + } + + // pour throws a run of chips from one place to another and grows the pile on + // the spot as each one lands — by the value of the chip that landed, so the + // total under the pile counts up the way the chips do. The last chip carries + // the remainder, because chipsFor caps how many chips it will make you watch + // and the pile still has to end on the real number. + function pour(from, to, amount, opts) { + if (amount <= 0) return Promise.resolve(); + var base = staked; + var chips = FX.chipsFor(amount, 8); + var run = 0; + return FX.flyMany(from, to, chips, Object.assign({ + onLand: function (d, i) { + run += d; + renderStack(base + (i === chips.length - 1 ? amount : run)); + }, + }, opts || {})); + } + + // stake moves chips from your pile onto the spot: the bet you build before a + // deal, and the second bet a double puts down beside it. + function stake(amount, from) { + return pour(from || purseEl, spotEl, amount); + } + + // settleChips is what the felt does about the outcome, after the cards have + // finished telling you what it is. It reads the same two numbers the ledger + // moved: `bet` (already off your pile since the deal) and `payout` (what comes + // back — stake plus winnings less rake, or nothing at all). + function settleChips(final) { + var payout = final.payout || 0; + var back = payout - final.bet; // what the house is adding, if anything + + if (payout <= 0) { + // The house takes it. The stack goes to the rack and doesn't come back. + var lost = FX.chipsFor(final.bet, 8); + var chain = FX.flyMany(spotEl, houseEl, lost, { gap: 45, lift: 0.6, fade: true }); + renderStack(0); + return chain; + } + + // The house pays first, into the spot beside your stake, so you watch the + // winnings arrive on top of the bet that earned them. + var pay = pour(houseEl, spotEl, back, { gap: 60 }); + + // Paid, then swept up: the whole lot comes back to your pile, and only then + // does the number in the bar move. + return pay + .then(function () { return wait(back > 0 ? 380 : 200); }) + .then(function () { + var home = FX.flyMany(spotEl, purseEl, FX.chipsFor(payout, 8), { gap: 40, lift: 0.8 }); + renderStack(0); + return home; + }); + } + function totals(v) { if (v.total) { pTotalEl.textContent = v.total + (v.soft ? " (soft)" : ""); @@ -198,15 +307,17 @@ } // paint puts a hand on the felt with no animation. This is the resume path: - // you reloaded, or Pete restarted, and your cards are simply there. + // you reloaded, or Pete restarted, and your cards are simply there — including + // the stake, which is still on the spot because the server still has it. function paint(v) { dealerEl.innerHTML = ""; playerEl.innerHTML = ""; - if (!v) { setPhase(null); return; } + if (!v) { setPhase(null); renderStack(0); return; } v.player.forEach(function (c) { playerEl.appendChild(cardEl(c)); }); v.dealer.forEach(function (c) { dealerEl.appendChild(cardEl(c)); }); if (v.hole) dealerEl.appendChild(cardEl(null)); + renderStack(v.phase === "done" ? 0 : v.bet); totals(v); setPhase(v); } @@ -227,7 +338,11 @@ else if (v.net < 0) text += " " + v.net.toLocaleString(); verdictEl.textContent = text; verdictEl.classList.remove("hidden"); - playerEl.dataset.won = v.net > 0 ? "1" : "0"; + playerEl.dataset.won = v.net > 0 ? "1" : v.net < 0 ? "-1" : "0"; + + // The one thing in this room that gets confetti. A natural is rare, it pays + // 3:2, and if everything celebrated then nothing would. + if (v.outcome === "blackjack") FX.burst(verdictEl, { count: 34 }); } // setPhase swaps the controls: bet between hands, act during one. @@ -237,12 +352,6 @@ betting.classList.toggle("hidden", live); actions.classList.toggle("hidden", !live); - if (v && v.bet) { - betChip.textContent = "Bet " + v.bet.toLocaleString(); - betChip.classList.remove("hidden"); - } else { - betChip.classList.add("hidden"); - } if (live) { var dbl = actions.querySelector('[data-move="double"]'); if (dbl) dbl.disabled = !v.can_double; @@ -255,11 +364,32 @@ // play walks the server's events, one card at a time. It is deliberately the // only thing that renders during a hand: the final state is applied at the end, // so what you watch and what the server says can't disagree halfway through. - function play(view) { + // + // `money` is the one exception, and it's a deliberate one. On a hand that is + // still running, the chip bar is right immediately — your stake left your pile + // when you pressed Deal, and it's sitting on the spot where you can see it. On + // a hand that *settles*, the bar is left alone until the chips have physically + // come home, because a counter that pays you before the dealer has turned over + // is a counter that has told you the ending. + function play(view, money) { var events = view.events || []; var final = view.hand; + var settles = !!final && final.phase === "done"; var hole = null; // the face-down card element, once one has been dealt var chain = Promise.resolve(); + var drew = false; // has the dealer drawn since the reveal? + + if (!settles) money(); + + // Whatever the server says the stake is, that's what has to be on the spot. + // Two things get here: a double, which puts a second bet down beside the + // first, and a deal whose bet was typed rather than stacked (you kept last + // hand's number and just pressed Deal). Either way the chips go down before + // the card they're buying does. + if (final && final.bet > staked) { + var extra = final.bet - staked; + chain = chain.then(function () { return stake(extra); }); + } events.forEach(function (e) { chain = chain.then(function () { @@ -276,8 +406,15 @@ return wait(DEAL_MS); case "dealer_card": - dealerEl.appendChild(cardEl(e.card)); - return wait(DEAL_MS); + // The dealer takes a moment before the first card they draw out. + // Card, card, card with no breath in between is a machine dealing; + // the pause is the only thing on this table that plays as suspense. + var beat = drew ? Promise.resolve() : think(); + drew = true; + return beat.then(function () { + dealerEl.appendChild(cardEl(e.card)); + return wait(DEAL_MS); + }); case "dealer_hole": hole = cardEl(null); @@ -300,13 +437,50 @@ }); return chain.then(function () { - if (final) { - totals(final); - setPhase(final); - if (final.phase === "done") verdict(final); - } else { - paint(null); - } + if (!final) { paint(null); money(); return; } + + totals(final); + if (!settles) { setPhase(final); return; } + + // The hand is over: nothing is on offer while the money is moving. Hit and + // Stand go now, and Deal comes back at the far end. + actions.classList.add("hidden"); + verdict(final); + // The chips move, and the bar catches up with them when they arrive. The + // betting controls come back last, once the felt is clear: offering Deal + // over a table that is still being paid out invites a click the table then + // has to refuse. + return settleChips(final) + .then(money) + .then(function () { return standing(final.bet); }) + .then(function () { setPhase(final); }); + }); + } + + // standing leaves your bet up for the next hand, the way you would at a table: + // the stake that just settled goes straight back on the spot. It costs nothing + // — chips on the spot are a proposal until you press Deal — and it's what keeps + // the number in the panel honest, because otherwise a settled hand leaves + // "your bet: 300" printed over an empty spot. + function standing(amount) { + var money = window.PeteGames.view(); + if (!amount || !money || money.chips < amount) { + bet = 0; + showBet(); + return; + } + bet = amount; + showBet(); + return stake(amount); + } + + // think is the dealer's beat: a pause with something to look at, so it reads as + // deliberation rather than as the page having hung. + function think() { + if (reduced || !dLabelEl) return wait(0); + dLabelEl.classList.add("pete-dealer-think"); + return wait(BEAT_MS).then(function () { + dLabelEl.classList.remove("pete-dealer-think"); }); } @@ -318,18 +492,24 @@ say(""); return window.PeteGames.post(path, body) .then(function (view) { - window.PeteGames.apply(view); - return play(view); + // play() decides *when* the money lands; see the note on it. + return play(view, function () { window.PeteGames.apply(view); }); }) .catch(function (err) { say(err.message, "bad"); // Whatever we thought was on the felt, the server is the authority on it. - return window.PeteGames.refresh(); + return window.PeteGames.refresh().then(function (v) { + if (v && !v.hand) renderStack(0); + }); }) .then(function () { busy = false; }); } // ---- betting -------------------------------------------------------------- + // + // A bet is built by putting chips on the spot, one at a time, and it is those + // chips the deal then rides on — the number under the pile is a readout of the + // pile, not the other way round. function showBet() { betAmount.textContent = bet.toLocaleString(); @@ -339,14 +519,37 @@ root.querySelectorAll("[data-chip]").forEach(function (btn) { btn.addEventListener("click", function () { - bet += parseInt(btn.dataset.chip, 10); + if (busy) return; + var d = parseInt(btn.dataset.chip, 10); + var money = window.PeteGames.view(); + if (money && bet + d > money.chips) { + say("You haven't got that many chips.", "bad"); + return; + } + bet += d; showBet(); + + // The chip you clicked is the chip that flies: same colour, same size, off + // the button and onto the felt. The pile only grows once it gets there — + // but `staked` moves now, so a Deal pressed mid-flight still knows the chip + // is on its way and doesn't put a second one down. + var target = bet; + staked = bet; + FX.fly(btn, spotEl, { denom: d }).then(function () { + if (bet >= target) renderStack(target); // unless Clear got there first + }); }); }); var clearBtn = root.querySelector("[data-bet-clear]"); if (clearBtn) { - clearBtn.addEventListener("click", function () { bet = 0; showBet(); }); + clearBtn.addEventListener("click", function () { + if (busy || !staked) { bet = 0; showBet(); return; } + FX.flyMany(spotEl, purseEl, FX.chipsFor(staked, 8), { gap: 40, lift: 0.7 }); + bet = 0; + renderStack(0); + showBet(); + }); } if (dealBtn) { diff --git a/internal/web/static/js/casino-fx.js b/internal/web/static/js/casino-fx.js new file mode 100644 index 0000000..5df5e1c --- /dev/null +++ b/internal/web/static/js/casino-fx.js @@ -0,0 +1,249 @@ +// The moving parts of the room. +// +// A casino is mostly one gesture repeated: something of value is picked up here +// and put down there, in front of you, slowly enough that you can object. So the +// only real idea in this file is fly() — take an element's place on screen, take +// another's, and move a chip between them on an arc that a hand would make. +// +// Everything flies in a fixed overlay pinned to the viewport, not inside the +// felt, because a chip's journey starts on a button *outside* the felt and any +// container in between would clip it halfway. Coordinates come from +// getBoundingClientRect, so the overlay needs no knowledge of the layout at all +// and nothing has to be positioned relative to anything else. +// +// Exposed as window.PeteFX. Nothing in here knows what blackjack is. +(function () { + "use strict"; + + var reduced = + window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + var layer = null; + function stage() { + if (!layer) { + layer = document.createElement("div"); + layer.className = "pete-fly-layer"; + layer.setAttribute("aria-hidden", "true"); + document.body.appendChild(layer); + } + return layer; + } + + // Where a thing is, in viewport coordinates: the middle of it. + function centre(target) { + if (!target) return { x: 0, y: 0 }; + if (typeof target.x === "number") return target; + var r = target.getBoundingClientRect(); + return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; + } + + // DENOMS, biggest first — the order you break an amount down in. + var DENOMS = [500, 100, 25, 5]; + + // chipsFor turns an amount into the chips a dealer would actually push across: + // as few as possible, biggest first. Capped, because a €10,000 bet is 20 purple + // chips and nobody wants to watch 20 of anything fly. Past the cap the stack + // just gets shorter than the number under it, which is what a real tall stack + // looks like from across a table anyway. + function chipsFor(amount, cap) { + var out = []; + var left = Math.max(0, Math.round(amount || 0)); + for (var i = 0; i < DENOMS.length && left > 0; i++) { + while (left >= DENOMS[i]) { + out.push(DENOMS[i]); + left -= DENOMS[i]; + } + } + if (left > 0) out.push(5); // a remainder below the smallest chip still shows up + var max = cap || 12; + return out.length > max ? out.slice(0, max) : out; + } + + function disc(denom) { + var el = document.createElement("div"); + el.className = "pete-disc"; + el.dataset.chip = String(denom); + return el; + } + + // A deterministic wobble per index, so a stack looks hand-placed but doesn't + // reshuffle itself every time the page repaints. + function jitter(i, spread) { + var n = Math.sin((i + 1) * 12.9898) * 43758.5453; + return ((n - Math.floor(n)) * 2 - 1) * (spread || 1); + } + + // fly moves one chip from one place to another and resolves when it lands. + // + // The arc is the point. A straight line between two rects reads as a UI + // transition; a chip that rises, travels and drops reads as a throw. WAAPI does + // this in one keyframe list because it can interpolate a midpoint — a CSS + // transition can't, which is why this isn't a class toggle. + function fly(from, to, opts) { + opts = opts || {}; + var a = centre(from); + var b = centre(to); + var el = disc(opts.denom || 25); + el.className += " pete-fly"; + stage().appendChild(el); + + var dur = opts.duration || 420; + if (reduced) dur = 1; + + // How high it goes: further throws arc higher, and a lob can be forced. + var dist = Math.hypot(b.x - a.x, b.y - a.y); + var lift = Math.min(120, 28 + dist * 0.18) * (opts.lift == null ? 1 : opts.lift); + var midX = (a.x + b.x) / 2; + var midY = (a.y + b.y) / 2 - lift; + var spin = opts.spin == null ? jitter(opts.index || 0, 90) : opts.spin; + + var anim = el.animate( + [ + { transform: t(a.x, a.y, 0.85, 0), opacity: 1, offset: 0 }, + { transform: t(midX, midY, 1.12, spin * 0.6), opacity: 1, offset: 0.5 }, + { transform: t(b.x, b.y, 1, 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" } + ); + + return anim.finished + .catch(function () {}) + .then(function () { + el.remove(); + }); + } + + function t(x, y, scale, rot) { + return "translate(" + x + "px," + y + "px) scale(" + scale + ") rotate(" + rot + "deg)"; + } + + // flyMany throws a whole bet across, one chip after another rather than all at + // once, because a pile of chips arriving as a single object is a progress bar. + // Resolves when the last one lands. + function flyMany(from, to, denoms, opts) { + opts = opts || {}; + var gap = reduced ? 0 : opts.gap == null ? 70 : opts.gap; + var each = denoms.map(function (d, i) { + return fly(from, to, { + denom: d, + index: i, + delay: (opts.delay || 0) + i * gap, + duration: opts.duration, + lift: opts.lift, + fade: opts.fade, + spin: opts.spin, + }); + }); + if (opts.onLand && !reduced) { + denoms.forEach(function (d, i) { + setTimeout(opts.onLand, (opts.delay || 0) + i * gap + (opts.duration || 420), d, i); + }); + } else if (opts.onLand) { + denoms.forEach(function (d, i) { opts.onLand(d, i); }); + } + return Promise.all(each); + } + + // burst: confetti out of a point. Saved for the things worth celebrating. + function burst(target, opts) { + if (reduced) return Promise.resolve(); + opts = opts || {}; + var c = centre(target); + var n = opts.count || 26; + var colours = opts.colours || ["#f2b53d", "#4caf7d", "#5aa9e6", "#b079d6", "#ffffff", "#cc3d4a"]; + var done = []; + + for (var i = 0; i < n; i++) { + var bit = document.createElement("div"); + bit.className = "pete-spark"; + bit.style.background = colours[i % colours.length]; + stage().appendChild(bit); + + // Fired into a cone that mostly goes up, then let gravity have it. + var angle = (-Math.PI / 2) + jitter(i, 1) * (opts.spread || 1.15); + var speed = 120 + Math.abs(jitter(i + 7, 1)) * 190; + var vx = Math.cos(angle) * speed; + var vy = Math.sin(angle) * speed; + var dropTo = c.y + 220 + jitter(i + 3, 60); + + done.push( + bit + .animate( + [ + { transform: t(c.x, c.y, 0.6, 0), opacity: 1, offset: 0 }, + { + transform: t(c.x + vx * 0.45, c.y + vy * 0.45, 1, jitter(i, 180)), + opacity: 1, + offset: 0.4, + }, + // Held at full colour most of the way down: a piece of confetti that + // starts fading the moment it's thrown reads as smoke. + { + transform: t(c.x + vx * 0.75, c.y + vy * 0.3 + 110, 0.95, jitter(i, 380)), + opacity: 1, + offset: 0.75, + }, + { + transform: t(c.x + vx * 0.9, dropTo, 0.9, jitter(i, 540)), + opacity: 0, + offset: 1, + }, + ], + { + duration: 900 + Math.abs(jitter(i + 11, 1)) * 500, + easing: "cubic-bezier(0.18, 0.7, 0.4, 1)", + fill: "both", + } + ) + .finished.catch(function () {}) + .then( + (function (b) { + return function () { b.remove(); }; + })(bit) + ) + ); + } + return Promise.all(done); + } + + // count rolls a number to a new value instead of swapping it. A chip count that + // jumps is a variable; one that climbs is a payout. + function count(el, to, opts) { + if (!el) return; + opts = opts || {}; + var from = parseInt(String(el.textContent).replace(/[^0-9-]/g, ""), 10); + if (isNaN(from) || reduced || from === to) { + el.textContent = (to || 0).toLocaleString(); + return; + } + var dur = opts.duration || 520; + var t0 = null; + if (el._petePop) el._petePop.cancel(); + el._petePop = el.animate( + [{ transform: "scale(1)" }, { transform: "scale(1.12)" }, { transform: "scale(1)" }], + { duration: dur, easing: "ease-out" } + ); + + function step(ts) { + if (t0 === null) t0 = ts; + var p = Math.min(1, (ts - t0) / dur); + var eased = 1 - Math.pow(1 - p, 3); + el.textContent = Math.round(from + (to - from) * eased).toLocaleString(); + if (p < 1) requestAnimationFrame(step); + else el.textContent = (to || 0).toLocaleString(); + } + requestAnimationFrame(step); + } + + window.PeteFX = { + reduced: reduced, + chipsFor: chipsFor, + disc: disc, + jitter: jitter, + fly: fly, + flyMany: flyMany, + burst: burst, + count: count, + centre: centre, + }; +})(); diff --git a/internal/web/static/js/games.js b/internal/web/static/js/games.js index 581a094..d66e0b5 100644 --- a/internal/web/static/js/games.js +++ b/internal/web/static/js/games.js @@ -41,8 +41,16 @@ } function paint(v) { + var first = view === null; view = v; - if (chipsEl) chipsEl.textContent = money(v.chips); + // The chip count rolls to its new value rather than jumping to it — the table + // times this to land with the chips that caused it, so a payout reads as the + // number catching up with the felt. On the first paint there's nothing to + // catch up with, so it just sets. + if (chipsEl) { + if (first || !window.PeteFX) chipsEl.textContent = money(v.chips); + else window.PeteFX.count(chipsEl, v.chips); + } if (eurosEl) eurosEl.textContent = (v.euros || 0).toFixed(2); if (pendingEl) { diff --git a/internal/web/templates/blackjack.html b/internal/web/templates/blackjack.html index 070c974..e89a77f 100644 --- a/internal/web/templates/blackjack.html +++ b/internal/web/templates/blackjack.html @@ -18,9 +18,18 @@ {{template "_chipbar" .}} - +
+ + @@ -29,7 +38,7 @@
- Dealer + Dealer
@@ -37,17 +46,25 @@
- + +
- +
You -
-
+
+
+ Bet +
+ +
+
+
@@ -58,14 +75,15 @@
Your bet
-
25
+
0
{{range .Denominations}} - + {{end}} @@ -108,6 +126,7 @@ {{end}} {{define "scripts"}} + {{end}} diff --git a/pete_games_plan.md b/pete_games_plan.md index 3afceea..e4174e3 100644 --- a/pete_games_plan.md +++ b/pete_games_plan.md @@ -91,16 +91,52 @@ A multi-session build. This section is the handover; read it before anything els bottom-half pips inverted, courts as a letter with the suit over each shoulder, and a screen-reader label that says "Queen of hearts" instead of "Q♥". +- **The money moves.** The felt grew the two things it was missing: a **bet spot** + in front of you and the **house's rack** beside the shoe, so every chip on the + table is always travelling between one of those and the other. A bet is *built* + by throwing chips onto the spot (the chip you clicked is the chip that flies); + the stake sits there through the hand; the house pays out of its rack into the + spot; the whole pile is then swept back to your pile. A loss goes to the rack and + doesn't come back. `casino-fx.js` is the shared engine — `fly`/`flyMany` (WAAPI, + on an arc, out of a fixed overlay so nothing clips them), `chipsFor` (an amount + broken into the fewest chips, capped at what's worth watching), `burst`, `count`. + + Two rules hold it together, and both are load-bearing: + + 1. **The number under the pile is a readout of the pile**, never the other way + round. So the bet starts at zero rather than at a default nobody put down, and + a settled hand puts your stake *back on the spot* as a standing bet — otherwise + the panel prints "your bet: 300" over an empty circle. + 2. **The chip bar does not move until the chips that justify it have landed.** On + a live hand the money applies immediately (your stake left your pile and is + visibly on the spot); on a settling hand `play()` holds the apply until the + payout has swept home. A counter that pays you before the dealer turns over is + a counter that has told you the ending. + + Also: cards land with weight (overshoot, a shadow that takes the hit, a degree or + two of resting tilt each), the dealer takes a beat before drawing out, and a + natural gets confetti — the only thing in the room that does. + +- **A way to actually look at it.** `internal/web/devcasino_test.go` is the casino on + a port with one signed-in, funded player: `PETE_DEV_CASINO=:7788 go test + ./internal/web -run TestDevCasino -timeout 0`. Skipped without the env var. It + wires its own routes because `New()` decides whether the casino exists at the + moment it builds the mux, and the test rig signs the player in afterwards. **Drive + the table in a real browser before believing anything about it** — this pass found + a white-on-white verdict pill, a rack that collided with the dealer, and Hit still + being offered over a table that was being paid out, none of which a Go test can see. + ### Next, in order -1. **Finish making the table lively.** Card faces are done. Still thin: chips never - physically move, and nothing celebrates. Ideas already sketched: chips that fly to - a bet spot and back on a win, cards landing with weight, a dealer beat before - drawing out, a burst on a natural. -2. **Deploy.** Add the `games.parodia.dev` redirect URI to the `pete` app in Authentik, +1. **Deploy.** Add the `games.parodia.dev` redirect URI to the `pete` app in Authentik, point Caddy at the same port, set `[web.games]` + `web.auth.cookie_domain` on the server. Nothing else is host-specific. -3. Then Phase 2 (trivia, hangman), 3 (UNO), 4 (hold'em) as below. +2. Then Phase 2 (trivia, hangman), 3 (UNO), 4 (hold'em) as below. + +Still open on the table itself, none of it blocking: **split** isn't implemented (the +engine has no move for it), the felt is roomy at desktop widths with only one seat on +it, and the chip animations are tuned for one player — a second seat would need the +spot to be per-seat rather than the singleton it is now. ### How the browser half fits together