diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-08 03:29:35 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-08 03:29:35 +0100 |
| commit | 058432130628d00764663ecf819a9adc6793768d (patch) | |
| tree | d3f49f111fbfaa48647cca2600adcf9017af2d30 /web/play/style.css | |
| parent | web: serve latest.php through PHP and refuse stray PHP files (diff) | |
| download | zagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip | |
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.
Diffstat (limited to 'web/play/style.css')
| -rw-r--r-- | web/play/style.css | 407 |
1 files changed, 407 insertions, 0 deletions
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 @@ | |||
| 1 | :root { | ||
| 2 | --bg: #0a0e12; | ||
| 3 | --surface: #11171d; | ||
| 4 | --raised: #171f27; | ||
| 5 | --hover: #1d2731; | ||
| 6 | --line: #242e38; | ||
| 7 | --text: #d9e2ea; | ||
| 8 | --dim: #8a98a6; | ||
| 9 | --faint: #5b6773; | ||
| 10 | --accent: #4fb8c9; | ||
| 11 | --win: #4ec27b; | ||
| 12 | --loss: #e05a5a; | ||
| 13 | --gold: #e0b64a; | ||
| 14 | --felt: #0d5c39; | ||
| 15 | --felt-dark: #09472c; | ||
| 16 | --felt-line: #1f7d52; | ||
| 17 | --felt-text: #ecf7f0; | ||
| 18 | --red: #b0313a; | ||
| 19 | --black: #151b21; | ||
| 20 | --zero: #106b45; | ||
| 21 | --radius-sm: 4px; | ||
| 22 | --radius-md: 8px; | ||
| 23 | --radius-lg: 12px; | ||
| 24 | --sans: "Open Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; | ||
| 25 | --mono: "Hack", ui-monospace, "SFMono-Regular", "Cascadia Code", monospace; | ||
| 26 | } | ||
| 27 | |||
| 28 | * { box-sizing: border-box; } | ||
| 29 | html, body { height: 100%; } | ||
| 30 | body { | ||
| 31 | margin: 0; | ||
| 32 | background: | ||
| 33 | radial-gradient(1100px 600px at 80% -10%, rgba(79, 184, 201, 0.06), transparent 60%), | ||
| 34 | var(--bg); | ||
| 35 | color: var(--text); | ||
| 36 | font-family: var(--sans); | ||
| 37 | font-size: 14px; | ||
| 38 | line-height: 1.5; | ||
| 39 | -webkit-font-smoothing: antialiased; | ||
| 40 | } | ||
| 41 | [hidden] { display: none !important; } | ||
| 42 | |||
| 43 | a { color: var(--accent); text-decoration: none; } | ||
| 44 | a:hover { text-decoration: underline; } | ||
| 45 | button, input { font: inherit; color: inherit; } | ||
| 46 | button { cursor: pointer; } | ||
| 47 | button:disabled { cursor: not-allowed; opacity: 0.45; } | ||
| 48 | |||
| 49 | .shell { min-height: 100%; display: flex; flex-direction: column; } | ||
| 50 | |||
| 51 | .topbar { | ||
| 52 | display: flex; | ||
| 53 | align-items: center; | ||
| 54 | gap: 16px; | ||
| 55 | min-height: 64px; | ||
| 56 | padding: 0 24px; | ||
| 57 | border-bottom: 1px solid var(--line); | ||
| 58 | background: rgba(10, 14, 18, 0.86); | ||
| 59 | backdrop-filter: blur(10px); | ||
| 60 | position: sticky; | ||
| 61 | top: 0; | ||
| 62 | z-index: 20; | ||
| 63 | } | ||
| 64 | .brand { | ||
| 65 | color: var(--text); | ||
| 66 | font-family: var(--mono); | ||
| 67 | font-size: 19px; | ||
| 68 | letter-spacing: 0.04em; | ||
| 69 | white-space: nowrap; | ||
| 70 | } | ||
| 71 | .brand span { color: var(--win); } | ||
| 72 | .brand:hover { text-decoration: none; } | ||
| 73 | .tagline { | ||
| 74 | color: var(--faint); | ||
| 75 | font-family: var(--mono); | ||
| 76 | font-size: 11px; | ||
| 77 | text-transform: uppercase; | ||
| 78 | letter-spacing: 0.12em; | ||
| 79 | } | ||
| 80 | .spacer { flex: 1; } | ||
| 81 | .session { display: flex; align-items: center; gap: 12px; } | ||
| 82 | .session .who { color: var(--dim); font-family: var(--mono); font-size: 12px; } | ||
| 83 | .session .who strong { color: var(--text); font-weight: 600; } | ||
| 84 | .balance { | ||
| 85 | color: var(--win); | ||
| 86 | font-family: var(--mono); | ||
| 87 | font-size: 16px; | ||
| 88 | padding: 4px 10px; | ||
| 89 | border: 1px solid var(--line); | ||
| 90 | border-radius: var(--radius-sm); | ||
| 91 | background: var(--surface); | ||
| 92 | white-space: nowrap; | ||
| 93 | } | ||
| 94 | |||
| 95 | main { width: min(1240px, 100%); margin: 0 auto; padding: 28px 24px 64px; flex: 1; } | ||
| 96 | |||
| 97 | h1, h2, h3 { margin: 0; font-weight: 600; } | ||
| 98 | h1 { font-size: 26px; letter-spacing: -0.01em; } | ||
| 99 | h2 { font-size: 18px; } | ||
| 100 | .muted { color: var(--dim); } | ||
| 101 | .faint { color: var(--faint); } | ||
| 102 | .mono { font-family: var(--mono); } | ||
| 103 | .label { | ||
| 104 | font-family: var(--mono); | ||
| 105 | font-size: 10px; | ||
| 106 | text-transform: uppercase; | ||
| 107 | letter-spacing: 0.16em; | ||
| 108 | color: var(--dim); | ||
| 109 | } | ||
| 110 | .error-text { color: var(--loss); } | ||
| 111 | .win-text { color: var(--win); } | ||
| 112 | |||
| 113 | .btn { | ||
| 114 | border: 1px solid var(--line); | ||
| 115 | background: var(--raised); | ||
| 116 | color: var(--text); | ||
| 117 | padding: 9px 14px; | ||
| 118 | border-radius: var(--radius-md); | ||
| 119 | line-height: 1.2; | ||
| 120 | transition: background 120ms ease, border-color 120ms ease, transform 80ms ease; | ||
| 121 | } | ||
| 122 | .btn:hover:not(:disabled) { background: var(--hover); border-color: #33404d; } | ||
| 123 | .btn:active:not(:disabled) { transform: translateY(1px); } | ||
| 124 | .btn.primary { | ||
| 125 | background: var(--win); | ||
| 126 | border-color: var(--win); | ||
| 127 | color: #06120b; | ||
| 128 | font-weight: 700; | ||
| 129 | } | ||
| 130 | .btn.primary:hover:not(:disabled) { filter: brightness(1.08); } | ||
| 131 | .btn.accent { | ||
| 132 | background: rgba(79, 184, 201, 0.14); | ||
| 133 | border-color: var(--accent); | ||
| 134 | color: var(--accent); | ||
| 135 | font-weight: 600; | ||
| 136 | } | ||
| 137 | .btn.danger { | ||
| 138 | background: rgba(224, 90, 90, 0.12); | ||
| 139 | border-color: rgba(224, 90, 90, 0.55); | ||
| 140 | color: var(--loss); | ||
| 141 | } | ||
| 142 | .btn.ghost { background: transparent; } | ||
| 143 | .btn.small { padding: 5px 9px; font-size: 12px; } | ||
| 144 | .btn.wide { width: 100%; } | ||
| 145 | .btn.big { padding: 13px 18px; font-size: 15px; } | ||
| 146 | |||
| 147 | .input { | ||
| 148 | width: 100%; | ||
| 149 | background: var(--bg); | ||
| 150 | border: 1px solid var(--line); | ||
| 151 | border-radius: var(--radius-md); | ||
| 152 | padding: 10px 12px; | ||
| 153 | outline: none; | ||
| 154 | } | ||
| 155 | .input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(79, 184, 201, 0.18); } | ||
| 156 | .field { display: grid; gap: 6px; } | ||
| 157 | .row { display: flex; align-items: center; gap: 10px; } | ||
| 158 | .row.wrap { flex-wrap: wrap; } | ||
| 159 | .hr { height: 1px; background: var(--line); border: 0; margin: 18px 0; } | ||
| 160 | |||
| 161 | /* Auth */ | ||
| 162 | .auth-wrap { display: grid; place-items: center; min-height: 52vh; } | ||
| 163 | .auth-card { | ||
| 164 | width: min(430px, 100%); | ||
| 165 | background: var(--surface); | ||
| 166 | border: 1px solid var(--line); | ||
| 167 | border-radius: var(--radius-lg); | ||
| 168 | padding: 26px; | ||
| 169 | box-shadow: 0 24px 70px rgba(0, 0, 0, 0.35); | ||
| 170 | } | ||
| 171 | .auth-tabs { display: flex; gap: 4px; margin-bottom: 20px; background: var(--bg); padding: 4px; border-radius: var(--radius-md); } | ||
| 172 | .auth-tab { flex: 1; border: 0; background: transparent; color: var(--dim); padding: 8px; border-radius: var(--radius-sm); } | ||
| 173 | .auth-tab.active { background: var(--raised); color: var(--text); } | ||
| 174 | .auth-card form { display: grid; gap: 14px; } | ||
| 175 | .auth-foot { margin-top: 16px; font-size: 12px; color: var(--faint); } | ||
| 176 | |||
| 177 | /* Lobby */ | ||
| 178 | .lobby-head { display: flex; align-items: end; gap: 18px; margin-bottom: 22px; flex-wrap: wrap; } | ||
| 179 | .lobby-head .spacer { flex: 1; } | ||
| 180 | .game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; } | ||
| 181 | .game-card { | ||
| 182 | background: var(--surface); | ||
| 183 | border: 1px solid var(--line); | ||
| 184 | border-radius: var(--radius-lg); | ||
| 185 | padding: 18px; | ||
| 186 | display: flex; | ||
| 187 | flex-direction: column; | ||
| 188 | gap: 12px; | ||
| 189 | min-height: 178px; | ||
| 190 | position: relative; | ||
| 191 | overflow: hidden; | ||
| 192 | } | ||
| 193 | .game-card::before { | ||
| 194 | content: ""; | ||
| 195 | position: absolute; | ||
| 196 | inset: 0 auto 0 0; | ||
| 197 | width: 3px; | ||
| 198 | background: var(--accent); | ||
| 199 | opacity: 0.7; | ||
| 200 | } | ||
| 201 | .game-card.crash::before { background: var(--gold); } | ||
| 202 | .game-card.roulette::before { background: var(--felt-line); } | ||
| 203 | .game-card.blackjack::before { background: var(--loss); } | ||
| 204 | .game-card h3 { font-size: 17px; } | ||
| 205 | .game-card p { margin: 0; color: var(--dim); font-size: 13px; flex: 1; } | ||
| 206 | .game-card .row { justify-content: space-between; } | ||
| 207 | .bet-input { width: 92px; } | ||
| 208 | |||
| 209 | /* Game shell */ | ||
| 210 | .game-head { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; } | ||
| 211 | .game-head .spacer { flex: 1; } | ||
| 212 | .game-body { | ||
| 213 | display: grid; | ||
| 214 | grid-template-columns: minmax(0, 1fr) 320px; | ||
| 215 | gap: 16px; | ||
| 216 | align-items: start; | ||
| 217 | } | ||
| 218 | .board { | ||
| 219 | background: var(--surface); | ||
| 220 | border: 1px solid var(--line); | ||
| 221 | border-radius: var(--radius-lg); | ||
| 222 | padding: 18px; | ||
| 223 | min-height: 560px; | ||
| 224 | overflow: hidden; | ||
| 225 | } | ||
| 226 | .sidebar { | ||
| 227 | background: var(--surface); | ||
| 228 | border: 1px solid var(--line); | ||
| 229 | border-radius: var(--radius-lg); | ||
| 230 | padding: 18px; | ||
| 231 | display: flex; | ||
| 232 | flex-direction: column; | ||
| 233 | gap: 14px; | ||
| 234 | position: sticky; | ||
| 235 | top: 84px; | ||
| 236 | } | ||
| 237 | .sidebar .actions { margin-top: auto; display: grid; gap: 8px; } | ||
| 238 | .sidebar .stat { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; } | ||
| 239 | .sidebar .stat strong { font-family: var(--mono); font-weight: 600; } | ||
| 240 | .note { color: var(--dim); font-size: 12px; } | ||
| 241 | .chip-row { display: flex; flex-wrap: wrap; gap: 6px; } | ||
| 242 | .chip-btn { | ||
| 243 | font-family: var(--mono); | ||
| 244 | font-size: 12px; | ||
| 245 | border-radius: 999px; | ||
| 246 | border: 1px solid var(--line); | ||
| 247 | background: var(--raised); | ||
| 248 | padding: 5px 10px; | ||
| 249 | } | ||
| 250 | .chip-btn.active { border-color: var(--gold); color: var(--gold); background: rgba(224, 182, 74, 0.1); } | ||
| 251 | |||
| 252 | /* Slots */ | ||
| 253 | .reels { | ||
| 254 | display: grid; | ||
| 255 | grid-template-columns: repeat(5, minmax(60px, 1fr)); | ||
| 256 | gap: 10px; | ||
| 257 | max-width: 760px; | ||
| 258 | margin: 24px auto 0; | ||
| 259 | } | ||
| 260 | .reel { display: grid; gap: 8px; } | ||
| 261 | .tile { | ||
| 262 | aspect-ratio: 1 / 0.92; | ||
| 263 | display: grid; | ||
| 264 | place-items: center; | ||
| 265 | border-radius: var(--radius-md); | ||
| 266 | border: 1px solid var(--line); | ||
| 267 | background: var(--bg); | ||
| 268 | font-family: var(--mono); | ||
| 269 | font-size: clamp(26px, 5vw, 54px); | ||
| 270 | font-weight: 700; | ||
| 271 | color: var(--dim); | ||
| 272 | transition: color 90ms ease, border-color 90ms ease, background 90ms ease; | ||
| 273 | } | ||
| 274 | .tile.sym-C { color: #c8d1da; } | ||
| 275 | .tile.sym-L { color: #7cc4d4; } | ||
| 276 | .tile.sym-O { color: #d8c27a; } | ||
| 277 | .tile.sym-G { color: #83d09a; } | ||
| 278 | .tile.sym-D { color: #d48d8d; } | ||
| 279 | .tile.sym-S { color: #e0b64a; } | ||
| 280 | .tile.win { border-color: var(--win); background: rgba(78, 194, 123, 0.12); color: var(--win); } | ||
| 281 | .tile.spinning { color: var(--faint); } | ||
| 282 | .win-lines { text-align: center; margin-top: 18px; color: var(--dim); font-size: 12px; min-height: 18px; } | ||
| 283 | |||
| 284 | /* Cards */ | ||
| 285 | .hand { margin: 16px 0; } | ||
| 286 | .hand .label { margin-bottom: 8px; } | ||
| 287 | .cards { display: flex; gap: 8px; flex-wrap: wrap; min-height: 116px; } | ||
| 288 | .card { | ||
| 289 | width: 76px; | ||
| 290 | height: 108px; | ||
| 291 | border-radius: 9px; | ||
| 292 | background: #f2f5f7; | ||
| 293 | color: #121820; | ||
| 294 | border: 1px solid #cfd8df; | ||
| 295 | padding: 8px; | ||
| 296 | display: flex; | ||
| 297 | flex-direction: column; | ||
| 298 | justify-content: space-between; | ||
| 299 | font-family: var(--mono); | ||
| 300 | font-weight: 700; | ||
| 301 | box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22); | ||
| 302 | } | ||
| 303 | .card.red { color: #a51f2d; } | ||
| 304 | .card.back { background: repeating-linear-gradient(135deg, #1d2731, #1d2731 6px, #26333f 6px, #26333f 12px); border-color: #33404d; } | ||
| 305 | .card .rank { font-size: 22px; line-height: 1; } | ||
| 306 | .card .suit { font-size: 22px; align-self: flex-end; } | ||
| 307 | .card.back .rank, .card.back .suit { color: var(--faint); } | ||
| 308 | .total { color: var(--dim); font-family: var(--mono); font-size: 12px; } | ||
| 309 | .total strong { color: var(--text); font-size: 15px; } | ||
| 310 | .outcome { margin-top: 20px; padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg); } | ||
| 311 | |||
| 312 | /* Crash */ | ||
| 313 | .crash-wrap { position: relative; } | ||
| 314 | .crash-number { | ||
| 315 | font-family: var(--mono); | ||
| 316 | font-size: clamp(44px, 9vw, 86px); | ||
| 317 | line-height: 1; | ||
| 318 | letter-spacing: -0.04em; | ||
| 319 | } | ||
| 320 | .crash-number.running { color: var(--gold); } | ||
| 321 | .crash-number.crashed { color: var(--loss); } | ||
| 322 | .crash-number.cashed { color: var(--win); } | ||
| 323 | .crash-meta { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; color: var(--faint); font-size: 11px; font-family: var(--mono); } | ||
| 324 | .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; } | ||
| 325 | .verify.ok { border-color: rgba(78, 194, 123, 0.4); color: var(--win); } | ||
| 326 | .verify.bad { border-color: rgba(224, 90, 90, 0.5); color: var(--loss); } | ||
| 327 | |||
| 328 | /* Roulette */ | ||
| 329 | .roulette-wrap { position: relative; } | ||
| 330 | canvas { display: block; width: 100%; } | ||
| 331 | .roulette-canvas { height: 540px; } | ||
| 332 | .crash-canvas { height: 360px; } | ||
| 333 | .wheel-result { | ||
| 334 | position: absolute; | ||
| 335 | top: 16px; | ||
| 336 | right: 16px; | ||
| 337 | padding: 8px 12px; | ||
| 338 | border-radius: var(--radius-md); | ||
| 339 | border: 1px solid var(--line); | ||
| 340 | background: rgba(10, 14, 18, 0.86); | ||
| 341 | font-family: var(--mono); | ||
| 342 | } | ||
| 343 | .wheel-result .num { font-size: 26px; font-weight: 700; } | ||
| 344 | .wheel-result.red .num { color: #f06a74; } | ||
| 345 | .wheel-result.black .num { color: #dfe8ef; } | ||
| 346 | .wheel-result.green .num { color: #66d49a; } | ||
| 347 | |||
| 348 | /* History */ | ||
| 349 | .table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); } | ||
| 350 | table { width: 100%; border-collapse: collapse; background: var(--surface); } | ||
| 351 | th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--line); font-size: 13px; white-space: nowrap; } | ||
| 352 | th { color: var(--dim); font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; } | ||
| 353 | tr:last-child td { border-bottom: 0; } | ||
| 354 | td.num { font-family: var(--mono); } | ||
| 355 | td.pos { color: var(--win); } | ||
| 356 | td.neg { color: var(--loss); } | ||
| 357 | |||
| 358 | /* Toasts and overlays */ | ||
| 359 | #toasts { position: fixed; right: 18px; bottom: 18px; display: grid; gap: 8px; z-index: 50; width: min(380px, calc(100vw - 36px)); } | ||
| 360 | .toast { | ||
| 361 | background: var(--surface); | ||
| 362 | border: 1px solid var(--line); | ||
| 363 | border-left: 3px solid var(--accent); | ||
| 364 | border-radius: var(--radius-md); | ||
| 365 | padding: 10px 12px; | ||
| 366 | font-size: 13px; | ||
| 367 | box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35); | ||
| 368 | animation: toast-in 160ms ease-out; | ||
| 369 | } | ||
| 370 | .toast.good { border-left-color: var(--win); } | ||
| 371 | .toast.bad { border-left-color: var(--loss); } | ||
| 372 | @keyframes toast-in { from { opacity: 0; transform: translateY(6px); } } | ||
| 373 | .overlay { | ||
| 374 | position: fixed; | ||
| 375 | inset: 0; | ||
| 376 | background: rgba(5, 8, 11, 0.78); | ||
| 377 | display: grid; | ||
| 378 | place-items: center; | ||
| 379 | z-index: 60; | ||
| 380 | padding: 24px; | ||
| 381 | } | ||
| 382 | .modal { | ||
| 383 | width: min(440px, 100%); | ||
| 384 | background: var(--surface); | ||
| 385 | border: 1px solid var(--line); | ||
| 386 | border-radius: var(--radius-lg); | ||
| 387 | padding: 24px; | ||
| 388 | text-align: center; | ||
| 389 | } | ||
| 390 | .modal p { color: var(--dim); } | ||
| 391 | |||
| 392 | @media (max-width: 900px) { | ||
| 393 | .game-body { grid-template-columns: 1fr; } | ||
| 394 | .sidebar { position: static; } | ||
| 395 | .tagline { display: none; } | ||
| 396 | } | ||
| 397 | @media (max-width: 620px) { | ||
| 398 | main { padding: 18px 14px 54px; } | ||
| 399 | .topbar { padding: 0 14px; gap: 10px; } | ||
| 400 | .session .who { display: none; } | ||
| 401 | .balance { font-size: 14px; } | ||
| 402 | .reels { gap: 6px; } | ||
| 403 | .tile { font-size: clamp(18px, 7vw, 34px); } | ||
| 404 | .card { width: 58px; height: 84px; } | ||
| 405 | .crash-canvas { height: 260px; } | ||
| 406 | .roulette-canvas { height: 400px; } | ||
| 407 | } | ||
