From 058432130628d00764663ecf819a9adc6793768d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 8 Nov 2026 03:29:35 +0100 Subject: v5.0.0: protocol v5 and the browser frontend Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO. --- web/play/style.css | 407 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 web/play/style.css (limited to 'web/play/style.css') diff --git a/web/play/style.css b/web/play/style.css new file mode 100644 index 0000000..20842d2 --- /dev/null +++ b/web/play/style.css @@ -0,0 +1,407 @@ +:root { + --bg: #0a0e12; + --surface: #11171d; + --raised: #171f27; + --hover: #1d2731; + --line: #242e38; + --text: #d9e2ea; + --dim: #8a98a6; + --faint: #5b6773; + --accent: #4fb8c9; + --win: #4ec27b; + --loss: #e05a5a; + --gold: #e0b64a; + --felt: #0d5c39; + --felt-dark: #09472c; + --felt-line: #1f7d52; + --felt-text: #ecf7f0; + --red: #b0313a; + --black: #151b21; + --zero: #106b45; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --sans: "Open Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --mono: "Hack", ui-monospace, "SFMono-Regular", "Cascadia Code", monospace; +} + +* { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; + background: + radial-gradient(1100px 600px at 80% -10%, rgba(79, 184, 201, 0.06), transparent 60%), + var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +[hidden] { display: none !important; } + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } +button, input { font: inherit; color: inherit; } +button { cursor: pointer; } +button:disabled { cursor: not-allowed; opacity: 0.45; } + +.shell { min-height: 100%; display: flex; flex-direction: column; } + +.topbar { + display: flex; + align-items: center; + gap: 16px; + min-height: 64px; + padding: 0 24px; + border-bottom: 1px solid var(--line); + background: rgba(10, 14, 18, 0.86); + backdrop-filter: blur(10px); + position: sticky; + top: 0; + z-index: 20; +} +.brand { + color: var(--text); + font-family: var(--mono); + font-size: 19px; + letter-spacing: 0.04em; + white-space: nowrap; +} +.brand span { color: var(--win); } +.brand:hover { text-decoration: none; } +.tagline { + color: var(--faint); + font-family: var(--mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; +} +.spacer { flex: 1; } +.session { display: flex; align-items: center; gap: 12px; } +.session .who { color: var(--dim); font-family: var(--mono); font-size: 12px; } +.session .who strong { color: var(--text); font-weight: 600; } +.balance { + color: var(--win); + font-family: var(--mono); + font-size: 16px; + padding: 4px 10px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + white-space: nowrap; +} + +main { width: min(1240px, 100%); margin: 0 auto; padding: 28px 24px 64px; flex: 1; } + +h1, h2, h3 { margin: 0; font-weight: 600; } +h1 { font-size: 26px; letter-spacing: -0.01em; } +h2 { font-size: 18px; } +.muted { color: var(--dim); } +.faint { color: var(--faint); } +.mono { font-family: var(--mono); } +.label { + font-family: var(--mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--dim); +} +.error-text { color: var(--loss); } +.win-text { color: var(--win); } + +.btn { + border: 1px solid var(--line); + background: var(--raised); + color: var(--text); + padding: 9px 14px; + border-radius: var(--radius-md); + line-height: 1.2; + transition: background 120ms ease, border-color 120ms ease, transform 80ms ease; +} +.btn:hover:not(:disabled) { background: var(--hover); border-color: #33404d; } +.btn:active:not(:disabled) { transform: translateY(1px); } +.btn.primary { + background: var(--win); + border-color: var(--win); + color: #06120b; + font-weight: 700; +} +.btn.primary:hover:not(:disabled) { filter: brightness(1.08); } +.btn.accent { + background: rgba(79, 184, 201, 0.14); + border-color: var(--accent); + color: var(--accent); + font-weight: 600; +} +.btn.danger { + background: rgba(224, 90, 90, 0.12); + border-color: rgba(224, 90, 90, 0.55); + color: var(--loss); +} +.btn.ghost { background: transparent; } +.btn.small { padding: 5px 9px; font-size: 12px; } +.btn.wide { width: 100%; } +.btn.big { padding: 13px 18px; font-size: 15px; } + +.input { + width: 100%; + background: var(--bg); + border: 1px solid var(--line); + border-radius: var(--radius-md); + padding: 10px 12px; + outline: none; +} +.input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(79, 184, 201, 0.18); } +.field { display: grid; gap: 6px; } +.row { display: flex; align-items: center; gap: 10px; } +.row.wrap { flex-wrap: wrap; } +.hr { height: 1px; background: var(--line); border: 0; margin: 18px 0; } + +/* Auth */ +.auth-wrap { display: grid; place-items: center; min-height: 52vh; } +.auth-card { + width: min(430px, 100%); + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 26px; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.35); +} +.auth-tabs { display: flex; gap: 4px; margin-bottom: 20px; background: var(--bg); padding: 4px; border-radius: var(--radius-md); } +.auth-tab { flex: 1; border: 0; background: transparent; color: var(--dim); padding: 8px; border-radius: var(--radius-sm); } +.auth-tab.active { background: var(--raised); color: var(--text); } +.auth-card form { display: grid; gap: 14px; } +.auth-foot { margin-top: 16px; font-size: 12px; color: var(--faint); } + +/* Lobby */ +.lobby-head { display: flex; align-items: end; gap: 18px; margin-bottom: 22px; flex-wrap: wrap; } +.lobby-head .spacer { flex: 1; } +.game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; } +.game-card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 18px; + display: flex; + flex-direction: column; + gap: 12px; + min-height: 178px; + position: relative; + overflow: hidden; +} +.game-card::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: var(--accent); + opacity: 0.7; +} +.game-card.crash::before { background: var(--gold); } +.game-card.roulette::before { background: var(--felt-line); } +.game-card.blackjack::before { background: var(--loss); } +.game-card h3 { font-size: 17px; } +.game-card p { margin: 0; color: var(--dim); font-size: 13px; flex: 1; } +.game-card .row { justify-content: space-between; } +.bet-input { width: 92px; } + +/* Game shell */ +.game-head { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; } +.game-head .spacer { flex: 1; } +.game-body { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 16px; + align-items: start; +} +.board { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 18px; + min-height: 560px; + overflow: hidden; +} +.sidebar { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; + position: sticky; + top: 84px; +} +.sidebar .actions { margin-top: auto; display: grid; gap: 8px; } +.sidebar .stat { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; } +.sidebar .stat strong { font-family: var(--mono); font-weight: 600; } +.note { color: var(--dim); font-size: 12px; } +.chip-row { display: flex; flex-wrap: wrap; gap: 6px; } +.chip-btn { + font-family: var(--mono); + font-size: 12px; + border-radius: 999px; + border: 1px solid var(--line); + background: var(--raised); + padding: 5px 10px; +} +.chip-btn.active { border-color: var(--gold); color: var(--gold); background: rgba(224, 182, 74, 0.1); } + +/* Slots */ +.reels { + display: grid; + grid-template-columns: repeat(5, minmax(60px, 1fr)); + gap: 10px; + max-width: 760px; + margin: 24px auto 0; +} +.reel { display: grid; gap: 8px; } +.tile { + aspect-ratio: 1 / 0.92; + display: grid; + place-items: center; + border-radius: var(--radius-md); + border: 1px solid var(--line); + background: var(--bg); + font-family: var(--mono); + font-size: clamp(26px, 5vw, 54px); + font-weight: 700; + color: var(--dim); + transition: color 90ms ease, border-color 90ms ease, background 90ms ease; +} +.tile.sym-C { color: #c8d1da; } +.tile.sym-L { color: #7cc4d4; } +.tile.sym-O { color: #d8c27a; } +.tile.sym-G { color: #83d09a; } +.tile.sym-D { color: #d48d8d; } +.tile.sym-S { color: #e0b64a; } +.tile.win { border-color: var(--win); background: rgba(78, 194, 123, 0.12); color: var(--win); } +.tile.spinning { color: var(--faint); } +.win-lines { text-align: center; margin-top: 18px; color: var(--dim); font-size: 12px; min-height: 18px; } + +/* Cards */ +.hand { margin: 16px 0; } +.hand .label { margin-bottom: 8px; } +.cards { display: flex; gap: 8px; flex-wrap: wrap; min-height: 116px; } +.card { + width: 76px; + height: 108px; + border-radius: 9px; + background: #f2f5f7; + color: #121820; + border: 1px solid #cfd8df; + padding: 8px; + display: flex; + flex-direction: column; + justify-content: space-between; + font-family: var(--mono); + font-weight: 700; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22); +} +.card.red { color: #a51f2d; } +.card.back { background: repeating-linear-gradient(135deg, #1d2731, #1d2731 6px, #26333f 6px, #26333f 12px); border-color: #33404d; } +.card .rank { font-size: 22px; line-height: 1; } +.card .suit { font-size: 22px; align-self: flex-end; } +.card.back .rank, .card.back .suit { color: var(--faint); } +.total { color: var(--dim); font-family: var(--mono); font-size: 12px; } +.total strong { color: var(--text); font-size: 15px; } +.outcome { margin-top: 20px; padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg); } + +/* Crash */ +.crash-wrap { position: relative; } +.crash-number { + font-family: var(--mono); + font-size: clamp(44px, 9vw, 86px); + line-height: 1; + letter-spacing: -0.04em; +} +.crash-number.running { color: var(--gold); } +.crash-number.crashed { color: var(--loss); } +.crash-number.cashed { color: var(--win); } +.crash-meta { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; color: var(--faint); font-size: 11px; font-family: var(--mono); } +.verify { margin-top: 12px; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); font-size: 11px; color: var(--dim); word-break: break-all; } +.verify.ok { border-color: rgba(78, 194, 123, 0.4); color: var(--win); } +.verify.bad { border-color: rgba(224, 90, 90, 0.5); color: var(--loss); } + +/* Roulette */ +.roulette-wrap { position: relative; } +canvas { display: block; width: 100%; } +.roulette-canvas { height: 540px; } +.crash-canvas { height: 360px; } +.wheel-result { + position: absolute; + top: 16px; + right: 16px; + padding: 8px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--line); + background: rgba(10, 14, 18, 0.86); + font-family: var(--mono); +} +.wheel-result .num { font-size: 26px; font-weight: 700; } +.wheel-result.red .num { color: #f06a74; } +.wheel-result.black .num { color: #dfe8ef; } +.wheel-result.green .num { color: #66d49a; } + +/* History */ +.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); } +table { width: 100%; border-collapse: collapse; background: var(--surface); } +th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--line); font-size: 13px; white-space: nowrap; } +th { color: var(--dim); font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; } +tr:last-child td { border-bottom: 0; } +td.num { font-family: var(--mono); } +td.pos { color: var(--win); } +td.neg { color: var(--loss); } + +/* Toasts and overlays */ +#toasts { position: fixed; right: 18px; bottom: 18px; display: grid; gap: 8px; z-index: 50; width: min(380px, calc(100vw - 36px)); } +.toast { + background: var(--surface); + border: 1px solid var(--line); + border-left: 3px solid var(--accent); + border-radius: var(--radius-md); + padding: 10px 12px; + font-size: 13px; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35); + animation: toast-in 160ms ease-out; +} +.toast.good { border-left-color: var(--win); } +.toast.bad { border-left-color: var(--loss); } +@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } } +.overlay { + position: fixed; + inset: 0; + background: rgba(5, 8, 11, 0.78); + display: grid; + place-items: center; + z-index: 60; + padding: 24px; +} +.modal { + width: min(440px, 100%); + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 24px; + text-align: center; +} +.modal p { color: var(--dim); } + +@media (max-width: 900px) { + .game-body { grid-template-columns: 1fr; } + .sidebar { position: static; } + .tagline { display: none; } +} +@media (max-width: 620px) { + main { padding: 18px 14px 54px; } + .topbar { padding: 0 14px; gap: 10px; } + .session .who { display: none; } + .balance { font-size: 14px; } + .reels { gap: 6px; } + .tile { font-size: clamp(18px, 7vw, 34px); } + .card { width: 58px; height: 84px; } + .crash-canvas { height: 260px; } + .roulette-canvas { height: 400px; } +} -- cgit v1.2.3