feat(ui): 3D scenes, mobile-first layout, portfolio charts
Three WebGL scenes on one renderer and one context, since a phone should not allocate a context per game: a rocket straining against gravity, a decaying orbit, and a tower that sways harder the higher it stacks. The loop stops when the tab is hidden. Navigation moves to the bottom, where a thumb already is. New Stats view with balance history, cash-out rate, and a distribution chart that plots observed crash points against what the published maths predicts — the honest version of a hot-numbers board. Charts are hand-built SVG, ~300 lines, rather than a library that would cost more to load than the 3D engine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -352,3 +352,162 @@ button:active { transform: translateY(1px); }
|
||||
.verify-out .ok { color: var(--green); font-weight: 700; }
|
||||
.verify-out .bad { color: var(--red); font-weight: 700; }
|
||||
.verify-out .kv code { font-size: 10px; }
|
||||
|
||||
/* ============================================================
|
||||
MOBILE-FIRST LAYER
|
||||
Everything above assumed a single column. This layer adds the
|
||||
bottom navigation, stat tiles, charts, and the desktop widening.
|
||||
============================================================ */
|
||||
|
||||
/* Bottom nav replaces the old top tabs: on a phone held one-handed,
|
||||
navigation belongs where the thumb already is. */
|
||||
.tabs {
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
|
||||
display: flex; gap: 0; padding: 0;
|
||||
max-width: none; margin: 0;
|
||||
background: #00120cf2;
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid var(--green-ghost);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.tabs .tab {
|
||||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||||
gap: 3px; padding: 9px 2px 7px;
|
||||
font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
border: none; border-top: 2px solid transparent; border-bottom: none;
|
||||
background: none; color: var(--green-dim); border-radius: 0;
|
||||
}
|
||||
.tabs .tab .ico { font-size: 15px; line-height: 1; }
|
||||
.tabs .tab.active {
|
||||
color: var(--green); border-top-color: var(--green);
|
||||
background: #00ff9c0a;
|
||||
}
|
||||
|
||||
/* Leave room for the fixed nav. */
|
||||
.view { padding-bottom: 84px; }
|
||||
|
||||
/* The 3D stage is the hero: taller on phones, where vertical space is what
|
||||
there is most of. */
|
||||
.stage { aspect-ratio: 1 / 1; }
|
||||
@media (min-width: 560px) { .stage { aspect-ratio: 16 / 11; } }
|
||||
|
||||
#scene { width: 100%; height: 100%; display: block; }
|
||||
|
||||
.stagetop {
|
||||
position: absolute; top: 0; left: 0; right: 0;
|
||||
display: flex; justify-content: center; padding: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pot {
|
||||
font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase;
|
||||
color: var(--green-dim);
|
||||
}
|
||||
|
||||
/* Crash history strip under the stage — the at-a-glance recent record. */
|
||||
.strip {
|
||||
display: flex; gap: 4px; overflow-x: auto; padding: 8px 0 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.strip::-webkit-scrollbar { display: none; }
|
||||
.strip .pip {
|
||||
flex: 0 0 auto; font-size: 10.5px; padding: 4px 7px; border-radius: 2px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
background: #00140e; border: 1px solid var(--green-ghost);
|
||||
color: var(--green-dim);
|
||||
}
|
||||
.strip .pip.mid { color: var(--amber); border-color: #4a3300; }
|
||||
.strip .pip.high { color: var(--magenta); border-color: #4a0f2c; }
|
||||
|
||||
/* Auto cash-out presets. */
|
||||
.presets { display: flex; gap: 3px; margin-left: auto; }
|
||||
.presets button {
|
||||
padding: 5px 8px; font-size: 11px; color: var(--green-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.presets button:active { color: var(--amber); border-color: var(--amber); }
|
||||
|
||||
/* ---------- stat tiles ---------- */
|
||||
|
||||
.tiles {
|
||||
display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@media (min-width: 560px) { .tiles { grid-template-columns: repeat(4, 1fr); } }
|
||||
|
||||
.tile {
|
||||
background: #00120c; border: 1px solid var(--green-ghost);
|
||||
border-radius: 3px; padding: 11px 12px;
|
||||
display: flex; flex-direction: column; gap: 2px; min-height: 76px;
|
||||
}
|
||||
.tile-label {
|
||||
font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: var(--green-dim);
|
||||
}
|
||||
.tile-value {
|
||||
font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;
|
||||
color: var(--green); line-height: 1.15;
|
||||
}
|
||||
.tile-value.up { color: var(--amber); }
|
||||
.tile-value.down { color: var(--red); }
|
||||
.tile-sub { font-size: 9.5px; color: var(--green-dim); }
|
||||
.tile-spark { height: 22px; margin-top: auto; }
|
||||
|
||||
/* ---------- charts ---------- */
|
||||
|
||||
.chart { width: 100%; height: 110px; }
|
||||
.chart-donut { height: 130px; display: grid; place-items: center; }
|
||||
.chart-donut svg { width: 130px; height: 130px; }
|
||||
.chart-empty { text-align: center; padding: 30px 10px; margin: 0; }
|
||||
|
||||
/* ---------- tech spec list ---------- */
|
||||
|
||||
.specs { margin: 0; font-size: 12px; }
|
||||
.specs dt {
|
||||
color: var(--amber); font-size: 9.5px; letter-spacing: 0.14em;
|
||||
text-transform: uppercase; margin-top: 10px;
|
||||
}
|
||||
.specs dt:first-child { margin-top: 0; }
|
||||
.specs dd { margin: 2px 0 0; color: var(--green-dim); }
|
||||
|
||||
/* ---------- crypto badge on the sign-in screen ---------- */
|
||||
|
||||
.cryptobadge {
|
||||
display: flex; gap: 6px; justify-content: center; margin-top: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cryptobadge span {
|
||||
font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase;
|
||||
color: var(--green-dim); border: 1px solid var(--green-ghost);
|
||||
padding: 4px 8px; border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ---------- desktop widening ---------- */
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.view { max-width: 860px; }
|
||||
/* Side-by-side once there is width for it, rather than a tall scroll. */
|
||||
#view-crash {
|
||||
display: grid; grid-template-columns: 1.35fr 1fr;
|
||||
grid-template-areas: "pick pick" "stage controls" "strip players" "proof proof";
|
||||
gap: 0 20px; align-items: start;
|
||||
}
|
||||
#view-crash .gamepick { grid-area: pick; }
|
||||
#view-crash .stage { grid-area: stage; }
|
||||
#view-crash .strip { grid-area: strip; }
|
||||
#view-crash .controls { grid-area: controls; margin-top: 0; }
|
||||
#view-crash .players { grid-area: players; }
|
||||
#view-crash .proof { grid-area: proof; }
|
||||
|
||||
#view-portfolio { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
|
||||
#view-portfolio .tiles { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
/* Respect a stated preference for less motion: the urgency pulse and the
|
||||
scanline flicker are exactly what that setting is asking about. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.primary.cashout { animation: none; }
|
||||
body::after { animation: none; }
|
||||
.multiplier.crashed { animation: none; }
|
||||
h1::after { animation: none; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user